サーマルカメラ(サーモ AI デバイス TiD) Python AMG8833 番外編

Panasonic 赤外線アレイセンサ Grid-EYE AMG8833からの温度データを、Raspberr PiからI2C通信で取得します。
データの取得自体は、Adafruit_AMG88xxをimportして、self._sensor = Adafruit_AMG88xx()などとして、インスタンス化すると、self._sensor.readPixels()で取得出来ます。

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
AMG8833 script.
===============
Human-detectable distance: MAX 7[m]
3.3[V] I2C(68h or 69h)
8x8 array 0~80[deg]
Nick name: Grid-EYE
"""
i2c_enable = False

# Raspberry Pi以外の環境でのディバック用です
try:
    from Adafruit_AMG88xx import Adafruit_AMG88xx
    i2c_enable = True
except:
    i2c_enable = False


class Sensor():
    def __init__(self, *args, **kwargs):
        self.enable = False
        self._sensor = None

        if i2c_enable:
            self.enable = True
            try:
                self._sensor = Adafruit_AMG88xx()
            except:
                self.enable = False

    @property
    def high(self):
        return 80.0

    @property
    def low(self):
        return 0.0

    @property
    def pixels(self):
        result = None
        if self._sensor is not None:
            result = self._sensor.readPixels()
        return result
```

class Sensorには、high,lowプロパティがありますが、amg8833.pyをimportするThermoクラスに必要です。

thermo.py

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
"""
MH Image process Thermo script.
===============================
Temperature data processing from device.
AMG8833
D6T-44L-06
"""
import numpy as np
import random

cv2_installed = False
try:
    import cv2
    cv2_installed = True
except ModuleNotFoundError:
    cv2_installed = False

# mhipはMH ソフトウェア&サービスの画像処理用パッケージです
import mhip


class Thermo(mhip.Method):
    def __init__(self, sensor, *args, **kwargs):
        """
        sensor='amg8833' or 'd6t_44l_06'
        Temepature adjust formula is,
        temp = (a*length^2 + b*length + c) * temp
        この計算式は、距離に対する温度を補正します
        """
        # sensorを指定する事で、OMRON D6T-44L-06とPanasonic AMG8833が変更できます
        if sensor.lower() == 'd6t_44l_06':
            # 通常、importは先頭に書きますが、条件分岐がある為、ここでimportします
            # さらに、単独でこのスクリプトを動かす場合とimportされる時で、importする場所を変更しています
            if __name__ == '__main__':
                from d6t_44l_06 import Sensor
            else:
                from .d6t_44l_06 import Sensor
        elif sensor.lower() == 'amg8833':
            if __name__ == '__main__':
                from amg8833 import Sensor
            else:
                from .amg8833 import Sensor

        super().__init__(*args, **kwargs)

        self.sensor = Sensor(*args, **kwargs)

        self.a = kwargs.get('a', 0)
        self.b = kwargs.get('b', 1)
        self.c = kwargs.get('c', 0)
        self._length = kwargs.get('length', 1)

        self.map_max = kwargs.get('map_max', 35.0)
        self.map_mix = kwargs.get('map_min', 9.5)
        self._size = kwargs.get('size', (320, 320))
        self._shutdowning = False

        # 分析結果は、mhipで必要です
        self._analysis = {
            'en': {'Max Temp.': 
                self.sensor.high, 'Min Temp.': self.sensor.low},
            'ja': {'最高温度': 
                self.sensor.high, '最低温度': self.sensor.low}}

        self._min = 0
        self._max = 0

        self.gain = 10
        self.offset_x = 0.2
        self.offset_green = 0.6

    @property
    def a(self):
        return self._a
    @a.setter
    def a(self, value):
        self._a = value

    @property
    def b(self):
        return self._b
    @b.setter
    def b(self, value):
        self._b = value

    @property
    def c(self):
        return self._c
    @c.setter
    def c(self, value):
        self._c = value

    def colorBarRGB(self, x):
        # 2020/6/24: red and blue is reversed...
        x = (x * 2) - 1
        blue = self.sigmoid(x, self.gain, -1 * self.offset_x)
        red = 1 - self.sigmoid(x, self.gain, self.offset_x)
        green = self.sigmoid(x, self.gain, self.offset_green) \
            + (1 - self.sigmoid(x, self.gain, -1 * self.offset_green))
        green = green - 1.0

        blue = blue * 255
        green = green * 255
        red = red * 255

        return [blue, green, red]

    def __del__(self):
        self._shutdowning = True

    # Rapebrry Pi以外の環境でのディバック用の温度データです
    def _dummy(self):
        result_64 = [
            [22.75, 23.5, 24.75, 24.5, 24.5, 23.75, 23.0, 23.75, 22.5, 23.5, 27.75, 27.25, 24.25, 22.75, 22.5, 23.0, 22.75, 24.25, 28.25, 27.5, 26.75, 24.0, 22.25, 22.25, 24.0, 26.5, 28.5, 28.0, 27.25, 24.25, 22.75, 21.75, 27.0, 27.75, 27.75, 28.25, 28.5, 24.75, 23.25, 23.5, 28.0, 27.0, 27.0, 28.75, 29.25, 26.75, 24.0, 23.75, 28.25, 27.0, 27.0, 27.5, 28.25, 26.5, 24.25, 24.0, 27.25, 27.25, 25.75, 25.5, 26.0, 25.75, 24.5, 24.25],
            [24.5, 25.0, 25.5, 26.5, 25.5, 25.5, 24.75, 25.5, 24.25, 25.0, 27.75, 29.25, 26.75, 25.0, 24.25, 24.75, 24.75, 27.0, 29.5, 30.5, 29.25, 28.75, 26.5, 26.0, 28.5, 30.0, 31.0, 37.75, 31.0, 31.0, 31.0, 31.0, 29.75, 30.75, 31.0, 31.0, 37.75, 31.5, 37.75, 31.5, 30.5, 30.5, 37.75, 31.5, 31.5, 31.5, 31.75, 37.75, 30.25, 30.25, 30.75, 37.75, 37.75, 31.75, 37.75, 32.0, 30.25, 30.5, 37.75, 31.0, 31.5, 31.5, 31.5, 37.75],
            [22.75, 22.75, 22.75, 24.25, 24.5, 24.75, 24.0, 24.0, 22.25, 22.5, 23.0, 24.0, 24.5, 24.25, 23.75, 23.75, 22.0, 22.25, 23.0, 24.0, 24.5, 24.0, 23.5, 23.0, 21.5, 22.25, 22.75, 26.5, 25.25, 22.75, 23.25, 22.5, 21.75, 21.75, 22.5, 26.75, 25.25, 22.75, 22.25, 22.0, 22.0, 22.25, 24.75, 26.0, 25.75, 24.5, 21.75, 21.25, 21.75, 22.0, 24.25, 24.5, 24.75, 25.0, 22.0, 22.0, 21.25, 21.75, 24.0, 24.5, 25.25, 24.75, 21.5, 21.5]
            ]
        result = [
            [22.75, 23.5, 24.75, 24.5, 24.5, 23.75, 23.0, 23.75, 22.5, 23.5, 27.75, 26.25, 24.25, 22.75, 22.5, 23.0],
            [24.5, 25.0, 25.5, 26.5, 25.5, 25.5, 24.75, 25.5, 24.25, 25.0, 27.75, 27.25, 26.75, 25.0, 24.25, 24.75],
            [22.75, 22.75, 22.75, 25.25, 26.5, 26.75, 24.0, 24.0, 22.25, 22.5, 23.0, 24.0, 24.5, 24.25, 23.75, 23.75]
            ]
        for i in range(len(result)):
            for j in range(len(result[i])):
                result[i][j] = result[i][j] * 1.15
        return result[random.randint(0, 2)]

    @property
    def enable(self):
        return self.sensor.enable

    @property
    def length(self):
        return self._length
    @length.setter
    def length(self, value):
        self._length = value
        self.sensor.length = self._length

    @property
    def max(self):
        return self._max

    @property
    def min(self):
        return self._min

    class Parameter():
        def __init__(self):
            pass

    # mhipで使用する処理済画像を作成します
    @property
    def processed_image(self):
        result = None

        pixels = []
        if self.sensor.enable:
            pixels = self.sensor.pixels

        temp_width = self.map_max - self.map_mix

        if pixels is None:
            # Rainbow!!!
            #pixels = []
            #_ratio = temp_width / (self.sensor.x * self.sensor.y)
            #for i in range(self.sensor.x * self.sensor.y):
            #    #pixels.append(round(self.sensor.high * random.uniform(0.0, 0.475), 1))
            #    pixels.append(i * _ratio + self.map_mix)
            pixels = self._dummy()
        elif len(pixels) == 0:
            pixels = self._dummy()

        for i, val in enumerate(pixels):
            pixels[i] = \
                (
                    self._a * self._length ** 2 \
                    + self._b * self._length \
                    + self._c
                ) * val

        self._min = min(pixels)
        self._max = max(pixels)

        self._analysis = {
            'en': {'Max Temp.': self._max, 'Min Temp.': self._min},
            'ja': {'最高温度': self._max, '最低温度': self._min}}

        # vol.4 熱画像のバージョン
        # 色々とweb上の処理を試させていただきました ありがとうございました
        # 最終的に、自分で作ったこの処理がシンプルで、まあまあの熱画像になると思います
        # やはりnumpy処理は速いです
        temp = np.array(pixels)
        temp = temp - self.map_mix
        temp = temp / temp_width
        sqrt = int(len(temp)**0.5)
        temp = temp.reshape(sqrt, sqrt)

        img = np.zeros(
            (sqrt, sqrt, 3),
            dtype='uint8')

        img[:, :, 0] = self.colorBarRGB(temp)[0]
        img[:, :, 1] = self.colorBarRGB(temp)[1]
        img[:, :, 2] = self.colorBarRGB(temp)[2]

        img = cv2.resize(img, (self.size[0], self.size[1]), cv2.INTER_CUBIC)

        return img

    def sigmoid(self, x, gain=1, offset_x=0):
        return ((np.tanh(((x + offset_x) * gain) / 2) + 1) / 2)

    @property
    def size(self):
        return self._size
    @size.setter
    def size(self, value):
        self._size = value

    def stop(self):
        self._shutdowning = True
        self.sensor.stop()


def main():
    # 単体で動かして、カメラ画像に熱画像を足しています
    th = Thermo(
        #sensor='d6t_44l_06',
        sensor='amg8833',
        map_max=35.0,
        map_min=10.0,
        a=0.000001,
        b=0.00025,
        c=1.186,
        size=(160, 160),
    )

    vcap = cv2.VideoCapture(0)

    while True:
        _, capture = vcap.read()
        ratio = 1
        #capture = cv2.resize(
        #    capture, 
        #    (
        #        int(capture.shape[1] * ratio),
        #        int(capture.shape[0] * ratio)
        #    ))

        thermo = th.processed_image
        thermo = thermo[:,:,::-1]
        thermo = np.rot90(thermo, 1)

        # attachはカメラ画像に、引数最後の座標に熱画像を足します
        image = attach(capture, thermo, (0,0))
        # full_blendは、カメラ画像と熱画像をブレンドします
        # 熱画像のサイズを調整すると、カメラ画像と合わせられます
        #image = full_blend(capture, thermo)

        cv2.imshow('thermo', image)
        cv2.waitKey(50)
        print(f'{th.max:0.1f}', f'{th.min:0.1f}')
    pass


def attach(large, small, offset):
    result = large
    result[
        offset[1]: offset[1] + small.shape[0],
        offset[0]: offset[0] + small.shape[1]] = small
    return result


def full_blend(large, small):
    small = cv2.resize(
        small, 
        (large.shape[1], large.shape[0]))
    alpha = 0.3
    result = cv2.addWeighted(
        large, alpha, 
        small, 1 - alpha, 0)
    return result


if __name__ == '__main__':
    main()
```

mhip.Methodクラスです。AMG8833、D6T-44L-06を扱うThermoクラスも、画像を扱います。
Methodクラスを継承します。

```python
class Method():
    def __init__(self, *args, **kwargs):
        self.area = [0, 0, 0, 0]
        self.judge = True
        self.output = 0
        self.analysis = {
            'en': {'Info': 'None'},
            'ja': {'検査結果': 'ありません'}
        }

        self.color_channel = kwargs.get('color_channel', 'rgb')
        self.source = kwargs.get('source', None)

        self.parameter = self.Parameter()

    @property
    def analysis(self):
        return self._analysis
    @analysis.setter
    def analysis(self, value):
        self._analysis = value

    @property
    def judge(self):
        return self._judge
    @judge.setter
    def judge(self, value):
        if type(value) == bool:
            self._judge = value

    @property
    def output(self):
        return self._output
    @output.setter
    def output(self, value):
        if mhfn.isnumeric(value):
            self._output = value

    @property
    def processed_image(self):
        return self.source

    @property
    def source(self):
        return self._source
    @source.setter
    def source(self, value):
        source = Image(source=value, color_channel=self.color_channel)
        self._source = source.numpy
```

***

こんな感じで、指の熱画像が表示されます。
![for_gif_320x240_fps2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/e4f6f2c5-18d7-a3e7-bfc2-b4905feddb05.gif)


[YouTube: サーマルカメラ(サーモ AI デバイス TiD) Python編](https://www.youtube.com/watch?v=fgwBsISDtXo)
[web: サーモ AI デバイス TiD Python D6T編](https://mhss.server-on.net/?f401c=Index&ae504=tid3/development/python/d6t) (URLが変更されました)