質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Q&A

解決済

2回答

3771閲覧

pythonで黒板を検出&座標の取得

HandbollIsGold

総合スコア1

OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

1グッド

0クリップ

投稿2021/11/17 14:05

編集2021/11/30 12:43

前提・実現したいこと

python3.8 opencv4.5.3.56を使っているプログラミング初心者です。
pythonでopencvを用いて黒板の角4点の座標を取得するプログラムを考えています。

現在は画像を表示し、マウスイベントによって4点の座標を取得するプログラムにしているのですが、5回以上クリックした際の配列格納に悪戦苦闘しています。ので、エッジ検出などユーザー非依存の取得方法を用いたプログラムにしたく質問させていただきました。

### 処理を行う画像の前提条件

  • 画像上には黒板が1つのみ写っている。
  • 黒板は全体が写っている。(黒板に物体が被っていない)が、教員が黒板に被っている状態の黒板を処理するものとする。
  • 黒板は45°以上の角度をつけて撮影していない。
  • 撮影する状況として、壁が黒板と似た色であり、黒板のふちは木製である。
  • 黒板にあたる光の強弱は考慮しないものとする。
  • 認識の難易度を上げてしまう何かはとりあえず考えないものとする。(最終的にはそこも対策したい)
  • 画像上で「黒板」が占める割合は約75%以上とする。

###実際の写真(例)
実際の写真(例)
実際に座標を取得しようとしている黒板は湾曲しています。

該当のソースコード

python

1import cv2 2import os 3 4 5all_xy = [] 6 7def save_frame(video_path, frame_num, result_path): 8 cap = cv2.VideoCapture(video_path) 9 10 if not cap.isOpened(): 11 return 12 13 os.makedirs(os.path.dirname(result_path), exist_ok=True) 14 15 cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num) 16 17 ret, frame = cap.read() 18 19 if ret: 20 cv2.imwrite(result_path, frame) 21 22save_frame('C:\Users\Mayod\PycharmProjects\pythonProject\sotsuken\WIN_20211112_10_43_35_Pro.mp4', 1, 'C:\Users\Mayod\PycharmProjects\pythonProject\sotsuken\sample_1.jpg') 23 24 25 26img = cv2.imread('sample_1.jpg', 1) 27cv2.namedWindow('image') 28 29 30def coordinates(event,x,y, flags,param): 31 sample_xy = [] 32 count = 0 33 if event == cv2.EVENT_LBUTTONDOWN: 34 sample_xy.append(x) 35 sample_xy.append(y) 36 print(x,y) 37 38 if (len(all_xy) < 4): 39 all_xy.append(sample_xy) 40 elif (count == 0): 41 all_xy[count] = sample_xy 42 count += 1 43 elif (count == 1): 44 all_xy[count] = sample_xy 45 count += 1 46 elif (count == 2): 47 all_xy[count] = sample_xy 48 count += 1 49 else: 50 all_xy[count] = sample_xy 51 count = 0 52 53 54 55 56 57 58cv2.imshow('image', img) 59cv2.setMouseCallback('image', coordinates) 60cv2.waitKey(0) 61print(all_xy) 62cv2.destroyAllWindows() 63 64 65 66
退会済みユーザー👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2021/11/17 21:15 編集

> エッジ検出などユーザー非依存の取得方法を用いたプログラムにしたく質問させていただきました。 - マウスでポチポチクリックするのではロバスト性が低いよね、めんどくさいし。 - 画像処理的にバチっと黒板が検出できたら楽だよね。 - もっと精度/ロバスト性のいい方法があれば知りたいなぁ であってますか?もしそうなら質問をそう修正ください。 もしもっと別の質問があるならそのように変更ください。 そうしないと「投げやりな質問 -1」とされてしまいます。 参考:精度は高いと思いますが多分ロバスト性は低いです。 https://stackoverflow.com/questions/66461085/opencv-find-blackboard-edges-on-video-and-images
fana

2021/11/18 01:33

> 精度/ロバスト性 といった話をする場合には,「どのような画像を相手にする想定なのか」という話が必要と思う. - 黒板よりも手前に様々な物があってオクルージョンがどうの…とか,黒板全体が写っていない(見切れている)とかいう絵でもやりたいかどうか - 黒板をほぼほぼ{真横から,真上から}みたいなすごい角度から見た絵でもやりたいとか - 「黒板」とか言ってるけど実際の色は不明だったり(緑とは限らないよね). - 黒板の一部にのみ激しく光が当たっておりそうでない部分は激しく暗い,といったような環境も考慮する必要があるとか(教室の黒板とかってわりとそうなるよね) - 黒板に 認識処理の難易度を激しく上げるような何か が描かれている場合は? 例えば「超リアルな黒板の絵」が描かれているかもしれないが? - 画像に黒板がいくつ写っているか?という前提条件は設けるのか? 設けないなら,1個も写ってない場合にも対応する必要ある? - 画像上で「黒板」の像が占める広さに関する前提条件は設けるのか? 黒板がめちゃくちゃ遠くにあるとかいう絵でも処理できないとダメ? - etc...
fana

2021/11/18 01:47 編集

(↑とはまた別の話) マウスでクリックするという作業を行うこと自体に問題が無い(:5回以上クリックした際にどうのこうの…という話さえ潰せばそれで実用できる)のであれば, 無理して「ユーザー非依存の取得方法」という方向に行かなくても良いんじゃないの? とも見えるけど,そこらへんはどんな感じなのでしょう?
HandbollIsGold

2021/11/25 14:43

ご指摘ありがとうございます。処理を行う画像の前提条件として、以下の通りに考えています。 ・画像上には黒板が1つのみ写っている。 ・黒板は全体が写っている。(黒板に物体が被っていない)が、教員が黒板に被っている状態の黒板を処理するものとする。 ・黒板は45°以上の角度をつけて撮影していない。 ・撮影する状況として、壁が黒板と似た色であり、黒板のふちは木製である。 ・黒板にあたる光の強弱は考慮しないものとする。 ・認識の難易度を上げてしまう何かはとりあえず考えないものとする。(最終的にはそこも対策したい) ・画像上で「黒板」が占める割合は約90%以上とする。 ・黒板を撮影する上で、 また、マウスクリックによる座標取得の問題点として取得した座標を格納する際に、[[x1,y1]...[x4,y4]]のように二次元配列に格納するのですが、5回以上クリックしてしまうと、[x5,y5]...も格納されてしまいその後の処理に影響がでてしまいます。この問題はリセットするキーを作成することで、やり直しをするよう考えています。 返答遅れてしまい、本当に申し訳ないです。
退会済みユーザー

退会済みユーザー

2021/11/25 14:48

実際の写真(例)を掲載できますか? > 5回以上クリックしてしまうと(略) 5回目でリセットして「リストを空にしたからまた初めからやってくれ」もありかもしれませんね。 (入力や特例は少ない方がプログラミングしやすいですし)
fana

2021/11/26 01:05

クリックに関しては,「点を最大4個まで配置できるもの」をどう作るか? というだけの話ですよね.例えば,以下のようなものとかは簡単に作れて操作もしやすいような気がします. [左クリックの機能] ・クリック位置に最も近い既存の点との距離が一定値以下の場合には,その点をクリック位置に移動させる.(:点の位置の修正機能) ・全ての既存点から一定以上離れている場合であって,且つ,既存の点の個数が4個に満たない場合には,クリック位置に新たな点を追加する.(:点を配置する機能) ・(上記のいずれにも該当しない場合は何もしない) [右クリックの機能] ・クリック位置に最も近い既存の点との距離が一定値以下の場合には,その点を削除する. ・(上記に該当しない場合は何もしない)
fana

2021/11/26 01:22 編集

認識処理に関しては,相手にする絵の条件(対象とする黒板と,その撮影状況とか)が具体的にあるならば,その条件をうまいこと利用した方法は無いだろうか…? と考えてみるとよいでしょう. 例えば, 黒板の面の色 も 縁の色 もわかっていて,黒板の縁の形状は(撮影画像上で)直線であると言える場合であれば… 面と縁の間の直線的な境界線(エッジ)を検出することは比較的容易と思える(色味や明るさを手掛かりにして頑張れそうである). その直線のどちら側が黒板の面であるのかも判断がつくから,境界線を1本見つけるごとに「画像内でこの線よりもこっち側」という絞り込みができる→4本見つけたら面の範囲が分かるでは…!? (4つ角の座標が欲しいなら直線の交点として求めればいい) …とかなんとかいうアイデアが浮かぶ. で,「でも面と縁の境界線ではない箇所から線を見つけてしまうことだって考えらるが,そういうのが多少あっても大丈夫にするにはどうするか…?」とか考えていく.
退会済みユーザー

退会済みユーザー

2021/11/30 12:57 編集

import cv2 import numpy as np img = cv2.imread("test.jpeg") # img = cv2.resize(img,None,fx=0.5,fy=0.5) # cv2.imshow("img",img) # cv2.waitKey(0) # HSVでマスクを作る img_HSV = cv2.cvtColor(img,cv2.COLOR_BGR2HSV) img_bin = cv2.inRange(img,(100,128,180),(255,255,255)) # マスクをかぶせる img_masked = img.copy() img_masked[img_bin==0] = (0,0,0) img_masked = cv2.resize(img_masked,None,fx=0.5,fy=0.5) # cv2.imshow("img_masked",img_masked) # cv2.waitKey(0) img_g = cv2.cvtColor(img_masked,cv2.COLOR_BGR2GRAY) # cv2.imshow("img_g",img_g) みたいな感じで「人間が見れば線」は検出できましたが、Hough検出で線を出そうとすると、背景の灰色と黒板の淵の茶色が灰色っぽい感じでうまくいかなさそうな感じがします。「なら黒板の四角をblob検出でだせるじゃないか」もあるとは思いますが、前提の「人が写るかもよ?」を加味するとどこまでうまくいくか未知数です。 パラメータを駆使してこの画像ではうまくいくパラメータは出せるかもしれませんが、汎用性は相当低そうです。多分写真の左に窓があって、そこから光が差し込んで白っぽくなっているのでハードルが上がっているのだと思います。 という感じで、fanaさんのクリックのやり方が一番いいと思います。 黒板の四隅に派手な赤のマークを付ければすぐに検出できそうですが…
HandbollIsGold

2021/11/30 15:49

ありがとうございます。光の差し込み等も考慮するとクリックによる座標取得が良さそうです。他の解決方法として、赤色や比較的わかりやすい色(黒板と酷似しない)の磁石を4つ(または4つ以上)用意し、画像処理で座標取得も考えています。この方法が良ければこの質門は解決としたいと思っています。
退会済みユーザー

退会済みユーザー

2021/11/30 21:22

> 赤色や比較的わかりやすい色(黒板と酷似しない)の磁石を4つ(または4つ以上)用意し、画像処理で座標取得も考えています。 これが抜群に簡単です。基本的なやり方は「2021/11/30 21:57」のコメントのやり方をベースに、赤色領域に対してblob検出と中心の計算でいけるはずです。
fana

2021/12/01 01:24 編集

人工的なマーカ(この話だと磁石)を配置する場合,画像処理でN個の候補が見つかったときに ・マーカではないものが検出されている可能性がある ・本物のマーカは所望の個数検出されていない可能性がある といったことを(どの程度頑張るべき話なのかは不明ですが)相応に考える必要はあるかもしれません. ・前者側について言えば,マーカが小さくて形状がシンプルであればあるほど誤検出の除去が難しくなるでしょう. ・人物がマーカに被る可能性があるならば画像処理の性能とは無関係に後者側が生じ得ます.
guest

回答2

0

ベストアンサー

古典的ですが、ハフ変換メインで実施してみました。

import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread('./sample_data/kokuban.jpeg') # 適度にリサイズします img_res = cv2.resize(img, dsize=None, fx=0.5, fy=0.5) # 輪郭取得 gray = cv2.cvtColor(img_res, cv2.COLOR_BGR2GRAY) blur = cv2.medianBlur(gray, 5) adapt_type = cv2.ADAPTIVE_THRESH_GAUSSIAN_C thresh_type = cv2.THRESH_BINARY_INV bin_img = cv2.adaptiveThreshold(blur, 255, adapt_type, thresh_type, 21, 2) plt.imshow(bin_img, cmap="gray")

イメージ説明

# ハフ変換の閾値 rho, theta, thresh = 2, np.pi/180, 300 c_v, r_v = np.pi, 0.01 # 垂直線の閾値 c_h, r_h = np.pi/2, 0.01 # 水平線の閾値 # 水平線、垂直線をそれぞれ個別に検出 lines_h = cv2.HoughLines(bin_img, rho, theta, thresh, min_theta=c_h-r_h, max_theta=c_h+r_h) lines_v = cv2.HoughLines(bin_img, rho, theta, thresh, min_theta=c_v-r_v, max_theta=c_v+r_v) # 垂直線に関しては中央近辺に余計な縦線(教壇のエッジ)が入るので描画範囲を指定する h, w = bin_img.shape div_num = 4 v_th_ner = w//div_num v_th_far = (div_num-1)*v_th_ner print(v_th_ner, v_th_far) # 条件に当てはまるものだけを残す(cv2.HoughLines()の返り値ρ,θのρで判定) lines_v = lines_v[(abs(lines_v[:,:,0]) < v_th_ner) | (v_th_far < abs(lines_v[:,:,0]))] lines_v = np.expand_dims(lines_v, 1) print(lines_v)

240 720
[[[ -41. 3.1315928]]

[[-911. 3.1315928]]

[[-925. 3.1315928]]

[[-919. 3.1315928]]

[[ -31. 3.1315928]]]

# 検出した線を描画する関数 def draw_line(img:np.ndarray, lines:list): for rho, theta in lines.squeeze(axis=1): 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),(255,255,255),2) return img # 描画状況を確認しておく draw = draw_line(img_res.copy(), lines_h) draw = draw_line(draw, lines_v) plt.imshow(cv2.cvtColor(draw, cv2.COLOR_BGR2RGB))

イメージ説明

# 黒い背景に検出線のみを描画 gray = np.zeros(img_res.shape[:2], np.uint8) gray = draw_line(gray, lines_h) gray = draw_line(gray, lines_v) # 反転 gray_inv = cv2.bitwise_not(gray) plt.imshow(gray_inv, cmap="gray")

イメージ説明

# ラベリング処理 label = cv2.connectedComponentsWithStats(gray_inv) # オブジェクト情報を項目別に抽出 stats = label[2] center = label[3] area = stats[:, cv2.CC_STAT_WIDTH] * stats[:, cv2.CC_STAT_HEIGHT] idx = area.argsort()[-2:-1] # 最も大きなものを検出(1番目は背景なので除く) # オブジェクト情報を利用してラベリング結果を表示 for i in idx: # 各オブジェクトの外接矩形を赤枠で表示 x0 = stats[i][0] y0 = stats[i][1] x1 = stats[i][0] + stats[i][2] y1 = stats[i][1] + stats[i][3] cv2.rectangle(img_res, (x0, y0), (x1, y1), (0, 0, 255), 2) plt.imshow(cv2.cvtColor(img_res, cv2.COLOR_BGR2RGB))

イメージ説明

矩形ではなくポリゴンが良いのであれば、この検出範囲を少し拡大させたものをマスクにして、その後、エッジ検出→convexHul→approxPolyDPというような手順をとると良いかもしれません。

投稿2021/12/03 16:10

taront

総合スコア59

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

taront

2021/12/04 01:49 編集

余談かもしれませんが、クリック回数の問題は以下のように循環する感じで回避できませんかね? all_xy = [] count=0 for i in range(10):  points_num = 4  if (len(all_xy) < points_num):   all_xy.append(i)  all_xy[count%points_num]=i  count+=1 print(all_xy)
fana

2021/12/07 02:08 編集

adaptiveThreshold をエッジ検出的に使うのは面白いですね. ハロー効果みたいなのを逆に利用する的な. 私の回答での adaptiveThreshold の目的が,ある程度大域的に{明るい所/暗い所}を分けること(ごくふつーの「輝度での二値化」)なのに対して, この回答では輝度の変化がある場所を得る使い方をしている. (個人的にこの点を高評価理由とする)
taront

2021/12/07 03:04

fana様のような演繹的な考察があったわけではなく、なんとなくやってみたというのが実際のところです。 フォーローのコメントありがとうございます(勉強になりました)。
guest

0

「回答」というわけではありませんが,何かしら一部でもヒントになるかもしれない(?)ので,
ちょっと画像をいじって見ていた話を書いておきます.

※画像サイズが大きかったので,てきとーに縮小して(480x270にして)用いました.

(1)明暗のマスク

今回の画像は壁も暗くて黒板の縁は明るいので,とりあえず脳死的に「明るさでの2値化」という話が思いつきます.
光の当たり方の明暗が存在してはいますが,くっきりした影のようなものが映っているわけではないので,
この程度のなだらかな変化であれば adaptiveThreshold で対処できると考えました.

グレースケール化して adaptiveThresholdした結果の絵:
適応二値化

(2)色味も使ってみる

黒板の縁の木の色も利用して,(1)の結果からちょっとでも要らない領域を捨てます.
HSVの空間で(あまり厳しく絞り込まないくらいラフに) inRange した結果の絵を作り…:
色相のマスク

これと(1)の結果の AND を取ってみました:
ANDしたマスク

(3)エッジの向きとか見てみる

黒板の像が画像の大部分を占めている,という話から,「画像の中央位置は黒板領域に含まれている」という仮定の元に,(2)のマスクで白の画素群に関して,輝度勾配方向を見てやることで「黒板の縁と黒板の内側の間のエッジかもしれない画素」を抽出してみました.
(黒板の縁の4辺の候補を見つけるような意図の処理です.)
結果は適当に上下左右4方向で色を変えて描画してあります:
エッジ色分け

※エッジ強度を見て捨てることをほぼしてないので,下の方でエッジでもない箇所にも黄色い描画結果がたくさん出てますが,これは「エッジに限る」とかすれば簡単に捨てることができるかと.


…と,まぁ,このくらい軽く見てみました.
勾配方向に基づいて候補を探しているのは,「人が被った際の影響を軽くできるかな?」とか考えてのことです.
(人のエッジの多くは勾配方向の制約条件で棄却できるのではないか?っていう)

この先は,どうしていけば良い方法になっていくか?というところは未知数ですが,
候補ごとに直線なりちょっとした2次関数を当てはめてみて,それらの式の交点を計算してみるだとか,
あるいは2種類(↑の描画結果で言えば2色)に近い位置のコーナー点を探してみるだとか,
まぁ色々と試してみる方向性はあるのかな? とか思ったりしました.はい.

投稿2021/12/03 09:24

編集2021/12/03 09:25
fana

総合スコア11996

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

fana

2021/12/03 09:35 編集

白黒のマスクの時点で 領域の面積とかを見て捨てるだとか,モルフォロジがどうのだとか, まぁやれることもあると思う. adaptiveThreshold は,ブロックサイズとしてはてきとーに「画像の横幅の1/5」を用い,調整値Cは-16とした. inRangeの閾値は,(15/2,16,128) と (60/2,255,255) を用いた. (今回の画像だけに特化しすぎてもどうかと思うので,ここはざっくりと「色味と明るさが厳しくない条件を満たすこと」くらいの感じの閾値とした) 最後の輝度勾配方向の算出については カーネルサイズ5 の Sobelフィルタの結果を用いた. (X方向とY方向の Sobel の結果値がそのまま2次元の勾配方向だとみなした)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問