モブ沢工房

プログラミングとかLinux関連(特にOSSのグラフィックツール関連)とかレトロゲームとか3Dプリンタやら日曜大工等、色々。

続・OpenCV@Pythonでスキャナのゴミ取り(コマンドラインツール化)

コマンドラインツール化に伴い、前回より少し工夫してみました。

前回の最大の問題点は、主線の中にあるムラがゴミと判断される問題。

方針としては、ゴミよりは大きい、ある程度の大きさの領域(適当に600平方ピクセルとしました)にはcv2.pointPolygonTest()を使ってチェックをかけ、その領域のピクセル平均を取ります。 それがある程度黒かったらその領域は「ムラがあるが塗りつぶされた領域」と断定し、hierarchyを見て、その領域配下のゴミはゴミ取り対象には含めないということにしました。

こんなピクセル単位の処理はpythonでは十分な速度がでないのではないかと危惧しましたが、いよいよとなればそっくりそのままCで書きなおせばいいか…と覚悟したものの、全くそんなことはなく拍子抜けでした…。 まぁ、使ってるCPUも分不相応なi5の3470S(世間的には大したことのないCPUですが、ワタクシ的には過ぎたCPUです)ということや、動画のフレーム処理ではなく1枚絵の処理なので、比較的処理時間に寛容ということもあるのでしょう。

今回からadaptiveThresholdではなく単純なThresholdを使ってます。こっちのほうが綺麗に主線を判定できる気がしたので…

f:id:dothiko:20150726103043j:plain

主線と判断された中規模領域は青、空の領域と判断された中規模領域は緑。ゴミは赤い最小枠で表示してみました。まぁ、こんなもんでいいかなと…

しかし、結構大きな問題が浮上。

元のイカ娘も、ワタクシの絵もそうなのですがいまどきの女の子の絵は鼻が点であります。 つまり、そのままでは、ゴミと区別が付かないのですな(´・ω・`)

今回のテストケースとして用いている絵は、大きくかけたからギリギリ大丈夫な感じですが、キャラの集合絵みたいな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)