用電腦顯示MLX90640熱像攝影模組的熱像
幾年前買了一個熱影像攝影模組/紅外線陣列 MLX90640(110度版本),當時搭配 Wio Terminal 一起購買,主要是將影像直接放在 Wio Terminal 的小螢幕中顯示。那時的程式碼直接套用廠商提供的範例,也沒有深入去管它的運作原理。
最近重新翻出這個有趣的機器,心裡冒出一個新目標:想要把熱影像直接在電腦螢幕上呈現。畢竟在教學現場或實驗演示時,這樣才方便直接投影到大螢幕上讓大家看清楚。稍微研究了一下它的規格,原來 MLX90640 的通訊方式是 $I^2C$。那基本邏輯就很簡單了:我只要用一個開發板負責透過 $I^2C$ 把 MLX90640 偵測到的溫度數據拿回來,再透過 UART 序列通訊(Serial)轉送進電腦,最後電腦端用 Python 來處理這些數據並即時繪圖就可以了!
架構:
MLX90640 (感測器) → [I2C] → Wio Terminal (轉運站) → [USB Serial] → 電腦 → [Python + OpenCV] → 大螢幕投影
既然原本就是搭配 Wio Terminal,那我就直接用它來當作數據的「轉運站」。以下是處理序列傳輸的 Arduino 端的程式碼:
Wio Terminal 端:序列轉運站程式碼
#include <Adafruit_MLX90640.h>
Adafruit_MLX90640 mlx;
float frame[32*24];
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
Wire.begin();
// 如果 1MHz 會卡死,先退回標準的快速模式 400kHz,這最安全穩定
Wire.setClock(400000);
if (!mlx.begin(MLX90640_I2CADDR_DEFAULT, &Wire)) {
while (1) delay(10);
}
mlx.setMode(MLX90640_CHESS);
// 將解析度降為 16 位元(或 MLX90640_ADC_17BIT)
mlx.setResolution(MLX90640_ADC_16BIT);
mlx.setRefreshRate(MLX90640_4_HZ); // 可以先從 2Hz/4Hz 驗證,再嘗試調成 MLX90640_8_HZ
}
void loop() {
// 抓取一影格的溫度資料 (這一步在高頻時會阻塞等待感測器準備好)
if (mlx.getFrame(frame) != 0) {
return;
}
// 將 768 (32x24) 個點的數據輸出到電腦
for (int i = 0; i < 768; i++) {
// 小數點改為 1 位即可(例如 26.5),減少一半的 Serial 傳輸字元量,讓速度更快
Serial.print(frame[i], 1);
if (i < 767) {
Serial.print(",");
}
}
Serial.println();
}
接著是電腦端的重頭戲。我們利用 Python 讀取 Serial 連接埠的資料,將收到的 768 個浮點數重新組裝成 $32 \times 24$ 的矩陣。為了讓視覺效果更好,程式中使用了雙三次插值(Bicubic Interpolation)將解析度平滑放大,並套用內建的 COLORMAP_TURBO ,加上了滑鼠移動即時顯示該點「真實溫度」的功能!
電腦端:Python 數據接收與熱影像視覺化
import serial
import serial.tools.list_ports
import numpy as np
import cv2
import time
BAUD_RATE = 115200
SCALE_FACTOR = 20 # 32x24 放大 20 倍 = 640x480
# 全域變數,用來記錄滑鼠當前在放大影像上的 (x, y) 座標
mouse_x = -1
mouse_y = -1
def mouse_move_callback(event, x, y, flags, param):
""" 監聽滑鼠移動事件 """
global mouse_x, mouse_y
if event == cv2.EVENT_MOUSEMOVE:
mouse_x = x
mouse_y = y
def find_wio_port():
""" 自動尋找或讓使用者手動選取序列埠 """
ports = list(serial.tools.list_ports.comports())
if not ports:
print(" 錯誤:電腦目前沒有連接任何序列埠裝置(請檢查 USB 線)。")
return None
for p in ports:
description = p.description.lower()
manufacturer = (p.manufacturer or "").lower()
if "seeed" in description or "arduino" in description or "seeed" in manufacturer:
print(f" 自動偵測到 Wio Terminal: {p.device}")
return p.device
if len(ports) == 1:
print(f" 自動選定唯一的序列埠: {ports[0].device}")
return ports[0].device
print("\n 找到多個序列埠,請手動選擇 Wio Terminal 連接的編號:")
for i, p in enumerate(ports):
print(f" [{i}] {p.device} - {p.description}")
while True:
try:
choice = input(f"\n請輸入編號 (0-{len(ports)-1}): ").strip()
idx = int(choice)
if 0 <= idx < len(ports):
return ports[idx].device
else:
print(f"請輸入 0 到 {len(ports)-1} 之間的數字。")
except ValueError:
print("請輸入有效的數字。")
def main():
global mouse_x, mouse_y
PORT = find_wio_port()
if not PORT:
return
try:
ser = serial.Serial(PORT, BAUD_RATE, timeout=1)
print(f" 成功連線至: {PORT}")
except Exception as e:
print(f" 錯誤:無法開啟序列埠 {PORT}。原因: {e}")
return
time.sleep(1.5)
ser.reset_input_buffer()
# 先建立視窗,這樣才能綁定滑鼠事件
window_name = "Wio Terminal MLX90640 Smooth Thermal"
cv2.namedWindow(window_name)
cv2.setMouseCallback(window_name, mouse_move_callback)
print("\n 影像視窗已啟動!")
print(" 提示:將滑鼠移入畫面即可顯示當前位置溫度。點擊影像視窗並按 'q' 鍵可關閉程式。")
while True:
if ser.in_waiting > 0:
try:
line = ser.readline().decode('utf-8').strip()
if not line:
continue
str_list = line.split(',')
if len(str_list) != 768:
continue
# 重組原始 24x32 的溫度矩陣
raw_matrix = np.array(str_list, dtype=np.float32).reshape(24, 32)
raw_matrix = np.fliplr(raw_matrix) # 鏡像修正(讓畫面與手同向)
min_temp = np.min(raw_matrix)
max_temp = np.max(raw_matrix)
# 正規化到 0~255 灰階
if max_temp == min_temp:
gray_img = np.zeros((24, 32), dtype=np.uint8)
else:
gray_img = (255 * (raw_matrix - min_temp) / (max_temp - min_temp)).astype(np.uint8)
# 雙三次插值美化放大
target_size = (32 * SCALE_FACTOR, 24 * SCALE_FACTOR)
smooth_img = cv2.resize(gray_img, target_size, interpolation=cv2.INTER_CUBIC)
# Turbo 高質感色圖
color_heatmap = cv2.applyColorMap(smooth_img, cv2.COLORMAP_TURBO)
# --- 核心:計算滑鼠所在位置的溫度 ---
if 0 <= mouse_x < target_size[0] and 0 <= mouse_y < target_size[1]:
# 將放大後的像素座標 (mouse_x, mouse_y) 反推回原始的 32x24 矩陣索引
orig_col = int(mouse_x / SCALE_FACTOR)
orig_row = int(mouse_y / SCALE_FACTOR)
# 邊界安全檢查
orig_col = max(0, min(orig_col, 31))
orig_row = max(0, min(orig_row, 23))
# 抓取未經插值的「真實溫度」
current_pixel_temp = raw_matrix[orig_row, orig_col]
# 在滑鼠游標右下方繪製十字與溫度標籤
text_x = mouse_x + 15
text_y = mouse_y + 15
# 防止文字超出右方與下方邊界
if text_x > target_size[0] - 120: text_x = mouse_x - 110
if text_y > target_size[1] - 10: text_y = mouse_y - 15
# 畫一個小小的中心點十字提示
cv2.drawMarker(color_heatmap, (mouse_x, mouse_y), (255, 255, 255),
markerType=cv2.MARKER_CROSS, markerSize=10, thickness=1, line_type=cv2.LINE_AA)
# 秀出滑鼠指著的溫度(黑框白字看得比較清楚)
cv2.putText(color_heatmap, f"{current_pixel_temp:.1f} C", (text_x, text_y),
cv2.FONT_HERSHEY_DUPLEX, 0.5, (0, 0, 0), 3, cv2.LINE_AA)
cv2.putText(color_heatmap, f"{current_pixel_temp:.1f} C", (text_x, text_y),
cv2.FONT_HERSHEY_DUPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA)
# 固定顯示左上角的最高/最低溫
cv2.putText(color_heatmap, f"Max: {max_temp:.1f} C", (15, 40),
cv2.FONT_HERSHEY_DUPLEX, 0.6, (255, 255, 255), 1, cv2.LINE_AA)
cv2.putText(color_heatmap, f"Min: {min_temp:.1f} C", (15, 70),
cv2.FONT_HERSHEY_DUPLEX, 0.6, (255, 255, 255), 1, cv2.LINE_AA)
cv2.imshow(window_name, color_heatmap)
except Exception as e:
pass
if cv2.waitKey(1) & 0xFF == ord('q'):
break
ser.close()
cv2.destroyAllWindows()
print("程式已安全結束。")
if __name__ == "__main__":
main()
這樣一來,原本在 2.4 吋小螢幕裡的熱影像,就能順利以 640x480 的畫面投影到教室的大螢幕上了!就能拿來做生物體溫、植物蒸散作用或環境物理性質的演示。
