技能実習生に対する聴取票PDFをOpenCVで加工する
要約
技能実習生に対する聴取票PDFをOpenCVで画像解析して機械的に集計しようとしたが、前処理がすでに難しかった。
背景
政府が非公開としていた失踪した技能実習生に対する聴取票を野党の国会議員が書き写し、その全PDFが公開された。
聴取票を手で書き写すのも大変な労力だが、手書きの聴取票を集計するのも大変な労力である。集計だけでも機械的にできれば、社会的にも意義があろう。
そこで、OpenCV を用いて聴取票画像を集計する上で扱いやすい形に加工する試みをする。いわゆる前処理である。実装する過程で「画像処理しやすい画像」と「画像処理しにくい画像」があることがわかったので、そのことについても書く。
ななめにスキャンされた聴取票の画像をまっすぐにする
サンプルとして No.04 の聴取票を扱う。
若干ななめにスキャンされているので、まっすぐな画像に直す。
結果はこのようになる。
コードはこうである。輪郭検出と射影変換を組み合わせる。
import cv2 import numpy as np def straighten(img): """ 聴取票画像の大枠だけを取り出してまっすぐ長方形に伸ばした画像にする Paramaters ---------- img: ndarray (2 dim) Returns ------- img: ndarray (2 dim) """ # ノイズ除去 sigma_x = 5 average_square = (sigma_x, sigma_x) img_gauss = cv2.GaussianBlur(img, average_square, sigma_x) ret, thres = cv2.threshold(img_gauss, 200, 255, cv2.THRESH_BINARY_INV) # 輪郭を抽出 _, contours, _ = cv2.findContours(thres, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 輪郭のうち面積最大のもの(=大枠)を取得 max_area_cnt = max(contours, key=lambda cnt: cv2.contourArea(cnt)) # 大枠の角4点を取得 points = np.float32([ min(max_area_cnt, key=lambda position: position[0][0] + position[0][1]), max(max_area_cnt, key=lambda position: position[0][0] - position[0][1]), max(max_area_cnt, key=lambda position: position[0][0] + position[0][1]), min(max_area_cnt, key=lambda position: position[0][0] - position[0][1]), ]) # 射影変換で大枠を画像の4隅に合わせる rows,cols = img.shape points2 = np.float32([[10, 10], [cols-10, 10], [cols-10, rows-10], [10, rows-10]]) M = cv2.getPerspectiveTransform(points,points2) dst = cv2.warpPerspective(img,M,(cols, rows)) return dst if __name__ == "__main__": img = cv2.imread("聴取票 1_50 4.jpeg", 0) cv2.imwrite("聴取票1_50_4_straight.jpeg", straighten(img))
これだけでもだいぶ画像処理する上で扱いやすくなる。たとえばチェック項目の検出は、チェックボックスのおよその位置が画像内で特定できるので、実装しやすいだろう。
聴取票の画像をセクションごとに分割する
もう少し加工しよう。聴取票の画像をセクションごとに分割する。
結果はこのようになる。
コードはこうである。最初のコードと同じようなことをしている。
import cv2 import numpy as np def split_sections(img): """ 聴取票画像の大枠からセクションを分割する Paramaters ---------- img: ndarray (2 dim) Returns ------- section_imgs: ndarray (2 dim) tuple (7 len) """ # ノイズ除去 sigma_x = 5 average_square = (sigma_x, sigma_x) img_gauss = cv2.GaussianBlur(img, average_square, sigma_x) ret, thres = cv2.threshold(img_gauss, 200, 255, cv2.THRESH_BINARY_INV) # 輪郭を抽出 _, contours, _ = cv2.findContours(thres, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) # セクションの領域を選ぶ max_area_cnt = max(contours, key=lambda cnt: cv2.contourArea(cnt)) max_area = cv2.contourArea(max_area_cnt) # 最大面積の領域と一定以上小さな領域を除外する def filter_area(cnt): area = cv2.contourArea(cnt) return 200000 < area and area < cv2.contourArea(max_area_cnt) section_contours = list(filter(filter_area, contours)) if len(section_contours) != 7: raise Exception("Section is 7, but {}".format(len(section_contours))) # セクションを長方形に近似 section_rects = [ cv2.boundingRect(cnt) for cnt in section_contours ] section_rects = sorted(section_rects, key=lambda rect: rect[1]) # y でソート # セクションの画像を切り取る def trim_img(rect): x,y,w,h = rect return img[y:y+h,x:x+w] section_imgs = [ trim_img(rect) for rect in section_rects ] return section_imgs if __name__ == "__main__": img = cv2.imread("聴取票1_50_4_straight.jpeg", 0) imgs = split_sections(img) for i, dst in enumerate(imgs): cv2.imwrite("聴取票1_50_4_{}.jpeg".format(i), dst)
「画像処理しやすい画像」と「画像処理しにくい画像」
ここまで実装して、他の聴取票にもこの処理を実行したところ、うまくいかない画像がいくつかあった。
たとえば No.35 である。
おわかりのとおり、外枠がはみ出してしまってスキャンできていない。上記のアルゴリズムだと、この画像をまっすぐに直すことはできない。これが「画像処理しにくい画像」である。
画像処理ではこういうちょっとした例外でアルゴリズムが破綻する。前処理は難しい。