スキャンした書類をPythonで水平にする
以前自炊なんかもやっていましたが、読み取ったあとOCRがうまく行かなくて、結局あまり便利にならなかった経験があります。
この度、思うところがありまして再度書籍の自炊を始めてみました。最近のソフトは読み取った画像を良き方向に回転させてくれるので以前に比べて格段に使いやすくなっています。
ところが、読み取った画像の中に微妙に角度が水平じゃないページが紛れていまして、このままOCRをかけると誤読しそうなので、書類を水平に変換するコードを作ってみました。
参考にしたのはこちらの記事です。
こちらの記事の考え方を参考にさせていただいてGoogleColab上で書類を水平に回転させるコードを書きました。
まずはコード
以下が作ったコードです。参考記事はC++で開発していますのでPythonで実行できるように変更を加えました。また、スキャン画像が10度以上傾いていたらそもそも内容が毀損してるだろうということで(その時はスキャンし直し)現在の角度±10度から最適角度を見つけるようアレンジしました。行ごとの有効画素数の偏差計算も計算速度が上がるかと思い、2値化して少し変えてみました。同じ部分があるので関数化すればよかったと後悔。
import cv2
import numpy as np
from matplotlib import pyplot as plt
# PNGファイルの読み込み(グレースケールで読み取る)
image_path = "/content/drive/My Drive/Colab Notebooks/image.png" # 画像のファイルパスを指定してください
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# 倍のピクセル数の白い背景の画像の作成
background_color = 255 # 白色
background_image = np.full((image.shape[0] * 2, image.shape[1] * 2), background_color, dtype=np.uint8)
# 画像を背景画像の中央に配置
x_offset = (background_image.shape[1] - image.shape[1]) // 2
y_offset = (background_image.shape[0] - image.shape[0]) // 2
background_image[y_offset:y_offset+image.shape[0], x_offset:x_offset+image.shape[1]] = image
# -10度から+10度まで0.5度ずつ回転させ、回転によってできた余白を白で塗りつぶす
max_deviation_sum = -1
optimal_angle = 0
max_optimal_angle = 0
for angle in np.arange(-10, 11, 0.5):
# 画像を回転させる
center = (background_image.shape[1] // 2, background_image.shape[0] // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated_image = cv2.warpAffine(background_image, rotation_matrix, (background_image.shape[1], background_image.shape[0]))
# 回転後の画像に白で余白を塗りつぶす
mask = (rotated_image == 0)
rotated_image[mask] = 255
# 画像を2値化
_, binary = cv2.threshold(rotated_image, 128, 255, cv2.THRESH_BINARY)
# 水平方向に各ピクセルの黒の個数を合計
black_pixels_per_row = np.sum(binary == 0, axis=1)
# 各行の値の偏差の合計を計算
deviation_sum = np.sum(np.abs(np.diff(black_pixels_per_row)))
# 偏差の合計が最大となる回転角度を記録
if deviation_sum > max_deviation_sum:
max_deviation_sum = deviation_sum
optimal_angle = angle
for angle in np.arange(optimal_angle - 0.25, optimal_angle + 0.25, 0.005):
# 画像を回転させる
center = (background_image.shape[1] // 2, background_image.shape[0] // 2)
rotation_matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
rotated_image = cv2.warpAffine(background_image, rotation_matrix, (background_image.shape[1], background_image.shape[0]))
# 回転後の画像に白で余白を塗りつぶす
mask = (rotated_image == 0)
rotated_image[mask] = 255
# 画像を2値化
_, binary = cv2.threshold(rotated_image, 128, 255, cv2.THRESH_BINARY)
# 水平方向に各ピクセルの黒の個数を合計
black_pixels_per_row = np.sum(binary == 0, axis=1)
# 各行の値の偏差の合計を計算
deviation_sum = np.sum(np.abs(np.diff(black_pixels_per_row)))
# 偏差の合計が最大となる回転角度を記録
if deviation_sum > max_deviation_sum:
max_deviation_sum = deviation_sum
optimal_angle = angle
optimal_image = rotated_image
# 画像のサイズを取得
height, width = optimal_image.shape[:2]
# 切り抜く領域のサイズを設定
crop_height, crop_width = image.shape[:2] # 切り抜く幅
# 中心座標を計算
center_x = width // 2
center_y = height // 2
# 切り抜く領域の左上座標を計算
start_x = center_x - crop_width // 2
start_y = center_y - crop_height // 2
# 切り抜き
cropped_image = optimal_image[start_y:start_y+crop_height, start_x:start_x+crop_width]
# 切り抜いた画像を保存する場合
#cv2.imwrite("cropped_image.png", cropped_image)
# 切り出した画像を表示するなどの処理を行う
plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
plt.axis('off')
plt.show()
plt.imshow(cv2.cvtColor(cropped_image, cv2.COLOR_BGR2RGB))
plt.axis('off')
plt.show()
実行すると変更前と後の画面が表示されます。
あとはこれを基に、すべてのスキャンページを調整していくコードを書けば、スキャン画像の水平化が達成できそうです。