OpenCVを使って点線を含めた縦棒を画像から削除する

以下のような画像から数字や横棒を残しつつ点線を削除したい。

f:id:KYudy:20191026131016j:plain

方法はいくつか考えられるが、 一番最初に思いついたのはハフ変換を使って直線検出し、検出された線を画像から削除するという方法だった。

ただやってみると気づくのだが、検出された線の太さがわからないので、決め打ちの太さで線を打ち消すことになる。

上の画像みたいに太線と細線と点線が混じっている場合は、太い方を消すために、細い方の線に対しては本来線ではない部分も含めて削除することになる。 場合によっては隣接する文字を削ってしまい文字の判別が不可能になるかもしれない。

また、ハフ変換の場合は点線を直線として検出することができない場合がある。

よく考えると線が画像に対し水平垂直であるという前提条件がおけるのであれば、ハフ変換を用いるのは大仰である。 そこで、点線を含めて縦の線を削除するための簡単なアルゴリズムを作ったので紹介する。

アルゴリズムは、以下の2つで成り立つ

  • 点線をつなぐための膨張・収縮テクニックの応用
  • 投影(Vertical Projection) を使った直線が通るX軸の座標の求め方

点線をつなぐための膨張・収縮テクニックの応用

膨張・収縮は有名なテクニックであり、それらを組み合わせるとノイズ除去、細線化等が実現される。 OpenCVではそれぞれcv2.dilate()cv2.erode()という関数として提供されている。 labs.eecs.tottori-u.ac.jp

当初は「クロージング」と同じ仕組みで点線をつなぐ事ができると考えた。 しかし点の間の距離が大きい(5pxとか?)場合には膨張の回数を大きめにしないと点同士がつながらず、 一方でその回数を増やしすぎると点線に隣接する文字が癒着してしまうという問題が発生することに気づいた。

これを回避する方法はcv2dilate()の第2引数であるkernelにあった。 具体的にはkernelに以下の様な配列を指定しただけだ。

[[0 0 1 0 0]
 [0 0 1 0 0]
 [0 0 1 0 0]
 [0 0 1 0 0]
 [0 0 1 0 0]]

ご覧いただくと分かる通り、真ん中の縦の列のみ1、ほかは0という2次元の配列だ。 このカーネルによって膨張を縦方向に限定することができ、線と隣接する文字との癒着を防ぐことができる。 これで、癒着を防ぎつつ、点線を実線に変更することが可能になった。

このテクニックを上記の画像に適用すると以下のような画像が得られる。(色がおかしいけど・・・) 見事に点線がつながっている。一方で横線と数字の下部と癒着しているが、これは後述の縦線検出するのに障害にならない。 f:id:KYudy:20191026133518p:plain

投影(Vertical Projection) を使った直線が通るX軸の座標の求め方

ここまでで縦線は画像上にしっかり描画された状態になった。 あとは、直線を検出する上で太さを含めて検出したい。

「投影」とは、もともと画像上に横に並んでいる文字列を各文字をセグメンテーションするための方法である。 画像の同じX座標にあるピクセルを合計するというもので、np.sum()の引数axisに1を指定すれば「投影」できる。

文字がある場所は合計値が大きくなり、文字間の空白はノイズが値が小さくなる。 散布図をかけば文字のところは山になり、文字間は谷になる。

これを縦線を含む画像に適用した場合は、縦線は切り立って山(スパイク)を発生させる。 実際に膨張・収縮を適用した画像に投影を適用すると以下のような散布図を得られる。 f:id:KYudy:20191026134600p:plain

縦線を表すスパイクと文字を表す山が存在することがわかる。縦線と文字は高さで区別可能なので、しきい値でふるいにかけるとスパイクが発生しているX軸上の座標を求めれる。

実際に座標を求めて、そこに線を引いた結果は以下である。 f:id:KYudy:20191026134855p:plain

赤線の太さがもとの縦線と同じであることが確認できる。

技術的な制約

このアルゴリズムの限界は以下である

  1. 直線が歪んでいる場合はうまく検出できない可能性がある
  2. 画像に対して縦線が完全に垂直でなければならない

2.に関しては縦線を画像に対して垂直にする方法はある。縦線が傾いている場合に比べて、垂直になっている場合はスパイクの高さが大きくなる。これを利用する方法である。簡単に実現できると思うが、今回は詳細を割愛する。

サンプルコード

最後にサンプルコードを載せる。

import cv2
import numpy as np
import matplotlib.pyplot as plt


img_path = "cell.jpg"

thresh_spike = 200

img_org = cv2.imread(img_path)
img = img_org.copy()

_, img_th = cv2.threshold(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), 0, 255, cv2.THRESH_OTSU)

# reverse white and black for dilate and erode
img_th = 255 - img_th

# vertical kernel to connect dot line to solid line
kernel = np.zeros((5,5), np.uint8)
kernel[:, 2] = 1

img_th = cv2.dilate(img_th, kernel, iterations=8)
img_th = cv2.erode(img_th, kernel, iterations=2)

vp = np.sum((img_th != 0).astype(np.uint8), axis=0)
loc_x_spike = np.where(vp > thresh_spike)

# draw vertical lines
for x in loc_x_spike[0]:
    line_color = (255, 0, 0) # red
    cv2.line(img, (x, 0), (x, img.shape[0]), line_color, thickness=1)

_, (ax1, ax2, ax3, ax4) = plt.subplots(nrows=4)
ax1.imshow(img_org)
ax2.scatter(range(len(vp)), vp)
ax3.imshow(img_th)
ax4.imshow(img)
plt.show()