跳至主要内容

捕捉影像 - Wio Terminal

在本課程的這部分,你將為你的 Wio Terminal 添加一個相機,並從中捕捉影像。

硬體

Wio Terminal 需要一個相機。

你將使用的相機是 ArduCam Mini 2MP Plus。這是一個基於 OV2640 影像感測器的 2 百萬像素相機。它通過 SPI 介面來捕捉影像,並使用 I2C 來配置感測器。

連接相機

ArduCam 沒有 Grove 插座,而是通過 Wio Terminal 上的 GPIO 引腳連接到 SPI 和 I2C 匯流排。

任務 - 連接相機

連接相機。

An ArduCam sensor

  1. ArduCam 底部的引腳需要連接到 Wio Terminal 的 GPIO 引腳。為了更容易找到正確的引腳,將 Wio Terminal 附帶的 GPIO 引腳貼紙貼在引腳周圍:

    The wio terminal with the GPIO pin sticker on

  2. 使用跳線,進行以下連接:

    ArduCAM 引腳Wio Terminal 引腳描述
    CS24 (SPI_CS)SPI 晶片選擇
    MOSI19 (SPI_MOSI)SPI 控制器輸出,外設輸入
    MISO21 (SPI_MISO)SPI 控制器輸入,外設輸出
    SCK23 (SPI_SCLK)SPI 串行時鐘
    GND6 (GND)地線 - 0V
    VCC4 (5V)5V 電源供應
    SDA3 (I2C1_SDA)I2C 串行數據
    SCL5 (I2C1_SCL)I2C 串行時鐘

    The wio terminal connected to the ArduCam with jumper wires

    GND 和 VCC 連接為 ArduCam 提供 5V 電源。它以 5V 運行,不像以 3V 運行的 Grove 感測器。這個電源直接來自為設備供電的 USB-C 連接。

    💁 對於 SPI 連接,ArduCam 和 Wio Terminal 引腳名稱在代碼中仍然使用舊的命名約定。本課程中的指示將使用新的命名約定,除非在代碼中使用引腳名稱。

  3. 現在你可以將 Wio Terminal 連接到你的電腦。

編程設備以連接相機

現在可以編程 Wio Terminal 以使用附加的 ArduCAM 相機。

任務 - 編程設備以連接相機

  1. 使用 PlatformIO 創建一個全新的 Wio Terminal 專案。將此專案命名為 fruit-quality-detector。在 setup 函數中添加代碼以配置串行端口。

  2. 添加代碼以連接 WiFi,將你的 WiFi 憑證放在名為 config.h 的文件中。不要忘記將所需的庫添加到 platformio.ini 文件中。

  3. ArduCam 庫無法作為 Arduino 庫從 platformio.ini 文件中安裝。相反,需要從他們的 GitHub 頁面安裝源代碼。你可以通過以下方式獲取:

  4. 你只需要這個代碼中的 ArduCAM 文件夾。將整個文件夾複製到你的專案中的 lib 文件夾中。

    ⚠️ 必須複製整個文件夾,因此代碼位於 lib/ArduCam 中。不要僅將 ArduCam 文件夾的內容複製到 lib 文件夾中,而是將整個文件夾複製過去。

  5. ArduCam 庫代碼適用於多種類型的相機。你想要使用的相機類型是通過編譯器標誌配置的 - 這使得構建的庫盡可能小,通過刪除你未使用的相機的代碼。要將庫配置為 OV2640 相機,請在 platformio.ini 文件的末尾添加以下內容:

    build_flags =
    -DARDUCAM_SHIELD_V2
    -DOV2640_CAM

    這設置了兩個編譯器標誌:

    • ARDUCAM_SHIELD_V2 告訴庫相機在 Arduino 板上,稱為盾。
    • OV2640_CAM 告訴庫僅包含 OV2640 相機的代碼
  6. src 文件夾中添加一個名為 camera.h 的頭文件。這將包含與相機通信的代碼。將以下代碼添加到此文件中:

    #pragma once

    #include <ArduCAM.h>
    #include <Wire.h>

    class Camera
    {
    public:
    Camera(int format, int image_size) : _arducam(OV2640, PIN_SPI_SS)
    {
    _format = format;
    _image_size = image_size;
    }

    bool init()
    {
    // 重置 CPLD
    _arducam.write_reg(0x07, 0x80);
    delay(100);

    _arducam.write_reg(0x07, 0x00);
    delay(100);

    // 檢查 ArduCAM SPI 匯流排是否正常
    _arducam.write_reg(ARDUCHIP_TEST1, 0x55);
    if (_arducam.read_reg(ARDUCHIP_TEST1) != 0x55)
    {
    return false;
    }

    // 更改 MCU 模式
    _arducam.set_mode(MCU2LCD_MODE);

    uint8_t vid, pid;

    // 檢查相機模組類型是否為 OV2640
    _arducam.wrSensorReg8_8(0xff, 0x01);
    _arducam.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid);
    _arducam.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid);
    if ((vid != 0x26) && ((pid != 0x41) || (pid != 0x42)))
    {
    return false;
    }

    _arducam.set_format(_format);
    _arducam.InitCAM();
    _arducam.OV2640_set_JPEG_size(_image_size);
    _arducam.OV2640_set_Light_Mode(Auto);
    _arducam.OV2640_set_Special_effects(Normal);
    delay(1000);

    return true;
    }

    void startCapture()
    {
    _arducam.flush_fifo();
    _arducam.clear_fifo_flag();
    _arducam.start_capture();
    }

    bool captureReady()
    {
    return _arducam.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK);
    }

    bool readImageToBuffer(byte **buffer, uint32_t &buffer_length)
    {
    if (!captureReady()) return false;

    // 獲取影像文件長度
    uint32_t length = _arducam.read_fifo_length();
    buffer_length = length;

    if (length >= MAX_FIFO_SIZE)
    {
    return false;
    }
    if (length == 0)
    {
    return false;
    }

    // 創建緩衝區
    byte *buf = new byte[length];

    uint8_t temp = 0, temp_last = 0;
    int i = 0;
    uint32_t buffer_pos = 0;
    bool is_header = false;

    _arducam.CS_LOW();
    _arducam.set_fifo_burst();

    while (length--)
    {
    temp_last = temp;
    temp = SPI.transfer(0x00);
    //從 FIFO 讀取 JPEG 數據
    if ((temp == 0xD9) && (temp_last == 0xFF)) //如果找到結尾,則中斷 while 循環
    {
    buf[buffer_pos] = temp;

    buffer_pos++;
    i++;

    _arducam.CS_HIGH();
    }
    if (is_header == true)
    {
    //如果緩衝區未滿,則將影像數據寫入緩衝區
    if (i < 256)
    {
    buf[buffer_pos] = temp;
    buffer_pos++;
    i++;
    }
    else
    {
    _arducam.CS_HIGH();

    i = 0;
    buf[buffer_pos] = temp;

    buffer_pos++;
    i++;

    _arducam.CS_LOW();
    _arducam.set_fifo_burst();
    }
    }
    else if ((temp == 0xD8) & (temp_last == 0xFF))
    {
    is_header = true;

    buf[buffer_pos] = temp_last;
    buffer_pos++;
    i++;

    buf[buffer_pos] = temp;
    buffer_pos++;
    i++;
    }
    }

    _arducam.clear_fifo_flag();

    _arducam.set_format(_format);
    _arducam.InitCAM();
    _arducam.OV2640_set_JPEG_size(_image_size);

    // 返回緩衝區
    *buffer = buf;
    }

    private:
    ArduCAM _arducam;
    int _format;
    int _image_size;
    };

    這是使用 ArduCam 庫配置相機的低級代碼,並在需要時使用 SPI 匯流排提取影像。這段代碼非常特定於 ArduCam,因此你不需要擔心它是如何工作的。

  7. main.cpp 中,在其他 include 語句下方添加以下代碼以包含這個新文件並創建相機類的實例:

    #include "camera.h"

    Camera camera = Camera(JPEG, OV2640_640x480);

    這將創建一個 Camera,將影像保存為 640x480 分辨率的 JPEG。雖然支持更高的分辨率(最高 3280x2464),但影像分類器在更小的影像(227x227)上工作,因此不需要捕捉和傳送更大的影像。

  8. 在此下方添加以下代碼以定義一個設置相機的函數:

    void setupCamera()
    {
    pinMode(PIN_SPI_SS, OUTPUT);
    digitalWrite(PIN_SPI_SS, HIGH);

    Wire.begin();
    SPI.begin();

    if (!camera.init())
    {
    Serial.println("設置相機時出錯!");
    }
    }

    這個 setupCamera 函數首先將 SPI 晶片選擇引腳(PIN_SPI_SS)配置為高,使 Wio Terminal 成為 SPI 控制器。然後它啟動 I2C 和 SPI 匯流排。最後,它初始化相機類,配置相機感測器設置並確保所有連線正確。

  9. setup 函數的末尾調用此函數:

    setupCamera();
  10. 構建並上傳此代碼,並檢查串行監視器的輸出。如果你看到 設置相機時出錯!,請檢查連線以確保所有電纜正確連接 ArduCam 的引腳和 Wio Terminal 的 GPIO 引腳,並且所有跳線都正確插入。

捕捉影像

現在可以編程 Wio Terminal 以在按下按鈕時捕捉影像。

任務 - 捕捉影像

  1. 微控制器會不斷運行你的代碼,因此在不響應感測器的情況下觸發拍照並不容易。Wio Terminal 有按鈕,因此可以設置相機以通過其中一個按鈕觸發。將以下代碼添加到 setup 函數的末尾以配置 C 按鈕(頂部的三個按鈕之一,最靠近電源開關的那個)。

    The C button on the top closest to the power switch

    pinMode(WIO_KEY_C, INPUT_PULLUP);

    INPUT_PULLUP 模式本質上是反轉輸入。例如,通常按鈕在未按下時會發送低信號,按下時會發送高信號。設置為 INPUT_PULLUP 時,它們在未按下時發送高信號,按下時發送低信號。

  2. loop 函數之前添加一個空函數以響應按鈕按下:

    void buttonPressed()
    {

    }
  3. 在按下按鈕時在 loop 方法中調用此函數:

    void loop()
    {
    if (digitalRead(WIO_KEY_C) == LOW)
    {
    buttonPressed();
    delay(2000);
    }

    delay(200);
    }

    這個鍵檢查按鈕是否被按下。如果按下,則調用 buttonPressed 函數,並且循環延遲 2 秒。這是為了允許按鈕釋放的時間,以便長按不會被註冊兩次。

    💁 Wio Terminal 上的按鈕設置為 INPUT_PULLUP,因此在未按下時發送高信號,按下時發送低信號。

  4. 將以下代碼添加到 buttonPressed 函數中:

    camera.startCapture();

    while (!camera.captureReady())
    delay(100);

    Serial.println("影像已捕捉");

    byte *buffer;
    uint32_t length;

    if (camera.readImageToBuffer(&buffer, length))
    {
    Serial.print("影像已讀取到緩衝區,長度為 ");
    Serial.println(length);

    delete(buffer);
    }

    這段代碼通過調用 startCapture 開始相機捕捉。相機硬體不會在你請求時返回數據,而是你發送指令開始捕捉,相機將在背景中工作以捕捉影像,將其轉換為 JPEG,並將其存儲在相機本地緩衝區中。然後 captureReady 調用檢查影像捕捉是否完成。

    一旦捕捉完成,影像數據將從相機上的緩衝區複製到本地緩衝區(字節數組)中,使用 readImageToBuffer 調用。然後將緩衝區的長度發送到串行監視器。

  5. 構建並上傳此代碼,並檢查串行監視器上的輸出。每次按下 C 按鈕時,將捕捉一張影像,並且你將看到影像大小發送到串行監視器。

    Connecting to WiFi..
    Connected!
    影像已捕捉
    影像已讀取到緩衝區,長度為 9224
    影像已捕捉
    影像已讀取到緩衝區,長度為 11272

    不同的影像將有不同的大小。它們被壓縮為 JPEG,給定分辨率的 JPEG 文件大小取決於影像中的內容。

💁 你可以在 code-camera/wio-terminal 文件夾中找到此代碼。

😀 你已成功使用 Wio Terminal 捕捉影像。

可選 - 使用 SD 卡驗證相機影像

查看相機捕捉的影像的最簡單方法是將它們寫入 Wio Terminal 中的 SD 卡,然後在電腦上查看。如果你有多餘的 microSD 卡和電腦上的 microSD 卡插槽或適配器,請執行此步驟。

Wio Terminal 僅支持最大 16GB 的 microSD 卡。如果你有更大的 SD 卡,則無法使用。

任務 - 使用 SD 卡驗證相機影像

  1. 使用電腦上的相關應用程序(macOS 上的磁碟工具,Windows 上的文件資源管理器,或使用 Linux 中的命令行工具)將 microSD 卡格式化為 FAT32 或 exFAT

  2. 將 microSD 卡插入電源開關下方的插槽。確保它完全插入直到卡住,你可能需要使用指甲或細工具推入。

  3. main.cpp 文件的頂部添加以下 include 語句:

    #include "SD/Seeed_SD.h"
    #include <Seeed_FS.h>
  4. setup 函數之前添加以下函數:

    void setupSDCard()
    {
    while (!SD.begin(SDCARD_SS_PIN, SDCARD_SPI))
    {
    Serial.println("SD 卡錯誤");
    }
    }

    這使用 SPI 匯流排配置 SD 卡。

  5. setup 函數調用此函數:

    setupSDCard();
  6. buttonPressed 函數之前添加以下代碼:

    int fileNum = 1;

    void saveToSDCard(byte *buffer, uint32_t length)
    {
    char buff[16];
    sprintf(buff, "%d.jpg", fileNum);
    fileNum++;

    File outFile = SD.open(buff, FILE_WRITE );
    outFile.write(buffer, length);
    outFile.close();

    Serial.print("影像已寫入文件 ");
    Serial.println(buff);
    }

    這定義了一個文件計數的全局變量。這用於影像文件名,因此可以捕捉多個影像並使用遞增的文件名 - 1.jpg2.jpg 等。

    然後它定義了 saveToSDCard,該函數接受一個字節數據的緩衝區和緩衝區的長度。使用文件計數創建一個文件名,並遞增文件計數以準備下一個文件。然後將緩衝區中的二進制數據寫入文件。

  7. buttonPressed 函數調用 saveToSDCard 函數。調用應在刪除緩衝區之前:

    Serial.print("影像已讀取到緩衝區,長度為 ");
    Serial.println(length);

    saveToSDCard(buffer, length);

    delete(buffer);
  8. 構建並上傳此代碼,並檢查串行監視器上的輸出。每次按下 C 按鈕時,將捕捉一張影像並保存到 SD 卡。

    Connecting to WiFi..
    Connected!
    Image captured
    Image read to buffer with length 16392
    Image written to file 1.jpg
    Image captured
    Image read to buffer with length 14344
    Image written to file 2.jpg
  9. 關閉 microSD 卡並通過輕推並釋放來彈出,然後它會彈出。你可能需要使用細工具來完成此操作。將 microSD 卡插入電腦以查看影像。

    A picture of a banana captured using the ArduCam

    💁 相機的白平衡可能需要幾張影像來調整。你會根據捕捉的影像顏色注意到這一點,前幾張可能顏色不對。你可以通過更改代碼在 setup 函數中捕捉幾張被忽略的影像來解決這個問題。