コンテンツにスキップ

Top

/

Ubuntu で PureThermal 2 に接続した Flir Lepton 3.5 を OpenCV(C++言語) でプログラムから制御する方法

タイトル

PureThermal 2 にセットした Flir Lepton 3.5 から温度データ(160x120) の配列を取得。

それらの値を 0-255 の範囲にシュリンクして、OpenCVで使えるcv::Mat形式のグレースケール画像に変換するコード。

いや、OpenCVならVideoCaptureで普通に出せるよ、という人もいるかもしれないけど、OpenCVのVideoCaptureを使ってしまうと裏ではUVCを使っているので、Leptonのコマンドを叩くときにUVCが使えなくなって困る。

あと、VideoCaptureで取得した値は温度ではない。

よって、温度が欲しい場合には問題となる。

以下にUVCのAPIを使って温度情報を取得した後、その温度情報をグレースケールのOpenCVで使えるcv::Mat形式に変換するコードを載せる。

これだと任意の点の温度も扱いやすいし、いろんな加工もしやすい。

ソース

まぁなんだかんだいってソース見てもらうのが一番。

わかりにくかったのは、UVC_FRAME_FORMAT_Y16形式のデータの1バイトに温度データが入っていると思いこんでいたが、2バイトを使って温度が入っていた点。


(2022.02.09 追記)
Ubuntu20.04で同じような環境を作ったところ、 UVC_FRAME_FORMAT_Y16 がない、とエラーが。
error: ‘UVC_FRAME_FORMAT_Y16’ was not declared in this scope; did you mean ‘UVC_FRAME_FORMAT_NV12’?
なんかのバグのようだ。 とりあえず UVC_FRAME_FORMAT_GRAY16 に置き換えることで問題がなくなる模様。
(2022.02.09 追記終了)


100倍されたケルビン値が入っているので摂氏にしたい場合は100で割ってから-273.15をすると摂氏になる。
なので値が0なら-273.15度、unsigned short int の上限値は65,535 なら382.20度である。

ただ、Flir C5とかで設定でカメラ温度レンジが0-400とかあるのだけど、これ、400度までだせてるってことだよね。
ぐるっと回って0とかの小さい値を高温部に割り当てるとかして逃げてるんだろうか?そこらへんはよくわからん。
400度より高い温度を撮影しながら実際の値見ればわかるかも。


(2022.1.17 追記)
LeptonコマンドでTlinarのresolution(またはscaleと呼ばれたりもしている)を変更すれば良いことが判明。
GetThermal の lepton_sdk で言うと LEP_SetRadTLinearResolution() のところ。
これに LEP_RAD_RESOLUTION_0_1 をセットしたら返却される温度がケルビン値を100倍した値から10倍した値に変更できる。
具体的には今まで摂氏0度なら27315という100倍した値だったのに、2731という10倍の値が返ってくる。
(2022.1.17 追記終了)


摂氏に変換する際には、今までは27315を引いて100で割っていたが、2731を引いて10で割る、というふうになる。 なので、short intの上限値に引っかからないので問題なく382.2度以上の温度が取得できるようになる。
ただし、ここからわかるように精度は1桁落ちることになる。
また、以下のソースは100で割った値が返ってくることを前提としたソースなので、max_tempやmin_tempに指定した値を1桁下げないといけない。

#include <libuvc/libuvc.h>
#include <opencv2/opencv.hpp>
#include <unistd.h>

#define VENDOR_ID  0x1e4e
#define PRODUCT_ID 0x0100

uvc_context_t*       ctx;
uvc_device_t*        dev;
uvc_device_handle_t* devh;
uvc_stream_ctrl_t    ctrl;

void cb(uvc_frame_t *frame, void *ptr) {
    uint16_t max_temp = 32315; // 上限温度を50度に(Leptonではケルビン値が100倍された値が格納される。よって摂氏50度は32315)
    uint16_t min_temp = 27315; // 下減温度を 0度に

    cv::Mat image(frame->height, frame->width, CV_8U, cv::Scalar::all(255));

    for (int y = 0; y < image.rows; y++) {
        for (int x = 0; x < image.cols; x++) {
            uint16_t temp = 0;
            void* _temp = &((uchar*)frame->data)[y * frame->width * 2 + x * 2];
            temp = *((uint16_t*)_temp);
            if (temp < min_temp)
                temp = min_temp;
            if (temp > max_temp)
                temp = max_temp;
            temp = (uint8_t)(((float)(temp - min_temp) / (float)(max_temp - min_temp)) * 255.0f);
            image.data[y * image.cols + x] = (unsigned char)temp;
        }
    }

    // 表示
    //cv::imshow("image", image);
    cv::Mat resizeImage;
    cv::resize(image, resizeImage, cv::Size(640, 480), cv::INTER_CUBIC);
    cv::imshow("image", resizeImage);
    cv::waitKey(1);
}

int main()
{
    uvc_error_t res;

    res = uvc_init(&ctx, NULL);
    if (res < 0) {
        uvc_perror(res, "uvc_init");
        return -1;
    }

    res = uvc_find_device(ctx, &dev, VENDOR_ID, PRODUCT_ID, NULL);
    if (res < 0) {
        uvc_perror(res, "uvc_find_device");
        return -1;
    }

    res = uvc_open(dev, &devh);
    if (res < 0) {
        uvc_perror(res, "uvc_open");
        uvc_unref_device(dev);
        dev = NULL;
        return -1;
    }

    res = uvc_get_stream_ctrl_format_size(devh, &ctrl, UVC_FRAME_FORMAT_Y16, 160, 120, 9);
    //res = uvc_get_stream_ctrl_format_size(devh, &ctrl, UVC_FRAME_FORMAT_GRAY16, 160, 120, 9); // もしUVC_FRAME_FORMAT_Y16でエラーだったらこっち
    uvc_print_stream_ctrl(&ctrl, stderr);

    res = uvc_start_streaming(devh, &ctrl, cb, (void*)0, 0);
    if (res < 0) {
        uvc_perror(res, "start_streaming");
    } else {
        //uvc_set_ae_mode(devh, 1);
        sleep(1000);
        uvc_stop_streaming(devh);
    }

    // 後処理
    if (devh != NULL) {
        uvc_stop_streaming(devh);
        uvc_close(devh);
    }
    if (dev != NULL) {
        uvc_unref_device(dev);
    }
    if (ctx != NULL) {   
        uvc_exit(ctx);
        puts("UVC exited");
    }

    return 0;
}

実行は以下。sudoが必要。

$ g++ main.cpp -o sample `pkg-config --cflags --libs gtkmm-3.0 opencv` -luvc
$ sudo ./sample 

タイトル

以上!

LEPTON のコマンドを叩く

Lepton の 温度校正 であったり、 カメラ温度レンジを変更したりするにはUVC経由でコマンドを叩かないといけない。

が、仕様書を見ながらごりごり書くのは流石にしんどい。

誰か作ってくれてないかなぁ、と思ったら、 GetThermal に lepton_sdk としてガッツリ作ってくれているのでこれをそのまま使えば良い。

が、ほんのちょっとだけ試したい、という人は、以下のようにすればとりあえずコマンドを叩ける。

温度校正

温度校正をする LEP_RunSysFFCNormalization() という関数の実装。

まぁソースを見ればわかると思うが、仕様で決められている値の定義をつらつらと書いた後、 FLR_CID_SYS_RUN_FFC というコマンドを UVC経由で実行しているだけ。

その下のwhileループはただの待ち。シャッターが降りて校正処理して、とかは時間がかかるので終わるのを待たないといけない。

校正中は LEP_SYS_STATUS_BUSY が返却されるのでそれがなくなるまでぐるぐる待ってるだけ。

シンプル。

そしてこの関数 LEP_RunSysFFCNormalization(); を 上記のプログラムでいうところの uvc_open 以降の任意の場所で呼んであげれば校正が実行される。

カチャッてシャッターがおりたら成功。

#define LEP_CID_AGC_MODULE (0x0100)
#define LEP_CID_OEM_MODULE (0x0800)
#define LEP_CID_RAD_MODULE (0x0E00)
#define LEP_CID_SYS_MODULE (0x0200)
#define LEP_CID_VID_MODULE (0x0300)

#define LEP_GET_TYPE (0x0000)
#define LEP_SET_TYPE (0x0001)

#define LEP_SYS_MODULE_BASE    (0x0200)
#define FLR_CID_SYS_RUN_FFC    (LEP_SYS_MODULE_BASE + 0x0042 )
#define LEP_CID_SYS_FFC_STATUS (LEP_SYS_MODULE_BASE + 0x0044 )

typedef enum {
    VC_CONTROL_XU_LEP_AGC_ID = 3,
    VC_CONTROL_XU_LEP_OEM_ID,
    VC_CONTROL_XU_LEP_RAD_ID,
    VC_CONTROL_XU_LEP_SYS_ID,
    VC_CONTROL_XU_LEP_VID_ID,
} VC_TERMINAL_ID;

typedef enum LEP_SYS_STATUS_E_TAG {
   LEP_SYS_STATUS_WRITE_ERROR = -2,
   LEP_SYS_STATUS_ERROR = -1,
   LEP_SYS_STATUS_READY = 0,
   LEP_SYS_STATUS_BUSY,
   LEP_SYS_FRAME_AVERAGE_COLLECTING_FRAMES,
   LEP_SYS_STATUS_END
} LEP_SYS_STATUS_E, *LEP_SYS_STATUS_E_PTR;

int LEP_RunSysFFCNormalization()
{
   int ret = 0;
   int status = (int)LEP_SYS_STATUS_BUSY;

   ret = run_command((uint16_t)FLR_CID_SYS_RUN_FFC);

   while (status == (int)LEP_SYS_STATUS_BUSY) {
      get_attribute((uint16_t)LEP_CID_SYS_FFC_STATUS, status, 2);
   }

   return ret;
}

int lepton_commandid_to_unitid(uint16_t command_id) {
    int unit_id;
    switch (command_id & 0x3f00) // Ignore upper 2 bits including OEM bit
    {
    case LEP_CID_AGC_MODULE:
        unit_id = VC_CONTROL_XU_LEP_AGC_ID;
        break;
    case LEP_CID_OEM_MODULE:
        unit_id = VC_CONTROL_XU_LEP_OEM_ID;
        break;
    case LEP_CID_RAD_MODULE:
        unit_id = VC_CONTROL_XU_LEP_RAD_ID;
        break;
    case LEP_CID_SYS_MODULE:
        unit_id = VC_CONTROL_XU_LEP_SYS_ID;
        break;
    case LEP_CID_VID_MODULE:
        unit_id = VC_CONTROL_XU_LEP_VID_ID;
        break;
    default:
        return -1;
    }
    return unit_id;
}

int run_command(uint16_t command_id)
{
    int unit_id;
    int control_id;
    int result;

    unit_id = lepton_commandid_to_unitid(command_id);
    if (unit_id < 0) {
        return unit_id;
    }
    control_id = ((command_id & 0x00ff) >> 2) + 1;

    result = uvc_set_ctrl(devh, unit_id, control_id, &control_id, 1);
    if (result != 1) {
        printf("run_command failed: %d", result);
        return -1;
    }

    return 0;
}

int get_attribute(uint16_t command_id, int& attr, uint16_t word_len) {
    int unit_id;
    int control_id;
    int result;

    command_id |= LEP_GET_TYPE;

    unit_id = lepton_commandid_to_unitid(command_id);
    if (unit_id < 0) {
        return unit_id;
    }

    control_id = ((command_id & 0x00ff) >> 2) + 1;

    word_len *= 2;

    result = uvc_get_ctrl(devh, unit_id, control_id, &attr, word_len, UVC_GET_CUR);
    if (result != word_len) {
        printf("get_attribute failed: %d", result);
        return -1;
    }

    return 0;
}

int set_attribute(uint16_t command_id, int attr, uint16_t word_len)
{
    int unit_id;
    int control_id;
    int result;

    command_id |= LEP_SET_TYPE;

    unit_id = lepton_commandid_to_unitid(command_id);
    if (unit_id < 0) {
        return unit_id;
    }

    control_id = ((command_id & 0x00ff) >> 2) + 1;

    word_len *= 2;

    result = uvc_set_ctrl(devh, unit_id, control_id, &attr, word_len);
    if (result != word_len) {
        printf("set_attribute failed: %d", result);
        return -1;
    }

    return 0;
}

が、この実装をいちいち書くのは無駄。

GetThermal に含まれる lepton_sdk が全部やってくれているわけで、ライセンス的にも商用利用できるし、lepton_sdkを使うのが正しい選択。

このサンプルはあくまで、lepton_sdkって何やってるの?どうやってUVC経由で叩いてんの?の理解を促すために最小単位で切り出してみただけ。

以上!