赤外線アレイセンサ(AMG8833)を使って、非接触体温計に挑戦 ~完結編~

赤外線アレイセンサを使って、非接触体温計として体温測定を行ったので紹介します。具体的には、センサ温度と測定距離の補正値を算出して反映しました。(精度を上げるには、センサ温度の間隔を0.5℃くらいで細かく区切って、近似式を算出した方が良いと思います。)

  • センサ温度(センサ内サーミスタ温度)を変化させて補正値を算出

センサ温度が上がると、出力温度が下がる傾向にあるので、測定環境での誤差を最小限にするため補正値を算出します。(数時間使っているとセンサ内のサーミスタ温度が数℃変化するのでこれを考慮しないと時間とともに測定結果がずれてきます。)

センサ温度と出力温度(補正値)のグラフ
  • 測定対象物までの距離を変化させて補正値を算出
    測定対象とセンサまでの距離は、出力温度に対して、負の特性を持つことが分かりました。実測してみると、測定範囲(60cm~20cm)の間でも2.4℃くらい変化量があります。測距センサで対象物の距離を測定して、補正値に反映しました。
出力温度と距離の関係グラフ

何だか、いろいろ試していると、実機確認もこんな感じになってしまいました・・・

ラズパイとの配線接続図は以下です。

ラズパイとの配線接続図

参考までに測定で使用した簡単なプログラム(python)です。あまり特殊なことはやっていないので、皆さんの参考になれば幸いです。補正値などは個体差があるので、ご自身の環境で測定して算出する必要があります。もっと良いやり方や間違いの指摘などは大歓迎です。皆で良いものを作ってコロナ禍を乗り越えましょう!

# -*- coding: utf-8 -*-
import cv2
import numpy as np
import time
from signal import signal, SIGPIPE, SIG_DFL
import RPi.GPIO as GPIO

import busio
import board

from PIL import ImageFont, ImageDraw, Image

import matplotlib.pyplot as plt

import smbus
import adafruit_amg88xx

import datetime
#import threading

import csv

# I2Cバスの初期化
i2c_bus = busio.I2C(board.SCL, board.SDA)
i2c = smbus.SMBus(1)
i2c_adr = 0x69

## センサーの初期化
sensor2 = adafruit_amg88xx.AMG88XX(i2c_bus, addr=i2c_adr)
## センサーの初期化待ち
time.sleep(.1)

# AMG8833追加設定
# フレームレート(0x00:10fps, 0x01:1fps)
i2c.write_byte_data(i2c_adr, 0x02, 0x00)
# INT出力無効
i2c.write_byte_data(i2c_adr, 0x03, 0x00)
# 移動平均モードを有効
i2c.write_byte_data(i2c_adr, 0x1F, 0x50)
i2c.write_byte_data(i2c_adr, 0x1F, 0x45)
i2c.write_byte_data(i2c_adr, 0x1F, 0x57)
i2c.write_byte_data(i2c_adr, 0x07, 0x20)
i2c.write_byte_data(i2c_adr, 0x1F, 0x00)
##移動平均モードを無効
#i2c.write_byte_data(i2c_adr, 0x1F, 0x50)
#i2c.write_byte_data(i2c_adr, 0x1F, 0x45)
#i2c.write_byte_data(i2c_adr, 0x1F, 0x57)
#i2c.write_byte_data(i2c_adr, 0x07, 0x00)
#i2c.write_byte_data(i2c_adr, 0x1F, 0x00)

plt.ion()
plt.subplots(figsize=(4, 4))

GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)       # Number GPIO
signal(SIGPIPE,SIG_DFL)

print("\n 体温測定システムを起動しています。暫くお待ちください...")

####### オプション設定#########
# サーモモニタ表示設定
THERMO_MONITOR_USE = False     # True:USE, False:Not Use
# ログ出力設定
CSV_OUTPUT_USE = False         # True:USE, False:Not Use
# ブザー設定
BUZZER_USE = False             # True:USE, False:Not Use
###############################

# ブザー初期設定
if BUZZER_USE==True:
    BuzzerPin = 17    # GPIO_17
    GPIO.setup(BuzzerPin, GPIO.OUT)
    GPIO.output(BuzzerPin, GPIO.LOW)

# 測距センサー(HC-SR04)初期設定
TRIG = 27
ECHO = 22
GPIO.setup(TRIG,GPIO.OUT)
GPIO.setup(ECHO,GPIO.IN)
GPIO.output(TRIG, GPIO.LOW)

# 測距センサー(HC-SR04)制御
def read_hcsr04():
    #GPIO.output(TRIG, GPIO.LOW)
    #time.sleep(0.1)
    GPIO.output(TRIG, GPIO.HIGH)
    time.sleep(0.000011)
    GPIO.output(TRIG, GPIO.LOW)
    # EchoパルスがHighになる時間
    t0 = time.perf_counter()
    echo_on = time.perf_counter()
    while GPIO.input(ECHO) == 0:
        echo_on = time.perf_counter()
        wait_echo = echo_on - t0
        if wait_echo >= 0.5: # ECHO=0状態を回避
            break
    # EchoパルスがLowになる時間
    t0 = time.perf_counter()
    echo_off = time.perf_counter()
    while GPIO.input(ECHO) == 1:
        echo_off = time.perf_counter()
        wait_echo = echo_off - t0
        if wait_echo >= 0.5: # ECHO=1状態を回避
            break
    # Echoパルスのパルス幅(us)
    echo_pulse_width = (echo_off - echo_on) * 1000000
    # 距離を算出:Distance in cm = echo pulse width in uS/58
    distance = echo_pulse_width / 58
    return distance

################## CSV出力の初期設定 ####################
if CSV_OUTPUT_USE==True:
    # ログ(CSVファイル)設定
    now = datetime.datetime.today()     #現在時刻取得
    hourstr = "_" + now.strftime("%H")  #時刻(時)を文字列化
    minutestr = "_" + now.strftime("%M")   #時刻(分)を文字列化
    filename = "amg8833_temp_" + now.strftime("%Y%m%d") + hourstr + minutestr + ".csv"
    
    with open(filename,'a',newline='') as f:  #csvファイルの生成
        writer = csv.writer(f)
        writer.writerow(["pixels_max2","offset_temp2","thermistor_temp2","distance"])  #1行目:見出し
######################################################

# 日本語表示用のフォント設定
font = cv2.FONT_HERSHEY_SIMPLEX
font_pil = ImageFont.truetype('/usr/share/fonts/truetype/fonts-japanese-gothic.ttf', 20)

# ビデオキャプチャ用のインスタンス生成
cam = cv2.VideoCapture(0) 
# 画像サイズをVGAに合わせて設定(640)
cam.set(3, 640) # set video widht
cam.set(4, 480) # set video height

print("\n 体温測定システムを終了する場合は、このウィンドウ上で[Ctrl+C]を押してください。")

try:
    while True:
        ############################### 体温測定 ########################
        distance = read_hcsr04()
        pixels_array2 = np.array(sensor2.pixels)
        pixels_ave2 = np.average(pixels_array2)
        pixels_max2 = np.amax(pixels_array2[1:6,2:6])   
        pixels_min2 = np.amin(pixels_array2)
        thermistor_temp2 = i2c.read_word_data(i2c_adr, 0xE)
        thermistor_temp2 = thermistor_temp2 * 0.0625
        offset_thrm2 = (-0.6857*thermistor_temp2+27.187)# 補正式
        if distance <= 60:
            offset_thrm2 = offset_thrm2-((60-distance)*0.064)# 補正式(対距離)
        
        offset_temp2 = offset_thrm2
        max_temp2 =  round(pixels_max2 + offset_temp2, 1)   #体温を算出
        print('サーミスタ温度:' + str(thermistor_temp2) + ' ℃')
        print('最大温度:' + str(pixels_max2) + ' ℃')
        print('オフセット値:' + str(offset_temp2) + ' ℃')
        print('体温:' + str(max_temp2) + ' ℃')
        print(pixels_array2)
        print('対象物までの距離:' + str(distance) + ' cm')

        ############################### ブザー設定 ########################
        if BUZZER_USE==True:
            if distance <= 60 and distance >= 25:
            #if distance <= 60:
                GPIO.output(BuzzerPin, GPIO.HIGH)
                time.sleep(0.1)
                GPIO.output(BuzzerPin, GPIO.LOW)
        #########################################################################

        ########################## CSVファイルへのログ出力 ########################
        if CSV_OUTPUT_USE==True:
            if distance <= 60:  
                with open(filename,'a',newline='') as f:  #csvファイルの生成
                    writer = csv.writer(f)
                    #csvファイルへの書き込みデータ
                    data = [pixels_max2,offset_temp2,thermistor_temp2,distance] 
                    #データの書き込み
                    writer.writerow(data)  
                    f.flush()              
        #########################################################################

        ########################## カラーマップ表示 ########################
        if THERMO_MONITOR_USE==True:
            plt.subplot(1,1,1)
            plt.imshow(sensor2.pixels, cmap="jet", interpolation="bicubic",vmin=25,vmax=30,origin='lower') # for AMG8833
            plt.colorbar()
            plt.show
            plt.draw()
            plt.pause(0.01)
            plt.clf()
        #########################################################################

        ############################### カメラ映像表示 ########################
        # 画像の読み込み
        ret, img =cam.read()

        img_pil = Image.fromarray(img)
        draw = ImageDraw.Draw(img_pil)
        
        # 体温測定結果の表示
        if distance <= 60 and distance >= 25:
            if max_temp2 <= 38.0:
                draw.text((20, 50), str(max_temp2)+'℃  体温は正常です', font = font_pil, fill = (255,0,0))
            else:
                draw.text((20, 50), str(max_temp2)+'℃ 体温が高いので、検温してください', font = font_pil, fill = (0,0,255))
                #draw.text((20, 50), '体温は' + str(max_temp)+'℃', font = font_pil, fill = (255,0,0))
        draw.text((200, 80), '顔をこの枠に合わせて近づいてください', font = font_pil, fill = (255,255,255))
        img = np.array(img_pil)
        cv2.rectangle(img, (200,100), (400,350), (255,255,255), 2)
        cv2.imshow('Face-Recognition',img)
        #########################################################################

        # ESCキーで終了
        k = cv2.waitKey(10) & 0xff 
        if k == 27:
            break

except KeyboardInterrupt:
    pass

GPIO.cleanup()
# Do a bit of cleanup
print("\n プログラムが強制終了されました。")
cam.release()
cv2.destr

実行するとこんな感じで表示されます。サーモグラフィを表示すると遅くなるので、運用では非表示にした方が良さそうです。

もうちょっと測定サンプル数を増やして、さらなる向上を目指そうと思います。

何か面白いことを発見したら紹介します。

お気づきの点や、アドバイスなどありましたらお気軽にご連絡ください。

非接触体温計(AMG8833)に関する参考記事です。

測距センサ(HC-SR04)に関する参考記事です。

Follow me!

“赤外線アレイセンサ(AMG8833)を使って、非接触体温計に挑戦 ~完結編~” への33件の返信

  1. 初心者である私から見ても分かりやすく解説されているので、
    是非参考にさせて頂き作成してみたいと思います。
    1点教えて頂きたいのですが写真上ではAMG8833が2つ
    あるように見えますが配線図を見ると1つしか繋がっていないに見えます。
    このソリューションはAMG8833が2つ必要でしょうか?
    もし2つ必要であれば2つ接続verの配線図を頂く事は可能でしょうか?
    すみませんがご教示お願いします。

    1. 初めまして、サイトの管理者です。

      サイトを見て頂きありがとうございます。

      ご質問に関しまして、以下に回答します。

      >このシステムはAMG8833は2つ必要でしょうか?
      AMG8833は一つで大丈夫です。
      写真は個体差を見るため2つ接続して試していた時のものです。

      >出来ればブザー等も型番あると助かります。
      単体での型番が不明なのですが、
      「BONROB 40in1 RaspberrPiスターターキット」
      に入っていたActiveブザーを使用しています。
      Amazonだと3299円でいろんな部品が入っているので
      これからいろいろ試したいのであればおすすめです。
      単品で購入する場合は5VのActiveブザーであれば
      問題ないと思います。

      以上、宜しくお願いします。

  2. 個人的なエラーで分からないのであれば申し訳ないのですが、
    k = cv2.waitkey(10) & 0xff
    Nameerror: name ‘amp’ is not defined
    というエラーを吐いてしまいます。
    解決方法わかるでしょうか。

    1. すいません。コピペした時におかしな文字が入っていました。
      “amp;”の文字を削除してください。
      (誤)k = cv2.waitKey(10) &”amp;” 0xff
      (正)k = cv2.waitKey(10) & 0xff

      1. 返信ありがとうございます。
        ちゃんと動作することができました。
        ありがとうございます!

  3. カメラ、サーモグラフィ無しでプログラムを起動したら一回だけ距離と表面温度を測定して体温を出力しようとしているのですが、なかなか上手くいきません。
    迷惑でなければ、どのようにやればよいか教えていただけると幸いです。

    1. サーモグラフィは、オプション設定でON/OFF切替可能ですが、
      カメラの有無は直接ソースを変更しないといけないですね。
      このプログラムではカメラを使わないケースは考慮していませんでした。

      カメラなしの場合は、
      128行目付近のビデオキャプチャ用のインスタンス生成箇所と、
      カメラ映像表示部分をコメントアウトすることになりそうです。

      また、1回だけ測定して出力されないとのことですが、
      AMG8833の2回移動平均モードが影響している可能性もありそうです。
      移動平均を無効にして試してみてください。

      以上、宜しくお願い致します。

  4. 再びの質問失礼します。
    プログラムを実行しても距離が0.074、0.03など明らかにおかしい値が出てしまいます。
    原因分かりますでしょうか

    1. HC-SR04は反射したパルスを受信して距離を測定するので
      まず、平らな段ボールなどで測定しみることで、
      HC-SR04の問題なのか対象物の反射が問題なのかを切り分けできると思います。

      ↓以下、参考までに実際に段ボールをずらしながら測定した結果です。
      https://raspi-katsuyou.com/index.php/2020/09/18/19/03/15/1821/

      あと、気になるのがHC-SR04の一部に不具合があるようです。
      https://mag.switch-science.com/2015/07/23/hc-sr04_failure/
      上記に該当しないかが気になります。

      以上、宜しくお願い致します。

  5. 貴重な情報ありがとうございます。
    自分で1から組んだら何ヶ月掛かったか分かりません。
    とても助かりました。

    私の環境では
    26行目
    i2c_adr = 0x69

    i2c_adr = 0x68

    129行目
    cam = cv2.VideoCapture(0)

    cam = cv2.VideoCapture(-1)

    とするとうまく動きました。

    また、Webカメラはロジクール C922n を使用していますが顔の位置を合わせるときに動きが左右逆になってしまいましたので
    194行目に
    img = cv2.flip(img, 1)
    を追記しました。

    重ねてお礼申し上げます。

    1. 当サイトの情報が少しでもお役に立てたということで嬉しく思います。
      また、変更箇所の情報ありがとうございます。
      助かります。

      これからも少しでもお役に立てるような内容をアップしていこうと思いますので、今後ともよろしくお願い致します。

  6. こちらのサイトを参考にして初心者なりに楽しく制作しているのですが、
    どうしてもCSVが作成できず悩んでいます。

    私の環境がpython3.7.3だからでしょうか?

    同じフォルダ内で別のプログラムを実行するとcsvが作成されるので、私が何かおかしな設定をしたのかな…もう少し頑張ってみます。

    1. ↑ ログ出力設定をTrueにしていませんでした。汗
      権限がおかしいのかとずっと悩んでいたのですが、失礼いたしました。

      1. 頭の方でオプション設定をFalseにしてましたね。
        解決してよかったです。
        また何かあれば宜しくお願いいたします。

  7. font_pil = ImageFont.truetype(‘/usr/share/fonts/truetype/fonts-japanese-gothic.ttf’, 20)
    この部分でエラーが出てしまうのですがどのようにしたらよろしいでしょうか?

    1. 日本語フォントのパッケージをインストールする必要があります。
      ↓以下のコマンドで、fonts-ipaexfont パッケージをインストール

      sudo apt install fonts-ipaexfont

      fonts-japanese-gothic.ttfが「/usr/share/fonts/truetype/fonts-japanese-gothic.ttf」にインストールされると思います。

      以上、宜しくお願いします。

  8. プログラム初心者の僕でも分かりやすい説明できないとても良かったです。
    僕は体温表示の色を青から変えたいのですがどの部分で変更すればよろしいでしょうか。また、カメラ表示の部分をもうちょっと拡大したいのですが(可能なら全画面表示に)それもどこを変えればよろしいでしょうか。

    1. ご質問内容に関しまして、以下に回答いたします。

      僕は体温表示の色を青から変えたいのですがどの部分で変更すればよろしいでしょうか。
      ⇒201行目のfill = (*,*,*)の部分を変更すれば色を変更することができます。
        draw.text((20, 50), str(max_temp2)+’℃ 体温は正常です’, font = font_pil, fill = (255,0,0)) 
          fill = (255,0,0):青の場合
          fill = (0,255,0):緑の場合 
          fill = (0,0,255):赤の場合
          fill = (0,255,255) :黄色の場合

      カメラ表示の部分をもうちょっと拡大したいのですが(可能なら全画面表示に)
      それもどこを変えればよろしいでしょうか。
      ⇒131,132行目でカメラの表示サイズを変更できます。
        # 画像サイズをVGAに合わせて設定(640)
         cam.set(3, 640) # set video widht
         cam.set(4, 480) # set video height

       208行目のimshowの前に、setWindowPropertyを入れると
       全画面表示になると思います。

      cv2.setWindowProperty(‘Face-Recognition’, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
      cv2.imshow(‘Face-Recognition’,img)

      以上、宜しくお願いします。

      1. すいません。全画面表示の件で追加コメントです。

        全画面表示の場合は、cv2.namedWindowでウィンドウの設定を行う必要があります。
        設定をcv2.WINDOW_NORMALにすると、ウィンドウの大きさを自分で調整可能です。

        具体的には、208行目を以下の3行に変更します。
        —————————————————
        cv2.namedWindow(‘Face-Recognition’, cv2.WINDOW_NORMAL)
        cv2.setWindowProperty(‘Face-Recognition’, cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
        cv2.imshow(‘Face-Recognition’,img)
        ————————————————–

        デフォルトでは、cv2.WINDOW_AUTOSIZEが設定されているので
        自動で大きさが設定されるようです。

        全画面表示を設定しないまま、以下の画像サイズを変更すると
         # 画像サイズをVGAに合わせて設定(640)
          cam.set(3, 640) # set video widht
          cam.set(4, 480) # set video height

        変更後のサイズでカメラ映像が表示されます。

        自分も忘れないようにメモとして残しておきます。
        ご質問ありがとうございました。

  9. escキーを押しても cv2.destr の部分でエラーが起こるのですが対処方法を教えてください

    1. 返信遅くなり申し訳ありません。
      cv2.destrの部分を以下に修正してみてください。

      (誤)cv2.destr⇒(正)cv2.destroyAllWindows()

  10. 同じような体温計を作ろうとしていまして、参考にさせていただいています。
    測定値の誤差が大きくてこずっています。 33℃の手のひらが28℃の表示とか。 
    まだ、センサー温度と測定値の関係を測定していないのですが、教えてください。
    センサ温度が上がると、出力温度が下がる傾向とのことですが、センサ温度が上がった時にセンサーの測定値をプラスに補正する必要が有るということでしょうか?
    プログラムではセンサ温度が上がった時にマイナスに補正されているように見えるのですが。 

    1. ご質問ありがとうございます。
      以下に回答致します。

      >センサーの測定値をプラスに補正する必要が有るということでしょうか?
      はい。プラスに補正する必要があります。

      >プログラムではセンサ温度が上がった時にマイナスに補正されているように見えるのですが。
      offset_thrm2 = (-0.6857*thermistor_temp2+27.187)# 補正式

      室温の範囲内で測定した場合、サーミスタ温度は30℃~25℃の間くらいで変化していたので、offset_thrm2の計算結果はプラスに補正されます。
      #測定結果からExcelの近似式を用いて補正しています。
      実際には、恒温槽などでゆっくり変化させながら広い範囲で測定するともっと良い近似式になると思うのですが、そこまでやる環境がありませんでした…

      誤差補正については自分も試行錯誤中で、何か良い方法などありましたら教えてください。

      以上、宜しくお願い致します。

      1. 申し訳ありません、記載が誤っていました。

        (誤)センサ温度が上がると、出力温度が下がる傾向
        (正)センサ温度が上がると、補正値は小さくなる傾向

        センサ温度が28℃の時の補正値:+6.9874
        センサ温度が32℃の時の補正値:+4.2446

        以上、宜しくお願い致します。

        1. サーミスタ温度が上がるとセンサー出力値も上がるということですね。 ありがとうございます。 こちらでも試してみます。 
          話がそれますが、こちらではセンサーから出力される補正前のデータは3℃~10℃ぐらいの誤差があります。 測定対象の温度が高くなると誤差が大きくなります。  
          データシート上で精度2.5℃となっているで根本的に何か間違えているのかと心配になります。 
          yasu さんの offset_thrm2はサーミスタ温度30度で6.6℃になりますから2.5℃の誤差には収まっていないということですね。 

        2. サーミスタ温度と出力の関係を測定してみました。 こちらではサーミスタ温度が上がると出力が下がる傾向でした。 従いましてサーミスタ温度が上がるとプラスに補正することになりそうです。 また、測定物の温度に対して出力の傾きが1より小さな値のようです。 引き続き調べています。メールいただければこちらで調べた結果をお渡しできます。

          1. 測定結果の貴重な情報ありがとうございます。自分の測定結果と逆の傾向になっているんですね。個体差なのか測定環境の差があるのかもしれません。
            自分の測定環境は、あまり自信がありませんのでその影響もあるかもしれません。
            また、何か新たな情報がありましたらコメント頂けると助かります。

  11. サーミスタ温度の影響を受けるだけでなく、センサー出力と温度は傾きが1でないようです。まとめて補正できないかと試してみました。
    結果的にはうまく補正できませんでした。 ギブアップです。
    やったこと。
    1.黒鉛のブロックをホットプレートに乗せ、センサーにはヒータを付けて 実測温度 temp サーミスタ温度 th センサー出力 x を3点取りました。
    temp  th x    
    33.1  18.44 26.25
    42.3  23.44 33.45
    39.5  32.81 23.45
    2.このデータを temp=A*th+B*x+C
     で補正できる係数を連立方程式としてA,B,Cを求めました。
    A=0.611591637 B=0.853061363 C=-0.57061057
    3.この係数で別の測定を補正してみました
    temp  th x
    38.1 19.62 30.02
    補正結果 37.03771947 誤差-1.1
    体温を0.2℃ぐらいの誤差で測定するなんてとてもとても。。
    カタログの誤差2.5℃って何でしょう? 私はとんでもない間違いをしているのでしょうか? どなたかうまく行ったら助けてください。

  12. 195行目で↓このようなエラーが出るのですが解決法が見当たらないので解決法を教えてください。
    Traceback (most recent call last):
    File “/home/pi/Desktop/loser.py”, line 195, in
    img_pil = Image.fromarray(img)
    File “/usr/lib/python3/dist-packages/PIL/Image.py”, line 2754, in fromarray
    arr = obj.__array_interface__
    AttributeError: ‘NoneType’ object has no attribute ‘__array_interface__’

    1. エラー内容からすると、オブジェクトの属性がないようです。
      思いついた解決方法(原因?)をご連絡致します。
      #はずしていたらごめんなさい。

      【エラー内容】
      AttributeError: ‘NoneType’ object has no attribute ‘__array_interface__’

      【想定される原因】
      カメラ映像の読み込みが問題ないか確認できますでしょうか?

      # カメラから映像を取得(引数のIDが正しいか?)
      # 1台目のカメラだと0ですが、環境にあわせて修正が必要です。
      cam = cv2.VideoCapture(0)

      # read()メソッドの返り値は正しいか?
      # ret:フレームの画像が読み込めたかどうかを示すbool値を確認
      ret, img =cam.read()
      print(ret) # True/False

      # 引数のスペルミスがないか?
      img_pil = Image.fromarray(img)

      思いついたのは上記くらいです。
      宜しくお願い致します。

  13. 画面の最大化の質問を見て自分も同じようにやってみたのですがカメラの画面だけが最大化されませんでした
    どのようにすればいいでしょうか?

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

CAPTCHA