Тепловизор из MLX90640 на CircuitPython и OpenCV

Датчик MLX90640 представляет собой матрицу микроболометров, с помощью которой можно сделать настоящий тепловизор, хоть и с невысоким разрешением — всего 32 x 24 точки. Мы уже рассмотрели, как получить из него данные в виде температурного массива (урок про mlx90640 на circuitpython). На этот раз займемся визуализацией этих данных на компьютере.

Для эксперимента используем один из контроллеров с поддержкой CircuitPython, например: Графит-S2 или Графит-RP2040. Оба этих контроллера имеют разъём QIIC для удобного соединения модулей по шине I2C. Так что мы просто соединяем модуль MLX90640 с контроллером одним кабелем QIIC, а затем контроллер соединяем с компьютером USB-кабелем. На этом сборка нашего тепловизора будет закончена.

Графит-S2 и тепловизор mlx90640 robotclass qiic

СПИСОК НЕОБХОДИМЫХ КОМПОНЕНТОВ

Для выполнения примеров из данного урока, кроме модуля MLX90640, потребуется отладочная плата Графит-S2 или аналогичная с установленным CircuitPython и кабель для шины QIIC. Если вам не хватает что-то из этого, можно добавить эти компоненты в корзину прямо здесь и затем оформить заказ в нашем интернет-магазине.

Компоненты для урока "Тепловизор из MLX90640 на CircuitPython и OpenCV" на shop.robotclass.ru
В корзину
В корзину
В корзину

Программы

Для работы устройства нам понадобятся две программы. Задача той, которая будет работать на контроллере, будет заключаться в получении данных от MLX90640 и в отправке их через последовательный порт (COM-порт) на компьютер.

В свою очередь, программа на компьютере будет получать данные из COM-порта и выводить их на дисплей в виде цветного изображения.

Программа для CircuitPython

Получить температурную матрицу нам удалось ещё в прошлом уроке. В том же уроке мы вывели значения матрицы в последовательный порт, в консоль python (REPL). Однако, для того чтобы принять эти данные на компьютере в своей программе, нам нужно будет провести некоторую манипуляцию с CircuitPython.

Дело в том, что CircuitPython умеет подключать к USB сразу два последовательных порта. Один, как уже упоминалось, ассоциирован с консолью REPL — нам он не подходит. А вот другой, служит именно для передачи данных, но он выключен по умолчанию.

Чтобы его включить, нужно создать в корне диска CircuitPython файл boot.py и добавим в него две строчки:

import usb_cdc
usb_cdc.enable(console=False, data=True)

Этим мы отключаем REPL, и включаем порт данных. После изменения файла, контроллер нужно сбросить.

Теперь ищем необходимые библиотеки и пишем программу.

Дополнительные библиотеки

Кроме библиотек из прошлого урока про MLX90640, установим ещё кое что.

Наш датчик выдает пакеты по 768 байт с заданной частотой. Будем отправлять их в компьютер с помощью простого протокола, реализованного в библиотеке SerialFlow. Так что нам необходимо подключить ещё и эту библиотеку:

  • robotclass_serialflow

Также, для работы с последовательным портом, реализованным через USB, потребуется такая библиотека:

  • usb_cdc.

Программа на CircuitPython

Исходный код программы ниже:

import board
import busio
import adafruit_mlx90640
import robotclass_serialflow
import usb_cdc

# проверяем, подключен ли последовательный порт для данных
if usb_cdc.data is None:
    print("Need to enable USB CDC serial data in boot.py!")
    while True:
        pass

# инициализация шины I2C
i2c = busio.I2C(board.SCL, board.SDA, frequency=800000)
# инициализация датчика
mlx = adafruit_mlx90640.MLX90640(i2c)
mlx.refresh_rate = adafruit_mlx90640.RefreshRate.REFRESH_4_HZ

# инициализация библиотеки SerialFlow
# в качестве аргумента - последовательный порт для данных
sflow = robotclass_serialflow.SerialFlow(usb_cdc.data)

# настройка структуры пакета
# размер данных - 1 байт
# данных в пакете - 768
sflow.setPacketFormat(1, 768, 0)

frame = [0] * 768
while True:
    try:
        mlx.getFrame(frame)
    except ValueError:
        continue

    for v in frame:
        sflow.setPacketValue(int((v-15)*10) & 0xFF) # добавление числа в пакет
    sflow.sendPacket() # отправка пакета

Здесь может потребоваться небольшое пояснение формулы в самом конце программы:

int((v-15)*10) & 0xFF

Дело в том, что датчик выдаёт вещественное число для температуры каждой точки, а передавать в последовательный порт мы будем байт — целое число от 0 до 255. Конечно, можно просто округлить температуру — int(v), но тогда мы потеряем всё, что после запятой.

Пусть наша камера смотрит на объекты внутри жилого помещения. Тогда, чтобы по максимуму использовать размер байта, сделаем следующее. Вычтем из температуры каждой точки 15 градусов Цельсия, так как в комнате вряд-ли есть что-то холоднее. Затем умножим результат на 10. Допустим, что самый теплый объект в комнате — это человек с температурой 40 градусов, тогда наша формула даст максимальное число — (40-15)*10 = 250. Это укладывается в байт.

Напоследок, на всякий случай, обрежем один байт от результата, чтобы точно влезть в байт при внезапном появлении, например, паяльника — «& 0xFF».

При наблюдении за объектами с температурой ниже нуля, потребуется другая формула. Например, такая:

int((v+40)*3) & 0xFF

В таком случае мы увидим объекты c температурой от -40 до +45.

Сохраняем код в файле code.py и проверяем, что данные идут. Для этого достаточно открыть любой терминал последовательного порта, например «Монитор порта» в Arduino IDE и указать скорость обмена 115200. В терминале должен появится поток непонятных символов — значит всё работает.

Программа для компьютера

Для визуализации данных используем пакет OpenCV — это, пожалуй, самый быстрый способ создать окно и вывести в него какое-то изображение. Задача программы — принимать данные от контроллера по последовательному порту, распаковывать их и отображать в окне. Нам потребуется два дополнительных python-модуля:

  • com_monitor.py — организует приём данных в отдельном потоке;
  • SerialFlow.py — нужен для распаковки данных, упакованных в пакеты при отправке.

Программа будет работать в многопоточном режиме: один поток формирует изображение и выводит его в окно, второй — принимает данные из порта.

Для визуализации данных используем простой алгоритм: чем холоднее точка, тем синяя компонента цвета больше, а красная — меньше. Таким образом, самая горячая точка будет красной, а самая холодная — синей.

Картинка 32×24 — слишком маленькая, так что мы увеличим её в 10 раз. Сделаем это так: вместо точки, будем рисовать закрашенный квадрат со стороной 10 пикселей. При желании, можно легко поменять масштаб константой DOT_SIZE.

from threading import Thread
import queue
import cv2
import numpy as np
from com_monitor import ComMonitorThread, FMT_VECTOR

COM = 'COM3' # номер COM-порта
BAUD_RATE = 115200 # скорость COM-порта

DOT_SIZE = 10 # масштаб изображения

class Thermo(Thread):
    def __init__( self ):
        Thread.__init__(self)
        self.data_q = queue.Queue()
        self.error_q = queue.Queue()

    def poll( self ):
        # получение данных
        colors = self.data_q.get()
        if not colors:
            return

        # построение изображения
        img = np.zeros((HEIGHT,WIDTH,3), np.uint8)
        for x in range(32):
            for y in range(24):
                c = colors[y*32+x]
                color = ( 255 - c, 0, c )
                cv2.rectangle( img, 
                               (x*DOT_SIZE, y*DOT_SIZE), 
                               (x*DOT_SIZE+DOT_SIZE, y*DOT_SIZE+DOT_SIZE), 
                               color, -1)

        # вывод изображения в окно
        cv2.imshow('thermal', img)  

if __name__ == '__main__':
    # создание окна
    cv2.namedWindow( "thermal", cv2.WINDOW_AUTOSIZE )

    # запуск генератора изображения
    thermo = Thermo()
    thermo.start()

    # запуск монитора COM порта
    com_monitor = ComMonitorThread(
        thermo.data_q,
        thermo.error_q,
        COM,
        BAUD_RATE,
        data_format = FMT_VECTOR,
        value_size = 1,
        packet_size = 768,
        separator = 0)

    com_monitor.start()

    while True:
        try:
            thermo.poll()
        except:
            cap = None
            thermo = None
            raise
            
        thermo.join()

        ch = cv2.waitKey(5)
        if ch == 27:
            break

    com_monitor.join()
    cv2.destroyAllWindows() 			

Узнать имя последовательно порта можно из диспетчера устройств в Windows, или в папке /dev в Linux.

Запускаем программу и наблюдаем то, что видит наш датчик.

Полезные ссылки

Библиотека SerialFlow для CircuitPython:

https://download.robotclass.ru/circuitpython/libs/robotclass_serialflow.mpy

Приложение SFMonitor (файлы com_monitor.py и SerialFlow.py):

https://github.com/makeitlab/software_tools/tree/master/SFMonitor


Изменено:

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.