会社でアナログメーターを撮影し、そこからメーターの値を自動で読み取りたい、という相談を受けました。
てことで、そういうものを早速、作ってみました。
なお、世の中にはこういうソリューション(物体検出を使用)もあるそうですが。
「アナログメーター解析AI」を創りました! | | 株式会社プライムキャスト(PRIMECAST)
ここでは敢えて、機械学習を使いません。数字はともかく、メーターの針の角度くらい、画像解析だけで行けるんじゃないか、と。
ということで、OpenCVのハフ変換というのを使います。
ここのサイトを、参考にしました。
アナログメーター 1(直線検出)|uPyC|note
で、ハフ変換というのは、画像中の直線や円などの図形を読み取る手法、とでも思ってください。
その会社の方の相談で読み取りたいメーターは、ガスボンベのメーターでした。
自宅には手ごろなメーターがないので、ネットのメーカーのサイトやらなんやらやらを探って、テスト画像を手に入れてきました。
このメーターの針の角度を、真下を0度として、時計回りに角度を読み取るというものを作りました。
コードは以下。
「meter_read.py」
# cv2.HoughLines() 関数
import cv2
import numpy as np
import statistics
img = cv2.imread('./meter.jpg')
# 画像の大きさを取得
height, width, channels = img.shape[:3]
# 二値化
threshold = 100
ret,img_thresh = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)
# エッジ画像へ変換(ハフ変換で直線を求めるため)
edges = cv2.Canny(img_thresh,50,200,apertureSize = 3)
cv2.imwrite('houghlines2.jpg',edges)
cv2.imwrite('houghlines1.jpg',img_thresh)
# 自動的に直線が2本となるパラメータを検出
# minn:何点の点が並んでいたら、直線を引くか?のパラメーター値
for m in range(10,161,1):
lines = cv2.HoughLines(edges,1,np.pi/180,m)
if lines is None:
break
print(len(lines))
if len(lines)==2:
minn = m
print('minn = ', minn)
lines = cv2.HoughLines(edges,1,np.pi/180,minn)
theta_t = [] # 原点から直線に向かって下した法線と、水平線との角度 (ラジアン) を格納する配列
aa = [] # 直線の傾きを格納する配列
bb = [] # 直線の切片を格納する配列
i = 0
for i in range(len(lines)):
for rho,theta in lines[i]:
print('rho = ', rho)
print('theta = ', theta)
theta_t.append(theta)
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b))
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img,(x1,y1),(x2,y2),(0,0,255),2)
# 2点を通る直線の式は、y = (y2-y1)/(x2-x1)x - (y2-y1)/(x2-x1)x1 + y1
# 傾き a = (y2-y1)/(x2-x1) 、 b = y1 - (y2-y1)/(x2-x1)x1
a0 = (y2 - y1) / (x2 - x1)
b0 = y1 - (y2 - y1) / (x2 - x1)* x1
aa.append(a0)
bb.append(b0)
# 針が画像の左上、左下、右上、右下 のどこにいるかを、2直線の交点の位置で判断し、角度の式を変更
# なお、針の中心は画像の中心にあるとして、計算
# 交点の式は、((b[1] - b[0]) / (a[0] - a[1]) , (a[0] * b[1] - b[0] * a[1]) / (a[0] - a[1]) )
x_t = (bb[1] - bb[0]) / (aa[0] - aa[1])
y_t = (aa[0] * bb[1] - bb[0] * aa[1]) / (aa[0] - aa[1])
if x_t < width/2: # 針が左上か左下にいるとき
theta_hor = statistics.mean(theta_t)*180/np.pi
else: # 針が右上か右下にいるとき
theta_hor = 270 - (90 - statistics.mean(theta_t)*180/np.pi)
print(theta_hor)
cv2.imwrite('meter_line.jpg',img)
必要なライブラリは、たった2つ。OpenCV、Numpyです。
> pip install opencv-python numpy
で、使い方は、上のメーターの画像(ここではmeter.jpg)を同じフォルダ内に置いて、Windows PowerShellなどで
> python meter_read.py
と打ち込むだけ。
先のリンクではグレースケールの画像からエッジ処理をかけてましたが、ここでは二値化した画像からエッジを読み込ませてます。
その方が、安定していたので。
で、ここから針の部分の直線の実を読み出すために、21行目の「for m in range(10,161,1):」以降の繰り返し分で最適なパラメータ値を決めています。
具体的には、2本の直線を抽出できた時のパラメータ値のみを探り出します。
で、ハフ変換を実行。
すると、こんな感じの直線が得られます。
ちなみに、この時の針の角度は、この2本の線の中間の角度から割り出してます。
この場合は約78度と返ってきました。真下が0度なので……いい感じに読み取れてます。
なお、針の角度を出すのには、ちょっと工夫が必要です。
実は針が左半分を向いているときと、右半分の方向を向いているかで、角度の式を変えないといけません。
具体的には、左下を向いているか、右上を向いているかが、このハフ変換から得られた直線の角度だけでは判定できないんです。
そこで、得られた2本の直線の交点が、右か左かどっちにあるかで、その式を分けるように工夫してます。
詳しくは、OpenCVのハフ変換のリファレンス、および上のコードをご覧ください。
なお、本当に針の向きが変わっても読み取れるのかが心配だったため、画像を回して確認してみました。
例えばこの向きでも、
こういう画像が出力されて、角度は「348度」と返ってきます。
うん、いい感じですね。
メーターごとに角度→目盛り値の変換係数を定義してやれば、物体検出など無しに使えます。
それこそ、Raspberry Piでも動かせるほど軽いので、Raspberry Pi Zero + カメラだけで運用できます。
なお、針の向きが右か左かを判定する際に、画像の中心から右か左かで判定させてますが、本当ならこのメーターの中心点から見て右か左かを見ないといけないのですが、そこはさぼってますね。
本来なら、ハフ変換で円を抽出させ、そこから中心点を探すというのを入れた方がより精度が上がります。
この辺が、参考になります。
アナログメーター 2(円を検出)|uPyC|note
最近ですが、何でもかんでも物体検出、画像認識を使うよりも、まずこの手の画像処理を試すことから始めた方が良いことが多いです。
事前処理をした上で画像認識などを使う方が、認識率が上がることも多いですし。
もし仕事で、自動でアナログメーターを読ませたくなったら、ぜひご参考になさって下さい。
最近のコメント