用電腦顯示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 的畫面投影到教室的大螢幕上了!就能拿來做生物體溫、植物蒸散作用或環境物理性質的演示。




« 上一篇 Previous 較舊的文章 下一篇 Next » 較新的文章