コマンドラインツール化に伴い、前回より少し工夫してみました。
前回の最大の問題点は、主線の中にあるムラがゴミと判断される問題。
方針としては、ゴミよりは大きい、ある程度の大きさの領域(適当に600平方ピクセルとしました)にはcv2.pointPolygonTest()を使ってチェックをかけ、その領域のピクセル平均を取ります。 それがある程度黒かったらその領域は「ムラがあるが塗りつぶされた領域」と断定し、hierarchyを見て、その領域配下のゴミはゴミ取り対象には含めないということにしました。
こんなピクセル単位の処理はpythonでは十分な速度がでないのではないかと危惧しましたが、いよいよとなればそっくりそのままCで書きなおせばいいか…と覚悟したものの、全くそんなことはなく拍子抜けでした…。 まぁ、使ってるCPUも分不相応なi5の3470S(世間的には大したことのないCPUですが、ワタクシ的には過ぎたCPUです)ということや、動画のフレーム処理ではなく1枚絵の処理なので、比較的処理時間に寛容ということもあるのでしょう。
今回からadaptiveThresholdではなく単純なThresholdを使ってます。こっちのほうが綺麗に主線を判定できる気がしたので…
主線と判断された中規模領域は青、空の領域と判断された中規模領域は緑。ゴミは赤い最小枠で表示してみました。まぁ、こんなもんでいいかなと…
しかし、結構大きな問題が浮上。
元のイカ娘も、ワタクシの絵もそうなのですがいまどきの女の子の絵は鼻が点であります。 つまり、そのままでは、ゴミと区別が付かないのですな(´・ω・`)
今回のテストケースとして用いている絵は、大きくかけたからギリギリ大丈夫な感じですが、キャラの集合絵みたいな1キャラが小さいものになると、ほぼ消されると思います。
ここはいよいよ、顔認識の出番!…と思いましたが、既存の顔認識器ではどうも白黒線画は全く認識されないようです。全て試したわけではないですが…
まぁ、学習させるほかないか、と。もともと、やってみたかったことではあります。しかし、サンプル収集が実に面倒くさい…
いずれそれはやるとして、今回は実に安易な方法…「鼻の位置をコマンドラインオプションで指定することで回避する」といたしました。
あと今回より、コマンドラインオプションの解読にargparseをようやく使用することにしました。 argparse、いいですね。ずっとgetoptを使ってましたが、もっと早く知るべきだった…
ソースコード
#!/usr/bin/env python # -*- coding: UTF-8 -*- import cv2 import numpy as np import math import os def get_contour_average_inside(img,c): x,y,w,h=cv2.boundingRect(c) tpix=0 pixcnt=0 for ty in range(y,y+h): for tx in range(x,x+w): if cv2.pointPolygonTest(c,(tx,ty),False) > 0.0: # False means 'only check whether exactly inside or not' tpix+=img[ty,tx] pixcnt+=1 if pixcnt==0: return 255 # this contour consists from edge only - this should be black = inverted -> 255! return int(round(float(tpix) / pixcnt)) def parse_nosepoint(nsrc): try: ret=[] for cn in nsrc.split('&'): x,y = nsrc.split(',') ret.append( (int(x),int(y)) ) return ret except ValueError: print("[ERROR] error at decoding nose point: %s . this value ignored." % nsrc) def load_nosepoints(fname): retlst=[] with open(fname,'r') as ifp: for cl in ifp: n=parse_nosepoint(cl) if n: retlst+=n return retlst def main(args): # 画像の取得 cim = cv2.imread(args.input) im = cv2.cvtColor(cim,cv2.COLOR_BGR2GRAY) #im = cv2.imread(args.input,cv2.IMREAD_GRAYSCALE) h=im.shape[0] w=im.shape[1] md=max(w,h) scale_ratio=md / 3508.0 if args.dustsize and args.dustsize>0: pass else: args.dustsize=50*scale_ratio if args.involvesize and args.involvesize>0: pass else: args.involvesize=600*scale_ratio # モノクロ変換 ret,bimg=cv2.threshold(im,240,255,cv2.THRESH_BINARY_INV) #cv2.imwrite("/tmp/binimg.png",bimg) # for debug contours,hierarchy = cv2.findContours( bimg, cv2.RETR_EXTERNAL | cv2.RETR_TREE , cv2.CHAIN_APPROX_NONE) # ゴミ除去,面積args.dustsize以下を排除 new_contours={} process_contours=[] black_contours_idx=[] SWEEP_COLOR=(255,255,255) through_contours=[] hit_contours=[] print("[INFO] total %d areas detected." % len(contours)) # search contours to be processed for i,c in enumerate(contours): s=abs(cv2.contourArea(c)) if s <= args.dustsize: new_contours[i]=c elif s <= args.involvesize: avg=get_contour_average_inside(im,c) if avg < args.blacklevel:#BMIN: # blobs under this contours should not be processed. black_contours_idx.append(i) hit_contours.append(c) else: through_contours.append(c) # re-scan contours to reject black contours for i in new_contours.keys(): parents_idx=hierarchy[0][i][3] if parents_idx>-1 and parents_idx in black_contours_idx: pass else: process_contours.append(new_contours[i]) # and then,filter contours when nose points assigned. if args.nosefile: args.nose=load_nosepoints(args.nosefile) elif args.nose: args.nose=parse_nosepoint(args.nose) if args.nose and len(args.nose) > 0: print("[INFO] nose assignment found.") cnt=0 for i,cc in enumerate(process_contours[:]): for n in args.nose: if n and cv2.pointPolygonTest(cc,n,False) >= 0.0: # greater OR equal zero - it means include edges. del process_contours[i] cnt+=1 break print("[INFO] %d contours rejected as nose." % cnt) print("[INFO] total %d dusts found." % len(process_contours)) if args.output_debug: LARGE_CONTOUR_COLOR=(255,0,0) THROUGH_COLOR=(0,255,0) cv2.drawContours( cim, hit_contours, -1,LARGE_CONTOUR_COLOR,-1) # for visualize cv2.drawContours( cim, through_contours, -1,THROUGH_COLOR,-1) # for visualize for cc in process_contours: x,y,w,h=cv2.boundingRect(cc) cv2.rectangle(cim,(x,y),(x+w-1,y+h-1),(0,0,255)) else: cv2.drawContours( cim, process_contours, -1,SWEEP_COLOR,-1) if not args.force_overwrite and os.path.exists(args.output): import sys print("there is already exists %s.overwrite it?" % args.output) ans=sys.stdin.readline().strip().lower() if ans in ('y','yes','ok'): print('output file "%s" has overwritten.' % args.output) pass else: print('file output cancelled.please try again.') return cv2.imwrite(args.output,cim) if __name__ == '__main__': import argparse parser=argparse.ArgumentParser() parser.add_argument('-i','--input',help='入力画像ファイル',required=True) parser.add_argument('-o','--output',help='出力画像ファイル') parser.add_argument('-n','--nose',help='鼻位置. 鼻の中心がx=12 y=23の座標の時、--nose 12,23 のように表記。複数の場合は、&記号で挟み--nose 12,23&34,56のように表記') parser.add_argument('--nosefile',help='鼻位置を列挙したテキストファイル') parser.add_argument('-d','--dustsize',help='ゴミ点の面積(平方ピクセル)、 デフォルトは自動計算、300dpiで50程度',type=int,default=-1) parser.add_argument('-v','--involvesize',help='ゴミ点内包判定の対象となる領域の面積(平方ピクセル)、 デフォルトは自動計算、300dpiで600程度',type=int,default=-1) parser.add_argument('-k','--blacklevel',help='内包判定レベル(0-255)、値が高いほど領域判定が厳しくなる。',type=int,default=240) parser.add_argument('--force-overwrite',help='同一名の出力ファイルがすでに存在する時、問い合わせずに上書きする',action="store_true",default=False) parser.add_argument('--output-debug',help='デバッグ用描画を行う',action="store_true",default=False) args=parser.parse_args() if args.output==None: basename,ext=os.path.splitext(args.input) args.output="%s.png" % basename main(args)