Help us understand the problem. What is going on with this article?

[C#/WindowsIoT] RaspberryPi3にI2Cサーマルカメラ(サーモグラフィ)をつなげて温度を画像化する

もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4f

WinIoT on ラズパイでのI2C通信関連
- [C#/WinIoT/I2C] ラズパイ+WindowsIoTCore+C# で9軸センサ(MPU-9150)の値をとる

やりたいこと・やったこと

電子工作で、ラズパイ3にWindows IoT Coreを入れて、サーマルカメラをつなげて、いわゆるサーモグラフィを作ってみたい。

さっと調べたところ、一番安くて(amazonで7000円くらい)手に入りやすそうな「MLX90640」を使おうと思うが、そのサーマルカメラがI2C接続のようなので、以前、9軸センサで練習したI2Cを生かせそう。

と思い立って作ったのが下記のようなもの。作るうえでいろいろ調べた事、やったことをメモしておく。

■動画
https://youtu.be/cPm-WGY2Y8c

■コード一式
https://github.com/tera1707/ThermalCamera

■写真(私の手をサーマル撮影)
image.png

使った機材

全体の流れ

  • 回路作成
  • サーマルカメラとのI2C通信実装
    • VisualStudio2019ソリューションの設定
    • I2C通信のデータ送受信の準備
    • 初期化処理
    • EEPROM読み出し
    • サーマル生データを取得
    • 生データから温度に変換
    • 温度データからサーマル画像作成

回路作成

下図のような回路をつくる。
image.png
実際の配線
image.png
電源は、作成中、デバッグ中はコンセント~USBで。実際動かすときはモバイルバッテリーでの予定。

サーマルカメラとのI2C通信実装

以前実験した9軸センサのI2Cをもとに、通信部分の実装を行う。

VisualStudio2019ソリューションの設定

9軸センサのI2Cの方で同じ作業をしているので、そちら参照。

I2C通信のデータ送受信の準備

データシートによると、このサーマルカメラは、2バイトを一単位としてデータのやり取りを行う。

9軸センサ(MPU-9150)の場合は、1バイト単位でデータをやり取りしていたので、そこが違う。
9軸センサの場合は、下記のようにしていた。

9軸センサの送受信例.cs
// 書き込み:1バイト目に書き込みたいレジスタアドレス、2バイト目に書く内容を載せて送信
WriteBuf = new byte[] { 0x6B, 0x00 };
I2CAccel.Write(WriteBuf);

// 読み込み:1バイト目に読み込みたいレジスタアドレスを載せておくると、
// そのアドレスを先頭にした、指定バイト数分のデータが返ってくる
// (バイト数の指定は、ReadBufの配列数で行う(下記の場合はnew byte[1]なので1バイト))
WriteBuf = new byte[] { 0x75 };
ReadBuf = new byte[1];
I2CAccel.WriteRead(WriteBuf, ReadBuf);

今回のサーマルカメラでは、下記のようなメソッドを作って、ushortでアドレス指定、データ指定を行うようにした。

サーマルカメラの送受信例.cs
// 書き込み
private void WriteRegisterData(ushort writeAddr, ushort data)
{
    // 書き込むデータ作成(最初の2バイトが書き込み先アドレス、その後の2バイトがそこに書き込むデータ)
    var writeByteData = new byte[]
    {
        (byte)(writeAddr / 0x100),  (byte)(writeAddr % 0x100),      // 書き込み先アドレス
        (byte)(data / 0x100),       (byte)(data % 0x100),           // 書き込みデータ
    };

    // 書き込み実施
    I2CThermalCamera.Write(writeByteData);
}

// 読み込み
private ushort[] ReadRegisterData(ushort readAddr, int NumberOfData)
{
    // 返すデータ(受信したbyteデータをushortに直したもの)
    ushort[] ret = new ushort[NumberOfData];

    // アドレスを上位/下位に分解
    var destAddr = new byte[] { (byte)(readAddr / 0x100), (byte)(readAddr % 0x100) };
    // 受信用バッファを確保(このサーマルカメラのレジスタは1つで2バイト)
    var readBuf = new byte[NumberOfData * 2];

    // 読み込み実施
    I2CThermalCamera.WriteRead(destAddr, readBuf);

    // 読み込んだbyteデータをushortに直す
    for (int i = 0; i < NumberOfData; i++)
    {
        ret[i] = (ushort)(readBuf[2 * i] * 0x100 + readBuf[2 * i + 1]);
    }
    return ret;
}

初期化処理

下記のような流れで初期化を行う。
(というか、I2cDevice.FromIdAsync();まではWinIotのI2C通信の準備)

初期化処理時の通信.cs
public async Task InitThermalCamera()
{
    // すべてのI2Cデバイスを取得するためのセレクタ文字列を取得
    string aqs = I2cDevice.GetDeviceSelector(I2cDeviceName);
    DeviceInformationCollection dis = null;

    try
    {
        // セレクタ文字列を使ってI2Cコントローラデバイスを取得
        dis = await DeviceInformation.FindAllAsync(aqs);

        if (dis.Count == 0)
        {
            Debug.WriteLine("I2Cコントローラデバイスが見つかりませんでした");
            return;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.Message);
        throw;
    }

    // I2Cアドレスを指定して、デフォルトのI2C設定を作成する
    var settings = new I2cConnectionSettings(ThermalCameraI2CAddress);

    // バス速度を設定(FastMode:400 kHz)(指定しないと、標準設定(StandardMode:100kHz)になる)
    settings.BusSpeed = I2cBusSpeed.FastMode;

    // 取得したI2Cデバイスと作成した設定で、I2cDeviceのインスタンスを作成
    I2CThermalCamera = await I2cDevice.FromIdAsync(dis[0].Id, settings);

    if (I2CThermalCamera == null)
    {
        Debug.WriteLine(string.Format("スレーブアドレス {0} の I2C コントローラー {1} はほかのアプリで使用されています。他のアプリで使用されていないか、確認してください。", settings.SlaveAddress, dis[0].Id));
        return;
    }

    // サーマルカメラの設定
    try
    {
        // コントロールレジスタを取得
        var ctrreg = ReadRegisterData(0x800D, 1).FirstOrDefault();

        // リフレッシュレートを変更する(現在のコントロールレジスタを読み出して、そいつに対して変更を実施)
        var ctrregset = (ushort)(ctrreg | 0x0300);
        WriteRegisterData(0x800D, ctrregset);
    }
    catch (Exception ex)
    {
        Debug.WriteLine("デバイスとの通信に失敗しました。: " + ex.Message);
        return;
    }

    // EEPROM読み出し
    this.MLX90640_DumpEE();
}

ここで「リフレッシュレートを変更」しているが、今回は32Hzを使用した。(最速の64Hzにしなかったことに特に理由なし。)
image.png

試したところ、リフレッシュレートの設定によって、下のデータ取得のところで出てくる「isReady」フラグ(0x8000のbit3)がONになるまでの時間が変わってくるので注意。

※以降の内容について

ここから下は、ほぼサンプルプログラムを基に作成しています。
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example

サンプルはC言語で描かれていますので、それをもとに、C#の処理を作成しました。

また、EEPROMの個別の中身や生データから温度に変換する計算など、データシートをじっくり読めばわかるのかもしれませんが、今回はあまり理解せずに(というか難しくてすぐに理解できなかった)サンプルを基に作らせて頂いてます。

EEPROM読み出し

上の初期化の中の一番下、下記の部分。

// EEPROM読み出し
this.MLX90640_DumpEE();

EEPROMに保存されているパラメータが、読み出したサーマルデータの生データを温度の値に変換する際に使われるので、EEPROMから各種パラメータを読みだして、所定のクラスに格納しておく。(ここではParamsMLX90640クラスにいれた。)

データシートより、EEPROMはレジスタ0x2400番地から0x273F番地。
image.png

※EEPROMの読み出し項目は非常に多数あるので、コードは、githubのこちらを参照。

サーマル(生)データを取得 ~ 生データを温度データに変換

まずは、I2Cでサーマルの生データを取得する。
生データは温度データではないので、EEPROMから読み出したパラメータを使って温度に変換が必要。

データ取得は、データシートの「Measurement Flow」の項の流れに沿った処理を行う。
image.png

生データ読み出し.cs
public double[] GetTemperatureData()
{
    for (int i = 0; i < 2; i++)
    {
        byte isReady = 0;
        while (isReady == 0)
        {
            // ステータスレジスタ取得
            isReady = (byte)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0008);
        }

        //// ステータスレジスタ書き込み(MeasurementStartをON)
        WriteRegisterData(0x8000, 0x0030);

        // アIRデータ取得
        // 0x0400~0x06FF:IRデータ
        // 0x0700~0x070F:Ta_Vbe、CP.GAIN
        // 0x0720~Ta_PATA,CP,VddPix
        var frameDataS = ReadRegisterData(0x0400, FrameDataLength);

        // ステータスレジスタ読み出し(SubPage番号)
        //ReadRegisterData(0x8000, 1);
        StatusRegister = (ushort)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0001);

        // コントロールレジスタ読み出し
        ControlRegister = ReadRegisterData(0x800D, 1).FirstOrDefault();

        /////////////////////////
        // データ読み出し終了、データから温度への変換計算実施
        /////////////////////////
        var ta = this.MLX90640_GetTa(frameDataS, CamParameters);
        double tr = ta - 8;
        double[] ret = new double[FrameDataLength];

        // 生データを温度データに変換
        MLX90640_CalculateTo(frameDataS, CamParameters, 0.95, tr, ret);

        for (int l = 0; l < frameDataS.Length; l++)
        {
            if (ret[l] > 0.0)
            {
                TotalFrameData[l] = ret[l];
            }
        }
    }

    return TotalFrameData;
}

生データ取得

メソッドMLX90640_CalculateTo()より上は、生データ取得処理。

生データを取る際、for分で2回回している。
データを採る際、「Subpage」という番号も一緒にとるのだが、これで全体のどの部分のデータが取れているかがわかる。
具体的には、データは下記の図のようなイメージで2回とって1画面分のデータとなる。
image.png

ステータスレジスタの設定により2パターンの取れ方があるが、今回は上のパターン(横一行分のデータが1行飛ばしで取れるパターン)を使った。つまり、subPageが0のデータは奇数行目のデータ、subPageが1のデータが1のデータは偶数行目のデータとなっている。

温度データに変換

そのsubPageの値と、取れてきた1行飛ばしのデータは、生データを温度データに変換するメソッドMLX90640_CalculateTo()の中で使っている。(中の計算ロジックは難しいのであまり見ず。)

変換した温度データを、最終的に配列に格納。(ここではTotalFrameData[])

この配列の中身が、サーマルカメラに映った画面(32*24)の1ピクセルごとの温度の値となる。

温度データからサーマル画像作成

温度データを画面に表示するための値にさらに変換する。
画面に温度を表す点を打つ方法は、こちらの以前の記事を参照。

温度の値を32*24のピクセルの描画データに変換する.cs
private async Task<BitmapImage> DoubleToRaindowColor(double[] totalFrameData)//temp:温度の値
{
    int width = 32;
    int height = 24;
    byte[] data = new byte[width * height * 4];

    for (int i = 0; i < width; i++)
    {
        for (int j = 0; j < height; j++)
        {
            // 指定の温度下限~上限の値を、0.0~1.0の値に変換する
            var v = TemperatureTo0to1Double(totalFrameData[i + j * width]);

            // 0.0~1.0の値を、虹色を表すバイト列に変換する
            var c = ColorScaleBCGYR(v);

            data[4 * (i + j * width)] = c.Item4;            // Blue
            data[4 * (i + j * width) + 1] = c.Item3;        // Green
            data[4 * (i + j * width) + 2] = c.Item2;        // Red
            data[4 * (i + j * width) + 3] = c.Item1;        // alpha
        }
    }

    // サーマル画像を作成
    WriteableBitmap bitmap = new WriteableBitmap(width, height);
    InMemoryRandomAccessStream inMRAS = new InMemoryRandomAccessStream();
    BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, inMRAS);
    encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96.0, 96.0, data);
    await encoder.FlushAsync();
    BitmapImage bitmapImage = new BitmapImage();
    bitmapImage.SetSource(inMRAS);

    return bitmapImage;
}

上記の中の、指定の温度下限~上限の値を、0.0~1.0の値に変換するメソッドColorScaleBCGYR()は、こちらのサイトを参考にさせていただいています。ありがとうございます。

完成

これで作成したbitmapを画面に表示したら、サーモグラフィの完成。
image.png

意外となめらかに動いているが、サーマルカメラの解像度が32*24と結構低いので、モザイク状に見える。最初、解像度、一桁間違えてないか?320*240の間違いでは?と思ったが、32*24であってた。
サーマルカメラは、割と解像度が低いものらしい。(高いものには、もっと高解像度のものもあるが、手が出ない)

コード一式

ここまでで挙げてきた初期化やらデータ取得やらのメソッド以外にも、画面だったりC++のサンプルをもとに作ったEEPROM読み込み機能やらが多数ある。下記に一式置いているので参照ください。

https://github.com/tera1707/ThermalCamera




■191116 追記

コンセントから離れても動かせるよう、携帯の乾電池式充電器に接続。
あとサーマルカメラもブランブランならないよう、手抜きだが形だけユニバーサル基盤にくっつけて一旦のできあがり。
電気電子やらメカのプロの方からしたら怒られそうだが、個人的には手作り感満載で、落としたら一撃死しそうな感じがたまらなく良い。

image.png
image.png
image.png

↓↓↓敬礼する、メガネをかけた嫁
image.png
※真ん中の四角の中の、3*3マスの平均温度を画面下に表示しているのだが、
 体温が異常に低く見えてる。放射率?とかパラメータ調整必要なのかも。

参考

公式ページ
https://shop.pimoroni.com/products/mlx90640-thermal-camera-breakout

データシート
https://cdn.sparkfun.com/assets/7/b/f/2/d/MLX90640-Datasheet-Melexis.pdf

値の大きさをサーモグラフィのような色に変換する
https://qiita.com/krsak/items/94fad1d3fffa997cb651

サンプルプログラム(arduino)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/tree/master/Firmware

通信手順(どういうデータが取れるか、とか通信のお作法の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_API.cpp

通信ドライバ(データの送り方の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_I2C_Driver.cpp

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away