Pythonで画像の余白を削除する(OpenCV編)

PythonのOpenCVモジュールを使って、画像の余白部分を削除(クロップ、トリミング)します。

背景差分法と2値化の2種類について述べます。画像によって適切な方法を選択してください。

Pillowを使う方法については、こちらを参照してください。

目次

  1. サンプル画像
  2. OpenCVで背景差分法によるクロップを行う
  3. OpenCVで2値化によるクロップを行う
  4. 2値化の場合のコード例
  5. 2値化での実施例
  6. 主なメソッド

サンプル画像

本稿でクロップする画像はこちらです。

サンプル画像

プロ生ちゃんこと暮井慧さんです。

OpenCVで背景差分法によるクロップを行う

背景差分法というのは、クロップ(トリミング)したい画像と背景画像の差分からクロップする範囲を求める方法です。

OpenCVには背景差分を計算する便利な関数が用意されています。ですが、本稿ではそれらを使わずに背景差分を計算してクロップします。

大凡のな手順は下記です。

  1. 背景部分の色を求める。

  2. 背景色で元画像と同じ大きさの画像を作成する。

  3. 元画像と背景色画像の差を計算する。(差分画像の生成)

  4. 差分画像の輪郭を抽出する。

  5. 輪郭のボックスを計算する。

  6. 求めたボックスで元画像をクロップする。

PythonのOpenCVモジュールはMATをNumPyのndarrayで表現しますので、ndarrayとしての操作も活用します。

背景差分法のコード例

import numpy as np
import cv2

img = cv2.imread('test.jpg')

# 元画像と同じサイズのndarray(背景画像)の生成
bg_img = np.zeros(img.shape, np.uint8)
# 元画像の(0,0)の画素の色を背景画像にコピー
bg_img[:,:,:] = img[0,0,:]

# 元画像と背景画像の差分の計算
diff_img = cv2.absdiff(img, bg_img)

# 差分画像から黒色部分を抜き出し、白黒反転
mask_img = cv2.bitwise_not(cv2.inRange(diff_img, np.array([0,0,0]),np.array([0,0,0])))

# 輪郭抽出
contours, h_ = cv2.findContours(mask_img, cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)

# 輪郭からバウンディングボックスの計算
rect = cv2.boundingRect(contours[-1])

# バウンディングボックスの座標に合わせてndarrayを切り出す
crop_img = img[rect[1]:rect[1]+rect[3]-1, rect[0]:rect[0]+rect[2]-1]

cv2.imwrite('crop_img.jpg', crop_img)

背景差分法の実施例

元画像は347x347のJpegファイルで、(0,0)画素の色は白(255,255,255)です。ですから、背景画像は347x347の白色の画像になります。

背景画像と元画像の差分は、このような画像になります。

差分画像

背景(余白)部分が黒になっています。

この画像から黒部分だけを抜き出して、さらに白黒反転すると、このような画像になります。

マスク画像

この画像から輪郭を抽出すると、抽出された輪郭はこのようになります。

輪郭画像

抽出された輪郭から外接する矩形を計算して、元画像にプロットすると、このようになります。

範囲画像

得られた矩形の座標をもとに、元画像のndarrayから行列の切り出し(スライス)をします。そうすると、このような画像になります。

クロップ画像

少し余白が残ってますね。元画像がJpegだからかもしれません。

この方法は背景との差分からクロップ範囲を計算しますので、背景が完全に単一色であればどのような色にも応用できると思います。

OpenCVで2値化によるクロップを行う

背景が白に近い色であるならば、2値化してクロップの範囲を決めてしまいましょう。

大凡のな手順は下記です。

  1. 画像を2値化する。

  2. 2値化した画像の輪郭を抽出する。

  3. 輪郭のボックスを計算する。

  4. 求めたボックスで元画像をクロップする。

2値化の場合のコード例

import numpy as np
import cv2

img = cv2.imread('test.jpg')

# 元画像をグレースケールに変換
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# グレースケール画像を白黒に変換
ret_, bw_img = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 白黒画像の輪郭を抽出
contours, h_ = cv2.findContours(bw_img, cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)

# 輪郭からバンディングボックスを計算
rect = cv2.boundingRect(contours[-1])

# バウンディングボックスの座標で元画像をクロップ
crop_img = img[rect[1]:rect[1]+rect[3]-1, rect[0]:rect[0]+rect[2]-1]

cv2.imwrite('crop_img.jpg', crop_img)

2値化での実施例

元画像をグレースケールに変換すると、このような画像になります。

グレースケール画像

このグレースケールの画像を2値化します。2値化の際に、次の輪郭抽出のために白黒反転します。閾値は大津の方法にしました。そうすると、このような画像になります。

2値化画像

この2値化画像から輪郭を抽出します。こんな感じです。

2値化輪郭画像

輪郭からバウンディングボックスを計算して、元画像に描画すると、こんな感じになります。

2値化範囲画像

このバウンディングボックスに従って元画像をクロップすると、このような画像になります。

2値化クロップ画像

この2値化での方法は、背景が白に近い色で、かつある程度のコントラストがある画像(例えば書類のスキャン画像)であることが前提になると思います。

主なメソッド

cv2.findContours()の使い方については、輪郭を検出する記事を参考にしてください。

cv2.boundingRect()の使い方については、外接矩形を描く記事を参考にしてください。

cv2.threshold()の使い方については、画像を2値化する記事を参考にしてください。

cv2.absdiff

import cv2

dst = cv2.absdiff(src1, src2, [dst])

変数

内容

src1

ndarray

画像を示す行列1

src2

ndarray

画像を示す行列2

dst

ndarray

差分の行列

行列の各要素の差分の絶対値を計算して行列で返す関数です。画像データの要素の値は0~255の整数のことが多いと思いますが、単純にこの差分をとるとマイナスの値になる場合がありますので、差分の絶対値を使用することで計算結果がマイナスにならないようにします。

cv2.inRange

import cv2

dst = cv2.inRange(src, lower, upper, [dst])

変数

内容

src

ndarray

入力画像を示す行列

lower

ndarray, scalar

下限を示す行列または数値

upper

ndarray, scalar

上限を示す行列または数値

dst

ndarray

判定結果の行列

行列の要素が指定した上下限の間にあるかどうかを確認します。出力はsrcと同じサイズの行列です。各要素の値は、上下限の範囲内の場合255に、範囲外の場合は0になります。

公開日

広告