今回は、Pythonを用いて、文字画像の画角を自然体にするために、アフィン変換を用いて画像の補正をする方法を記述していきます。
サンプル
よくわからない方も多いと思いますので、実例をもとに、どのような処理をするのかを先に説明しておきます。
日本人が書くローマン体の文字は、文字の画角が右肩上がりになっていることが多いです。その画角を水平に補正していきます。一方で、英語や一部のフォントではイタリック体と呼ばれ、傾斜のついた文字が書かれることがあります。
このような文字をまっすぐに補正することを目的としています。
ローマン体の結果
元画像の影響で、完成形がイタリック体※になっていますが気にしないでください。
(※文字が斜めに傾いている文字)
文字の横画が水平に補正されているのが分かると思います。
| 元画像 | 補正後 | 完成形 |
|---|---|---|
![]() | ![]() | |
![]() | ![]() | |
![]() | ![]() | |
![]() | ![]() |
イタリック体の結果
元画像の影響で、完成形がローマン体※になっていますが気にしないでください。
(※文字の画角が右肩上がりになっている文字)
文字の縦画が垂直に補正されているのが分かると思います。
| 元画像 | 補正後 | 完成形 |
|---|---|---|
![]() | ||
![]() | ||
![]() | ||
![]() |
傾き補正の方法
文字の画角を水平に補正するために、アフィン変換という手法を用います。
アフィン変換とは、画像を回転や、移動、拡大、縮小など、処理が簡単に行うことができる手法です。これらの処理を合成するには、変換するための行列の式をかけるとこで複雑な処理が実行できます。
詳しくは別の方が書いているQiitaを参考にしてください。超絶詳しく書いてあります。
ローマン体の場合(slant補正)
「文字の画角を水平に補正する」ためには、画像全体を画像内のある座標を(x, y)、変換後の座標を(x’, y’)としたときに、以下の行列で変換する必要があります。
$$\begin{bmatrix}x’ \\y’ \\ 1 \end {bmatrix}=\begin{bmatrix}1 &0 &0\\\tan\theta &1 &0\\0 &0 &1\end{bmatrix}\begin{bmatrix}x \\y\\ 1 \end{bmatrix}$$
イタリック体の場合(傾斜補正)
文字の傾斜を補正する場合は、以下の行列で変換します。なぜこのような式になるのかは次の「実装」の部分で解説しています。
$$\begin{bmatrix}x’ \\y’ \\ 1 \end {bmatrix}=\begin{bmatrix}1 &sin\theta &0\\ 0 &1 &0\\0 &0 &1\end{bmatrix}\begin{bmatrix}x \\y\\ 1 \end{bmatrix}$$
実装
ここからは具体的にサンプルコードを紹介していきます。
座標の変換を行うと、最初のサンプルで見たように、画像の一部分がはみ出てしまうために、変換の前に画像に十分な余白を追加した後で行います。
今回用いた画像ファイルを置いておきます。
画像の傾き補正「Affine」
# imageをthetaの角度で傾ける
def Affine(image, theta):
h, w = image.shape[:2]
src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
# slant補正の場合
dest = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, np.tan(theta)]], np.float32)
# 傾斜補正の場合
#dest = np.array([[0.0, 0.0],[np.sin(theta), 1.0],[1.0, 0.0]], np.float32)
affine = cv2.getAffineTransform(src, dest)
# 傾けた時の画像を返す
return cv2.warpAffine(image, affine, (w, h), borderMode=cv2.BORDER_CONSTANT, borderValue=255)ここでの「src」と「dest」の決定方法を説明します。
「src」では、変換前の3つの座標(A, B, C)を設定します。そして、slant補正を行う場合での変換後の座標を(A’, B’, C’)とすると次のようになります。
・ A(0, 0) → A'(0, 0)
・ B(0, 1) → B'(0, 1)
・ C(1, 0) → C'(1, tan(theta))

一方で、傾斜補正を行う場合での 変換後の座標を(A’, B’, C’)とすると次のようになります。
・ A(0, 0) → A'(0, 0)
・ B(0, 1) → B'(sin(theta), 1)
・ C(1, 0) → C'(1, 0)

画像の切り取り「cut_img」
def cut_img(img):
img_copy = img.copy()
# 大津の二値化をしておく
_, img_copy = cv2.threshold(img_copy, 0, 255, cv2.THRESH_OTSU)
for h in range(img_copy.shape[0]):
for w in range(img_copy.shape[1]):
# 黒ならば「1」、白ならば「0」
if img_copy[h][w] == 0:
img_copy[h][w] = 1
elif img_copy[h][w] == 255:
img_copy[h][w] = 0
# 縦軸で見た時の黒の個数を数える
x_dot = np.sum(img_copy, axis = 0)
# 横軸で見た時の黒の個数を数える
y_dot = np.sum(img_copy, axis = 1)
# 黒がある最小・最大のインデックス番号を取得する
x_min, x_max = np.min(np.where(x_dot != 0)), np.max(np.where(x_dot != 0))
y_min, y_max = np.min(np.where(y_dot != 0)), np.max(np.where(y_dot != 0))
return img[y_min - 1 : y_max + 1, x_min - 1 : x_max + 1]ここで処理する内容について解説していきます。
画像を2値化し、画像内の各ピクセルに対して、黒(画素値0)ならば、「1」を、白(画素値255)ならば「0」を割り当てます。
この時の、縦、横方向で見た時の黒のピクセル数を数えることで、画像の必要な部分を切り取るためのインデックス番号を求めます。

メイン関数
import cv2
import numpy as np
import glob
from google.colab.patches import cv2_imshow
if __name__ == "__main__":
# 「characters/」の中にあるファイルをすべて取得
Files = glob.glob("characters/*")
# 画像を傾ける角度の設定
theta = 10
for File in Files:
# グレースケールで画像を読みとる
image = cv2.imread(File, 0)
H, W = image.shape[:2]
# 画像に白の余白を追加
image = cv2.copyMakeBorder(image, H, H, H, H, cv2.BORDER_CONSTANT, value=255)
#cv2_imshow(image)
# imageをthetaの角度で傾ける
Affine_img = Affine(image, theta * np.pi / 180)
#cv2_imshow(Affine_img)
# 画像の必要な部分のみを切り出す
center_y, center_x = map(int, [Affine_img.shape[0] / 2, Affine_img.shape[1] / 2])
#cv2_imshow(Affine_img[center_y - H : center_y + H, center_x - W : center_x + W])
cutted_img = cut_img(Affine_img[center_y - H : center_y + H, center_x - W : center_x + W])
cv2_imshow(cutted_img)ここでは、コメントアウトで解説している通りです。
全体のコード
最後にコメントなしのコードの全体を記述していきます。
import cv2
import numpy as np
import glob
from google.colab.patches import cv2_imshow
def Affine(image, theta):
h, w = image.shape[:2]
src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
dest = np.array([[0.0, 0.0], [0.0, 1.0], [1.0, np.tan(theta)]], np.float32)
affine = cv2.getAffineTransform(src, dest)
return cv2.warpAffine(image, affine, (w, h), borderMode=cv2.BORDER_CONSTANT, borderValue=255)
def cut_img(img):
img_copy = img.copy()
_, img_copy = cv2.threshold(img_copy, 0, 255, cv2.THRESH_OTSU)
for h in range(img_copy.shape[0]):
for w in range(img_copy.shape[1]):
if img_copy[h][w] == 0:
img_copy[h][w] = 1
elif img_copy[h][w] == 255:
img_copy[h][w] = 0
x_dot = np.sum(img_copy, axis = 0)
y_dot = np.sum(img_copy, axis = 1)
x_min, x_max = np.min(np.where(x_dot != 0)), np.max(np.where(x_dot != 0))
y_min, y_max = np.min(np.where(y_dot != 0)), np.max(np.where(y_dot != 0))
return img[y_min - 1 : y_max + 1, x_min - 1 : x_max + 1]
if __name__ == "__main__":
Files = glob.glob("characters/*")
theta = 10
for File in Files:
image = cv2.imread(File, 0)
H, W = image.shape[:2]
image = cv2.copyMakeBorder(image, H, H, H, H, cv2.BORDER_CONSTANT, value=255)
#cv2_imshow(image)
Affine_img = Affine(image, theta * np.pi / 180)
#cv2_imshow(Affine_img)
center_y, center_x = map(int, [Affine_img.shape[0] / 2, Affine_img.shape[1] / 2])
#cv2_imshow(Affine_img[center_y - H : center_y + H, center_x - W : center_x + W])
cutted_img = cut_img(Affine_img[center_y - H : center_y + H, center_x - W : center_x + W])
cv2_imshow(cutted_img)出力結果に関しては最初に示したサンプルを参照してください。
合成
最後に、ローマン体に対する「slant」補正と、イタリック体に対する「傾斜補正」を両方とも行う場合の結果を記述していきます。
| 元画像 | 完成形 |
|---|---|
![]() | |
![]() | |
![]() | |
![]() |
美しいですね~!
先ほどの「全体のコード」と違う箇所は、関数「Affine_slant」を追加したということだけです。
画像に対して「Affine_slant」を行い、slant補正をします。補正された画像に対してさらに「Affine」を行うことで傾斜も補正するという流れになっています。
import cv2
import numpy as np
import glob
from google.colab.patches import cv2_imshow
def Affine_slant(image, theta):
h, w = image.shape[:2]
src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
dest = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, np.tan(theta)]], np.float32)
affine = cv2.getAffineTransform(src, dest)
return cv2.warpAffine(image, affine, (w, h), borderMode=cv2.BORDER_CONSTANT, borderValue=255)
def Affine(image, theta):
h, w = image.shape[:2]
src = np.array([[0.0, 0.0],[0.0, 1.0],[1.0, 0.0]], np.float32)
dest = np.array([[0.0, 0.0],[np.sin(theta), 1.0],[1.0, 0.0]], np.float32)
affine = cv2.getAffineTransform(src, dest)
return cv2.warpAffine(image, affine, (w, h), borderMode=cv2.BORDER_CONSTANT, borderValue=255)
def cut_img(img):
img_copy = img.copy()
_, img_copy = cv2.threshold(img_copy, 0, 255, cv2.THRESH_OTSU)
for h in range(img_copy.shape[0]):
for w in range(img_copy.shape[1]):
if img_copy[h][w] == 0:
img_copy[h][w] = 1
elif img_copy[h][w] == 255:
img_copy[h][w] = 0
x_dot = np.sum(img_copy, axis = 0)
y_dot = np.sum(img_copy, axis = 1)
x_min, x_max = np.min(np.where(x_dot != 0)), np.max(np.where(x_dot != 0))
y_min, y_max = np.min(np.where(y_dot != 0)), np.max(np.where(y_dot != 0))
return img[y_min - 1 : y_max + 1, x_min - 1 : x_max + 1]
if __name__ == "__main__":
Files = glob.glob("characters/*")
theta = 10
for File in Files:
image = cv2.imread(File, 0)
H, W = image.shape[:2]
image = cv2.copyMakeBorder(image, H, H, H, H, cv2.BORDER_CONSTANT, value=255)
#cv2_imshow(image)
Affine_img = Affine_slant(image, theta * np.pi / 180)
#cv2_imshow(Affine_img)
Affine_img = Affine(Affine_img, theta * np.pi / 180)
#cv2_imshow(Affine_img)
center_y, center_x = map(int, [Affine_img.shape[0] / 2, Affine_img.shape[1] / 2])
#cv2_imshow(Affine_img[center_y - H : center_y + H, center_x - W : center_x + W])
cutted_img = cut_img(Affine_img[center_y - H : center_y + H, center_x - W : center_x + W])
cv2_imshow(cutted_img)まとめ
今回は、傾斜文字の画角をアフィン変換を用いて補正する方法を記述しました。
紹介したプログラムでは、補正する画角の角度を直接指定して画角の補正を行っていましたが、「その角度を自動的に求めたい」と思う方も多いと思います。
次の記事では、文字の画角を水平にするための角度を自動的に求めて補正する方法を解説していきます。
ナノトイラボ 









