「大阪万博まで500日を切りました」というようなニュースをちらほら聞いたりして、万博のロゴマークやマスコットキャラクターが話題になっているのを思い出しました。なんだか気持ち悪いだとかなんだとか。
「円とか楕円とかで組み合わせれば出来そうな形だなぁ」と思い、実際に作るとしたらどうなるのか、やってみました。
真剣なものを期待されていたら申し訳ありません。
実装環境
(そんな人はいないとは思うのですが)万が一「作りたい」という人がいてもいいように、とても気軽なGoogleColaboratoryを利用しています。
作業工程
円を検出したい
画像はロゴだけになるように編集しました。
import cv2
import numpy as np
from PIL import Image
import matplotlib.pyplot as plt
%matplotlib inline
img = cv2.imread("logo.png")
plt.figure(figsize=(10, 10))
img2 = img[:,:,::-1]
plt.xticks([]), plt.yticks([])
plt.imshow(img2)
まずロゴに特別な手を加えないでOpenCVにかけて図形を検出するかどうか試してみます。図形の輪郭を抽出するのに、画像自体をグレースケール化する必要があります。
import cv2
import numpy as np
# ファイルを読み込み グレースケール化
img = cv2.imread("logo.png", cv2.IMREAD_GRAYSCALE)
# しきい値指定によるフィルタリング
_, threshold = cv2.threshold(img, 240, 255, cv2.THRESH_BINARY)
# 輪郭を抽出
contours, _ = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
# フォントの指定(おまけ)
font = cv2.FONT_HERSHEY_DUPLEX
### 図形の数の変数
circle = 0
for cnt in contours:
approx = cv2.approxPolyDP(cnt, 0.01*cv2.arcLength(cnt, True), True)
cv2.drawContours(img, [approx], 0, (0), 2)
x = approx.ravel()[0]
y = approx.ravel()[1]
if len(approx) > 10:
### 図形を数える
circle +=1
cv2.putText(img, "circle{}".format(circle), (x, y), font, 0.8, (0))
# 結果の画像作成
cv2.imwrite('output_logo.png',img)
### 図形の数の結果
print('Number of circle = ' , circle)
img = cv2.imread("output_logo.png")
plt.figure(figsize=(10, 10))
img2 = img[:,:,::-1]
plt.xticks([]), plt.yticks([])
plt.imshow(img2)
私の想定では白い円と青い円をそれぞれ検出してくれるかなと予想していましたが、目玉の白い部分だけを綺麗に読み取っていました。
いったん青色を白色に変えて、白色の円を綺麗に検出していこうと思います。
画像内のカラーピックはPEKO STEPでやってみた結果、BGRで青(161,81,10)、赤(1,0,255)、白(255,255,255)でした。
青い部分を白くして白い部分を読み取ろうとしました。
import cv2
# 画像の読み込み
img2 = cv2.imread('logo.png')
# 指定色
target_color = np.array([161,81,10])
# 変更後の色
new_color = np.array([255, 255, 255])
# 各ピクセルを順に処理
rows, cols, _ = img2.shape
for i in range(rows):
for j in range(cols):
# 各ピクセルのBGR値を取得
pixel = img2[i, j]
# 特定の色かどうかを比較
if np.array_equal(pixel, target_color):
# 特定の色ならば新しい色に変更
img2[i, j] = new_color
plt.figure(figsize=(10, 10))
img2 = img2[:,:,::-1]
plt.xticks([]), plt.yticks([])
plt.imshow(img2)
# 変更後の画像を保存
cv2.imwrite('output.png', img2)
うわ、なんか怖くなってしまった…調べてみたところ、青色と白色の境目のところで多くの種類の色が混じっているようで、これらをすべて検出して、変更するのは正直手間だったので別の手立てで変更していくことにしました。
白い部分も青い部分も全部塗りつぶすのはこのサイトでやっちゃいました。人によっては画像編集ソフトなどで出来るとは思いますが、このサイトで目玉部分を塗りつぶしました。BANNER KOUBOU
青色の円を検出
青色でないところ以外すべて白色にして、青色の縁部分をハッキリ検出できるようにしていく。
# 画像の読み込み
img2 = cv2.imread('logo.png')
# 指定色
target_color = np.array([161,81,10])
# 変更後の色
new_color = np.array([255, 255, 255])
# 各ピクセルを順に処理
rows, cols, _ = img2.shape
for i in range(rows):
for j in range(cols):
# 各ピクセルのBGR値を取得
pixel = img2[i, j]
# 特定の色かどうかを比較
if not np.array_equal(pixel, target_color):
# 特定の色でないならば新しい色に変更
img2[i, j] = new_color
# 結果の画像作成
cv2.imwrite('copy_blue.png',img2)
この画像で円を検出していきます。
# ファイルを読み込み グレースケール化
img = cv2.imread("copy_blue.png", cv2.IMREAD_GRAYSCALE)
# しきい値指定によるフィルタリング
ret, threshold = cv2.threshold(img, 240, 255, cv2.THRESH_BINARY)
# 輪郭を抽出
contours, hierarchy = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
font = cv2.FONT_HERSHEY_DUPLEX
### 図形の数の変数
circle = 0
for cnt in contours:
approx = cv2.approxPolyDP(cnt, 0.01*cv2.arcLength(cnt, True), True)
cv2.drawContours(img, [approx], 0, (0), 2)
x = approx.ravel()[0]
y = approx.ravel()[1]
if len(approx) > 10:
### 図形を数える
circle +=1
cv2.putText(img, "circle{}".format(circle), (x, y), font, 0.8, (0))
# 結果の画像作成
cv2.imwrite('output_blue.png',img)
### 図形の数の結果
print('Number of circle = ' , circle)
円が3つしか検出できなかったと言われました。恐らくサイズが小さいこともあって頂点?と言える部分が10個も見つからなかったかもしれません。「if len(approx) > 10:」の設定を8に変更。
ちゃんと検出できました。ここから、それぞれの円の中心を見つけていきたいので、検出できたものを確認します。
for i in range(len(contours)):
print(len(contours[i]))
# 出力結果
# 3166
# 88
# 155
# 125
# 122
# 141
何故か6種類。円5以外に何が…と思いましたが、外枠を大きな四角形と認識したらしいです。最初のcontour以外は円なので、それらの中心と半径を求めるcv2.minEnclosingCircleを使います。
blue_circle_center = []
blue_circle_radius = []
for i in range(1,len(contours)):
# 外接円を求める
center, radius = cv2.minEnclosingCircle(contours[i])
# 半径と直径を表示
print(f"circle{i}")
print(center)
print("半径:", radius)
print("直径:", 2 * radius)
blue_circle_center.append(center)
blue_circle_radius.append(radius)
それぞれの(x座標、y座標)、半径と直径すべて算出できた。一応ちゃんと中心に行くかどうか確認。
img = cv2.imread("output_blue.png")
# cv2.circleは整数の座標でしか描画できないので、座標の小数部分を四捨五入する。
for center in blue_circle_center:
cv2.circle(img, (round(center[0]), round(center[1])), 3, (255, 255, 255),thickness=3, lineType=cv2.LINE_AA)
plt.figure(figsize=(10, 10))
img2 = img[:,:,::-1]
plt.xticks([]), plt.yticks([])
plt.imshow(img2)
青色は比較的綺麗な円が多いので、中心になっているかどうかも分かりやすいですね。
白色の円を検出
次は白色の円・楕円部分を検出していきます。事前に編集ソフトなどで青色部分を白色に塗りつぶします。
おお、こわいこわい。さっきとほぼ同じようにやっていく。
# ファイルを読み込み グレースケール化
img = cv2.imread("logo_white.png", cv2.IMREAD_GRAYSCALE)
# しきい値指定によるフィルタリング
ret, threshold = cv2.threshold(img, 240, 255, cv2.THRESH_BINARY)
# 輪郭を抽出
contours, hierarchy = cv2.findContours(threshold, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
font = cv2.FONT_HERSHEY_DUPLEX
### 図形の数の変数
circle = 0
for cnt in contours:
approx = cv2.approxPolyDP(cnt, 0.01*cv2.arcLength(cnt, True), True)
cv2.drawContours(img, [approx], 0, (0), 2)
x = approx.ravel()[0]
y = approx.ravel()[1]
if len(approx) > 10:
### 図形を数える
circle +=1
cv2.putText(img, "circle{}".format(circle), (x, y), font, 0.8, (0))
# 結果の画像作成
cv2.imwrite('output_white_gray.png',img)
img = cv2.imread("output_white_gray.png")
plt.figure(figsize=(10, 10))
img2 = img[:,:,::-1]
plt.xticks([]), plt.yticks([])
plt.imshow(img2)
circle3が楕円気味なので、すべて楕円として以降は処理していきます。もっと簡単な書き方もあるとは思います。
white_circle_center = []
white_circle_width = []
white_circle_height = []
white_circle_angle = []
for i in range(len(contours)):
# 外接円を求める
center, (width, height), angle = cv2.fitEllipse(contours[i])
# 半径と直径を表示
print(f"circle{1+i}")
print(center)
print("幅:", width)
print("高さ:", height)
print(angle)
white_circle_center.append(center)
white_circle_width.append(width)
white_circle_height.append(height)
white_circle_angle.append(angle)
算出できたので、当てはめてどうなるか確認します。
img = cv2.imread("output_white_gray.png")
# cv2.circleは整数の座標でしか描画できないので、座標の小数部分を四捨五入する。
for center in white_circle_center:
cv2.circle(img, (round(center[0]), round(center[1])), 3, (0, 0, 0),thickness=3, lineType=cv2.LINE_AA)
plt.figure(figsize=(10, 10))
img2 = img[:,:,::-1]
plt.xticks([]), plt.yticks([])
plt.imshow(img2)
ちょっと怖い目玉ができましたね。
これで青、白ができたので、赤い部分を検出していきます。
赤色の楕円を検出
この赤い輪が12個の円・楕円で組み合わさっていると見ることで、検出できるようにしていきます。このままだと円として検出できないので、白い円をちょうど重ねるようにして検出できるようにしました。正直に言ってしまうと、ここで多少の誤差が生まれてしまうとは思うのですが、「透過させてピッタリにする」みたいな方法が思いつかなかったのでかなりゴリ押しです。Google 図形描画で頑張りました。
それぞれの画像ごとに白色の円を検出していきます。と行きたかったのですが、cv2.fitEllipseは5つ以上検出しないと毎回エラーが発生する+3枚目の左側の白い円が何度やっても検出できないため、使う画像を3枚目だけ変更しました。
背景が白色だと何が何だか分からないと思いますが、検出する図形を1枚目・2枚目と同じ個所のものを増やしました。
やることは白色の円・楕円でやったことと変わりありませんので、ここは省略しちゃいます。うまく12種類の赤色の円・楕円の情報がわかったら作成していきます。
実装
import numpy as np
import cv2
# 土台
img = np.full((830, 755, 3), 255, dtype=np.uint8)
for i in range(12):
cv2.ellipse(img, ((round(red_circle_center[i][0]), round(red_circle_center[i][1])), ((round(red_circle_width[i]), round(red_circle_height[i]))), red_circle_angle[i]), (1, 0, 255), thickness=-1)
for i in range(5):
cv2.ellipse(img, ((round(white_circle_center[i][0]), round(white_circle_center[i][1])), ((round(white_circle_width[i]), round(white_circle_height[i]))), white_circle_angle[i]), (255, 255, 255), thickness=-1)
for i in range(5):
cv2.circle(img, (round(blue_circle_center[i][0]), round(blue_circle_center[i][1])), round(blue_circle_radius[i]), (161, 81, 10),thickness=-1)
cv2.imwrite("myaku.png", img)
それまでに検出した値などをまとめて描画していきます。赤→白→青の順に描画していくことで、上書きされていきます。やってみましょう。
出来たぁ~~~~。結構大変でした…OpenCVでどんなことができるのかを知るいいきっかけになりました。
終わりに
今回は正直言ってしょうもないことに利用しましたが、本当は特定の物体を検知するのに使えたりするはずです。少しでも興味を持っていただけたら嬉しいです。