2022年1月17日 星期一

以ESP32-CAM擷取照片並儲存在Micro S卡上



在這個範例中會示範如何使用ESP32-CAM建構一照片擷取與儲存裝置,此儲存裝置乃使用ESP32-CAM上內建的Micro SD讀寫模,組配合外加的Micro SD卡來實現;至於擷取照片的動作則以常見的焦電型紅外線PIR移動偵測模組來觸發,此外也可以用磁簧開關、微動開關或一般的按鈕來啟動。


功能與動作說明

在我們的系統中將具備下面的動作與功能:

  1. 當系統測到外部的輸入觸發信號時,便會啟動ESP32-CAM拍攝照片的功能,並將所拍攝到的照片儲存到ESP32-CAM上內建的Micro SD讀寫模中所放置的Micro SD卡上。

  2. 此外部觸發輸入(輸入腳為GPIO16)為一高態的脈波信號,每一次的脈波輸入會啟動一次拍攝和擷取及儲存照片的動作。

  3. 所拍攝及儲存的照片名稱為”pictureXXX.jpg”其中的”XXX”為流水號數字每次系統開機啟動時數字會由1開始如果Micro SD卡上之前有相同檔案名稱的照片存在將會被新的覆蓋掉。

  4. 系統最多可拍攝及儲存的照片數目由所使用的micro SD卡容量所決定一般在Arduino IDE中保證可用的Micro SD卡的容量為4GB而在本範例中所設定的ESP32-CAM解析度所拍攝到的照片檔案大小約為26KB左右

  5. 當系統進入待機狀態可以開始擷取照片時ESP32-CAM上內建的LED1(接在GPIO33)會點亮以提醒使用者可以開始使用了


電路圖

在本範例中會使用到下列零組件:

1. 一塊ESP32-CAM模組

2. 一個編號為HC-SR501的人體紅外線移動感測器

3. 一個按鍵

4. 一個10K電阻

5. 磁簧開關(選用)

6. 微動開關(選用)


下面的電路便是本次範例的電路圖:


電路中這顆編號為HC-SR501的人體紅外線移動感測器的實體照片如下:

如下圖所示,目前在市面上所販售的這款感測器模組,在旁邊右上方處有一組3 Pin的跳線短接腳座,供使用者選擇模組的工作電壓;由於我們所使用的ESP32-CAM模組基本上是工作在3.3V的電壓,為了避免HC-SR501人體紅外線移動感測器使用5V的工作電壓時,所輸出的信號位準過高,以至於傷害到ESP32-CAM模組,建議把短接Jump接在下方的3.3V位置上。



由於這個HC-SR501的人體紅外線移動感測器主要是給一般燈光感應控制之用,所以除了可以感應有溫度的紅外線物體移動之外,還可以調整紅外線感應的靈敏度,此外也可以調整動作的時間。下圖中上方兩顆橘色的半可變電阻便是用來調整這兩個參數之用由於我們所需要的動作脈波越短越好建議把右邊那顆調整延遲時間用的半可變電阻往逆時鐘的方向轉到最底這時動作的時間約在一秒左右這樣反應會比較快。

至於上面圖片中下方的3隻黃色排針接腳,中間的那根便是感應信號的輸出腳,當模組沒有感應到移動的物體時,輸出位準是在低態(Low),如果有偵測到移動的物體便會轉為高態(High)輸出並會持續輸出一段時間此時間是可依使用者需求去調整的。

為了方便測試我們的電路中使用了一顆按鈕作為手動觸發之用當按鈕被按下時電源正端會透過一顆10KΩ電組連接到觸發輸入腳(GPIO16)啟動拍照的動作如果我們把這個系統應用在家裡面大門的門鈴裝置時這個按鈕就等同門鈴的按鈕當有人按門鈴時便可以同步把來訪客人的長相拍攝下來這樣如果我們剛好不在家之後也可以從儲存的相片去看有誰來拜訪過。

除此之外也可以把這個系統作一個延伸的應用例如把這個按鈕改成其他零組件像磁簧開關或微動開關等機械接點的裝置就可以構成一保全防護系統。下圖是這些零組件常見的一些外觀如果我們在家裡的門窗上裝上這些裝置並把相機鏡頭對準它們當門窗被非法打開時就可以把入侵者的容貌拍攝下來做為證物了。

門窗用磁簧開關

微動開關一


微動開關二


在ESP32-CAM模組上有一顆內建的LED也就是下圖標示為LED1的那顆它是接在ESP32的GPIO33上由於HC-SR501這塊人體紅外線移動感測器動作比較慢不管被觸發或恢復原狀都需要一點時間而且ESP32-CAM模組每次在拍完照要將資料處存在SD卡上時也要花一點時間為了讓使用者知道目前系統是否已經可以開始動作在我們的系統中會使用這顆LED做為系統已經準備好可以開始拍照的指示燈


程式列表與說明

以下便是本範例所使用的完整程式表

  1. #include "esp_camera.h"
  2. #include "Arduino.h"
  3. #include "FS.h"                // SD Card ESP32
  4. #include "SD_MMC.h"            // SD Card ESP32
  5. #include "soc/soc.h"           // Disable brownour problems
  6. #include "soc/rtc_cntl_reg.h"  // Disable brownour problems
  7. #include "driver/rtc_io.h"
  8. #include <EEPROM.h>            // read and write from flash memory
  9.  
  10. // define the number of bytes you want to access
  11. #define EEPROM_SIZE 1
  12.  
  13. // Pin definition for CAMERA_MODEL_AI_THINKER
  14. #define PWDN_GPIO_NUM     32
  15. #define RESET_GPIO_NUM    -1
  16. #define XCLK_GPIO_NUM      0
  17. #define SIOD_GPIO_NUM     26
  18. #define SIOC_GPIO_NUM     27
  19.  
  20. #define Y9_GPIO_NUM       35
  21. #define Y8_GPIO_NUM       34
  22. #define Y7_GPIO_NUM       39
  23. #define Y6_GPIO_NUM       36
  24. #define Y5_GPIO_NUM       21
  25. #define Y4_GPIO_NUM       19
  26. #define Y3_GPIO_NUM       18
  27. #define Y2_GPIO_NUM        5
  28. #define VSYNC_GPIO_NUM    25
  29. #define HREF_GPIO_NUM     23
  30. #define PCLK_GPIO_NUM     22
  31.  
  32. unsigned int pictureNumber = 1;
  33. const byte shootPin = 16, picLED=33;
  34.  
  35. void setup() {
  36.   WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); 
  37.   pinMode(shootPin,INPUT_PULLUP);
  38.   
  39.   Serial.begin(115200);
  40.   //Serial.setDebugOutput(true);
  41.   //Serial.println();
  42.   
  43.   camera_config_t config;
  44.   config.ledc_channel = LEDC_CHANNEL_0;
  45.   config.ledc_timer = LEDC_TIMER_0;
  46.   config.pin_d0 = Y2_GPIO_NUM;
  47.   config.pin_d1 = Y3_GPIO_NUM;
  48.   config.pin_d2 = Y4_GPIO_NUM;
  49.   config.pin_d3 = Y5_GPIO_NUM;
  50.   config.pin_d4 = Y6_GPIO_NUM;
  51.   config.pin_d5 = Y7_GPIO_NUM;
  52.   config.pin_d6 = Y8_GPIO_NUM;
  53.   config.pin_d7 = Y9_GPIO_NUM;
  54.   config.pin_xclk = XCLK_GPIO_NUM;
  55.   config.pin_pclk = PCLK_GPIO_NUM;
  56.   config.pin_vsync = VSYNC_GPIO_NUM;
  57.   config.pin_href = HREF_GPIO_NUM;
  58.   config.pin_sscb_sda = SIOD_GPIO_NUM;
  59.   config.pin_sscb_scl = SIOC_GPIO_NUM;
  60.   config.pin_pwdn = PWDN_GPIO_NUM;
  61.   config.pin_reset = RESET_GPIO_NUM;
  62.   config.xclk_freq_hz = 20000000;
  63.   config.pixel_format = PIXFORMAT_JPEG; 
  64.   
  65.   if(psramFound()){
  66.     config.frame_size = FRAMESIZE_UXGA;
  67.     config.jpeg_quality = 10;
  68.     config.fb_count = 2;
  69.   } else {
  70.     config.frame_size = FRAMESIZE_SVGA;
  71.     config.jpeg_quality = 12;
  72.     config.fb_count = 1;
  73.   }
  74.   
  75.   // Init Camera
  76.   esp_err_t err = esp_camera_init(&config);
  77.   if (err != ESP_OK) {
  78.     Serial.printf("ESP32 照相機初始化失敗! 錯誤代碼為: 0x%x", err);
  79.     return;
  80.   }
  81.   
  82.   // initialize EEPROM with predefined size
  83.   EEPROM.begin(EEPROM_SIZE);
  84.  
  85.   //Serial.println("Starting SD Card");
  86.   if(!SD_MMC.begin()){
  87.     Serial.println("SD 卡掛載失敗!");
  88.     return;
  89.   }
  90.   
  91.   uint8_t cardType = SD_MMC.cardType();
  92.   if(cardType == CARD_NONE){
  93.     Serial.println("找不到 SD卡!");
  94.     return;
  95.   }
  96.     
  97.   pinMode(picLED,OUTPUT);
  98.   
  99.   Serial.println("程式開始!");
  100.   delay(1000);
  101. }
  102.  
  103. void loop() {
  104.   digitalWrite(picLED,0);
  105.   while(digitalRead(shootPin)==0) {delay(5);};
  106.   
  107. delay(100);
  108.   digitalWrite(picLED,1);
  109.   // Path where new picture will be saved in SD Card
  110.   String path = "/picture" + String(pictureNumber) +".jpg";
  111.   takeSavePhoto(path);
  112.   pictureNumber++;
  113.   while(digitalRead(shootPin)==1) { delay(5); };
  114.   
  115.   delay(1000);
  116.   Serial.println("可以開始抓下一張圖了....");
  117.   
  118. }
  119.  
  120. void takeSavePhoto(String path) {
  121.   // 使用相機抓取照片
  122.   camera_fb_t * fb=esp_camera_fb_get();
  123.   if(!fb)
  124.   {
  125.     Serial.println("相機抓取錯誤!");
  126.     return;
  127.   }
  128.   fs::FS &fs = SD_MMC;
  129.   File file=fs.open(path.c_str(), FILE_WRITE);
  130.   if(!file)
  131.     Serial.println("使用寫入模式時開啟檔案失敗!");
  132.   else  {
  133.     file.write(fb->buf,fb->len);
  134.     Serial.printf("檔案儲存至路徑 : %s\n",path.c_str());
  135.   }
  136.   file.close();
  137.   esp_camera_fb_return(fb);
  138. }

程式名稱:ESP32_CAM_SaveSD1.ino


程式開始的1~8行是本範例程式必須引用到的函式庫如果有寫過ESP32-CAM模組相關程式的朋友應該都不陌生才是在此就不多做說明

  1. #include "esp_camera.h"
  2. #include "Arduino.h"
  3. #include "FS.h"                // SD Card ESP32
  4. #include "SD_MMC.h"            // SD Card ESP32
  5. #include "soc/soc.h"           // Disable brownour problems
  6. #include "soc/rtc_cntl_reg.h"  // Disable brownour problems
  7. #include "driver/rtc_io.h"
  8. #include <EEPROM.h>            // read and write from flash memory

不過最後一個函式庫<EEPROM.h>在我們的程式中並沒有真正的用到這是因為這個範例程式主要是從https://randomnerdtutorials.com/esp32-cam-take-photo-save-microsd-card/這個網站的「ESP32-CAM Take Photo and Save to MicroSD Card」這篇文章中的程式改寫而來的在這篇文章中所抓取的照片流水編號會儲存在ESP32內部模擬的EEPROM記憶體上也就是說照片的最後的編號會被儲存起來不會因為關機或重新開機而重新開始計數這樣的好處是之前已經儲存的照片不會被覆蓋掉可是原來的程式如果抓取的照片超過256張之後也一樣會被覆蓋掉,這是因為他用的編號值只有一個byte而已,與其這樣還不如每次開機就重新來過,至少這樣可以存多一點(在此為65536張)。有興趣試做的讀者,不妨參考原來網站的程式自行修改,把儲存照片編號的資料長度改為2個byte,這樣一來就可以抓取更多數目的照片而不會被覆蓋掉。

接著的程式是在定義由安可信(AI_THINKER) 所設計的ESP32-CAM模組相機的相關參數,也就是相機鏡頭所使用到的一些I/O腳位,這些接腳在初始化(setup())部分程式會用到。

// Pin definition for CAMERA_MODEL_AI_THINKER

#define PWDN_GPIO_NUM     32

#define RESET_GPIO_NUM    -1

#define XCLK_GPIO_NUM      0

#define SIOD_GPIO_NUM     26

#define SIOC_GPIO_NUM     27

 

#define Y9_GPIO_NUM       35

#define Y8_GPIO_NUM       34

#define Y7_GPIO_NUM       39

#define Y6_GPIO_NUM       36

#define Y5_GPIO_NUM       21

#define Y4_GPIO_NUM       19

#define Y3_GPIO_NUM       18

#define Y2_GPIO_NUM        5

#define VSYNC_GPIO_NUM    25

#define HREF_GPIO_NUM     23

#define PCLK_GPIO_NUM     22


下面程式的第一行是在定義照片編號用的變數,由於在標準的Arduino中整數(int)變數的資料長度只有2個byte,而且範圍是由-32,768~+32767,為了能有更多的編號,所以要定義成無正負號的整數型態才行。至於後面的”shootPin”變數,是指定啟動拍照功能的觸發輸入腳(即GPIO16),而”picLED”則是指到ESP32-CAM模組上那顆內建的LED所使用GPIO腳的編號。

unsigned int pictureNumber = 1;

const byte shootPin = 16, picLED=33;


接著的程式是屬於初始化(setup())程式區的部分下面這一塊程式主要就是規劃及配置ESP32-CAM模組上相機鏡頭所使用的GPIO腳和所抓取相片的檔案格式(在此為jpg檔)

camera_config_t config;

  config.ledc_channel = LEDC_CHANNEL_0;

  config.ledc_timer = LEDC_TIMER_0;

  config.pin_d0 = Y2_GPIO_NUM;

  config.pin_d1 = Y3_GPIO_NUM;

  config.pin_d2 = Y4_GPIO_NUM;

  config.pin_d3 = Y5_GPIO_NUM;

  config.pin_d4 = Y6_GPIO_NUM;

  config.pin_d5 = Y7_GPIO_NUM;

  config.pin_d6 = Y8_GPIO_NUM;

  config.pin_d7 = Y9_GPIO_NUM;

  config.pin_xclk = XCLK_GPIO_NUM;

  config.pin_pclk = PCLK_GPIO_NUM;

  config.pin_vsync = VSYNC_GPIO_NUM;

  config.pin_href = HREF_GPIO_NUM;

  config.pin_sscb_sda = SIOD_GPIO_NUM;

  config.pin_sscb_scl = SIOC_GPIO_NUM;

  config.pin_pwdn = PWDN_GPIO_NUM;

  config.pin_reset = RESET_GPIO_NUM;

  config.xclk_freq_hz = 20000000;

  config.pixel_format = PIXFORMAT_JPEG; 


再來就是依照ESP32-CAM模組上是否有內建psram來決定攝影時圖像的框架與圖片的品質如果有會使用「UXGA」模式如果沒有則設定為「SVGA」模式。


  if(psramFound()){

    config.frame_size = FRAMESIZE_UXGA;

    config.jpeg_quality = 10;

    config.fb_count = 2;

  } else {

    config.frame_size = FRAMESIZE_SVGA;

    config.jpeg_quality = 12;

    config.fb_count = 1;

  }


然後程式開始初始化ESP32-CAM模組上的相機,假如初始化失敗,程式會在Arduino的監控視窗上顯示提示訊息及錯誤原因的代碼,以供使用者參考。

esp_err_t err = esp_camera_init(&config);

  if (err != ESP_OK) {

    Serial.printf("ESP32 照相機初始化失敗! 錯誤代碼為: 0x%x", err);

    return;

  }


下面這行程式會將ESP32-CAM模組上的Flash記憶體取出一部分初始化成模擬的EEPROM記憶體不過在我們這個範例程式中其實並沒有使用到EEPROM記憶體的功能所以做不做都沒差純粹是保留給以後擴充之用

  // initialize EEPROM with predefined size

  EEPROM.begin(EEPROM_SIZE);


再來就是初始化ESP32-CAM模組上內建掛載的Micro SD模組板同樣的程式會在Arduino的監控視窗上顯示是否初始化成功的提示訊息,以供使用者參考。

  //Serial.println("Starting SD Card");

  if(!SD_MMC.begin()){

    Serial.println("SD 卡掛載失敗!");

    return;

  }


如果初始化ESP32-CAM模組上內建掛載的Micro SD模組板成功接下來就是檢查有沒有插上Micro SD卡當然結果一樣會顯示在Arduino的監控視窗上

  uint8_t cardType = SD_MMC.cardType();

  if(cardType == CARD_NONE){

    Serial.println("找不到 SD卡!");

    return;

  }


初始化(setup())程式區的最後部分會在Arduino的監控視窗上顯"程式開始!"的提示訊息

    pinMode(picLED,OUTPUT);

  

  Serial.println("程式開始!");


由於本範例程式把抓取照片與儲存照片的部分寫成了一個副程式(也就是後面的【void takeSavePhoto(String path)】),所以整個主迴圈程式區(loop())就變得很精簡,下面就是屬於主迴圈程式區(loop())的部分:

程式一開始先點亮ESP32-CAM模組上內建的LED以提醒使用者可以開始使用抓取照片的功能了然後就是等待外部的觸發信號到來也就是shootPin」這隻接腳出現高態的輸入信號。

void loop() {

  digitalWrite(picLED,0); // 內建LED點亮位準為0

  while(digitalRead(shootPin)==0) {delay(5);};


接著程式會把指示用的LED熄滅,好讓使用者知道目前系統正在處理拍照與儲存的動作,無暇他顧並在合成目前的照片檔案名稱(字串變數”path”)之後,呼叫【void takeSavePhoto(String path)】)這個副程式去拍照及儲存所攝得的照片到Micro SD卡上,最後把照片的編號數(pictureNumber)加一。

  delay(100);

  digitalWrite(picLED,1);

  // Path where new picture will be saved in SD Card

  String path = "/picture" + String(pictureNumber) +".jpg";

  takeSavePhoto(path);

  pictureNumber++;


在主迴圈程式區(loop())的最後會等待觸發輸入信號位準恢復(回到Low)然後在Arduino的監控視窗上顯示"可以開始抓下一張圖了...."的提示訊息,再重新開始整個觸發🡪抓取照片🡪儲存照片至Micro SD卡的過程。

  while(digitalRead(shootPin)==1) { delay(5); };

  

  delay(1000);

  Serial.println("可以開始抓下一張圖了....");

  

}


整個【void takeSavePhoto(String path)】)副程式主要分成兩個部分首先如下面程式所示啟動相機拍攝照片的功能並將結果暫存在變數”fb”上;假如拍攝失敗程式會在Arduino的監控視窗上顯"相機抓取錯誤!"的提示訊息,並且馬上結束這個副程式

void takeSavePhoto(String path) {

  // 使用相機抓取照片

  camera_fb_t * fb=esp_camera_fb_get();

  if(!fb)

  {

    Serial.println("相機抓取錯誤!");

    return;

  }


如果相機拍攝成功程式會依外界傳來的檔案名稱(path)將資料寫入到Micro SD卡上,並依照寫入結果是否成功在Arduino的監控視窗上顯相對應的提示訊息。

  fs::FS &fs = SD_MMC;

  File file=fs.open(path.c_str(), FILE_WRITE);

  if(!file)

    Serial.println("使用寫入模式時開啟檔案失敗!");

  else  {

    file.write(fb->buf,fb->len);

    Serial.printf("檔案儲存至路徑 : %s\n",path.c_str());

  }

  file.close();

  esp_camera_fb_return(fb); // 釋放相機所使用的記憶體

}


執行結果:

下面的畫面是系統啟動時會在Arduino IDE的監控視窗上看到的內容前面的部分是ESP32-CAM模組上內建的啟動輸出訊息一直到初始化(setup())程式區的最後部分也就是在Arduino的監控視窗上顯"程式開始!"的標記1的提示訊息為止。至於標記2則是拍照功能被觸發後呼叫【void takeSavePhoto(String path)】)這個副程式執行拍照與儲存照片完成的提示訊息接著的標記3則是主迴圈程式區(loop())的最後部分亦即觸發輸入信號位準恢復(回到Low)的提示訊息



下面兩張照片是本系統裝置實際抓取到的結果由照片的畫質可以發現本次範例所使用的ESP32-CAM這個模組比較適合拍攝中遠距離的場景(第一張)太近了(第二張)對焦就會有些不清楚



沒有留言:

張貼留言

三、使用Line Notify傳送照片之安全監控系統之二---低功耗篇

在前一個章節中 , 我們建構了一個標準照片擷取 、 傳送與儲存的按全監控裝置 , 不過假如我們使用的場域中有許多的地方都必須按裝這類的裝置時 , 例如在一個有許多門 、 窗的家庭或辦公室 , 由於我們的系統使用無線WiFi作為信號傳輸之用 , 所以信號的傳輸除非裝置離WiFi分享...