@@ -139,7 +139,95 @@ <h3 class="masthead-title">
139
139
< div class ="container content ">
140
140
< div class ="post ">
141
141
< h1 class ="post-title "> 画像中の図形検出</ h1 >
142
-
142
+ < p > 今回はOpenCVを用いて画像中から図形を検出する。図形の検出は、画像からのエッジの情報を取り出す処理と、エッジ情報を元に境界線を認識する処理、境界線から図形の形状を分類する処理の三つに分けられる。</ p >
143
+
144
+ < h2 id ="エッジの検出 "> エッジの検出</ h2 >
145
+
146
+ < p > 画像処理におけるエッジ抽出で広く用いられているのはSobelフィルタとCanny法だろう。</ p >
147
+
148
+ < h3 id ="sobelフィルタ "> Sobelフィルタ</ h3 >
149
+
150
+ < p > Sobelフィルタは画像のある方向に対する勾配をとるときに、その方向と直交する方向に平滑化をかけることで、ノイズの影響を抑えつつエッジを検出する方法である。画像処理の教科書だけでなくWeb上にも多くの解説があるので、各自調べて見てほしい。</ p >
151
+
152
+ < h3 id ="canny法 "> Canny法</ h3 >
153
+
154
+ < p > Canny法はSobelフィルタと比べると少し処理が複雑で、いくつかの処理の組み合わせからなる。</ p >
155
+
156
+ < p > まず画像に対してガウシアンフィルタをかけてノイズの影響を抑える。その後、画像の縦方向と横方向について画像の勾配を計算する。この処理には多くの場合、Sobelフィルタが用いられる。Sobelフィルタでは、一定の幅を持つエッジが検出されるが、Canny法は勾配の方向に沿って、勾配の値が最大となる場所だけをエッジ上の画素の候補とする。</ p >
157
+
158
+ < p > その後、候補となる画素の中で勾配が一定以上となる場所を閾値処理により取り出すことで、最終的なエッジが得られる。なお、最後の閾値処理が一般的な閾値処理ではなくヒステリシス閾値処理と呼ばれる二つの閾値を用いた適応的な閾値処理である。これについても各自、どのようなものか調べてみてほしい(この二つの閾値がOpenCVの関数の引数になっている)。</ p >
159
+
160
+ < p > 以下がOpenCVでSobelフィルタとCanny法によりエッジ画像を得るプログラムと、その結果である。なお、以下のコードでは結果を見やすくするため、Sobelフィルタの結果を強調した後、二値化の処理をいれている。</ p >
161
+
162
+ < div class ="language-python highlighter-rouge "> < div class ="highlight "> < pre class ="highlight "> < code > < span class ="c1 "> # 画像をグレースケールにする
163
+ </ span > < span class ="n "> gray</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> cvtColor</ span > < span class ="p "> (</ span > < span class ="n "> image</ span > < span class ="p "> ,</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> COLOR_BGR2GRAY</ span > < span class ="p "> )</ span >
164
+ < span class ="c1 "> # Sobelフィルタ
165
+ </ span > < span class ="n "> dx</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> Sobel</ span > < span class ="p "> (</ span > < span class ="n "> gray</ span > < span class ="p "> ,</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> CV_8U</ span > < span class ="p "> ,</ span > < span class ="mi "> 1</ span > < span class ="p "> ,</ span > < span class ="mi "> 0</ span > < span class ="p "> )</ span >
166
+ < span class ="n "> dy</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> Sobel</ span > < span class ="p "> (</ span > < span class ="n "> gray</ span > < span class ="p "> ,</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> CV_8U</ span > < span class ="p "> ,</ span > < span class ="mi "> 0</ span > < span class ="p "> ,</ span > < span class ="mi "> 1</ span > < span class ="p "> )</ span >
167
+ < span class ="n "> sobel</ span > < span class ="o "> =</ span > < span class ="n "> np</ span > < span class ="p "> .</ span > < span class ="n "> sqrt</ span > < span class ="p "> (</ span > < span class ="n "> dx</ span > < span class ="o "> *</ span > < span class ="n "> dx</ span > < span class ="o "> +</ span > < span class ="n "> dy</ span > < span class ="o "> *</ span > < span class ="n "> dy</ span > < span class ="p "> )</ span >
168
+ < span class ="n "> sobel</ span > < span class ="o "> =</ span > < span class ="p "> (</ span > < span class ="n "> sobel</ span > < span class ="o "> *</ span > < span class ="mf "> 128.0</ span > < span class ="p "> ).</ span > < span class ="n "> astype</ span > < span class ="p "> (</ span > < span class ="s "> 'uint8'</ span > < span class ="p "> )</ span >
169
+ < span class ="n "> _</ span > < span class ="p "> ,</ span > < span class ="n "> sobel</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> threshold</ span > < span class ="p "> (</ span > < span class ="n "> sobel</ span > < span class ="p "> ,</ span > < span class ="mi "> 0</ span > < span class ="p "> ,</ span > < span class ="mi "> 255</ span > < span class ="p "> ,</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> THRESH_BINARY</ span > < span class ="o "> +</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> THRESH_OTSU</ span > < span class ="p "> )</ span >
170
+ < span class ="c1 "> # Canny法
171
+ </ span > < span class ="n "> canny</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> Canny</ span > < span class ="p "> (</ span > < span class ="n "> gray</ span > < span class ="p "> ,</ span > < span class ="mi "> 100</ span > < span class ="p "> ,</ span > < span class ="mi "> 200</ span > < span class ="p "> )</ span >
172
+ </ code > </ pre > </ div > </ div >
173
+
174
+ < table class ="images ">
175
+ < tr >
176
+ < td > < p > < strong > 入力画像</ strong > </ p >
177
+ </ td >
178
+ < td > < p > < strong > Sobelフィルタ</ strong > </ p >
179
+ </ td >
180
+ < td > < p > < strong > Canny法</ strong > </ p >
181
+ </ td >
182
+ </ tr >
183
+ < tr >
184
+ < td > < a class ="lightbox-link " href ="/cpp-python-beginners/public/images/figure_detection/figures.jpg " data-lightbox ="images-1 " data-title =""> < img src ="/cpp-python-beginners/public/images/figure_detection/figures.jpg " alt ="" style ="" /> </ a >
185
+ </ td >
186
+ < td > < a class ="lightbox-link " href ="/cpp-python-beginners/public/images/figure_detection/sobel.jpg " data-lightbox ="images-1 " data-title =""> < img src ="/cpp-python-beginners/public/images/figure_detection/sobel.jpg " alt ="" style ="" /> </ a >
187
+ </ td >
188
+ < td > < a class ="lightbox-link " href ="/cpp-python-beginners/public/images/figure_detection/canny.jpg " data-lightbox ="images-1 " data-title =""> < img src ="/cpp-python-beginners/public/images/figure_detection/canny.jpg " alt ="" style ="" /> </ a >
189
+ </ td >
190
+ </ tr >
191
+ </ table >
192
+
193
+ < h3 id ="エッジの統合 "> エッジの統合</ h3 >
194
+
195
+ < p > 結果を見てもらうと分かる通り、上記の入力画像は図形の輪郭線が幅を持っており、そのせいでエッジが二重線になっていることが分かる。もちろん、これはこれで間違いとは言えないのだが、今回のケースではエッジが内側の線と外側の線の間で曖昧に検出されており、この後の輪郭線検出に影響を及ぼす可能性がある。この問題は自然画像を入力とする場合には、より顕著となる。</ p >
196
+
197
+ < p > そこで、輪郭線を抽出する前に、モルフォロジー演算によりエッジの統合を行っておく。モルフォロジー演算は一般的には二値画像を入力として、白色の領域を広げたり狭めたりする操作である(モルフォロジー演算の数学的な意味とは解釈が異なるので注意)。今回は白色領域を広げる処理であるdilationと白色領域を狭める処理であるerosionを連続して施すことでエッジの統合を行う。</ p >
198
+
199
+ < p > モルフォロジー演算は構造要素と呼ばれる小さな画像のようなものを入力の二値画像にしたがって繰り返す操作に対応する。したがって、OpenCV(やMATLABなどの他のライブラリ)では、モルフォロジー演算の関数は二値画像と構造要素を引数にとる。OpenCVを用いる場合には、dilationとerosionはそれぞれ以下のようなコードで実現される。</ p >
200
+
201
+ < div class ="language-python highlighter-rouge "> < div class ="highlight "> < pre class ="highlight "> < code > < span class ="n "> res_erode</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> erode</ span > < span class ="p "> (</ span > < span class ="n "> binary</ span > < span class ="p "> ,</ span > < span class ="n "> np</ span > < span class ="p "> .</ span > < span class ="n "> ones</ span > < span class ="p "> ((</ span > < span class ="mi "> 5</ span > < span class ="p "> ,</ span > < span class ="mi "> 5</ span > < span class ="p "> ),</ span > < span class ="n "> dtype</ span > < span class ="o "> =</ span > < span class ="n "> binary</ span > < span class ="p "> .</ span > < span class ="n "> dtype</ span > < span class ="p "> ))</ span >
202
+ < span class ="n "> res_dilate</ span > < span class ="o "> =</ span > < span class ="n "> cv2</ span > < span class ="p "> .</ span > < span class ="n "> dilate</ span > < span class ="p "> (</ span > < span class ="n "> binary</ span > < span class ="p "> ,</ span > < span class ="n "> np</ span > < span class ="p "> .</ span > < span class ="n "> ones</ span > < span class ="p "> ((</ span > < span class ="mi "> 5</ span > < span class ="p "> ,</ span > < span class ="mi "> 5</ span > < span class ="p "> ),</ span > < span class ="n "> dtype</ span > < span class ="o "> =</ span > < span class ="n "> binary</ span > < span class ="p "> .</ span > < span class ="n "> dtype</ span > < span class ="p "> ))</ span >
203
+ </ code > </ pre > </ div > </ div >
204
+
205
+ < p > エッジを統合する場合、エッジが白色の領域に検出されているとすると、先にdilation、次にerosionの順序で処理を施す。dilationにより二重のエッジのそれぞれが太くなり、重なることで、太い1本のエッジとなる。その後、erosionをかけることで、そのエッジが細くなり、1本の細いエッジとなる。</ p >
206
+
207
+ < table class ="images ">
208
+ < tr >
209
+ < td > < p > < strong > dilation後</ strong > </ p >
210
+ </ td >
211
+ < td > < p > < strong > dilation + erosion後</ strong > </ p >
212
+ </ td >
213
+ </ tr >
214
+ < tr >
215
+ < td > < a class ="lightbox-link " href ="/cpp-python-beginners/public/images/figure_detection/dilate.jpg " data-lightbox ="images-1 " data-title =""> < img src ="/cpp-python-beginners/public/images/figure_detection/dilate.jpg " alt ="" style ="" /> </ a >
216
+ </ td >
217
+ < td > < a class ="lightbox-link " href ="/cpp-python-beginners/public/images/figure_detection/unified_edges.jpg " data-lightbox ="images-1 " data-title =""> < img src ="/cpp-python-beginners/public/images/figure_detection/unified_edges.jpg " alt ="" style ="" /> </ a >
218
+ </ td >
219
+ </ tr >
220
+ </ table >
221
+
222
+ < p > 上記の結果の通り、dilationとerosionを組み合わせることで二本の細いエッジが一本のエッジに統合されていることが分かる。</ p >
223
+
224
+ < h2 id ="輪郭線の検出 "> 輪郭線の検出</ h2 >
225
+
226
+ < p > ここまでに検出したエッジの情報を使って輪郭線を抽出する。輪郭線の抽出にはOpenCVの< code class ="highlighter-rouge "> findContours</ code > という関数を用いる。余談だが、この関数には1985年にSuzukiさんとAbeさんという日本人が提案したアルゴリズムが実装されている(*1)。</ p >
227
+
228
+ < p > (*1) S. Suzuki and K. Abe, “Topological structural analysis of digitized binary images by border following”, Computer Vision, Graphics and Image Processing”, 1985.</ p >
229
+
230
+ < p > このアルゴリズムは二値画像(1がエッジで0が背景の色とする)をスキャンラインオーダー (左上から右下の順番) に走査していき、1が見つかったら、そのエッジに階層番号を付与する。階層番号は例えば二重丸のような物があったときに、外側の輪郭が1、内側の輪郭が2になるような番号である。この階層番号は最初1に初期化しておいて、1の画素(エッジと思われる画素)が見つかったら、そこに今の階層番号を記録します。以後1の画素が連続して見つかる間は同じ番号を記録していきますが、1のあとに</ p >
143
231
144
232
</ div >
145
233
0 commit comments