[OpenCV] 画像処理で気圧計の値を読み取ってみました

[OpenCV] 画像処理で気圧計の値を読み取ってみました

工場などには様々な計器があり、それらを監視するニーズは比較的多く存在します。今回は、気圧計(アナログメーター)をカメラで撮影した画像から値を読み取る作業を試みました。
Clock Icon2024.12.23

1 はじめに

製造ビジネステクノロジー部の平内(SIN)です。
本ブログは クラスメソッド発 製造業 Advent Calendar 2024 23日目の記事です。

工場などには様々な計器があり、それらを監視するニーズは比較的多く存在すると思います。

計器のメーター値をデジタル化したい場合、最も簡単で確実な方法は、数値を出力できるデジタルメーターに置き換えることでしょう。しかし、様々な制約により、アナログメーターを目視で読み取る作業がどうしても残ってしまうケースが考えられます。

今回は、そのような場面をイメージし、カメラで撮影した画像からメーターの値を読み取る作業を試してみました。

001

2 処理後の出力の説明

処理後の画像に表示されているものは、以下のとおりです。

  1. center: 圧力計の中心座標
  2. pointer: 針先の座標
  3. deg: 針の角度
  4. value: 読み取られた圧力計の値
  5. 緑色の円: 圧力計の外縁
  6. 水色の線: 指針
  7. 白色の矩形: 圧力計の中心エリア
  8. 赤色の線及び数値: 圧力計の始点と終点の角度

002

角度については、真下を0度として、時計回りに増加するものとして処理しています。

003

3 処理手順

圧力計の画像を処理して、その値を読み取る手順は、以下のとおりです。

(1) 画像の正規化(後段で、サイズによる抽出が可能になるよう、一定のサイズに変換しています)
(2) 外縁の検出(圧力計の外縁「円形」を取得します)
(3) 直線の検出(画像に写っている直線を検出します)
(4) 指針の検出(検出した直線の中から、「圧力計の針」を検出します)
(5) 針先の判定(「圧力計の針」の始点・終点のうち、針先がどちらであるかを判定しています)
(6) 角度の算出(針先の座標と中心座標の関係から、圧力計の針の角度を算出しています)
(7) 値の取得(針先の角度を、圧力計の値に変換しています)

(1) 画像の正規化

読み込んだ画像は、後段の外縁や指針の検出時に、サイズによる判定が可能となるように、横幅 640pxで正規化してます。

org_h, org_w, _ = image.shape
fx = 640 / org_w
image = cv2.resize(image, None, fx=fx, fy=fx)
new_h, new_w, _ = image.shape
print(f"read_image  {org_w} x {org_h} => fx: {fx} => {new_w} x {new_h}")

(2) 外縁の検出

圧力計の外縁を検出します。

OpenCVの findContours() で、画像の輪郭を検出できます。

# 2値化画像の作成
def create_binarize_image(image):
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 120, 220, cv2.THRESH_BINARY)
    return binary

# 気圧計の外円を検出
def detect_circle(binary):
    contours, _ = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    for contour in contours:
        (x, y), radius = cv2.minEnclosingCircle(contour)
        center = (int(x), int(y))
        radius = int(radius)
        if 110 < radius < 130:
            print(f"detect_circle center:{center} radius:{radius}")
            return center, radius
    return (0, 0), 0

binary_image = create_binarize_image(image)
center, radius = detect_circle(binary_image)

輪郭検出は、黒い背景から白い物体を検出する動作となっているため、入力画像は、2値化処理した画像を使用しています。

005

検出した輪郭から、最小外接円を計算する時は cv2.minEnclosingCircle()を使用します。そして、その結果が下記のとおりです。多数の円が検出されていることが分かります。

004

多数の円から、圧力計の外縁を抽出するには、妥当なサイズかどうかで判定してます。画像は、横幅が640pxに正規化されているため、そこに写る圧力計の半径は、概ね120px程度になります。
そこで、検出結果から、半径が、100px以上、130px未満のものだけを抽出することで、外縁としています。

円は、「中心点」及び「半径」で表現されているため、この作業で、同時に圧力計の 「中心」 も取得できたことになります。

006

(3) 直線の検出

圧力計の針を検出するための前作業として、画像に写っている直線を検出しています。

OpenCVの HoughLinesP() で、ハフ変換による直線検出が可能です。


# エッジ画像を生成
def create_edges_image(binary_image):
    return cv2.Canny(binary_image, 50, 150, apertureSize=3)

# 直線検出
def detect_lines(edges_image):
    return cv2.HoughLinesP(
        edges_image, 1, np.pi / 180, threshold=70, minLineLength=50, maxLineGap=10
    )

edges_image = create_edges_image(binary_image)
lines = detect_lines(edges_image)

直線を検出しやすいように、入力画像は、エッジ画層を使用しています。

007

下記は、HoughLinesP() で検出した直線を赤色で描画したものです。目的である 圧力計の針 以外の直線も検出されることがありますが、この時点では、いったん、このまま後段に送ります。

008

(4) 指針の検出

上記で検出した直線の中から指針を検出するには、圧力計の中心(一定サイズの矩形)と交差しているかどうかで判定しています。

# 中央部を表現する矩形
def get_center_area(center, margin):
    return (center[0] - margin, center[1] - margin, margin * 2, margin * 2)

# メータの針を検出
def detect_pointer(center_area, lines):
    for line in lines:
        x1, y1, x2, y2 = line[0]
        if crossing_detection((x1, y1, x2, y2), center_area):
            print(f"detect_pointer ({x1},{y1}) ({x2},{y2})")
            return (x1, y1), (x2, y2)
    return None

center_area = get_center_area(center, center_margin)
pointer = detect_pointer(center_area, lines)

圧力計の中心から一定のサイズの矩形(白色)を作成します。

009

続いて、検出された複数の直線が、矩形の、上辺、下辺、左辺、右辺の4つの直線と、交差しているかどうかを確認し、中央部を通過している直線を検出し、指針としています。

010

なお、交差検出( crossing_detection ) のコードは、以下のとおりです。

# 直線の交差検出
def crossing_detection_line(p1, p2, q1, q2):
    def is_clockwise(a, b, c):
        return (c[1] - a[1]) * (b[0] - a[0]) > (b[1] - a[1]) * (c[0] - a[0])

    return is_clockwise(p1, q1, q2) != is_clockwise(p2, q1, q2) and is_clockwise(
        p1, p2, q1
    ) != is_clockwise(p1, p2, q2)

# 直線と矩形の交差検出
def crossing_detection(line, rect):
    x1, y1, x2, y2 = line
    rx, ry, rw, rh = rect
    rect_lines = [
        ((rx, ry), (rx + rw, ry)),
        ((rx, ry), (rx, ry + rh)),
        ((rx + rw, ry), (rx + rw, ry + rh)),
        ((rx, ry + rh), (rx + rw, ry + rh)),
    ]
    for rect_line in rect_lines:
        if crossing_detection_line((x1, y1), (x2, y2), rect_line[0], rect_line[1]):
            return True
    return False

(5) 針先の判定

指針として検出した直線の始点と終点のうち、針先を表現しているのは、どちらであるかは、中心との距離で判定しています。

numpyの np.linalg.norm でベクトルの大きさを算出できるため、この処理は下記のように簡易に記述できます。

# 針先の座標を取得
def get_pointer_coordinates(pointer, center):
    c = np.array(center)
    a = np.array(pointer[0])
    b = np.array(pointer[1])
    return pointer[0] if np.linalg.norm(c - a) > np.linalg.norm(c - b) else pointer[1]

point = get_pointer_coordinates(pointer, center)

011

(6) 角度の算出

針先の座標と、中心座標を使用して、指針の角度を算出しています。
指針の座標は、中心点の座標で0,0からの差分座標として変換し、np.arctan2() でatan「ラジアン」を出力し、math.degrees() で「度」に変換しています。

arctan2() の出力は、真上を0とした、-pi < θ <= piとなるため、最後に、真下を基準にするため180°加算しています。

# 針先の角度を取得
def get_deg(point, center):
    x = point[0] - center[0]
    y = (point[1] - center[1]) * -1
    rad = np.arctan2(x, y)
    deg = math.degrees(rad) + 180
    return round(deg, 2)

012

(7) 値の取得

指針の値は、その角度と、メモリの始点と終点の角度からメータの値を計算できます。

# 針先の角度をメータ値に変換する
def convert_to_meter_value(deg, start_deg, end_deg):
    value = (deg - start_deg) / (end_deg - start_deg)
    return round(value, 2)

013

4 別の種類の圧力計

こちらは、別の種類の圧力計を読み取ってみたものです。上記で試したもの圧力計を比べると、始点・終点の角度や、目盛り(こちらは、0〜1 ではなく、0〜12でした)に違いがあるのですが、パラメータの一部を修正するだけで、利用可能であることが確認できました。

022

5 補足(メータ針の側面で、2つの直線が検出される場合)

針の形によっては、左の図のように、直線検出時に針の側面で2本の直線(緑色の表示)が検出されることがあります。
この事が、誤差発生に影響するように思いますが、ロジック上、メータ値を読み取るための角度計算は、直線の先端座標と、円の中心座標で計算(青色の表示)されるため、
どちらの直線を使用しても、結果は同じになります。

024

6 最後に

今回の作業は、圧力計が、垂直に写っていることを前提としています。もし、傾いた場合も考慮するのであれば、何らかの基準を設けて、その傾きを計算する必要があります。
また、外縁を基準としているので、カメラの角度等で、外縁の検出にヅレが生じると、結果的に全体の判定に影響が出ることを確認しました。

見て分かる通り、数値の判定には、やや誤差が生じてしまっていることをご了承ください。

すべてのコードは、Githubに置きました。
https://github.com/furuya02/barometer-reading

余談:買ってきた圧力計は、いろいろな数値を模擬するためにバラしました。すいません、もう、使えません。
021
014
015
019
017
023

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.