2024年11月30日 星期六

Python 學習筆記 : 簡單好用的 Web app 套件 gradio (二)

在前一篇測試中我們測試了在本機, Colab, 與 Gradio Playground 建立網頁應用程式的方法, 並且使用 Gradio Playground 可將 Web app 一鍵佈署至 Hugging Face Space 儲存空間. 但前一篇只是以最簡單的說 Hello 為範例, 事實上, Gradio 提供了豐富的輸出入元件可支援文字, 圖像, 影片, 與音訊等媒體形式, 本篇要測試的是 Gradio 的輸入元件.

本系列之前的文章參考 : 



以下測試皆在 Gradio Playground 上進行, 網址如下 :|


測試範例程式碼則放在 Hugging Face Space :



六. Gradio 的輸出入元件 :   

Gradio 的輸入元件類別非常豐富, 支援各種資料型態, 摘要如下表 :

 
 輸入元件類別 說明
 Textbox
 簡寫 'text' 或 'texbox'
 用於接收使用者的文字輸入, 傳入參數 :
 value: 預設值 (預設值 "")
 label: 標籤 (預設值 None) 
 lines: 行數 (預設值 1)
 placeholder: 提示文字 (預設值 None)  
 type: "text" 或 "password" (輸入會被遮蔽)
 TextArea
 簡寫 'textarea'    
 用於接收使用者的多行文字輸入, 傳入參數 :
 value: 預設值 (預設值 "")
 label: 標籤 (預設值 None) 
 lines: 初始顯示列數 (預設值 7, 輸入列數超過時會自動增加顯示列數)
 max_lines: 限制最大顯示列數 (輸入列數超過不會增加高度, 預設 None)
 placeholder: 提示文字 (預設值 None)  
 Number
 簡寫 'number'
 接收數值 (整數或浮點數) 輸入, 傳入參數 : 
 value: 預設值 (預設值 None)
 label: 標籤 (預設值 None) 
 minimum: 最小值 (預設值 None)
 maximum: 最大值 (預設值 None) 
 Slider 
 簡寫 'slider'
 用於選擇數字範圍內的值的滑桿元件, 傳入參數 : 
 value: 預設值 (預設值 50)  
 label: 標籤 (預設值 None) 
 minimum: 最小值 (預設值 0)  
 maximum: 最大值 (預設值 100)  
 step: 步長 (預設值 1) 
 Checkbox
 簡寫 'checkbox'
 核取方塊, 用來設定勾選 (True) / 未勾選 (False), 預設未勾選 :
 value: 初始值 True/False (預設值 False 未勾選)  
 label: 標籤 (預設值 None)
 info: 選項上方的說明文字 (預設值 None)
 interactive: 是否可勾選 True/False (預設 True)
 CheckboxGroup
 簡寫 'checkboxgroup'
 多選之核取方塊, 用來設定勾選 (True) / 未勾選 (False), 預設均未勾選 :
 value: 預設值串列 (預設值 []) 
 label: 元件標籤 (預設值 None)  
 choices: 選項串列 (必要參數)
 info: 選項群組上方, 元件標籤下方的說明文字 (預設值 None)
 interactive: 是否可勾選 True/False (預設 True)
 Radio
 簡寫 'radio'
 單選按鈕, 用於從多個選項中選擇一個, 傳入參數 : 
 value: 預設值串列 (預設值 None)
 label: 標籤 (預設值 None)  
 choices: 選項串列 (預設值 []) 
 Dropdown
 簡寫 'dropdown'
 下拉式選單, 用於從多個選項中選擇一個 (與 Radio 一樣), 傳入參數 : 
 value: 預設值串列 (預設值 None)   
 label: 標籤 (預設值 None)  
 choices: 選項串列 (預設值 [])  
 Image 
 簡寫 'image'
 接收圖片輸入 (可拍照或上傳圖片), 支援 PNG, JPEG 等格式, 傳入參數 : 
 value: 本機的圖片路徑檔名或網路圖檔的 URL (預設值 None) 
 label: 標籤 (預設值 None) 
 tool: "sketch" 為繪圖板 (輸入), "editor" 為編輯器, None 為拖曳上傳 (預設)
 type: 傳回的資料類型, 值 'numpy' 或 'pil' 或 'filepath' (預設值)
 sources: 圖片輸入來源串列 (預設值 ['upload', 'webcam', 'clipboard'])  
 width: 指定圖片寬度 (px)
 height: 指定圖片高度 (px)
 Audio
 簡寫 'audio'
 接收音訊輸入, 可拖曳/上傳音訊檔或用麥克風錄音
 value: 本機的圖片路徑檔名或網路圖檔的 URL (預設值 None)  
 label: 標籤 (預設值 None)   
 sources: 音訊輸入來源串列 (預設值 ['upload', 'microhone'])
 Video
 簡寫 'video'
 接收視訊輸入, 可  拖曳/上傳視訊檔或用 webcam 錄影
 value: 本機的影片路徑檔名或網路視訊檔的 URL (預設值 None)
 label: 標籤 (預設值 None)
 sources: 視訊輸入來源串列 (預設值 ['upload', 'webcam'])  
 width: 指定影片寬度 (px)
 height: 指定影片高度 (px)
 JSON
 簡寫 'json'
 接收 JSON 格式的輸入
 label: 元件標籤 (預設值 None)  
 value: 預設值 (預設值 None)
 TimeSeries
 簡寫 'timeseries'
 接收時間序列資料輸入 (立如 CSV 格式)
 label: 元件標籤 (預設值 None)
 x: x 軸標籤 (預設值 None)
 y: y 軸標籤 (預設值 None)
 JSON
 簡寫 'json'
 接收 JSON 格式的輸入
 label: 元件標籤 (預設值 None)  
 value: 預設值 (預設值 None)
 Dataframe
 簡寫 'dataframe'
 接收表格格式輸入 (支援文字和數字) 
 label: 元件標籤 (預設值 None) 
 headers: 表格欄名  
 row_count: 列數 (預設值 (1, 'dynamic'))
 col_count: 欄數 (預設值 (1, 'dynamic'))
 ColorPicker 
 簡寫 'colorpicker'
 顏色選擇器 (16 進制)
 label: 元件標籤 (預設值 None) 
 value: 預設值 (預設值 None)


Gradio 輸出元件超過 10 個, 有一些輸入元件例如 Textbox, Audio 等也兼做輸出元件用, 但在機器/深度學習應用方面最常用的是 Textbox 與 Label 元件, 常用輸出元件如下表 : 

 
 輸出元件類別 說明
 Textbox
 簡寫 'text' 或 'texbox'
 用於顯示處理函式傳回的結果, 傳入參數 :
 value: 預設值 (預設值 "")
 label: 元件標籤 (預設值 None) 
 lines: 行數 (預設值 1)
 placeholder: 提示文字 (預設值 None)  
 type: "text" 或 "password" (輸入會被遮蔽)
 Lebel 用於顯示用於機器學習分類器的結果 (分類標籤及其機率), 傳入參數 :
 label: 元件標籤 (預設值 None)  
 num_top_classes: 顯示前幾個分類
 type: 輸出型態 'value'=分類標籤, 'confidence'=信心, 'auto'=系統決定 (預設)
 Number
 簡寫 'number'
 顯示數值 (整數或浮點數) 輸出, 傳入參數 :
 value: 預設值 (預設值 None) 
 label: 元件標籤 (預設值 None)
 Image 
 簡寫 'image'
 顯示圖片
 label: 元件標籤 (預設值 None) 
 width: 指定圖片寬度 (px) 
 height: 指定圖片高度 (px)
 Audio
 簡寫 'audio' 
 播放音訊
 label: 標籤 (預設值 None) 
 interactive: 是否允許用戶播放 (True/False)
 Video 
 簡寫 'video'
 播放視訊 
 label: 元件標籤 (預設值 None)  
 width: 指定影片寬度 (px) 
 height: 指定影片高度 (px) 
 JSON
 簡寫 'json' 
 顯示 JSON 資料
 label: 元件標籤 (預設值 None)
 value: 預設值 (預設值 {})
 DataFrame 
 簡寫 'dataframe' 
 顯示表格資料
 label: 元件標籤 (預設值 None) 
 headers: 表格欄名
 LinePlot
 簡寫 'plot'
 
 繪製折線圖
 value: 要繪製的資料 (dataframe 或二維 ndarray)
 x: X 軸資料欄名
 y: Y 軸資料欄名
 title: 圖形標題
 color: 顏色分群之欄名
 xlabel: X 軸標籤
 ylabel: Y 軸標籤
 interactive: 是否互動式 (True/False)
 ScatterPlot
 簡寫 'scatter'
 
 繪製折線圖
 value: 要繪製的資料 (dataframe 或二維 ndarray)
 x: X 軸資料欄名
 y: Y 軸資料欄名
 title: 圖形標題
 color: 顏色分群之欄名
 xlabel: X 軸標籤
 ylabel: Y 軸標籤
 grouped:  是否分群 True/False (預設 False)
 stacked: 是否堆疊 True/False (預設 True)
 interactive: 是否互動式 (True/False)
 BarPlot
 簡寫 'bar'
 
 繪製折線圖
 value: 要繪製的資料 (dataframe 或二維 ndarray)
 x: X 軸資料欄名
 y: Y 軸資料欄名
 title: 圖形標題
 color: 顏色分群之欄名
 xlabel: X 軸標籤
 ylabel: Y 軸標籤
 interactive: 是否互動式 (True/False)


事實上 Gradio 還提供許多繪圖類別, 例如長條圖 BarChart, 散佈圖 ScatterPlot, 區域圖 AreaChart, ... 等等, 但最常用的是折線圖. 

下面以三個常見的範例說明 Gradio 輸出入元件的用法 : 


1. 從文本中擷取數字 : 

此例使用正規表達式 r'-?\d+\.?\d*' 從文本中擷取全部數值以串列傳回, 輸出入元件都是使用 Textbox 元件, 輸入框用來貼上含數字的文本, 輸出框用來顯示結果 : 

import gradio as gr
import re

def fetch_numbers(text):
    numbers=re.findall(r'-?\d+\.?\d*', text)
    return [float(num) for num in numbers]

inputs=gr.Textbox(label='從文本中擷取數字',
                  lines=7,
                  placeholder='輸入文數字')
demo=gr.Interface(fn=fetch_numbers, 
                  inputs=inputs, 
                  outputs="textbox")

if __name__ == "__main__":
    demo.launch()

此例輸入元件使用 gr.Textbox() 建構式建立, 這樣才能設定 lines 與 placeholder 等參數. 輸出元件不需要設定參數所以直接用簡寫的字串 'text' 或 'textbox' 即可, 輸入 "今天的溫度是30度,明天預計會上升到33.2度,最高可達35." 結果如下 : 




此 Web app 放在 Hugging Face Space : 


關於正規表示法參考 :



2. 計算 BMI : 

BMI 計算公式為 weight/(height*height), 可以設置兩個 Textbox 輸入框來讓使用者填寫身高 weight 與體重 height, 輸出框也是使用 Textbox, 程式碼如下 : 

import gradio as gr

def handler(weight, height):
    w=float(weight)
    h=float(height)/100    # 轉成公斤
    BMI=round(w/(h*h), 2)    # 取小數後兩位
    if BMI < 18.5:
      remark='體重太輕'
    elif BMI >= 18.5 and BMI < 24:
      remark='體重正常'
    else:
      remark='體重過重'
    return BMI, remark

weight=gr.Textbox(label='體重(公斤)')
height=gr.Textbox(label='身高(公分)')
bmi=gr.Textbox(label='BMI 指標')
remark=gr.Textbox(label='評語')
iface=gr.Interface(
    fn=handler,
    inputs=[weight, height],   
    outputs=[bmi, remark],
    title='BMI 計算器',
    description='請輸入身高與體重後按 Submit 鈕',
    article='BMI 是世衛組織衡量是否肥胖的健康指標',
    flagging_mode='never'
    )
iface.launch()

此處由於 Textbox 元件輸入的是字串型態, 故須先呼叫 float() 將其轉成浮點數. 注意, 兩個傳回值  BMI 與 remark 必須與 outputs 參數中的兩個串列元素 bmi 與 remark 次序相互對應, 否則輸出會放錯地方, 結果如下 : 




上面程式碼放在 Hugging Face 空間 :


但是此程式輸入框全部使用 Textbox 有個問題, 當使用者輸入含有非數值字元時, 呼叫 float() 會出現錯誤 : 




這時 Number 元件就可以派上用場取代 Textbox 元件來防呆了, 程式碼修改如下 :

import gradio as gr

def handler(weight, height):
    w=float(weight)
    h=float(height)/100
    BMI=round(w/(h*h), 2)
    if BMI < 18.5:
      remark='體重太輕'
    elif BMI >= 18.5 and BMI < 24:
      remark='體重正常'
    else:
      remark='體重過重'
    return BMI, remark

weight=gr.Number(label='體重(公斤)')
height=gr.Number(label='身高(公分)')
bmi=gr.Textbox(label='BMI 指標')
remark=gr.Textbox(label='評語')
iface=gr.Interface(
    fn=handler,
    inputs=[weight, height],   
    outputs=[bmi, remark],
    title='BMI 計算器',
    description='請輸入身高與體重後按 Submit 鈕',
    article='BMI 是世衛組織衡量是否肥胖的健康指標',
    flagging_mode='never'
    )
iface.launch()

這樣體重與身高這兩個輸入框就只能輸入數值, 無法輸入像 '公斤', '公分' 這種非數值字元了, 而且右邊還會出現上下調整小按鈕可用漸增與漸減方式改變輸入值, 結果如下 :




此例程式碼放在 Hugging Face 空間 :



3. 攝氏與華氏溫度轉換 : 

攝氏轉華氏公式 : f=c*9/5 + 32
華氏轉攝氏公式 : c=(f-32)*5/9

此處使用一個 Radio 元件選擇輸入的溫度是攝氏還是華氏, 然後用一個 Slider 滑桿元件來輸入溫度數值, 輸出則是使用 Textbox 元件來顯示兩種溫度制的對應轉換結果字串, 程式碼如下 : 

import gradio as gr

def handler(sel, temp):
    if sel == '攝氏':
        c=temp
        f=int(c*9/5 + 32)   # 轉成整數
        msg=f'攝氏 {c} 度=華氏 {f} 度'
    else:
        f=temp
        c=int((f-32)*5/9)   # 轉成整數
        msg=f'華氏 {f} 度=攝氏 {c} 度'
    return msg

sel=gr.Radio(label='溫度制', value='攝氏', choices=['攝氏', '華氏'])
temp=gr.Slider(label='溫度', value=0, minimum=-100, maximum=100)
output=gr.Textbox(label='轉換結果')
iface=gr.Interface(
    fn=handler,
    inputs=[sel, temp],   
    outputs=[output],
    title='攝氏與華氏溫度轉換',
    description='請選擇溫度制與移動溫度滑桿',
    flagging_mode='never',
    article='攝氏轉華氏公式 : f=c*9/5 + 32<br>' +\
            '華氏轉攝氏公式 : c=(f-32)*5/9',
    live=True
    )
iface.launch()

注意, 此處使用 live=True 讓輸入變化可以立即改變輸出結果而毋須按 Submit 鈕, 此設定會自動取消 Submit 鈕. 另外在 article 參數中使用了 HTML 的 <br> 標籤來達成換行, 結果如下 :




此例程式碼放在 Hugging Face 空間 :


如果不想即時輸出結果, 只要將 live=True 這個參數拿掉即可, 這樣就會出現 Submit 按鈕了. 

2024年11月24日 星期日

2024 年第 47 周記事

本周工作上業務較多, 很忙! 但每天下班還是去河堤快走兼餵貓, 不過這周河堤小咪不太愛吃我的貓乾糧, 吃兩口就走開, 可能是另一個女士比我先來餵. 很久沒遇到的玉桃小姐上周五有來, 前一次遇到時說最近很多事情沒法來餵貓, 叫我有空盡量來餵. 





我周一沒去快走, 而是去大地游泳, 因水某說大地好像賣掉了, 我的游泳券要趕快用掉. 我問了貴台小姐, 她說土地是賣掉了, 但游泳池沒這麼快拆, 我問明年會拆嗎? 回答是不會, 但我還是每周都去游 1~2 次盡快用掉為宜. 

小舅在菜園種的冬瓜似乎已可採摘, 他給了我一顆, 下午切了 1/3 來煮湯, 還剩一些舅切片煮味增冬瓜, 發現味道非常甘美, 這次小舅精心栽培果然與之前菜園亂長的廚餘冬瓜大大不同 :




下午完成側門庫房的太陽能感應燈安裝, 太陽能板裝在二樓東側平台, 電線走頂樓儲能太陽能系統的配線, 然後在紗窗右上角的塑膠板鑽個洞引到室內 :





下周打算來安裝西側浴室的太陽能感應燈.

PS : 剛剛台日冠軍爭霸戰, 台灣隊 4:0 完封日本! 讚讚讚!



博客來買書一本 : ChatGPT 4 Omni 領軍

今天我之前寫的博客來 66 折爬蟲 LINE 通報洪錦魁老師的 "ChatGPT 4 Omni領軍" 今日 66 折, 難得的機會當然立馬買一本 :





同時今晚 MQTT 社團有辦洪錦魁老師的本書分享會. 但晚上有棒球 12 強台日冠軍爭霸賽, 所以沒辦法專心聽, 時不時跑去書房看轉播, 六局結束台:日為 4:0, 讚讚讚!

好站 : *args 和 **kwargs 是什麼?

最近在寫 ESP32 的 MicroPython 函式時要用到 **kwargs 參數, 很久沒用有點模糊, 搜尋谷歌時找到下面這篇寫得言簡意賅的好文章 :


摘要如下 :
  • Python 函式的有預設值參數必須放在無預設值參數的後面
  • *args 與 **kwargs 都是用來簡化函式參數的寫法
  • *args 會將傳入的位置參數的值收集到一個 tuple
  • **kwargs 會將傳入的關鍵字參數的名稱與值收集到一個 dict
  • 參數用了 *arg 就不能再指名; 而用了 **kwargs 就一定要指名
  • 同時使用 *args 與 **kwargs 時, *args 必須放在 **kwargs 前面 (關鍵字參數殿後)
  • 可以用 * 區隔位置參數與關鍵字參數 (雖然沒必要), * 之前為位置參數, 之後為關鍵字參數
作者 Sky 已經將部落格移到 GitHub 去了, 新版網址如下 :


這種好站對偷學武功慣犯的我真是無上好物啊!

2024年11月23日 星期六

Python 學習筆記 : 簡單好用的 Web app 套件 gradio (一)

Gradio 是一個低代碼的 Python web app 套件, 只要簡單幾行程式碼就能完成一個網頁應用程式介面, 可以用來快速展示應用程式功能, 是教學與 web app 雛型開發的好工具, 比起用 Flask 手刻一個網站要簡單快速很多, 參考 Gradio 官網 :       


Gradio 最早是由美國史丹福大學的 Abubakar Abid 等人於 2019 年開發, 目的是可以用簡單的方式將機器學習模型以互動的網頁應用呈現, 以利使用者進行測試和反饋. 2021 年 Gradio 被 AI 模型網站 Hugging Face 收購與持續開發維運, 目前最新版為 v5.6, 教學文件參考 :


參考書籍 :

OpenAI API 基礎必修課 (蔡文龍, 碁峰, 2024) 第 4 章
Python 實戰聖經 (鄧文淵工作室, 碁峰, 2021) 第 14 章
成為 Python AI 深度學習達人的第一門課 (蔡炎龍, 全華, 2022) 第 1 篇


一. 安裝 gradio 套件 : 

Gradio 為 Python 第三方套件, 可用 pip 指令安裝 :

pip install gradio 

如果是在 Colab 上安裝, pip 前面要加驚嘆號 :

!pip install gradio 

安裝後匯入 gradio, 習慣上會取簡名 gr :

>>> import gradio as gr
>>> gr.__version__   
'3.44.3'

可在 pip install 指令後面加 -U 參數把已安裝的 gradio 更新到最新版 :

pip install gradio -U

如果使用 Thonny 編輯器, 可點選 "工具/套件管理" 搜尋 gradio 後安裝或更新 : 




注意, 如果是更新, 完成後必須關掉 Thonny 後重開才會抓到最新版套件 :

>>> import gradio as gr   
>>> gr.__version__   
'5.6.0'

可見已經升到最新版了. 


二. 在本機建立 gradio 網頁應用程式 : 

首先用來檢視 gradio 套件的內容 : 

>>> import gradio as gr 
>>> dir(gr)  
['Accordion', 'AnnotatedImage', 'Annotatedimage', 'Audio', 'BarPlot', 'Blocks', 'BrowserState', 'Brush', 'Button', 'CSVLogger', 'ChatInterface', 'ChatMessage', 'Chatbot', 'Checkbox', 'CheckboxGroup', 'Checkboxgroup', 'ClearButton', 'Code', 'ColorPicker', 'Column', 'DataFrame', 'Dataframe', 'Dataset', 'DateTime', 'DeletedFileData', 'DownloadButton', 'DownloadData', 'Dropdown', 'DuplicateButton', 'Eraser', 'Error', 'EventData', 'Examples', 'File', 'FileData', 'FileExplorer', 'FileSize', 'Files', 'FlaggingCallback', 'Gallery', 'Group', 'HTML', 'Highlight', 'HighlightedText', 'Highlightedtext', 'IS_WASM', 'Image', 'ImageEditor', 'ImageMask', 'Info', 'Interface', 'JSON', 'Json', 'KeyUpData', 'Label', 'LikeData', 'LinePlot', 'List', 'LoginButton', 'Markdown', 'Matrix', 'MessageDict', 'Mic', 'Microphone', 'Model3D', 'MultimodalTextbox', 'NO_RELOAD', 'Number', 'Numpy', 'OAuthProfile', 'OAuthToken', 'Paint', 'ParamViewer', 'PlayableVideo', 'Plot', 'Progress', 'Radio', 'Request', 'RetryData', 'Row', 'ScatterPlot', 'SelectData', 'SimpleCSVLogger', 'Sketchpad', 'Slider', 'State', 'Tab', 'TabItem', 'TabbedInterface', 'Tabs', 'Text', 'TextArea', 'Textbox', 'Theme', 'Timer', 'UndoData', 'UploadButton', 'Video', 'Warning', 'WaveformOptions', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_simple_templates', 'analytics', 'blocks', 'blocks_events', 'chat_interface', 'cli', 'close_all', 'component', 'component_meta', 'components', 'context', 'data_classes', 'deploy', 'events', 'exceptions', 'external', 'external_utils', 'flagging', 'get_package_version', 'gradio', 'helpers', 'image_utils', 'interface', 'ipython_ext', 'json', 'layouts', 'load', 'load_ipython_extension', 'mount_gradio_app', 'networking', 'node_server', 'oauth', 'on', 'pipelines', 'pipelines_utils', 'processing_utils', 'queueing', 'ranged_response', 'render', 'renderable', 'route_utils', 'routes', 'server_messages', 'set_static_paths', 'skip', 'state_holder', 'strings', 'templates', 'themes', 'tunneling', 'update', 'utils', 'wasm_utils']

可見 gradio 套件提供了非常多的類別, 函式, 特別是豐富的輸出入控件類別, 例如 Textbox, Text, TextArea, Number, Radio, Checkbox, Label 與 Slider 等, 參考 :


但這些類別中最重要的是 Interface 類別, 利用它的建構子 Interface() 函式可以建立一個網頁介面物件 Interface, 呼叫此物件的 launch() 方法會自動建構一個含有輸入與輸出控件的網頁, 讓使用者與應用程式互動, 基本語法如下 : 

interface=gr.Interface(fn=函式名稱, inputs=輸入控件, outputs=輸出控件)   

其中 fn 參數用來指定按下自動建立的網頁中的 Submit 按鈕後要呼叫的函式, inputs 參數指定可讓使用者輸入資訊的控件類型名稱, 而 outputs 參數則指定呼叫 fn 函式後用來顯示其回傳值的控件類型. 除了這三個基本參數外, 還有如下參數 :
  • title : 設定應用程式標題
  • description : 設定位於標題下方的說明文字 (可用純文字/HTML/Markdown)
  • article : 設定位於輸出入區下方的註解文字 (可用純文字/HTML/Markdown)
  • examples : 設定位於 App 最下方的輸入控件範例 (二維串列) 
  • flagging_mode: 'auto' (自動標記), 'never' (不需標記), 'manual' (手動, 預設手動標記)
  • flagging_dir: 設定存放標記檔案 flagged_data.csv 的資料夾路徑
  • live : 設為 True 時輸入資料馬上進行運算與輸出, 不須等到按 Submit 鈕才輸出
注意, 新版的 Gradio 已將標記輸入輸出資訊的標記模式參數 allow_flagging 改為 flagging_mode, 但舊參數名稱還是可以使用, 只是執行時會出現警語而已. 當標記模式為 'manual' 時在頁面最底下會出現一個 Flag 按鈕讓使用者于需要時按下該按鈕標記當時的輸出入資料. 標記的輸出入資料預設會被記錄在目前工作目錄下的 flagged 資料夾內的 flagged_data.csv 檔案內, 但可以透過 flagging_dir 指定其他資料夾路徑.  

呼叫 Interface 物件的 launch() 方法即可發布此網頁應用程式 :

interface.launch()   

這樣就會建立一個有輸出入控件與 Submit 按鈕的網頁應用程式介面了. 

建立一個 Gradio 網頁應用程式首先要定義 app 函式, 此為運算邏輯的核心, Interface() 中 inputs 所指定的輸入控件內容會傳遞給此函式, 而運算結果則會傳回給 outputs 所指定之輸出控件, 基本的 web app 程式結構如下 :

import gradio as gr     

def app(參數):     # 傳入參數為輸出入控件串列 (會被當成控件的 label)
    ... (運算) ...
    return 結果     # 給輸出控件

interface=gr.Interface(fn=app, inputs=輸入控件, outputs=輸出控件)    
interface.launch()   

例如下面的打招呼 app, 首先定義 fn 參數要綁定的函式 hello() :

>>> def hello(name):      
    return f'Hello {name}!'    

此函式的輸入參數 name 就是使用者在輸入控件中所輸入的內容. 接著呼叫 Interface() 建構式來建立網頁介面物件 :  

>>> interface=gr.Interface(fn=hello, inputs='textbox', outputs='textbox')  

此處 fn 參數綁定了上面定義的函式 hello(), 輸入控件與輸出控件指定為 'textbox' 表示皆為文字輸入欄位, 這是 Gradio 控件的簡寫用法, 也可以用 'text', 它們都是輸入元件 Textbox 的簡寫用法. 

用 type() 檢視建構子的傳回值是一個 Interface 物件 : 

>>> type(interface)   
<class 'gradio.interface.Interface'>   

此 Interface 物件用上面的 members 模組檢視會出現錯誤  (原因待查), 只好用 dir() 檢視 : 

>>> dir(interface)    
['FRONTEND_DIR', 'GRADIO_CACHE', 'TEMPLATE_DIR', '__annotations__', '__call__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_id', '_is_running_in_reload_thread', '_queue', 'add', 'add_child', 'allow_duplication', 'allow_flagging', 'allowed_paths', 'analytics_enabled', 'api_mode', 'api_name', 'api_open', 'app', 'app_id', 'article', 'attach_clear_events', 'attach_flagging_events', 'attach_load_events', 'attach_submit_events', 'auth', 'batch', 'block_thread', 'blocked_paths', 'blocks', 'cache_examples', 'call_function', 'children', 'clear', 'close', 'component_class_id', 'concurrency_limit', 'config', 'constructor_args', 'create_limiter', 'css', 'dependencies', 'description', 'deserialize_data', 'dev_mode', 'elem_classes', 'elem_id', 'enable_queue', 'encrypt', 'events', 'examples', 'examples_per_page', 'exited', 'expects_oauth', 'favicon_path', 'fill_expected_parents', 'flagging_callback', 'flagging_dir', 'flagging_options', 'fn', 'fn_durations', 'fns', 'from_config', 'from_pipeline', 'get_api_info', 'get_block_name', 'get_component', 'get_component_class_id', 'get_config', 'get_config_file', 'get_expected_parent', 'get_instances', 'handle_streaming_outputs', 'head', 'height', 'input_components', 'instances', 'integrate', 'interface_type', 'is_callable', 'is_rendered', 'is_running', 'js', 'launch', 'limiter', 'live', 'load', 'local_url', 'max_batch_size', 'max_threads', 'mode', 'move_resource_to_block_cache', 'output_components', 'parent', 'pending_streams', 'postprocess', 'postprocess_data', 'predict', 'preprocess_data', 'process_api', 'progress_tracking', 'proxy_url', 'proxy_urls', 'queue', 'queue_enabled_for_fn', 'recover_kwargs', 'render', 'render_article', 'render_examples', 'render_flag_btns', 'render_input_column', 'render_output_column', 'render_title_description', 'root_path', 'serialize_data', 'set_event_trigger', 'share', 'share_token', 'share_url', 'show_api', 'show_error', 'simple_description', 'simple_server', 'skip_api', 'space_id', 'ssl_verify', 'startup_events', 'state_session_capacity', 'stylesheets', 'temp_file_sets', 'temp_files', 'theme', 'theme_css', 'thumbnail', 'title', 'unrender', 'validate_inputs', 'validate_outputs', 'validate_queue_settings', 'visible', 'width']

可見 Interface 物件的屬性方法非常多, 但最常用到的是 queue() 與 launch() 方法 : 
  • queue(max_size=100, concurrency_count=3) :
    用來啟動請求佇列 (先進先出), 所有請求會先放入佇列中排隊依序執行以維持系統效能, 避免伺服器超載, Gradio 會向請求者顯示處理進度條. Gradio 預設可處理 3 個同時執行的請求, 其餘的請求會加入佇列中排隊等待執行, 但可傳入 concurrency_count 參數來設定可同時處理的請求數量, 也可傳入 max_size 參數來設定最大佇列長度. 
  • launch(share=False, inbrowser=False, debug=False) :
    用來佈署並啟動一個本地的 HTTP 網頁伺服器, 讓使用者能透過瀏覽器使用 Gradio 互動式應用程式 (例如測是機器學習模型或與大語言模型互動). 如果傳入 share=True 參數, Gradio 會生成一個有效期限為 72 小時的臨時公開 HTTPS 網址以便讓公眾網路使用者訪問 Gradio 應用程式 (此服務由 Gradio 的母公司 Hugging Face 提供). 傳入 inbrowser=True 會在啟動伺服器的同時自動開啟瀏覽器拜訪網站; 傳入 debug=True 會在發生錯誤時顯示錯誤訊息. Gradio 使用 FastAPI 框架與 Uvicorn 非同步伺服器來建構這個本地的 Web 伺服器, 並利用 WebSocket 與 Vue.js 等技術來提供前後端互動功能. 
這兩個方法的一般的用法是先呼叫 Interface 物件的 queue() 啟動請求的佇列功能, 然後再呼叫 launch() 佈署網站 :

interface.queue() 
interface.launch() 

也可以用鏈式呼叫一氣呵成 :

interface.queue().launch() 

注意, 在生產環境中必須先呼叫 queue() 啟動佇列功能以保證伺服器的穩定性, 避免高負載時因為資源耗盡導致系統崩潰. 但如果是實驗室的 prototype 輕量測試則毋須啟動佇列功能, 直接呼叫 launch() 方法佈署測試網站即可 : 

>>> interface.launch()    
Running on local URL:  http://127.0.0.1:7860  

To create a public link, set `share=True` in `launch()`.

這時開啟瀏覽器拜訪 http://127.0.0.1:7860 網址即可看到 Gradio 建立的網頁 : 




可見預設會將 Submit 按鈕處理函式 hello 的傳入變數名稱 name 作為輸入框的欄位標題, 在輸入框輸入命字按下按鈕後, 欄位內容會被傳送給 hello() 輸出在 outputs 控件的輸出框裡. 注意, 由於 flagging_mode 參數預設為 'manual', 因此網頁最底下會出現一個 Flag 按鈕, 測試者可按下此鈕標記當時的輸出入資料以利偵錯除錯之用, 系統上線時則可設為 'auto' 來記錄使用者的輸出入訊息. 如上所述被標記的資料預設會放在目前工作目錄下 flagged_dir 資料夾下的 flagged_data.csv 裡. 


三. 利用 share=True 參數取得臨時公眾網址 :    

如果在呼叫 launch() 時傳入 share=True 的話, 除了本地伺服器固定的 http://127.0.0.1:7860 網址外, 還會獲得 Huggin Face 提供的 72 小時公眾 HTTPS 網址 : 

>>> interface.launch(share=True)     
* Running on local URL:  http://127.0.0.1:7860
* Running on public URL: https://ac8b278a1e74320ccf.gradio.live   

This share link expires in 72 hours. For free permanent hosting and GPU upgrades, run `gradio deploy` from the terminal in the working directory to deploy to Hugging Face Spaces (https://huggingface.co/spaces)

將此 HTTPS 公眾網址貼到瀏覽器網址列同樣可拜訪此 Web app :




而這整個功能卻只用了下面 5 行程式碼 :

import gradio as gr

def hello(name):      
    return f'Hello {name}!'

interface=gr.Interface(fn=hello, inputs='text', outputs='text')
interface.launch(share=True)

這就是 Gradio 簡單迷人之處, 你完全不用懂 HTML, CSS, Javascript 與 Flask 就輕輕鬆鬆完成了一個網頁應用程式! 讓資料科學, 機器學習, 以及語言模型的測試開發者節省了很多技術負擔. 


四. 在 Colab 建立 gradio 網頁應用程式 :      

如果是在 Colab 上執行更方便, 本地的網站會直接顯示在輸出儲存格裡 :

先用 !pip install gradio 安裝套件 : 




輸入程式碼發布網站 : 




按其中的公眾網址會開啟分頁顯示 Web app :





五. 使用 Gradio Playground 建立網頁應用程式並佈署至 Hugging Face 空間 :   

Gradio 官網提供 Playground 讓開發者直接在其官網上撰寫 Web app, 毋須在本機或 Colab 上安裝 gradio, 網址如下 : 





預設中間已經有一個與上面範例一樣的 hello 應用程式了, 右邊是執行結果頁面, 按右上角的 "Deploy to .. Space" 鈕可將此網頁應用程式佈署至 Hugginh Face 提供的免費 Web app 執行空間 (也可付費購買較多空間與主機性能), 首先會要求登入 Hugging Face :




然後填寫 space name, description, 勾選授權方式等, 其餘都用預設值即可 : 




按 "Create Space" 鈕就會將此 Web app 佈署至 Hugging Face 空間了, 網址如下 :


等佈署程序跑完出現下列 started 頁面時, 點一下上面的灰色區域即可 : 





注意, 這個網址與在本地呼叫 launch(share=True) 所獲得的 72 小時臨時網址不同, 發佈到 Hugging Face 空間時獲得的網址是永久的. 

2024年11月19日 星期二

製作鋰聚合物電池包充電器

前陣子在露天買了兩個 JST PH 2.0mm 接頭的鋰電池包 (因為 LOLIN D32 開發板的電池插槽是 PH 2.0mm 母座), 參考 : 


但是要怎麼給電池包充電呢? 市面上並無現成的充電器可買, 只好自己動手製作了. 我在零件櫃翻出很久之前買的小型雙面洞洞板, 尺寸 3cm*7cm 中間鋸一半剛好可以用來製作兩個充電器 :




目前需要為兩種 LiPo 包的接頭製作充電器, 一是 LOLIN D32 開發板的 JST PH2 鋰電池插頭; 二是之前買的三架固定翼與一架四軸遙控飛機使用的扁平接頭 (我在長明街的大林電子有找到公插頭與母座). 

先量好充電模組 TP4060 在洞洞板上的位置, 插上 JST PH2 母座與針腳, 用快乾劑固定後就可以進行焊接, 最後將航模 LiPo 的扁平充電母座線頭也焊在 BAT 側就完成了 : 





插上航模電池包測量充電電流 :




改插 LOLIN D32 的 JST PH2 電池包測量 : 




TP4060 在充電時板上 LED 亮紅色, 充飽後亮藍色. 充電電流會隨著逐漸充飽而遞減.

露天購買機械堂五檔變速無線小電鑽鐵盒全套組

11/11購物節時在 momo 買了四本書, 另外在 momo 店+ 買了一組機械堂五檔變速無線小電鑽 485 元, 參考 :


但結帳時沒發現這項並未加入總價中, 也沒出現在訂購單商品項目裡, 我以為那是店+廠商自行送貨. 但所有貨上周末都已收到了, 唯獨小電鑽沒有, 回去點店+商品頁也不見了, 真是太奇怪了, 總之結論是小電鑽根本沒買. 今天上露天找到一模一樣的, 還免運費更便宜 : 


 



全家取貨免運 435 元. 

2024年11月18日 星期一

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

今天繼續測試 ESP32-WROVER-DEV 開發板應用, 本篇要利用一個 PIR 紅外線感測器來觸發 OV2640 來拍照, 這樣當 PIR 感測到人或動物的體溫發射的紅外線時就會觸發影像擷取動作. 


關於 PIR 模組特性參考好久之前玩 Arduino 時整理的筆記 :
摘要如下 :
  • PIR=Passive InRared Sensor (被動人體紅外線感測器)
  • 紅外線波長 780nm (近紅外線, 無輻射熱) ~ 100um (遠紅外線, 有輻射熱)
  • 人或動物常溫下釋放的紅外線波長約為 10um
  • PIR 偵測四周紅外線能量分布, 將其分成兩部分來比較能量分布是否平衡, 偵測到能量失衡即表示有在移動而發出觸發信號, 但 PIR 只能偵測熱輻射源是否有在移動, 無法偵測移動的距離, 方向與位置
  • PIR 模組感應距離最遠 7 公尺, 範圍可達 110 度角
PIR 模組內主要是利用焦電型 (pyroelectric) 紅外線感測器 D203S 將溫度變化轉成電子訊號, 外面有一個聚乙烯平凸透鏡外罩, 用來過濾與聚焦波長 8um~14um 的紅外線 : 




背面是電路板與接腳 : 




PIR 模組有三隻接腳 :
  • VCC : +5V
  • GND : 接地
  • OUT : 3.3V (偵測到人體紅外線) 與 0V (未偵測到人體紅外線)
PIR 模組運作電源為 5V (不要接 3.3V 電源, 會無法偵測紅外線), 但輸出腳 OUT 偵測到人體紅外線時卻是輸出 3.3V, 因此可直接與 3.3V 系統的 ESP32 相連而無須做位準轉換. 在此次實驗中, 我將此 OUT 腳接到 GPIO13, 將其設定為輸入腳以接收 PIR 的偵測觸發信號. 


1. PIR 模組輸出信號檢視 : 

接下來我將上面 "Arduino 測試 : PIR 紅外線移動偵測 (二)" 這篇 Arduino 程式改寫為 MicroPython 版本, 用來觀察 PIR 模組是否正確輸出信號 : 

from machine import Pin
import time

pir_pin=Pin(13, Pin.IN)  # 初始化 GPIO13 為輸入
counter=0  # 用來計算已顯示的標記數量
while True:
    val=pir_pin.value()  # 讀取 PIR 模組的輸出
    mark='-'             # 預設標記 '-' 表示未偵測到
    if val == 1:         # 偵測到人體標記改為 '*'
        mark='*'  
    print(mark, end='')  # 列印標記不換行
    counter += 1         # 標記計數增量 1
    if counter >= 50:    # 超過 50 個標記換行
        print()  
        counter=0        # 重設計數器
    time.sleep(0.1)      # 等待 100 毫秒避免過多輸出

執行結果如下 : 

-----------------------------------***************
************************************************--
--------------------------------------------------
------------------****************************----
------------*******************************-------
--------------------*******************-----******
*******-----*************-------------------------

其中 ---- 表示未偵測到人體紅外線, 而 **** 表示有偵測到人體紅外線, 確認 PIR 模組有正常輸出. 接下來就可以利用 PIR 輸出來觸發 IRQ, 讓中斷處理函式呼叫拍照函式擷取影像並存檔. 


2. 用 PIR 觸發中斷拍照並存檔 : 

此例使用 GPIO13 作為中斷輸入腳, 上面範例的接線不用改, PIR 輸出仍然是接到 GPIO13, 但程式改為將 GPIO13 設為偵測 PIR 送來的上升緣觸發中斷信號. 關於中斷用法參考 :


透過 ChatGPT 協作, 經過多次測試修改後得到下面可正常運作的程式 : 

# pir_camera_capture.py
import camera
import time
from machine import Pin
from time import localtime
import gc

def init_camera():  # 初始化鏡頭
    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)
    print("Camera initialized.")

def capture_image():  # 擷取影像 & 存檔
    now=localtime()
    file_name='cap_{:04d}{:02d}{:02d}_{:02d}{:02d}{:02d}.jpg'.format(
        now[0], now[1], now[2], now[3], now[4], now[5])
    buf=camera.capture()
    if buf:
        with open(file_name, 'wb') as f:
            f.write(buf)
        print(f'Image has been saved as {file_name}')
    else:
        print('Failed to capture image')
    del buf
    gc.collect()  # 強制垃圾回收

def pir_trigger(pin):  # 有防抖動的 IRQ 中斷處理函式
    global motion_detected, last_trigger_time  # 取得全域變數
    current_time=time.ticks_ms()  # 紀錄現在毫秒數時戳
    if time.ticks_diff(current_time, last_trigger_time) > 2000:  # 防抖
        print('Motion detected!') # 超過 2 秒表示已穩定(非信號抖動)
        motion_detected=True      # 設定拍照旗標
        last_trigger_time=current_time  # 更新防抖動時戳

# 初始化全域變數
motion_detected=False  # 觸發中斷旗標 : 用來通知主程式執行拍照
last_trigger_time=0    # PIR 輸出防抖動時間戳
# 初始化攝像頭
init_camera()   
# 設定 PIR 引腳及中斷
pir_pin=Pin(13, Pin.IN, Pin.PULL_DOWN)
pir_pin.irq(trigger=Pin.IRQ_RISING, handler=pir_trigger)
# 主迴圈負責監視拍照旗標, 若被設定就拍照存檔
try:
    while True:
        if motion_detected:
            capture_image()
            motion_detected=False  # 拍照旗標
        time.sleep(0.1)
except KeyboardInterrupt:
    print("Exiting...")
    camera.deinit()

此程式與之前的範例有一個重大差別, 那就是此處攝像頭只初始化一次, 之前的作法是每拍一次就呼叫 camera.deinit() 釋放記憶體資源, 下次拍照前重新呼叫 init_camera() 初始化, 這樣做是多此一舉, 因為初始化只要一次即可, 就能多次呼叫 capture() 進行多次拍照, 也可以避免在中段處理中減少了頻繁的初始化與資源釋放. 

此程式使用全域變數來記錄拍照旗標狀態, 在中斷處理函式中只是簡單判斷一下是否為穩定之處發信號而非抖動以過濾雜訊, 若為穩定之觸發信號就設定拍照旗標讓主程式的無窮迴圈去處理拍照事宜, 因為中斷處理函式不應占用太長的處理時間, 否則可能運作會不如預期. 


測試結果如下 : 

True
Camera initialized.
<IRQ>
Motion detected!
5214
Image has been saved as capture_20241117_194500.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
8688
Image has been saved as capture_20241117_194507.jpg
Motion detected!
7787
Image has been saved as capture_20241117_194514.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7787
Image has been saved as capture_20241117_194520.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
8742
Image has been saved as capture_20241117_194523.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8272
Image has been saved as capture_20241117_194526.jpg
Motion detected!
7016
Image has been saved as capture_20241117_194533.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
7314
Image has been saved as capture_20241117_194537.jpg
Motion detected!
8005
Image has been saved as capture_20241117_194540.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8005
Image has been saved as capture_20241117_194545.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7881
Image has been saved as capture_20241117_194547.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8102
Image has been saved as capture_20241117_194554.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7914
Image has been saved as capture_20241117_194600.jpg
Motion detected!
cam_hal: EV-EOF-OVF
7023
Image has been saved as capture_20241117_194606.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
8062
Image has been saved as capture_20241117_194613.jpg
Motion detected!
8543
Image has been saved as capture_20241117_194616.jpg
Motion detected!
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
cam_hal: EV-EOF-OVF
.... (略) ....
Motion detected!
7794
Image has been saved as capture_20241117_195246.jpg
Motion detected!
7928
Image has been saved as capture_20241117_195248.jpg
Motion detected!
8078
Image has been saved as capture_20241117_195311.jpg
Motion detected!
cam_hal: EV-EOF-OVF
8084
Image has been saved as capture_20241117_195316.jpg
Motion detected!
7924
Image has been saved as capture_20241117_195403.jpg
Motion detected!
7924
Image has been saved as capture_20241117_195407.jpg
Exiting...
True

檢視開發板根目錄果然建立了許多 jpeg 圖檔 :




下面是擷取影像中的一張圖 capture_20241117_195240.jpg :




這是我鄉下家的書房兼臥室哈哈. 

2024年11月17日 星期日

2024 年第 46 周記事

本周的空閒時間主要在研究 ESP32-WROVER-DEV 開發板的影像擷取測試, 此板利用特製 MicroPython 韌體中的 camera 模組拍照沒問題, 但特效我經測試似乎無效. 接下來可以試試用 Aduino 的 C 語言來控制, 可用的功能應該會更多. 

菁菁週二空閒時打電話說她找了很久的 MAZDA MX-5 有一個新北車商通知有一台 2018 年式里程數三萬多的二手車上架, 她覺得很喜歡打算買, 下周一就要交車付尾款, 但要考駕照客戶又排滿滿沒時間去臨櫃領錢, 我說我先墊, 但她線上匯款給我要分好幾次且跨行要收手續費, 叫我去開中國信託帳戶. 所以週四午休時間就去民族分行預約下周一開戶, 同時還辦了中信的中油卡, 綁卡加油有 4% 回饋挺划算的. 

週四附近的診所來公司打流感與新冠疫苗 (莫德納), 一隻手臂打一支疫苗, 過了兩天打新冠的右手臂還有腫痛感, 回來查之前的疫苗卡, 發現這次是第六劑因新冠了, 上一次已經是快兩年前的事了. 那時曾因菁菁確診我還居家上班了半年哩. 

今天繼續上周未完成的浴室太陽能感應燈安裝, 由於要穿過兩層鋁門窗, 洞是穿好了, 但是電線要穿過去卻不是那麼容易, 費了好多時間才終於穿線完成, 測試功能如預期, 晚上趁洗澡順便測試, 故意不開燈讓感應燈偵測到人在動一直開啟, 雖然亮度不如一般燈泡, 但至少停電時不怕沒照明啦! 






2024年11月16日 星期六

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

完成 camera 模組的初始化與擷取函式測試後, 接下來就可以來做一些應用實驗了, 本篇要做的測試是每當按下按鈕時就擷取鏡頭影像並存檔 (即按鈕拍照). 

本系列之前的文章參考 :


首先準備一個按鈕, 它的短邊 (距離較短) 接點平時是 OFF 不相接, 按下時 ON 相接, 將其一端接 ESP32 的 GND, 另一端接一個 GPIO 腳, 例如 GPIO13, 在程式中設定其為輸入腳並開啟此上拉電阻, 這樣當按鈕未按下時輸入為 HIGH, 按下時為 LOW. 

程式碼如下 :

# push_shot_1.py
import time, camera
from machine import reset, Pin
    
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
    
button=Pin(13, Pin.IN, Pin.PULL_UP)
file_counter=1  # 檔案計數器
try:
    while True:
        if button.value() == 0:  # 按鈕被按下
            file_name=f'capture{file_counter:02d}.jpg'  # 格式化檔名
            print(f'Button pressed! Capturing image as {file_name}...')
            init_camera()  # 初始化鏡頭
            capture_image(file_name)  # 擷取影像
            file_counter += 1  # 更新檔案編號
            time.sleep(1)  # 避免按鈕抖動影響
except KeyboardInterrupt:
    print("Program stopped.")

此程式使用 polling 法以無窮迴圈偵測鈕是否欸按下, 是的話就先初始化鏡頭後拍照存檔, 下面是執行後連按 5 次的輸出 :

Button pressed! Capturing image as capture01.jpg...
0
4
10000000
5
True
10132
Image has been saved as capture01.jpg
True
Button pressed! Capturing image as capture02.jpg...
0
4
10000000
5
True
9959
Image has been saved as capture02.jpg
True
Button pressed! Capturing image as capture03.jpg...
0
4
10000000
5
True
9948
Image has been saved as capture03.jpg
True
Button pressed! Capturing image as capture04.jpg...
0
4
10000000
5
True
9898
Image has been saved as capture04.jpg
True
Button pressed! Capturing image as capture05.jpg...
0
4
10000000
5
True
cam_hal: EV-EOF-OVF
cam_hal: EV-VSYNC-OVF
9974
Image has been saved as capture05.jpg
True

開發板上的檔案列表顯示有 5 個 jpeg 檔 :




這五張 jpeg 都是如下相同的圖 : 




其實就是我的電腦桌啦.

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) 

關於關鍵字參數 **kwargs 用法參考 :



但這樣會出現參數重複的錯誤 (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




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