サーマルカメラ(サーモ AI デバイス TiD) Python ヒートマップ作成

# はじめに
サーマルカメラとしては、取得した温度データをヒートマップにしたいのです。
MH ソフトウェア&サービスでは、Raspberry Piを使用して実現したいので、温度データ -> ヒートマップ変換は、4種類ほど作成してみました。(高速化が必要だったのです。)

# 使用機材
[サーマルカメラ(サーモ AI デバイス TiD) Python AMG8833 番外編](https://qiita.com/MH-SS/items/4b58de0debc4199c2e05)で使用したwebカメラとPanasonic AMG8833、Raspberry Piを使用して取得した温度データを使用してみます。
![カメラ&AMG8833.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/2b077c95-107e-52c4-4284-316f41b0f0f4.jpeg)
テスト用にwebカメラのレンズ付近に、Panasonic AMG8833を取り付けているだけの簡素な仕様です。
温度データは
![for_gif_320x240_fps2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/e4f6f2c5-18d7-a3e7-bfc2-b4905feddb05.gif)
この動画の一部の、指を開いている個所の温度データです。回転処理をしていませんので、ヒートマップの画像は180度回転しています。

# メインクラス(HeatMap)
このクラスからversion1.py ～ version4.pyを開きます。
それぞれ10回の処理を実行して、処理速度を比較します。処理はRaspberry Pi 4を使用しています。

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np
import PIL
import PIL.Image
import PIL.ImageTk
import time

import cv2

from version1 import Version1
from version2 import Version2
from version3 import Version3
from version4 import Version4


class HeatMap():
    def __init__(self):
        self.heat_map = [
            None,
            Version1(),
            Version2(),
            Version3(),
            Version4()
        ]

    def __call__(self):
        img = None
        timer = ProcessingTime()

        # Version1から4まで処理速度を比較してみます
        for version in range(1, 5):
            # 計測タイマリセット
            timer.get()
            # 処理回数は10回です
            for _ in range(10):
                img = self.heat_map[version](self.parameters)

            message = f'Version{version} {timer.get():.02f}[msec]'
            print(message)

            img = resize(img, self.parameters.size, self.parameters.size)
            cv2.imshow(message, img)
            cv2.waitKey(5000)
        while True:
            pass

    class parameters():
        gain = 10
        max = 40
        min = 20
        offset_x = 0.2
        offset_green = 0.6
        pixels = 8 * 8
        size = 320

class ProcessingTime():
    """
    Return processing time [msec].
    """
    def __init__(self):
        self._start = time.time()

    def get(self) -> float:
        result = (time.time() - self._start) * 1000
        self._start = time.time()
        return result


def resize(image: np.ndarray, width: int, height: int) -> np.ndarray:
    result = cv2.resize(image, (width, height), cv2.INTER_CUBIC)
    return result


if __name__ == '__main__':
    heat_map = HeatMap()
    heat_map()
```

# Version1クラス
webからサンプルを取得して作成したクラスです。ごめんなさい。引用元が不明になってしまいました。
![version1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/d9572f4f-2c73-9ad1-72d7-2a2b7b27ba42.jpeg)

## 処理時間: 3,907[msec]
ヒートマップは、指だな?と感じられます。
for文を多用しているせいか、これ以上の高速化は望めませんでした。色のブレンドはカッコ良いと思います。元ソースに感謝です。


version1.py

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import math

from colour import Color
import numpy as np
from scipy.interpolate import griddata

from temp_data import temp_data


class Version1():
    # クラスにする必要は無いと思います
    def __call__(self, parameters: dict) -> np.array:
        color_depth = 1024
        pixels = temp_data()
        pixels = [
            map_value(
                p, parameters.min, parameters.max, 0, color_depth - 1)
                for p in pixels]

        sqrt = int(parameters.pixels ** 0.5)
        points = [(
            math.floor(ix / sqrt),
            (ix % sqrt)) for ix in range(0, sqrt * sqrt)]

        grid_x, grid_y = np.mgrid[
            0: (sqrt - 1): 32j,
            0: (sqrt - 1): 32j]

        bicubic = griddata(
            points,
            pixels,
            (grid_x, grid_y),
            method='cubic')

        low = Color('darkblue')
        high = Color('red')
        colors = list(low.range_to(high, color_depth))

        img = [[[255] * 3] * bicubic.shape[0]] * bicubic.shape[1]
        img = np.asarray(img)

        # やはりfor文を使うと時間がかかります
        for ix, row in enumerate(bicubic):
            for jx, pixel in enumerate(row):
                rgb = colors[
                    constrain(
                        int(pixel),
                        0,
                        color_depth- 1
                    )].rgb
                img[ix, jx, 0] = rgb[2] * 255
                img[ix, jx, 1] = rgb[1] * 255
                img[ix, jx, 2] = rgb[0] * 255
        img = img.astype('uint8')
        return img


def constrain(val: int, min_val: int, max_val: int) -> int:
    return min(max_val, max(min_val, val))


def map_value(x: int, in_min: int, in_max: int,
              out_min: int, out_max: int) -> float:
    result = 0
    try:
        result = (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
    except:
        pass
    return result


if __name__ == '__main__':
    pass
```

# Version2クラス
Version1クラスで、高速化が望めず、それならばnumpyで処理!と意気込んで作成したクラスです。
![version2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/b9ce4b92-19b5-993e-bf72-f3382f9c0cf2.jpeg)

## 処理時間: 25[msec]
ヒートマップは、なんとか指だな?と感じられます。
黄色が足らない・・・。と感じます。ほぼ、青と赤ですね・・・。
数値と色変換が甘く、赤成分と青成分のみ演算しています。


```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np

from temp_data import temp_data


class Version2():
    # クラスにする必要は無いと思います
    def __call__(self, parameters: dict) -> np.array:
        sqrt = int(parameters.pixels ** 0.5)
        img = np.zeros((sqrt, sqrt, 3), dtype='uint8')
        pitch = (parameters.max - parameters.min) / 255

        for x in range(sqrt):
            for y in range(sqrt):
                temp = temp_data()[x * 8 + y]
                img[x, y, 2] = (temp - parameters.min) / pitch
                img[x, y, 0] = (parameters.max - temp) / pitch

        return img


if __name__ == '__main__':
    pass
```

# Version3クラス
![version3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/34319bc1-b151-9b48-e073-15ba6a68e705.jpeg)

## 処理時間: 3[msec]
もはや、何が表示されているかわかりません。
緑成分を入れようと作成を始めたのですが、温度から緑ってどうするんだろう?と悩んだ結果です。

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np

from temp_data import temp_data


class Version3():
    # クラスにする必要は無いと思います
    def __call__(self, parameters: dict) -> np.array:
        temp = np.array(temp_data())
        temp *= 1.5
        temp = temp.reshape(8, 8)

        blue = temp - parameters.min
        green = 0
        red = parameters.max - temp

        sqrt = int(parameters.pixels ** 0.5)
        img = np.zeros((sqrt, sqrt, 3), dtype='uint8')
        pitch = (parameters.max - parameters.min) / 255
        img[:, :, 0] = 255 - blue / pitch
        img[:, :, 1] = green
        img[:, :, 2] = 255 - red / pitch

        return img


if __name__ == '__main__':
    pass
```

# Version4クラス
![version4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/da7402b2-f375-b8b1-c7fe-387c251cd01b.jpeg)

## 処理時間: 10[msec]
ヒートマップは、指だな?と感じられます。
webでいろいろ情報を集めたところ、シグモイド関数を使うと、良いらしいことが分かりました。
画像データをnumpy.ndarrayで処理できるように、def sigmoid()を作成しました。

現在のサーモ AI デバイス TiDは、この処理を用いています。

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import numpy as np
from temp_data import temp_data


class Version4():
    def __call__(self, parameters: dict) -> np.array:
        self.parameters = parameters

        width = self.parameters.max - self.parameters.min
        temp = np.array(temp_data())
        temp = temp - self.parameters.min
        temp = temp / width
        temp = temp.reshape(8, 8)

        sqrt = int(self.parameters.pixels ** 0.5)
        img = np.zeros((sqrt, sqrt, 3), dtype='uint8')

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

        return img


    def color_bar_rgb(self, x: np.ndarray) -> list:
        """
        Return list of numpy.ndarray.
        """
        x = (x * 2) - 1
        red = sigmoid(x, self.parameters.gain, -1 * self.parameters.offset_x)
        blue = 1 - sigmoid(x, self.parameters.gain, self.parameters.offset_x)

        green = sigmoid(x, self.parameters.gain, self.parameters.offset_green)
        green += (1 - sigmoid(
            x, self.parameters.gain, -1 * self.parameters.offset_green))
        green = green - 1.0

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

        return [blue, green, red]


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


if __name__ == '__main__':
    pass
```

# まとめ
| Version1 | Version2 | Version3 | Version4 |
| :-: | :-: | :-: | :-: |
| ![version1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/d9572f4f-2c73-9ad1-72d7-2a2b7b27ba42.jpeg) | ![version2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/b9ce4b92-19b5-993e-bf72-f3382f9c0cf2.jpeg) | ![version3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/34319bc1-b151-9b48-e073-15ba6a68e705.jpeg) | ![version4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/da7402b2-f375-b8b1-c7fe-387c251cd01b.jpeg) |
| 指です | 指です | なんとなく指かも? | 指です |

***

[サーマルカメラ(サーモ AI デバイス TiD) Python AMG8833 番外編](https://qiita.com/MH-SS/items/4b58de0debc4199c2e05)