數位學習平台 ↗ EN 聯絡我們

登月小車 AMB82 MINI AI 自駕車(二)資料蒐集:布置跑道並拍攝訓練照片

登月小車 AMB82 MINI AI 自駕車(二)資料蒐集:布置跑道並拍攝訓練照片

本系列使用 登月小車(AMB82 MINI),從硬體安裝、蒐集與標註資料、訓練 YOLOv7-tiny 模型,一路做到能辨識交通標誌的自駕小車。這是第二篇。

2-1. 場地準備

首先購買兩張黑色大壁報紙,或者現有的場地都可以,然後準備白色電工膠帶與黃色電工膠帶。然後如下圖依照小車寬度再寬一點的間距,用電工膠帶黏貼小車跑道。

白色電工膠帶做為外線,黃色電工膠帶以段落黏貼做為中線。

白色電工膠帶做為外線,黃色電工膠帶以段落黏貼做為中線。

如果想以地板為場地也可以,可以把電工膠帶的顏色做替換,只要跟地板顏色區分開來,用什麼顏色都可以,只是外線和中線的顏色要不同。

接著準備標誌,你可以自己設計或是用我們提供的,PDF 連結如下:

接著準備標誌,你可以自己設計或是用我們提供的,PDF 連結如下

請彩色列印並裁剪下來,可以黏到任何能立起來的盒子或自己用紙板做立牌。

2-2. 程式撰寫

我們主要是想透過AI 模型,去分辨道路與標誌,讓小車可以沿著道路走,並依照標誌做反應。像是遇到停,希望它停車再開,遇到限速就減慢行駛速度……等。

為了讓AI 看得懂這些東西,我們需要告訴它什麼是道路,各個標誌代表什麼意思。所以我們需要用小車上的攝影機拍攝道路與標誌,並且將這些拍攝到的畫面儲存下來,之後我們會手動標註。

我們程式功能希望是這樣設計:

  • 保留RTSP 串流讓我們可以知道小車現在拍攝到的畫面。
  • 按下小車上面的按鈕後拍攝並自動儲存至SD Card。
  • OLED 除了顯示IP 也要顯示目前拍攝的張數。

SD Card 除了作為之後存放模型之外,在此處兼具儲存照片。所以我們需要準備一張SD Card,大小約略在8GB ~ 32GB 即可,圖片大小與模型大小都不大。

程式重點說明如下:

宣告兩組 VideoSetting 物件,以符合一個作為RTSP 來源,另一個作為儲存照片來源。VIDEO_VGA 代表 640×480 解析度,兼顧傳輸速度與後續 YOLO 神經網路推論需求。configJPEG 的第四個參數設定為 1(snapshot = 1),代表此通道專門為「隨選單張畫格壓縮」優化。

Camera.videoInit() 會啟動底層視訊驅動與感光元件通訊。

VideoSetting configVID(VIDEO_VGA, CAM_FPS, VIDEO_H264, 0);
VideoSetting configJPEG(VIDEO_VGA, CAM_FPS, VIDEO_JPEG, 1);

configVID.setBitrate(2 * 1024 * 1024);
Camera.configVideoChannel(CHANNEL_VID, configVID);
Camera.configVideoChannel(CHANNEL_JPEG, configJPEG);
Camera.videoInit();

因為AMB82 在小車上是上下顛倒放,所以也需要將取得的畫面旋轉180 度才可以。CAMERA_ORIENTATION 設定為3,就是旋轉180 度的意思。

#define CAMERA_ORIENTATION 3

extern "C" {
    int isp_set_orientation(int val);
}

// 在 setup() 內執行:
if (CAMERA_ORIENTATION > 0) {
    isp_set_orientation(CAMERA_ORIENTATION);
}

我們以按鈕A 為拍照按鈕,當按下時會觸發captureAndSaveJPEG() 將當前影像擷取下來。

// #define BUTTON_PIN   18
int reading = digitalRead(BUTTON_PIN);

if (reading != lastButtonState) {
    lastDebounceTime = millis();
}

if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != currentStableState) {
        currentStableState = reading;
        if (currentStableState == LOW) {
            captureAndSaveJPEG();
        }
    }
}

然後呼叫SD Card 將資料寫入,並安全移除,以釋放權限給下次儲存的時候。

if (fs.begin()) {
    char filename[64];
    while (true) {
        snprintf(filename, sizeof(filename), "%simg_%04d.jpg", fs.getRootPath(), fileCounter);
        if (!fs.exists(filename)) {
            break;
        }
        fileCounter++;
    }

    File file = fs.open(filename);
    if (file) {
        file.write((uint8_t *)img_addr, img_len);
        file.close();
        
        savedImageCount++;
        sdCardError = false;
        fileCounter++;
    } else {
        sdCardError = true;
    }
    
    fs.end();
} else {
    sdCardError = true;
}

完整程式如下:

記得輸入自己的Wi-Fi 名稱與密碼

#include "WiFi.h"
#include "StreamIO.h"
#include "VideoStream.h"
#include "RTSP.h"
#include "AmebaFatFS.h"
#include "VideoStreamOverlay.h"
#include <Wire.h>
#include <Adafruit_OLED_libraries/Adafruit_GFX.h>
#include <Adafruit_OLED_libraries/Adafruit_SSD1306.h>

#define BUTTON_PIN   18
#define CHANNEL_VID  0
#define CHANNEL_JPEG 1

#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET     -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
bool oledFound = false;

#define CAMERA_ORIENTATION 3

#ifdef __cplusplus
extern "C" {
#endif
int isp_set_orientation(int val);
#ifdef __cplusplus
}
#endif

VideoSetting configVID(VIDEO_VGA, CAM_FPS, VIDEO_H264, 0);
VideoSetting configJPEG(VIDEO_VGA, CAM_FPS, VIDEO_JPEG, 1);

RTSP rtsp;
StreamIO videoStreamer(1, 1);
AmebaFatFS fs;

char ssid[] = "";  // 記得輸入自己的Wi-Fi 名稱
char pass[] = "";  // 記得輸入自己的Wi-Fi 密碼

int status = WL_IDLE_STATUS;

int lastButtonState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;
static int currentStableState = HIGH;

int fileCounter = 1;
int savedImageCount = 0;
bool sdCardError = false;

void captureAndSaveJPEG();
void printInfo(void);
void updateOSD(void);
void updateOLED(void);

void setup()
{
    Serial.begin(115200);

    if (display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        oledFound = true;
        display.clearDisplay();
        display.setTextSize(1);
        display.setTextColor(SSD1306_WHITE);
        display.setCursor(0, 0);
        display.println(F("AMB82-MINI Camera"));
        display.println(F("Connecting WiFi..."));
        display.display();
    } else {
        Serial.println(F("[WARNING] SSD1306 OLED allocation failed or not detected"));
    }

    pinMode(BUTTON_PIN, INPUT_PULLUP);

    Serial.println("\r\n[INFO] Initializing MicroSD Card...");
    if (fs.begin()) {
        sdCardError = false;
        Serial.println("[INFO] MicroSD Card initialized successfully.");

        while (true) {
            char filename[64];
            snprintf(filename, sizeof(filename), "%simg_%04d.jpg", fs.getRootPath(), fileCounter);
            if (fs.exists(filename)) {
                savedImageCount++;
                fileCounter++;
            } else {
                break;
            }
        }
        Serial.print("[INFO] Existing images found on SD: ");
        Serial.println(savedImageCount);
        fs.end();
    } else {
        sdCardError = true;
        Serial.println("[WARNING] MicroSD Card not detected! Please ensure a FAT32 card is inserted.");
    }

    while (status != WL_CONNECTED) {
        Serial.print("Attempting to connect to WPA SSID: ");
        Serial.println(ssid);
        status = WiFi.begin(ssid, pass);

        delay(2000);
    }

    configVID.setRotation(0);
    configJPEG.setRotation(0);

    configVID.setBitrate(2 * 1024 * 1024);
    Camera.configVideoChannel(CHANNEL_VID, configVID);
    Camera.configVideoChannel(CHANNEL_JPEG, configJPEG);
    Camera.videoInit();

    rtsp.configVideo(configVID);
    rtsp.begin();

    videoStreamer.registerInput(Camera.getStream(CHANNEL_VID));
    videoStreamer.registerOutput(rtsp);
    if (videoStreamer.begin() != 0) {
        Serial.println("[ERROR] StreamIO link start failed");
    }

    Camera.channelBegin(CHANNEL_VID);
    Camera.channelBegin(CHANNEL_JPEG);

    delay(500);
    if (CAMERA_ORIENTATION > 0) {
        isp_set_orientation(CAMERA_ORIENTATION);
    }

    OSD.configVideo(CHANNEL_VID, configVID);
    OSD.begin();

    delay(1000);
    printInfo();
    updateOSD();
    updateOLED();
}

void loop()
{
    int reading = digitalRead(BUTTON_PIN);

    if (reading != lastButtonState) {
        lastDebounceTime = millis();
    }

    if ((millis() - lastDebounceTime) > debounceDelay) {
        if (reading != currentStableState) {
            currentStableState = reading;
            if (currentStableState == LOW) {
                captureAndSaveJPEG();
            }
        }
    }

    lastButtonState = reading;
    delay(10);
}

void captureAndSaveJPEG()
{
    uint32_t img_addr = 0;
    uint32_t img_len = 0;

    Serial.println("\r\n[ACTION] Button pressed (Pin 18 = false)! Capturing 640x480 image...");

    Camera.getImage(CHANNEL_JPEG, &img_addr, &img_len);

    if (img_addr == 0 || img_len == 0) {
        Serial.println("[ERROR] Failed to capture JPEG frame from camera.");
        return;
    }

    if (fs.begin()) {
        char filename[64];

        while (true) {
            snprintf(filename, sizeof(filename), "%simg_%04d.jpg", fs.getRootPath(), fileCounter);
            if (!fs.exists(filename)) {
                break;
            }
            fileCounter++;
        }

        File file = fs.open(filename);
        if (file) {
            file.write((uint8_t *)img_addr, img_len);
            file.close();
            savedImageCount++;
            sdCardError = false;
            Serial.print("[SUCCESS] Image saved: ");
            Serial.print(filename);
            Serial.print(" (Resolution: 640x480, Size: ");
            Serial.print(img_len);
            Serial.println(" bytes)");
            fileCounter++;
        } else {
            sdCardError = true;
            Serial.println("[ERROR] Failed to open file for writing on SD card.");
        }

        fs.end();
    } else {
        sdCardError = true;
        Serial.println("[ERROR] SD card initialization failed. Please check SD card connection.");
    }

    updateOSD();
    updateOLED();
}

void updateOSD(void)
{
    OSD.createBitmap(CHANNEL_VID);

    char countStr[32];
    char sdStr[32];
    snprintf(countStr, sizeof(countStr), "Images: %d", savedImageCount);
    snprintf(sdStr, sizeof(sdStr), "SD: %s", sdCardError ? "ERROR" : "OK");

    uint32_t sdColor = sdCardError ? OSD_COLOR_RED : OSD_COLOR_GREEN;

    OSD.drawText(CHANNEL_VID, 16, 16, countStr, OSD_COLOR_CYAN);
    OSD.drawText(CHANNEL_VID, 16, 16 + OSD.getTextHeight(CHANNEL_VID) + 4, sdStr, sdColor);

    OSD.update(CHANNEL_VID);
}

void updateOLED(void)
{
    if (!oledFound) return;

    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);

    display.setCursor(0, 0);
    display.println(F("AMB82-MINI (640x480)"));
    display.drawFastHLine(0, 10, SCREEN_WIDTH, SSD1306_WHITE);

    display.setCursor(0, 14);
    display.print(F("IP: "));
    display.println(WiFi.localIP());

    display.setCursor(0, 26);
    display.print(F("RTSP Port: "));
    display.println(rtsp.getPort());

    display.setCursor(0, 38);
    display.print(F("SD: "));
    display.print(sdCardError ? F("ERROR") : F("OK"));
    display.print(F(" | Img: "));
    display.println(savedImageCount);

    display.setCursor(0, 52);
    display.println(F("Pin18: Press to Snap"));

    display.display();
}

void printInfo(void)
{
    Serial.println(" ");
    Serial.println("==============================================");
    Serial.println("- AMB82-MINI 640x480 RTSP & Dataset Capture  -");
    Serial.println("==============================================");
    Camera.printInfo();

    IPAddress ip = WiFi.localIP();

    Serial.println("- RTSP Streaming Info -");
    rtsp.printInfo(ip.get_address());
    Serial.print("Stream URL: rtsp://");
    Serial.print(ip);
    Serial.print(":");
    Serial.println(rtsp.getPort());
    Serial.println("----------------------------------------------");
    Serial.println("Capture Trigger: Press button (Pin 18 -> GND)");
    Serial.println("Images will be saved to SD Card as 640x480 JPEG");
    Serial.println("==============================================");
}

將完整程式碼複製到Arduino IDE,並下載到AMB82 MINI 上面。

將完整程式碼複製到Arduino IDE,並下載到AMB82 MINI 上面。

2-3. 拍攝資料

插上準備的SD Card 並將小車放在跑道上打開電源,OLED 會顯示目前連線資訊,最後會顯示 IP。以及SD Card 是否有安裝?還有目前儲存的照片(Img) 有幾張。

插上準備的SD Card 並將小車放在跑道上打開電源,OLED 會顯示目前連線資訊,最後會顯示 IP。以及SD Card…

透過VLC 開啟串流影像,以方便拍攝目標。影像左上角也會顯示儲存資訊,照片數量與SD Card 是否正常運作。

透過VLC 開啟串流影像,以方便拍攝目標。影像左上角也會顯示儲存資訊,照片數量與SD Card 是否正常運作。

我們拍攝目標是取得道路上的黃色線段與標誌,線段要讓車子在道路上拍,標誌就沒有強制要擺在一起拍,可以分開沒關係。

請搭配VLC 查看畫面中是否有目標,確認無誤即可按下「按鈕 A」拍攝照片。

請搭配VLC 查看畫面中是否有目標,確認無誤即可按下「按鈕 A」拍攝照片。

然後請讓車子擺在不同角度與不同位置,像是放在直線或是彎道處,或是刻意讓它偏離一點跑道。盡可能蒐集不同的畫面,讓AI 能學習到各種情況。

然後請讓車子擺在不同角度與不同位置,像是放在直線或是彎道處,或是刻意讓它偏離一點跑道。盡可能蒐集不同的畫面,讓AI 能學…

標誌建議部分照片是全部放一起拍,有些則是單獨一個標誌拍攝,儘量增加不同背景或角度和位置,畢竟小車在移動時不會都是拍到正面的標誌。

標誌建議部分照片是全部放一起拍,有些則是單獨一個標誌拍攝,儘量增加不同背景或角度和位置,畢竟小車在移動時不會都是拍到正面…

數據多樣性與數量能多少張就多少張,但為了避免大家標注資料時頭昏眼花,建議道路與標誌各拍攝約50 ~ 100 張左右即可,前提是每張資料都要是獨一無二。

拍攝完畢後就能關閉小車電源,然後拔出SD Card,請記得關閉電源再拔,非常重要!!!

然後用讀卡機放到自己的電腦上,除了再檢查一次照片之外,也順便剔除模糊的照片。

總結

這個單元主要就是用AMB82 MINI 拍攝訓練AI 用的資料,請確實蒐集每張照片。下個單元將會教同學如何做資料標註,這將是一項大工程!

系列文章

  1. (一)基礎認識
  2. (二)資料蒐集(本篇)
  3. (三)資料標註
  4. (四)訓練模型與轉換
  5. (五)自動駕駛