2024年11月16日 星期六

MicroPython 學習筆記 : ESP32-WROVER-DEV 開發板測試 (三)

本篇主要是紀錄 camera 模組的初始化函式的改寫, 本系列之前的文章參考 :


在前兩篇的測試中所使用的初始化函式寫法其實有 bug, 原寫法如下 :

import time, camera
from machine import reset

def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_QVGA,
        **config)
    
def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf

其中 init_camera() 函式的傳入參數使用可變長度參數 **config,  原意是想如果要更改這些參數的預設值 (例如解析度預設是 QVGA 想改為 VGA), 就在呼叫 init_camera() 時傳給 **config, 例如 :

init_camera(framesize=camera.FRAME_VGA) 

但這樣會出現參數重複的錯誤 (extra keyword arguments given) : 

>>> init_camera(framesize=camera.FRAME_VGA)      
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 9, in init_camera
TypeError: extra keyword arguments given   

實踐原意的正確做法應該是要用字典的 setefault() 函式來設定傳入之關鍵字參數 :

import time, camera
from machine import reset
    
def init_camera(**config): # 初始化鏡頭
    config.setdefault('fb_location', camera.PSRAM) 
    config.setdefault('format', camera.JPEG)  
    config.setdefault('xclk_freq', camera.XCLK_10MHz)  
    config.setdefault('framesize', camera.FRAME_QVGA)   
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, **config)

def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf

其實這四個用 setdefault() 設定初始值的參數通常只有 xclk_freq 與 framesize 會需要設定.

測試結果如下 :

不傳入參數的話預設解析度是 320*240 : 

>>> init_camera()      
0
4
10000000
5
True
>>> capture_image()      
8872
Image has been saved as capture.jpg
True

可見呼叫 dict 的 setdefault() 時會傳回設定值. 




如果傳入 framesize 參數指定 VGA, 則解析度變成 640*480 :

>>> init_camera(framesize=camera.FRAME_VGA)   
0
4
10000000
8
True
>>> capture_image()    
cam_hal: EV-EOF-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
21140
Image has been saved as capture.jpg
True



下面範例是同時設定 framesize=VGA 與 xclk_freq=20mM 參數 :

>>> init_camera(framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_20MHz)   
0
4
20000000
8
True
>>> capture_image()    
cam_hal: EV-EOF-OVF
16460   
Image has been saved as capture.jpg
True




結果頻率高檔案大小比較小, 這可能是因為時脈越高, 影像數據擷取速率快, 引入較多信號噪音, 提高了演算法的壓縮比所致. 

MicroPython 學習筆記 : ESP32-WROVER-DEV 開發板測試 (二)

本篇繼續來測試 ESP32-WROVER-DEV 開發板的影像擷取, 本系列前一篇測試參考 :


使用的程式碼如下 : 

import time, camera
from machine import reset

def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_QVGA,
        **config)
    
def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf

先抓一張預設未調整影像 : 

>>> init_camera()   
True
>>> capture_image(file_name='capture1.jpg')     
14487
Image has been saved as capture1.jpg
True




接下來測試 camera 模組的各個函式. 


1. 亮度 (brightness) : 

預設的圖片亮度似乎有點暗, 呼叫 brightness(2) 設到最亮 : 

>>> init_camera()   
True
>>> camera.brightness(2)   
>>> capture_image(file_name='capture2.jpg')  
9985
Image has been saved as capture2.jpg
True




感覺反而比預設圖暗 (也可能是室內燈光關係). 下面是設成最暗 :

>>> init_camera()    
True
>>> camera.brightness(-2)   
>>> capture_image(file_name='capture3.jpg')   
9388
Image has been saved as capture3.jpg 
True




有比預設圖暗.  


2. 對比度 (contrast) : 

接著用 camera.contrast(2) 將對比度調到最大 : 

>>> init_camera()    
True
>>> camera.contrast(2)     
>>> capture_image(file_name='capture4.jpg')    
9422
Image has been saved as capture4.jpg
True




下面是對比最弱 :

>>> init_camera()    
True
>>> camera.contrast(-2)     
>>> capture_image(file_name='capture5.jpg')  
9417
Image has been saved as capture5.jpg
True




跟預設圖似乎差不多, 看不出明顯的對比變化. 


3. 飽和度 (saturation) : 

參數範圍 [-2, 2] 預設為 0, 負值偏灰, 正值偏彩. 下面是設定飽和度=2 : 

>>> init_camera()   
True
>>> camera.saturation(2)   
>>> capture_image(file_name='capture6.jpg')    
9402
Image has been saved as capture6.jpg 
True




下面測試飽和度 -2 :

>>> init_camera()       
True
>>> camera.saturation(-2)    
>>> capture_image(file_name='capture7.jpg')     
9339
Image has been saved as capture7.jpg
True




看不出有差異. 


4. 對比度 (contrast) : 

camera.contrast() 參數範圍 [-2, 2] 預設為 0, 設為 2 對比最高, -2 最低. 

>>> init_camera()    
True
>>> camera.contrast(2)       
>>> capture_image(file_name='capture8.jpg')    
9360
Image has been saved as capture8.jpg  
True




下面是對比度 -2 效果 :

>>> init_camera()   
True
>>> camera.contrast(-2)   
>>> capture_image(file_name='capture9.jpg')   
9233
Image has been saved as capture9.jpg
True




看不出有何變化. 


5. 特效 (speffect) : 

camera 有六種特效, 但測試發現並無明顯效果, 下面是負片測試 :

>>> init_camera()    
True
>>> camera.speffect(camera.EFFECT_NEG)    
>>> capture_image(file_name='capture10.jpg')     
9314
Image has been saved as capture10.jpg
True




下面黑白片測試無效 :

>>> init_camera()  
True
>>> camera.speffect(camera.EFFECT_BW)   
>>> capture_image(file_name='capture11.jpg')    
8717
Image has been saved as capture11.jpg
True




下面是紅色濾鏡效果 :

>>> init_camera()    
True
>>> camera.speffect(camera.EFFECT_RED)   
>>> capture_image(file_name='capture12.jpg')   
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
8751
Image has been saved as capture12.jpg
True




綠色濾鏡特效 :

>>> init_camera()   
True
>>> camera.speffect(camera.EFFECT_GREEN)    
>>> capture_image(file_name='capture13.jpg')    
8719
Image has been saved as capture13.jpg
True

藍色濾鏡效果 : 

>>> init_camera()    
True
>>> camera.speffect(camera.EFFECT_BLUE)    
>>> capture_image(file_name='capture14.jpg')    
8650
Image has been saved as capture14.jpg
True




>>> init_camera()     
True
>>> camera.speffect(camera.EFFECT_RETRO)     
>>> capture_image(file_name='capture15.jpg')    
8793
Image has been saved as capture15.jpg
True




這些特效似乎與預設圖沒啥差異. 


6. 垂直翻轉 (flip) : 

呼叫 flip() 並傳入 1 或 True 可讓擷取之影像垂直翻轉, 但並無效果 :

>>> init_camera()     
True
>>> camera.flip(True)    
>>> capture_image(file_name='capture16.jpg')    
8617
Image has been saved as capture16.jpg
True




7. 水平翻轉 (mirror) : 

呼叫 mirror() 並傳入 1 或 True 可讓擷取之影像水平翻轉, 但測試無效果 :

>>> init_camera()    
True
>>> camera.mirror(True)   
>>> capture_image(file_name='capture17.jpg')    
8497
Image has been saved as capture17.jpg
True



8. 畫質 (quality) : 

呼叫 quality() 設定影像品質, 參數範圍 [10, 63] 預設為 12, 值越大品質越高, 但測試無效果.

>>> init_camera()     
True
>>> camera.quality(30)       
>>> capture_image(file_name='capture18.jpg')     
cam_hal: EV-EOF-OVF
8545
Image has been saved as capture18.jpg
True




圖檔大小仍是一般 8KB, 解析度仍是 320*240, 畫質其實沒有變化. 

以上測試結果顯示, camera 模組雖然可以正常擷取影像, 但它的許多函式目前可能只是一個空殼的介面並未實作, 所以並無效果. 

2024年11月15日 星期五

MicroPython 學習筆記 : ESP32-WROVER-DEV 開發板測試 (一)

我十月底於露天買了一塊類似 ESP32-CAM 的開發板 ESP32-WROVER-DEV, 參考 : 


此開發板上有一個 OV2640 攝像頭插槽, 將上面的黑色長條往上撥變成垂直會露出底下的插槽 : 




將隨附的 OV2640 攝像頭線排放入插槽內 (要塞到底) : 




將黑色長條桿往下壓回, 扣住攝像頭的線排即完成攝像頭安裝 :




剪一小片雙面膠將鏡頭固定在金屬殼上以免晃動. 

接下來是燒錄韌體, 但不是下載 MicroPython 官網的 ESP32 韌體, 而是參考下面 Yesa 這篇踩坑文章所使用的韌體 :


韌體下載網址如下 :


我有複製一份放在我的 GitHub :



1. 燒錄韌體 :

韌體燒錄程序參考下面這篇 :


檢視 Flash 記憶體 :

D:\ESP32>esptool --port COM6 flash_id    
esptool.py v4.6.2
Serial port COM6
Connecting....
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting....
Detecting chip type... ESP32
Chip is ESP32-D0WDQ6 (revision v1.0)
Features: WiFi, BT, Dual Core, VRef calibration in efuse, BLK3 partially reserved, Coding Scheme 3/4
Crystal is 40MHz
MAC: 30:ae:a4:62:cc:c4
Uploading stub...
Running stub...
Stub running...
Manufacturer: c8
Device: 6016
Detected flash size: 4MB
Hard resetting via RTS pin...

可見此板有 4MB Flash 記憶體. 在燒錄之前先將 Flash 舊內容抹除 :

D:\ESP32>esptool --chip esp32 --port COM6 erase_flash   
esptool.py v4.6.2
Serial port COM6
Connecting....
Chip is ESP32-D0WDQ6 (revision v1.0)
Features: WiFi, BT, Dual Core, VRef calibration in efuse, BLK3 partially reserved, Coding Scheme 3/4
Crystal is 40MHz
MAC: 30:ae:a4:62:cc:c4
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 7.5s
Hard resetting via RTS pin...

然後燒錄韌體 : 

D:\ESP32>esptool --chip esp32 --port COM6 write_flash -z 0x1000 micropython_v1.21.0_camera_no_ble.bin     
esptool.py v4.6.2
Serial port COM6
Connecting....
Chip is ESP32-D0WDQ6 (revision v1.0)
Features: WiFi, BT, Dual Core, VRef calibration in efuse, BLK3 partially reserved, Coding Scheme 3/4
Crystal is 40MHz
MAC: 30:ae:a4:62:cc:c4
Uploading stub...
Running stub...
Stub running...
Configuring flash size...
Flash will be erased from 0x00001000 to 0x0015efff...
Compressed 1433072 bytes to 926248...
Wrote 1433072 bytes (926248 compressed) at 0x00001000 in 81.5 seconds (effective 140.7 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...


2. 檢視記憶體 :

按 Reset 鈕硬啟動可知此韌體是在 v1.21 版原始碼基礎上編譯來的 : 

MicroPython v1.21.0 on 2023-10-13; ESP32 module with Camera with ESP32
Type "help()" for more information.

先用 gc.mem_free() 檢視可用的 heap 記憶體容量 (包含內部 SRAM 與外部 PSRAM 總和) :

>>> import gc  
>>> gc.mem_free()   
4116880

有超過 4MB 的 heap 記憶體可用, 推測此開發板採用 ESP32-WROVER N4R4 模組. 

micropython.mem_info() 提供更詳細的記憶體分配 (區塊與堆疊) 與垃圾回收資訊 : 

>>> import micropython   
>>> micropython.mem_info()     
stack: 736 out of 15360
GC: total: 64000, used: 10800, free: 53200, max new split: 4063232  
 No. of 1-blocks: 236, 2-blocks: 31, max blk sz: 32, max free sz: 3313

其中 GC 的可用記憶體 53200 表示 SRAM 中剩餘的 heap 記憶體容量, 而 max new split 則顯示 PSRAM 有 4MB 可用. 


2. 檢視 camera 的函式與其參數 :

此韌體的主角是內建的 camera 模組, 匯入後用 dir() 檢視其內容 :

>>> import camera   
>>> dir(camera)   
['__class__', '__name__', '__dict__', 'DRAM', 'EFFECT_BLUE', 'EFFECT_BW', 'EFFECT_GREEN', 'EFFECT_NEG', 'EFFECT_NONE', 'EFFECT_RED', 'EFFECT_RETRO', 'FRAME_240X240', 'FRAME_96X96', 'FRAME_CIF', 'FRAME_FHD', 'FRAME_HD', 'FRAME_HQVGA', 'FRAME_HVGA', 'FRAME_P_3MP', 'FRAME_P_FHD', 'FRAME_P_HD', 'FRAME_QCIF', 'FRAME_QHD', 'FRAME_QQVGA', 'FRAME_QSXGA', 'FRAME_QVGA', 'FRAME_QXGA', 'FRAME_SVGA', 'FRAME_SXGA', 'FRAME_UXGA', 'FRAME_VGA', 'FRAME_WQXGA', 'FRAME_XGA', 'GRAYSCALE', 'JPEG', 'PSRAM', 'RGB565', 'WB_CLOUDY', 'WB_HOME', 'WB_NONE', 'WB_OFFICE', 'WB_SUNNY', 'XCLK_10MHz', 'XCLK_20MHz', 'YUV422', 'brightness', 'capture', 'contrast', 'deinit', 'flip', 'framesize', 'init', 'mirror', 'quality', 'saturation', 'speffect', 'whitebalance']

其中大寫的是呼叫設定函式時會用到的常數, 小寫的則是函式, 摘要如下表 :


 camera 的函式 說明
 init() 傳入接腳, 時脈頻率, 影像格式與解析度等參數以初始化攝像頭, 成功傳回 True
 deinit() 釋放攝像頭所佔資源
 framesize() 攝硬視訊框尺寸, 例如 FRAME_QVGA (320*240), FRAME_VGA (640x480)等
 flip() 設定是否要垂直翻轉影像 (True/False)
 mirror() 設定是否要水平翻轉影像 (True/False)
 speffect() 設定特效例如負片, 濾鏡等 
 whitebalance() 設定白平衡
 saturation() 設定色彩飽和度 , 參數範圍 [-2, 2] 預設為 0, 負值偏灰, 正值偏彩
 brightness() 設定亮度, 參數範圍 [-2, 2] 預設為 0, 設為 2 最亮, -2 最暗
 contrast() 設定對比度, 參數範圍 [-2, 2] 預設為 0, 設為 2 對比最高, -2 最低
 quality() 設定影像品質, 參數範圍 [10, 63] 預設為 12, 值越大品質越高
 capture() 擷取攝像頭影像並傳回 bytes 資料 (無傳入參數)


我將上面的兩個參考資料網址貼給 ChatGPT, 然後詢問它這些函式有哪些參數, 其意義與用法如何? 結果整理如下 : 

init() 函式用來初始化鏡頭, 成功會傳回 True, 否則 False, 其傳入參數較多, 有些有預設值如果接受不用刻意傳入, 但有些參數無預設值則必須在呼叫時傳入 (例如 d0~d7, xclk, pclk, sioc, siod, vsync, href 等 GPIO 接腳編號), 否則初始化不會成功, 說明如下 :
  • sensor_id : 攝像頭 id 編號, 一般開發板只有一個鏡頭, 預設是 0. 
  • reset : 重設腳編號, 大部分攝像頭初始化不需要重設, 傳入 -1 即可.
  • pwdn : 省電模式接腳編號, 此腳設為 High 攝像頭進入省電模式, 不使用傳入 -1 即可.
  • xclk : 主時脈 (20MHz) 接腳編號.
  • sioc 與 siod : 攝像頭的 I2C 通訊接腳編號, siod 為資料線 (SDA), sioc 為時脈線 (SCLK).
  • d0~d7 : 將影像傳給 ESP32 的資料接腳編號.
  • vsync : 影像垂直同步信號接腳編號.
  • href : 影像水平同步參考信號接腳編號.
  • pclk : 像素時鐘接腳編號, 此腳有變化時 ESP32 才會讀取像素資料. 
  • fb_location :  設定影像緩衝器的儲存位置, 可選 PSRAM (預設)/DRAM. 
  • format : 圖像輸出格式, 可選 JPEG (預設)/RGB565/GRAYSCALE. 
  • jpeg_quality : JPEG 品質, 值範圍 1~63 (預設 10), 值越小品質越高. 
  • xclk_freq : 主時脈頻率, 可選 XCLK_10MHz/XCLK_20MHz. 
其中 format 參數用來設定影像輸出格式, 有三個選項 :
  • JPEG : 有損壓縮但體積小, 適合無須再處理要直接上傳到伺服器場合, 值為 4 (預設)
  • RGB565 : 未壓縮的原始峨格式, 適合需要再處理場合, 值為 0 
  • GRAYSCALE : 以灰階格式儲存檔案小, 無彩色, 值為 3
fb_location 參數用來設定影像緩衝器的儲存位置, 有兩個選項 :
  • PSRAM : 即 SPI RAM, 容量較大 (WROVER 有 4M/8M), 值為 0  (預設)
  • DRAM : 容量有限 (ESP32 為 512KB SRAM), 解析度要小才行, 值為 1
注意, 除了 sensor_id, reset, pwdn, fb_location, format, jpeg_quality 這六個有預設值的參數外, 其於參數都必須刻意傳入, 否則初始化會失敗. 

其它函式的參數都只有一個, 說明如下 : 

framesize() 函式可傳入如下值來設定擷取的影像解析度 :
  • FRAME_QQVGA (160x120) : 值為 1
  • FRAME_QVGA (320x240) : 值為 5 (預設)
  • FRAME_VGA (640x480) : 值為 8
  • FRAME_XGA (1024x768) : 值為 10
  • FRAME_UXGA (1600x1200) : 值為 13
speffect() 可傳入下列值設定特效 :
  • EFFECT_NONE (無特效) : 值為 0
  • EFFECT_NEG (負片效果) : 值為 1
  • EFFECT_BW (黑白效果) : 值為 2
  • EFFECT_RED (紅色濾鏡) : 值為 3
  • EFFECT_GREEN (綠色濾鏡) : 值為 4
  • EFFECT_BLUE (藍色濾鏡) :值為 5
  • EFFECT_RETRO (復古效果) : 值為 6
whitelance() 可傳入下列值設定影像的白平衡模式 :
  • WB_NONE (自動白平衡) : 值為 0
  • WB_SUNNY (晴天) : 值為 1
  • WB_CLOUDY (陰天) : 值為 2
  • WB_OFFICE (辦公室燈光) : 值為 3
  • WB_HOME (家中燈光) : 值為 4
參考 :


3. 擷取影像 :

使用 camera 模組擷取影像之前須先呼叫 init() 函式並傳入無預設值的參數 (例如  d0~d7 等 GPIO 接腳編號) 來初始化攝像頭, 這樣才會初始化成功. 例如只傳入 format 與 fb_location 這兩個參數初始化會失敗 (傳入的都是預設值) :

>>> camera.init(0, format=camera.JPEG, fb_location=camera.PSRAM)  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: Camera Init Failed

傳入參數不正確也無法初始化成功, 例如 :

>>> camera.init(0, d0=32, d1=35, d2=34, d3=5, d4=39, d5=18, d6=36, d7=19,
            format=camera.JPEG, framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_10MHz,
            href=26, vsync=25, reset=15, sioc=23, siod=22, xclk=27, pclk=21, fb_location=camera.PSRAM)    
Traceback (most recent call last):
  File "<stdin>", line 3, in <module>
OSError: Camera Init Failed

使用 Yesa 的參數可成功初始化傳回 True :

>>> camera.init(0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
            format=camera.JPEG, framesize=camera.FRAME_VGA, xclk_freq=camera.XCLK_10MHz,
            href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21, pclk=22, fb_location=camera.PSRAM)   
True   

傳回 True 表示已成功初始化鏡頭, 接下來只要呼叫 camera.capture() 函式就可以擷取鏡頭的影像, 它會傳回一個 bytes 型態的影像資料 :

>>> buf=camera.capture()   
>>> type(buf)    
<class 'bytes'>   

內容節錄如下 :

>>> buf   
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00\x00\x00\x00\x00\x00\xff\xdb\x00C\x00\x0c\x08\t\x0b\t\x08\x0c\x0b\n\x0b\x0e\r\x0c\x0e\x12\x1e\x14\x12\x11\x11\x12%\x1a\x1c\x16\x1e,&.-+&*)06E;03A4)*<R=AGJMNM/:U[TKZELMJ\xff\xdb\x00C\x01\r\x0e\x0e\x12\x10\x12#\x14\x14#J2*2JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ\xff\xc4\x00 .....

用 with open 語法將此影像的 bytes 資料寫入檔案 : 

>>> with open('/capture.jpg', 'wb') as f:    
    f.write(buf)   
    
14631

這樣影像就存入開發板根目錄下了, 但是 Thonny 左下方的開發板檔案總管不會即時更新檔案列表, 因此看不到此圖檔名稱, 按右鍵也沒有 refresh 選項可手動刷新檔案列表, 解決辦法是從左上方本機檔案總管中隨便上傳一個檔案 (例如 config.py), 或者刪除左下角開發板上不要的檔案亦可 (例如前次擷取的圖檔), 這樣就會刷新左下方開發板的檔案列表了 :




點選該圖檔按滑鼠右鍵 "點選下載到 xxx" 即可在本機瀏覽擷取的圖檔了 : 




可見確實是 640*480 解析度的 VGA 圖檔. 

擷取影像後要呼叫 deinit() 釋放此次擷取所佔用的記憶體等資源, 才能進行下一次擷取 :

>>> camera.deinit()    
True

傳回 True 表示釋放資源成功. 注意, 如果沒有呼叫 deinit() 釋放資源而繼續呼叫 capture(), 則 buf 裡面的影像資料不會被更新, 存檔後還是之前的舊影像> 

初始化參數也可以先存在字典中, 然後用 ** 關鍵字引數傳入 init(), 例如 : 

>>> camera_config={
    'xclk': 21,
    'siod': 26,
    'sioc': 27,
    'd7': 35,
    'd6': 34,
    'd5': 39,
    'd4': 36,
    'd3': 19,
    'd2': 18,
    'd1': 5,
    'd0': 4,
    'vsync': 25,
    'href': 23,
    'pclk': 22
    }
>>> camera.init(0, **camera_config)   
True

挪動鏡頭擷取新影像 :

>>> buf=camera.capture()   
>>> with open("/picture2.jpg", "wb") as f:
    f.write(buf)
    
13012
>>> camera.deinit()  
True

結果如下 :




總之, 擷取影像就這四步驟 :

1. camera.init(**config) 初始化
2. buf=camera.capture() 擷取
3. with open("/picture.jpg", "wb") as f:  # 存檔
        f.write(buf)
4. camera.deinit()

我將原作的函式修改如下 : 

import time, camera
from machine import reset

def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_QVGA,
        **config)
    
def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf

測試結果如下 :

>>> import time, camera   
>>> from machine import reset    
>>> def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_VGA,
        **config)
>>> def capture_image(file_name='capture.jpg'): # 拍攝照片並存檔 
    time.sleep(2)    # 等待攝像頭穩定
    buf=camera.capture()
    if buf:
        with open(f'/{file_name}', 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    camera.deinit()
    del buf
>>> init_camera()     
True
>>> capture_image(file_name='capture.jpg')      
7694
Image has been saved as capture.jpg
True

但是再次拍照時 camera.capture() 卻傳回 "cam_hal: EV-EOF-OVF" 訊息 : 

>>> init_camera()      
True
>>> capture_image(file_name='capture1.jpg')      
cam_hal: EV-EOF-OVF   
16796
Image has been saved as capture1.jpg 
True
>>> init_camera()   
True

繼續拍照還出現 "am_hal: EV-VSYNC-OVF" 訊息 : 

>>> capture_image(file_name='capture2.jpg')     
cam_hal: EV-EOF-OVF
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
16211
Image has been saved as capture2.jpg
True

雖然似乎也不影響影響擷取, 但我還是拿去問 ChatGPT, 它指出 EV-EOF-OVF 與影像緩衝區的溢位有關 (OVF=OVer Flow), 原因是影像解析度較高或影像頻率過高, 導致緩衝區無法及時處理數據而造成數據溢位. EV-VSYNC-OVF 則是在標記新影像幀開始的垂直同步 (VSYNC) 事件時發生溢位, 原因與影像的幀速率, 緩衝空間不足或時鐘設置不匹配有關.

我已經將主時脈頻率設為較穩定較低頻的 10MHz, 剩下只有 framesize 可調, 於是在初始化後先呼叫 camera.framesize() 將影像解析度設為 QVGA (320*240) : 

>>> init_camera()      
True   
>>> camera.framesize(camera.FRAME_QVGA)      
>>> capture_image(file_name='capture4.jpg')      
25725
Image has been saved as capture4.jpg
True

果然這樣就沒出現溢位訊息了, 所以最好將 init_camera() 函式中的 framesize 參數改成 camera.FRAME_QVGA, 這樣就不需要呼叫 framesize() 函式了 : 

def init_camera(**config): # 初始化鏡頭
    camera.init(
        0, d0=4, d1=5, d2=18, d3=19, d4=36, d5=39, d6=34, d7=35,
        href=23, vsync=25, reset=-1, sioc=27, siod=26, xclk=21,
        pclk=22, fb_location=camera.PSRAM, format=camera.JPEG, 
        xclk_freq=camera.XCLK_10MHz, framesize=camera.FRAME_QVGA,
        **config)

不過, 經過多次測試發現, 即使改為 QVGA, 兩三次之後還是會出現溢位現象. 既然不影響影像擷取就不管它了.  

2024年11月13日 星期三

市圖還書 1 本 : 88張圖看懂技術分析

今天下班順路去河堤還下列這本 (被預約) :
我最近都在玩 AI 與 MicroPython, 沒時間看投資理財的書, 還了也好. 

露天購買 ESP32-S3 開發板 N16R8 (已焊) x 3

昨天解決 ESP32-S3 RGB LED 問題後, 覺得這塊開發板有 SPI RAM 真不錯, N16R8 有 16MB Flash 與 8MB SPI RAM 容量超大, 我找到下面這賣家一片才 223 元就買了三片 (之前一片買了 399 元) :





全家取貨免運 669 元, 比之前買兩片還便宜 (N16R8 與 N8R8 價格一樣當然是買 N16R8).

2024年11月12日 星期二

MicroPython 學習筆記 : ESP32-S3 開發板的 RGB LED 測試

我在九月底買了兩片 N16R8 (16MB Flash + 8MB 的 SPI RAM) 的 ESP32-S3 開發板, 內建 SRAM 為 512KB, 板上有一顆全彩 RGB LED (WS2812), 根據官網文件此 LED 在 v1.0 板子是接在 GPO48; 在 v1.1 板子則是接在 GPIO38 :  


我的板子前後都沒看到版本訊息, 應該是舊版的 v1.0, 但我用下列程式去測試, 發現不論是用 GPIO48 還是 GPO38 都不會點亮此全彩 RGB LED :

from machine import Pin
from time import sleep
from neopixel import NeoPixel
pin=Pin(48, Pin.OUT)    # Pin 38 for v1.1, Pin 48 for v1.0
np=NeoPixel(pin, 1)    # 1 for only one LED      
while (True):
    np[0]=(255,0,0)
    np.write()
    sleep(1)
    np[0]=(0,255,0)
    np.write()
    sleep(1)
    np[0]=(0,0,255)
    np.write()
    sleep(1)

搜尋了很多資料也問過 ChatGPT 都無法解決只好擱著ㄝ, 直到今天在下面這篇文章裡找到了線索, 原來是要用烙鐵將全彩 LED 旁有 "RGB" 字樣的焊點連接起來, LED 電路才會真正接到 GPIO8 或 GPIO36 上 :





按 here 會連到 Arduino 論壇上的文章 :





我檢查板子果然那兩個接點是分離的 :




焊好後是這樣 :




然後重新執行上面的程式就會看到 LED 持續閃爍紅黃綠三色了 : 




哈哈, 終於解決困擾我一個多月的問題了. 

參考 : 


好站 : 台大演講網-智慧農業

今天找到台大演講網兩年前舉辦的一場農業物聯網演講錄影 (講者為陽明交大陳文亮教授) :





農業是物聯網應用很廣的場域, 例如農作物生長監控, 病蟲害防治, 以及防盜等等. 

2024年11月11日 星期一

momo 買書四本 (LLM) + 磁石指壓板 + 機械堂小電鑽 (店+)

今天 1111 購物節我又上 momo 買了下面五項商品, 包含四本書 (79 折滿額再 84 折, 約 66 折), 特價的磁石指壓板, 以及 momo 店家+ 機械堂的特價小電鑽 : 


其中小電鑽是 momo 店+ 由廠商直接遞送.







總價 $3199, 優惠後應付 $2254 (約 7 折), 使用 200 元 momo 幣實付 2054 元 (約 64 折). 符合兩項 momo 幣登記抽 (今晚 00:00 開始) :




露天購買無線小電鑽 (已取消)

最近打算製作鋰電池組卻找不到很久以前買的小電鑽, 所以趁購物節有優惠上露天買過新的無線款 (USB 充電) :






全家取貨免運 479 元.

其他賣家 : 





PS : 因在 momo 店家找到更好的賣家機械堂, 所以跟賣家協議取消交易. 

LG Gram 鍵盤敲不出的鍵統計

這個周末我的 LG Gram 筆電右有一些鍵按不出來, 統計如下 :

W
R
U
I
O
[]
;
Home

買到機皇真的很傷腦筋, 它也不是一直會這樣 (Home 鍵除外, 它已壞好久), 時好時壞. 最近要問展碁是否有料可換. 

2024-11-11 補充 :

下午打電話給展碁的莊工程師 07-3352116), 說目前無黑色鍵盤須調料, 等料到在通知送修. 

2024年11月10日 星期日

momo 買書兩本 : Python & MicroPython

下面這兩本書放在 momo 購物車有一段時間了, 趁購物節打折買了 :





打 66 折後應付 880 元, 但折抵 momo 幣 187元, 實付 701 元 (約 52 折). 

本來還想買下列這本, 但一查母校圖書館有就用借的了 :


2024 年第 45 周記事 (安金添罐日課事宜)

好快, 離年底只剩 7 周了, 年底內訓課程較少, 最近都自學較多. 以前課程多時都是聽, 也沒時間複習, 趁年底空檔把錄影拿出來好好回顧. 最近接到菁菁國中時的理化家教 Aron 同學賴訊息, 說已轉職來我司報到, 除了叮嚀了一些資安上須注意事項, 也期勉他有空學一學 Pandas, 對工作會有很大助益. 

鄉下大門口的七里香本周開花了, 整個左側門都掛滿花絮非常好看 : 





上週颱風前一天接到大樹資源回收場謝老闆電話, 說之前預定的水桶已到貨, 約好本周五回鄉下途中去取貨, 但因為打算週五一下班就回鄉下, 順路去溫老師家拿阿蘭添罐的日課, 所以週四下班後提前去取. 因從未去過大樹久堂村, 我照導航規劃路線, 走本館路->澄清路->正修->大埤路->長庚->鳥松->神農路-美山路->大同路-北平路->中華路->久堂路, 下班時間較塞, 比預定時間 18:00 晚了快半個小時才到達, 原來是 "運強資源回收" : 




原以為我的 QRV 裝得下兩小桶三大桶叫老闆幫我預留, 結果只能載走兩小桶一大桶, 每個一百元, 然後就原路折返, 來回花了 1.5 小時. 下周五要回鄉下前再打電話問看看我沒載完的兩大桶還在不在 (購買者眾可能無法再留), 不過新的一批到貨會通知我. 好消息是 12 月起回收場周六會上班, 我覺得白天開那邊較好找路, 因我載完應該就不往回走高速, 而是走一般道路回鄉下較順路. 

上週六去溫老師家請她幫阿蘭添罐排日課, 週四通知已排好, 那天晚上突然發現我拿錯生辰了, 那不就要重排? 週六早上去取日課時向溫老師說明這個烏龍, 她說同年同月沒有差, 所以不需要重排, 日期訂在明年 1/15 日 (星期三, 農曆甲辰年 12 月 16 辰時), 因時辰甚早, 故六點過就要先去觀音廟請, 七點前到達墓園. 交代要準備的東西甚多, 我特別錄音備忘整理如下 : 
  1. 拜地藏王菩薩需準備三樣水果一份, 壽金一千 (約手掌高度), 香一把. 先燒香向菩薩稟告須將編號幾號某某移回墓園安金, 接著燒壽金, 不須擲茭, 將骨灰罈直接載到墓園. 
  2. 及早與泥做劉師傅聯絡預約當日時間, 請其準備 : 砂土 3 大包, 紅磚 12 塊與照明設備 (前一周與前一天電話提醒). 
  3. 安金需準備物品 :
    • 香爐灰
    • 金香炮竹防風燭四對
    • 五福紙 15 張 (僅福字無神明頭像者)
    • 神茶一罐
    • 神茶杯 6 只
    • 酒杯 20 只
    • 米酒 3 罐
    • 香菸檳榔各一包
    • 蒜仔芹菜各兩條 (用紅紙圈起來)
    • 鮮花一對
    • 鳳梨兩粒 (有掛尾的)
    • 水果 3~4 種四付
    • 新丁粄 12 個 (視親友多少再加)
    • 紅龜粄 3 包
    • 發粄 3 包
    • 五牲三付 (每付含豬, 雄雞, 魚罐頭, 麵, 鴨蛋五粒)
  4. 前幾天要先整理墓園, 當日要記得帶鑰匙. 
  5. 須先知會觀音廟管理人開寶塔門與協助 (前一周與前一天電話提醒). 
墓園其實我從十月就開始整理好了, 現在只要每兩周過去巡一巡即可. 

2024年11月9日 星期六

安裝浴室門口太陽能 LED 感應燈

今年六月中在露天買了五組太陽能 LED 感應燈, 但帶回鄉下後一直沒時間安裝, 今天下午到屋後觀察地形, 覺得先安裝浴室門口的較容易, 於是拿了梯子與電鑽等工具動手安裝, 小太陽能板安裝在浴室後面的牆上, 然後在後門的鋁門上鑽個洞穿過去 :





LED 感應燈裝在浴室門口的鋁門窗牆上, 因不好鑽洞, 我直接在固定板背後塗上快乾劑, 用黏的 其實也非常牢固. 

接電後要按 LED 燈面板右下方的按鈕一下進入感應模式, 這時 LED 會全亮, 如果沒有偵測到移動, 約 20 秒後燈會熄滅, 否則會持續點亮. 實測感應距離約 5 米左右. 


2024年11月8日 星期五

Python 學習筆記 : Google 搜尋爬蟲套件 googlesearch-python

今天在 "ChatGPT 4 開發手冊" 這本書上讀到 googlesearch-python 這個谷歌搜尋引擎爬蟲套件的介紹, 主要用在應用程式詢問大語言模型的主題超出其時空限制時, 可利用此套件去查詢谷歌取得最新資料. 

首先用 pip 安裝 googlesearch-python 套件 :

pip install googlesearch-python    

在 Colab 安裝前面要加驚嘆號 :

!pip install googlesearch-python   

D:\>pip install googlesearch-python    
Collecting googlesearch-python
  Downloading googlesearch_python-1.2.5-py3-none-any.whl.metadata (2.9 kB)
Requirement already satisfied: beautifulsoup4>=4.9 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from googlesearch-python) (4.12.3)
Requirement already satisfied: requests>=2.20 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from googlesearch-python) (2.31.0)
Requirement already satisfied: soupsieve>1.2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from beautifulsoup4>=4.9->googlesearch-python) (2.4.1)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.20->googlesearch-python) (3.2.0)
Requirement already satisfied: idna<4,>=2.5 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.20->googlesearch-python) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from requests>=2.20->googlesearch-python) (1.26.19)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests>=2.20->googlesearch-python) (2023.7.22)
Downloading googlesearch_python-1.2.5-py3-none-any.whl (4.8 kB)
Installing collected packages: googlesearch-python
Successfully installed googlesearch-python-1.2.5  

此處安裝的是 1.2.5 版, 書上說 1.2.3 版有個小 bug, 爬回來的資料會比指定筆數多兩筆, 這問題在新版應該已經解決了. 

雖然安裝的名稱是 googlesearch-python, 但使用時的套件名稱只要用 googlesearch 即可, 首先匯入整個套件, 用 dir() 檢視其內容 :

>>> import googlesearch   
>>> dir(googlesearch)    
['BeautifulSoup', 'SearchResult', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '_req', 'get', 'get_useragent', 'search', 'sleep', 'user_agents']    

可見此套件使用 BeautifulSoup 來解析網頁. 它使用的 User-Agent 可呼叫 get_useragent() 查詢 :

>>> googlesearch.get_useragent()  
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36'

使用此套件主要是透過呼叫 search() 函式並傳入要搜尋的關鍵字, 它會傳回一個生成器 (generator), 可以用迴圈列印出其網址 : 

>>> from googlesearch import search  
>>> type(search)  
<class 'function'>     

search() 的傳入參數如下表 : 


 search() 的參數 說明
 query 查詢的字詞 (必要參數)
 num_results 傳回搜尋結果的筆數 (預設 10 筆)
 advanced 是否開啟進階模式 (傳回物件), 預設 False
 language 指定搜尋結果的 ISO639-1 語言代碼 (預設 'en' 英文), 繁體中文為 'zh-tw'
 proxy 指定代理伺服器網址


search() 函式會傳回一個搜尋結果的生成器 :  

>>> items=search('小狐狸事務所')     
>>> type(items)    
<class 'generator'>    

可用迴圈遍歷每一個搜尋結果, 預設一般模式會傳回 10 個搜尋結果的網址 (字串) :

>>> for item in items:   
    print(item)     
    
http://yhhuang1966.blogspot.com/
http://yhhuang1966.blogspot.com/
https://www.instagram.com/huli_market/
https://tony1966.github.io/
https://www.blogger.com/profile/09435160519044041137
https://www.surveycake.com/s/WgZav
https://world.taobao.com/dianpu/114691087.htm
https://www.ruten.com.tw/item/show?22346154976481=&srsltid=AfmBOopVt_JXikBUIMbDCob7UKiSPfO8tWjWszgG7x0P6PXtv2dLZMIe
https://www.facebook.com/photo.php?fbid=2636368466439445&id=2194109117332051&set=a.2273434692732826
https://www.threads.net/@pottery_flower/post/C9SRI6fPNG2?hl=zh-tw

傳入 advanced=True 指定進階模式則會傳回 SearchResult 物件的生成器 : 

>>> items=search('小狐狸事務所', advanced=True)      
>>> type(items)    
<class 'generator'>    
>>> for item in items:    
    print(item)    
    
SearchResult(url=http://yhhuang1966.blogspot.com/, title=小狐狸事務所, description=小狐狸的生活紀錄,上班族心情記事,JavaScript,Python,PHP,HTML5,Autoit,C,WSH,ASP 等學習心得筆記,社會與政治評論等等。)
SearchResult(url=http://yhhuang1966.blogspot.com/, title=小狐狸事務所, description=小狐狸的生活紀錄,上班族心情記事,JavaScript,Python,PHP,HTML5,Autoit,C,WSH,ASP 等學習心得筆記,社會與政治評論等等。)
SearchResult(url=https://www.instagram.com/huli_market/, title=小狐狸市務所(@huli_market), description=【小狐狸市務所- 彰化卦山村】 日期: 6/22~6/25 & 7/8~7/9 時間: 13:00~18:00 地址: 彰化縣彰化市卦山路8-1號2F 合作單位: 彰化縣創藝美育協會市集報名連結: https://reurl ...)
SearchResult(url=https://tony1966.github.io/, title=小狐狸事務所の網路實驗室, description=Hi, 歡迎拜訪"小狐狸事務所の網頁實驗室" ! 此網站匯集我在探索各種網頁技術時所撰寫的測試程式, 程式的詳細說明紀錄在個人部落格"小狐狸事務所", 您可任意參考與複製 ...)
SearchResult(url=https://www.blogger.com/profile/09435160519044041137, title=小狐狸事務所- User Profile, description=熱愛自由不想被拘束, 無法忍受無聊而不斷學習的射手座, 因為記性不好必須在部落格紀錄思考學習與生活點滴的平凡上班族.)
SearchResult(url=https://www.surveycake.com/s/WgZav, title=小狐狸市務所- 11.12月敦化SOGO, description=【市集日期&時間】11/9(六)-11/10(日) > 週年慶12/14(六)-12/15(日) > 聖誕黨市集時間: 11:00~19:00【市集地點】敦化SOGO百貨正門口【收費標準】場租: 1200元/天租 ...)
SearchResult(url=https://world.taobao.com/dianpu/114691087.htm, title=狐狸事務所- 淘寶網|Taobao, description=歡迎光臨-狐狸事務所,淘寶網Taobao爲你提供最新商品圖片、價格、品牌、評價、折扣等信息,有問題可直接諮詢商家!淘寶數億熱銷好貨,官方物流可寄送至全球十地, ...)
SearchResult(url=https://www.instagram.com/huli_market/p/CrqWwNDrmhu/, title=小狐狸市務所| 大家最近還好嗎? 小 ..., description=下週開始即將開啟6、7月的報名規劃場次如下 6/22-6/25 (端午連假) -彰化卦山村 7/1-7/2 天母SOGO 7/8-7/9 天母SOGO+ 彰化卦山村 還請大家密切注意我們的 ...)
SearchResult(url=https://www.ruten.com.tw/item/show?22346154976481=&srsltid=AfmBOopRu51UoqOxEZHt8E0zxjKeNnWueZHLV6Cs2hLBLddYoIS_0k9q, title=【怨念事務所】現貨GSC 黏土人FLUFFY LAND 小狐狸River ..., description=品牌旗艦【怨念事務所】現貨GSC 黏土人FLUFFY LAND 小狐狸River 再版 · 優惠活動 · 預計出貨4~7天(2024/10/24 ~ 2024/10/27) · 運送NT$ 0 - NT$ 100合併運費規則 · 付款.)
SearchResult(url=https://www.threads.net/@pottery_flower/post/C9SRI6fPNG2?hl=zh-tw, title=米洛酷市集&小狐狸事務所台北室內-捷運大安森林公園, description=Jul 11, 2024 — 陶花巷弄民宿x工作室(@pottery_flower). 19 Likes. ➺➤米洛酷市集&小狐狸事務所台北室內-捷運大安森林公園-陽光大廳2024.6.8-9 ➤此為室內市集, ...)

可見 SearchResult 物件有 url, title, 與 description 三個屬性. 可以傳入 num_results 參數指定搜尋筆數 :

>>> items=search('小狐狸事務所', advanced=True, num_results=3)    
>>> for item in items:   
    print(item.title)   
    print(item.description)   
    print(item.url)   
    print()   
    
小狐狸事務所
小狐狸的生活紀錄,上班族心情記事,JavaScript,Python,PHP,HTML5,Autoit,C,WSH,ASP 等學習心得筆記,社會與政治評論等等。
http://yhhuang1966.blogspot.com/

小狐狸事務所
小狐狸的生活紀錄,上班族心情記事,JavaScript,Python,PHP,HTML5,Autoit,C,WSH,ASP 等學習心得筆記,社會與政治評論等等。
http://yhhuang1966.blogspot.com/

小狐狸市務所(@huli_market)
【小狐狸市務所- 彰化卦山村】 日期: 6/22~6/25 & 7/8~7/9 時間: 13:00~18:00 地址: 彰化縣彰化市卦山路8-1號2F 合作單位: 彰化縣創藝美育協會市集報名連結: https://reurl ...
https://www.instagram.com/huli_market/