画像から、全体の中心点と模様(濃い青丸)の半径、角度を取得したので、メモを修正して公開します。最終的に極座標グラフにします。
目次
- 目的
- 手順
- 環境
- コード
- 解説
目的
最近、画像から半径と角度をたくさん取得する必要に駆られました。その作業はかなり手間なので自動化してやろうというのが目的です。
私の使用する画像をそのまま使うわけにはいかないので、似たような画像をいらすとやで拾ってきました。輪郭があんまり丸くなかったので加工しています。
この画像から全体の中心点と模様(濃い青丸)の半径、角度を取得します。
手順
- グレースケール&位置座標を取得
- 円の取得の中心点の座標を取得
- 中心点を基準にして (rcosθ,rsinθ)で表現
- 各点(0~i)を(r, θ)で表記
- csvで出力
環境
- Python 3.8.0
- conda 4.8.0
- opencv 4.1.2
- matplotlib 3.1.2
- numpy 1.17.3
コード
import cv2
import matplotlib.pyplot as plt
import numpy as np
import csv
import math
import sys
"""
あらかじめ同じフォルダに「white.png」と「読み込み対象.png」をおいておく必要がある。
芯の中心を原点にして各点(小円)の半径、角度を取得する。
"""
#input
input='rasen'
img = cv2.imread('input'+input+'.png',cv2.IMREAD_COLOR)#ndarray.グレイスケールで読み込みとエラー。
white = cv2.imread('white.png',0)#ndarray
#事前処理
img = cv2.medianBlur(img, 5) #メディアンフィルタを用いて画像を平滑化
cv2.imwrite("output/img.png",img)#debag1。outputフォルダに作成される。
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv2.imwrite("output/gray.png",gray)#debag2
edge = cv2.Canny(gray, 20, 40)
cv2.imwrite("output/edge.png",edge)#debag3
#中心円の取得
circles = cv2.HoughCircles(edge, cv2.HOUGH_GRADIENT, 1, 1200, param1=60, param2=20, minRadius = 800, maxRadius =1000)
#print(circles)
circles = np.uint16(np.around(circles))
x_c=np.int64(circles[0][0][0])
y_c=-np.int64(circles[0][0][1])
r_c=np.int64(circles[0][0][2])
center =[x_c,y_c]
norm_c= np.linalg.norm(center, ord=2)
for i in circles[0,:]:
# 円を描く
cv2.circle(img,(i[0],i[1]),i[2],(0,0,0),4)
#小円の取得
circles = cv2.HoughCircles(edge, cv2.HOUGH_GRADIENT, 1, 200,param1=60,param2=20, minRadius = 15 ,maxRadius = 50) #param1は色の違い、param2は形の雑さ
circles = np.uint16(np.around(circles))#整数になり、16bitに変換された配列になる。
j=0
for i in circles[0,:]:
# 小円を描く
j+=1
cv2.circle(img,(i[0],i[1]),i[2],(0,0,0),4)
print(str(j)+"points")
#データの整形
#radiusは不要
radius=circles[:,:,2]
radius=np.array(radius[0,:])
x=circles[:,:,0]
x=x[0,:]
y=circles[:,:,1]
y=y[0,:]
#csvファイルの作成
xs=[]
ys=[]
points=[]
with open('output/'+input+'_x-y.csv', 'w') as f:
writer = csv.writer(f, lineterminator='\n')
writer.writerow(center)
for i in range(len(x)):
xs.append(np.int64(x[i]))
ys.append(-np.int64(y[i]))
points.append([xs[i],ys[i]])# points=[x,y]
writer.writerow([xs[i],ys[i]])
# L2ノルム(距離)の取得とCSVファイルの生成
ds = []
Theta=[]
cos=[]
with open('output/'+input+'_d-Theta.csv', 'w') as f:
writer = csv.writer(f, lineterminator='\n')
writer.writerow(['Radius','Deg[deg]'])#ラベル
for i in range(len(xs)):
X = np.array([xs[i]-x_c,ys[i]-y_c])#中心からのベクトル
norm = np.linalg.norm(X, ord=2)#中心からの距離
ds.append(norm)
cos.append(X[0]/norm)#cos = X[0]/SQRT(X[0]^2+X[1]^2)
if ys[i]>y_c:#芯の中心より上側。
Theta.append(360-math.degrees(math.acos(cos[i])))
else: #芯の中心より下側。
Theta.append(math.degrees(math.acos(cos[i])))
writer.writerow([ds[i],Theta[i]])
cv2.imwrite('output/'+input+'circle.png',img)# debag4
なお、角度は時計回りに作っています。
メモ:
画像は、色情報の2次元マトリックスになっており、画像処理の基本は左上から右上まで走査したあと一個下の行を走査するイメージです。取得した各点の順番は左上が先だと思います。
解説
1. グレースケール&xy座標で画像を取得
読み込み
cv2.imread(img,flag)
- - cv2.IMREAD_COLOR : カラー画像として読み込む.画像の透明度は無視される.デフォルト値
- - cv2.IMREAD_GRAYSCALE : グレースケール画像として読み込む
- - cv2.IMREAD_UNCHANGED : アルファチャンネルも含めた画像として読み込む 上記のフラグを使う代わりに,単に1, 0, -1 の整数値を与えて指定することもできます.
フィルタリング
ここではあまり働いてませんが、やると精度が上がります。下の関数はC++(のはず)の記法ですが、引数の参考まで。
medianBlur(const Mat& src, Mat& dst, int ksize)
グレイスケール
cv2.cvtColor(img, code)
- code – 色空間の変換コード.説明を参照してください
エッジ処理
輪郭を取得します。
cv2.Canny
2. ハフ変換で円を検出する
ハフ変換は画像中の直線や円などを検出する操作です。ここでもpython用のサイトより見やすかったのでC++(のはず)へのリンクを張っておきます。
HoughCircles(Mat& image, vector<Vec3f>& circles, int method, double dp, double minDist, double param1=100, double param2=100, int minRadius=0, int maxRadius=0)¶
検出された円の (中心の x 座標, 中心の y 座標, 半径) のタプルを返します。
- - image – 8ビット,シングルチャンネル,グレースケールの入力画像.
- - circles – 検出された円を出力するベクトル.各ベクトルは,3要素の浮動小数点型ベクトル (x, y, radius) としてエンコードされます.
- - method – 現在のところ, CV_HOUGH_GRADIENT メソッドのみが実装されています.基本的には 2段階ハフ変換 で,これについては Yuen90 で述べられています.
- - dp – 画像分解能に対する投票分解能の比率の逆数.例えば, dp=1 の場合は,投票空間は入力画像と同じ分解能をもちます.また dp=2 の場合は,投票空間の幅と高さは半分になります.
- - minDist – 検出される円の中心同士の最小距離.このパラメータが小さすぎると,正しい円の周辺に別の円が複数誤って検出されることになります.逆に大きすぎると,検出できない円がでてくる可能性があります.
- - param1 – 手法依存の 1 番目のパラメータ. CV_HOUGH_GRADIENT の場合は, Canny() エッジ検出器に渡される2つの閾値の内,大きい方の閾値を表します(小さい閾値は,この値の半分になります).
- - param2 – 手法依存の 2 番目のパラメータ. CV_HOUGH_GRADIENT の場合は,円の中心を検出する際の投票数の閾値を表します.これが小さくなるほど,より多くの誤検出が起こる可能性があります.より多くの投票を獲得した円が,最初に出力されます.
- - minRadius – 円の半径の最小値.
- - maxRadius – 円の半径の最大値.
出力は中心位置と半径です。単位は画素だと思われます。
メモ:
上記コードでは以下のように使用しています。
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 180, param1=60, param2=40, minRadius = 800, maxRadius =1100)
上のコードでは入力画像は2504*2607です.minRadius = 800なので、画像の半分以上を覆うような大きい円だけを検出しています。 param2は経験上、円の雑さに関するパラメータで、小さいほど歪な円を検出します。
円を描画
void circle(Mat& img, Point center, int radius, const Scalar& color, int thickness=1, int lineType=8, int shift=0)¶
- - img – 円を描画する画像.
- - center – 円の中心座標.
- - radius – 円の半径.
- - color – 円の色.
- - thickness – 円の枠線の太さ.負の値の場合,円が塗りつぶされます.
- - lineType – 円の枠線の種類, Line() の説明を参照してください.
- - shift – 中心点の座標と半径の値において,小数点以下の桁を表すビット数
3. 円の中心点の座標を取得
各点は(x,y,r)を取得します。芯の位置ベクトル($x_c,y_c$)から、中心点からの各点の位置ベクトル(x_i,y_i)=(x-x_c,y-y_c)を求めます。x軸単位ベクトルを(x_e,y_e)=(1,0)とすると、内積から、
$$cos\theta = \frac{(x-x_c)x_e+(y-y_c)y_e}{\sqrt{x_c^2+y_c^2}\sqrt{(x-x_c)^2+(y-y_c)^2}}$$
$$cos\theta = \frac{x-x_c}{\sqrt{(x-x_c)^2+(y-y_c)^2}}$$
$$\theta = deg(arccos(\frac{x-x_c}{\sqrt{(x-x_c)^2+(y-y_c)^2}}))[deg]$$
【NumPy入門】ベクトルの大きさ(ノルム)を計算するnp.linalg.norm
np.linalg.norm(X, ord=2)
L2ノルム(ユークリッドノルム)は一般的な"長さ"です。
ノルムの意味とL1,L2,L∞ノルム -高校数学の美しい物語
グラフにして確認
結果が正しいかを確認します。アバウトでOKなので、画像とグラフを重ねていきます。
細かいことはかつて書きました=>[python]データのインポートと極座標系のグラフを表示- イノマタの趣味部屋
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
"""
CSVファイルから極座標系のグラフをプロットする
入力:(CSVファイル名, 出力画像ファイル名,半径,角度)
出力:出力画像ファイル名.png
"""
def polar(in_file,out_file,param_r,param_theta):
df = pd.read_csv("output/"+in_file)#ファイル読み込み
r = df[param_r]
deg = df[param_theta]
radian = np.deg2rad(deg)#
Len=len(r)
fig = plt.figure(figsize=(10, 10))#新しい図面
ax = fig.add_subplot(111, projection='polar')#軸の表示
ax.set_theta_direction(-1)#時計回り
#ax.plot(radian,r, '-o')
ax.plot(radian,r, 'or')
i=2
#while i < Len:
# ax.plot([radian[i],radian[i-1]],[r[i], r[i-1]], 'xr-')
# i +=2
plt.savefig(out_file)
plt.show()
polar('0%_d-Theta.csv','0%.png','Radius','Deg[deg]')
なお、画像は下記サイトで背景を透明にしました。
はじめの画像と重ね合わせると、うまくグラフにできていることが確認できます。 取得したCSVは正確みたいですね。
もともとの画像を修正したりエッジを強調したりすれはもっといい結果が得られるはずです。
雑記
パラメータの変更も自動化できれば良かったですが、できませんでした。
目的は達成したものの、手で測定するのと同じくらい時間がかかってしまったのも悲しいです。追加があれば効果が発揮できるんですが。
参考
画像処理の基本は、古い本ですがこの本が良いと思います。
他に、最新のアルゴリズムとか実用的な部分はOpenCVを見るのが早いでしょう。