歪み補正・台形補正#

まっすぐ撮影できずに台形になっているような状態はperspective歪みなどと呼ばれる

射影変換 (projective transformation)によって台形補正を行う

非線形な歪みの補正はできない。例えば書類をスキャンするときに隙間が空いていて、たわんでいる状態でスキャンした文書の補正はできない。

方法#

例えば画像内に長方形になるべきものが写っている場合、4点を検出して、まっすぐな歪みのない長方形にアフィン変換してやればよい。

点はkeypoints検出や直線検出の結果をもとに推定する

  • 局所対応点(keypoints)を検出して、4点にまとめる

    • 例えばコーナー検出(corner detection)によって「角っぽい点」を全部検知してから適切にフィルタリング・統合する

  • 線が直交する点が角なので、線を検出して直交点を推測する

    • 例えばハフ変換。ただし、太い線だと並行に検出するとは限らない。候補の線を全部出してしまう。Cannyなどで細線化する方法があるが、細線化が失敗すると後の工程が全部失敗するリスクもある

    • 例えばLSD(Line Segment Detection)

画像の例#

Hide code cell source
import cv2
import numpy as np
import matplotlib.pyplot as plt

# 白い背景の画像を作成
width, height = 480, 640
img = np.ones((height, width, 3), dtype=np.uint8) * 255

# 台形の頂点を定義
pts = np.array([[100, 100], [400, 100], [450, 500], [50, 500]], np.int32)
pts = pts.reshape((-1, 1, 2))
# 台形を描画
cv2.polylines(img, [pts], isClosed=True, color=(0, 0, 0), thickness=2)

font = cv2.FONT_HERSHEY_SIMPLEX
cv2.putText(img, 'Hello, OpenCV!', (140, 250), font, 1, (0, 0, 0), 2, cv2.LINE_AA)

# 円を描く
# cv2.circle(img, (250, 250), 100, (0, 0, 0), 1)

plt.imshow(img)
plt.show()
../_images/7b9c7f2088d44656d8b21644f04c350c4f4fc62e5ba1d10422644ac77d086372.png

前処理:台形の輪郭線を取り出す#

# 前処理:台形の輪郭線を取り出す

# グレースケール画像の取得
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 二値化
_, binary = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)

# モルフォロジー変換:膨張と収縮を繰り返して線を抽出
kernel_length = max(1, img.shape[1] // 80)  # カーネルのサイズを画像の幅に基づいて決定
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (10, 1))
morph = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2)

plt.imshow(morph, cmap="gray")
plt.show()
../_images/90c0e2d39f7882a2b19109b33260e5095413721cbed7653fd243ae429cd74bf8.png
contours, _ = cv2.findContours(morph, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 輪郭を描画
contour_img = np.zeros_like(gray)
cv2.drawContours(contour_img, contours, -1, (255, 255, 255), 2)

plt.imshow(contour_img, cmap="gray")
plt.show()
../_images/f0c3a624c45ad46ed9e02f62c4988bdc41d942e59cd22fa0cef2a3de3240f12b.png

前処理:平滑化#

# 前処理2

# 白黒を反転
contour_img = 255 - contour_img

# 線がギザギザしているのでガウシアンブラーで平滑化
contour_img = cv2.GaussianBlur(contour_img, (5, 5), 0)

plt.imshow(contour_img, cmap="gray")
plt.show()
../_images/bf438b8558973933028e453174d35e1c28be0ff4a00346c7dccbf884ee163123.png

局所対応点を検出する方法#

Harrisによるコーナー検出#

# Harris corner detection
dst = cv2.cornerHarris(np.float32(contour_img), blockSize=2, ksize=3, k=0.04)
# Dilate the result to mark the corners
dst = cv2.dilate(dst, None)
# Threshold for an optimal value, marking the corners in the original image
img_harris = img.copy()
img_harris[dst > 0.01 * dst.max()] = [0, 0, 255]

plt.imshow(img_harris)
<matplotlib.image.AxesImage at 0x7f6201d1ec80>
../_images/97323fb891337ff983de3e5edcd439bec207b0223f095a5fb0853d79d72f09b4.png

FASTによるコーナー検出#

FASTは16x16ピクセルの範囲ごとにグレースケール画像の濃淡の領域を分割するように決定木を構築してコーナーを検出する手法

OpenCV: FAST Algorithm for Corner Detection

fast = cv2.FastFeatureDetector_create(threshold=100)
keypoints = fast.detect(contour_img, None)

# 検出されたコーナーを画像に描画
image_with_keypoints = cv2.drawKeypoints(img, keypoints, None, color=(255, 0, 0))

plt.imshow(image_with_keypoints)
plt.title("Corners Detected by FAST")
plt.show()
../_images/3f5a491e8f061a81b70cbeb9810324e69c6a2933ecf2c22dacaa0dd703b72513.png

検出したコーナーたちを4点にまとめる#

# コーナーの座標を取得
corners = cv2.KeyPoint_convert(keypoints)
corners
array([[101.,  99.],
       [403.,  99.],
       [104., 102.],
       [400., 102.],
       [ 55., 498.],
       [449., 498.],
       [ 51., 501.],
       [453., 501.]], dtype=float32)
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=4)
kmeans.fit(corners)
centroids = kmeans.cluster_centers_
centroids
array([[ 53. , 499.5],
       [401.5, 100.5],
       [451. , 499.5],
       [102.5, 100.5]], dtype=float32)
values = centroids[:, 0] + centroids[:, 1]
idx = np.argsort(values)
centroids = centroids[idx]
centroids
array([[102.5, 100.5],
       [401.5, 100.5],
       [ 53. , 499.5],
       [451. , 499.5]], dtype=float32)
img_centroids = image_with_keypoints.copy()
for centroid in centroids:
    cv2.circle(img_centroids, tuple(centroid.astype("uint32")), radius=5, color=(0, 0, 255), thickness=2)

plt.imshow(img_centroids)
plt.title("Corners Detected by FAST")
plt.show()
../_images/764472d42f0f52dbac1c4b630175da2132fc0d632541312290ed72696e6240d4.png

Perspective Transform#

pts_src = centroids
# 変換後の座標(出力画像のサイズを決定)
# width, height = 300, 400  # 出力画像の幅と高さを指定
margin = 50  # 余白
pts_dst = np.array([
    [margin, margin],
    [width - margin - 1, margin],
    [margin, height - margin - 1],
    [width - margin - 1, height - margin - 1]
], dtype='float32')
pts_dst
array([[ 50.,  50.],
       [429.,  50.],
       [ 50., 589.],
       [429., 589.]], dtype=float32)
# 変換行列を計算
matrix = cv2.getPerspectiveTransform(pts_src, pts_dst)
# 変換を適用
transformed_img = cv2.warpPerspective(img, matrix, (width, height))

plt.imshow(transformed_img, cmap="gray")
<matplotlib.image.AxesImage at 0x7f61dbf55b70>
../_images/f431d40b4dbb226e64d3d527223c67205c91dce435b24d31e7486361f9dcc128.png

直線を検出する場合#

LSDはライセンスの問題でOpenCVからは削除されたようで、使いたい場合は pylsd をインストールする必要がある。

代わりに FastLineDetector というものがある。(Python使用例:Python+OpenCVによる線分検出 - 社会人研究者が色々頑張るブログ

直線を検出する#

ぴったり4つではなく8つくらい出てる

length_threshold = 10
distance_threshold = 1.41421356
canny_th1 = 50
canny_th2 = 50
canny_aperture_size = 3
do_merge = False

fld = cv2.ximgproc.createFastLineDetector(
    length_threshold,
    distance_threshold,
    canny_th1,
    canny_th2,
    canny_aperture_size,
    do_merge
)

lines_raw = fld.detect(contour_img)
# 形状を整える
lines = [line[0] for line in lines_raw]

# drawSegmentsで描く場合
# img_lines = fld.drawSegments(img, lines)
# plt.imshow(img_lines)

# cv2.lineで描く場合
print(f"{len(lines)} lines are detected")
out = img.copy()
for line in lines:
    x1, y1, x2, y2 = map(int, line)
    cv2.line(out, (x1, y1), (x2, y2), (0, 0, 255), 1)
plt.imshow(out)
8 lines are detected
<matplotlib.image.AxesImage at 0x7f61dbf90f10>
../_images/c10a3a1f14693d28f5c3dbc999e5c2cabdb4c31324553b0a3a325975fae14e2f.png

交点を出すために直線を延長する#

\[\begin{split} \begin{align} \text{diff} &= P_1 - P_2 \\ &= \begin{pmatrix} x_1 \\ y_1 \end{pmatrix} - \begin{pmatrix} x_2 \\ y_2 \end{pmatrix} \\ \text{direction} &= \frac{\text{diff}}{ \|\text{diff}\| }\\ P_1^{new} &= \begin{pmatrix} x_1 \\ y_1 \end{pmatrix} - \text{direction} \times \text{length}\\ P_2^{new} &= \begin{pmatrix} x_2 \\ y_2 \end{pmatrix} + \text{direction} \times \text{length}\\ \end{align} \end{split}\]
def extend_line(p1, p2, length):
    diff = np.array(p2) - np.array(p1)
    direction = diff / np.linalg.norm(diff)
    new_p1 = np.array(p1) - direction * length
    new_p2 = np.array(p2) + direction * length
    new_p1 = tuple(map(int, new_p1))
    new_p2 = tuple(map(int, new_p2))
    return new_p1, new_p2
out = img.copy()
for line in lines:
    x1, y1, x2, y2 = map(int, line)
    p1_, p2_ = extend_line(p1=(x1, y1), p2=(x2, y2), length=50)
    cv2.line(out, p1_, p2_, (0, 255, 0), 1)
    # break
plt.imshow(out)
<matplotlib.image.AxesImage at 0x7f61dc006fe0>
../_images/69b91006f952989aa22c38b0c7a6ffdb3ea8f47ec19898d9edbe91b758d833cd.png

2つの線の交点を出す#

4つの点

\[ P_1 = (x_1, y_1), \quad P_2 = (x_2, y_2), \quad P_3 = (x_3, y_3), \quad P_4 = (x_4, y_4) \]

からなる2つの線分

\[ L_1(t) = (1 - t) P_1 + t P_2, \quad (0\leq t \leq 1) L_2(u) = (1 - u) P_3 + u P_4, \quad (0\leq u \leq 1) \]

を用いて、交点\((x,y)\)を求める連立方程式を組む

\[\begin{split} \left\{\begin{array}{l} x=(1-t) x_1+t x_2 \\ x=(1-u) x_3+u x_4 \\ y=(1-t) y_1+t y_2 \\ y=(1-u) y_3+u y_4 \end{array}\right. \end{split}\]

整理すると

\[\begin{split} \left\{\begin{array}{l} \left(x_2-x_1\right) t-\left(x_4-x_3\right) u=x_3-x_1 \\ \left(y_2-y_1\right) t-\left(y_4-y_3\right) u=y_3-y_1 \end{array}\right. \end{split}\]

となる。行列表記にすると以下のようになる。

\[\begin{split} \left(\begin{array}{ll} x_2-x_1 & -\left(x_4-x_3\right) \\ y_2-y_1 & -\left(y_4-y_3\right) \end{array}\right)\binom{t}{u}=\binom{x_3-x_1}{y_3-y_1} \end{split}\]

行列式

\[\begin{split} \operatorname{det} =\left|\begin{array}{ll} x_2-x_1 & -\left(x_4-x_3\right) \\ y_2-y_1 & -\left(y_4-y_3\right) \end{array}\right| =\left(x_2-x_1\right)\left(y_4-y_3\right)-\left(y_2-y_1\right)\left(x_4-x_3\right) \end{split}\]

がゼロでなければ、クラメルの公式により

\[\begin{split} \begin{aligned} & t=\frac{\left(x_3-x_1\right)\left(y_4-y_3\right)-\left(y_3-y_1\right)\left(x_4-x_3\right)}{\operatorname{det}} \\ & u=\frac{\left(x_2-x_1\right)\left(y_3-y_1\right)-\left(y_2-y_1\right)\left(x_3-x_1\right)}{\operatorname{det}} \end{aligned} \end{split}\]

として解が求まる

def get_line_intersection(line1, line2):
    x1, y1, x2, y2 = line1
    x3, y3, x4, y4 = line2
    # Calculate the determinant
    det = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
    if det == 0:
        return None  # Lines are parallel or coincident
    # Calculate the intersection point
    px = ((x1*y2 - y1*x2) * (x3 - x4) - (x1 - x2) * (x3*y4 - y3*x4)) / det
    py = ((x1*y2 - y1*x2) * (y3 - y4) - (y1 - y2) * (x3*y4 - y3*x4)) / det
    return int(px), int(py)
out = img.copy()
intersects = []
for i in range(len(lines)):
    for j in range(i, len(lines)):
        x1, y1, x2, y2 = map(int, lines[i])
        p1_, p2_ = extend_line(p1=(x1, y1), p2=(x2, y2), length=50)
        cv2.line(out, p1_, p2_, (0, 255, 0), 1)

        intersect = get_line_intersection(lines[i], lines[j])
        if intersect:
            x_, y_ = intersect
            if abs(x_) > out.shape[1] or abs(y_) > out.shape[0]:
                # 異常値は無視
                continue
            cv2.circle(out, intersect, radius=5, color=(0, 0, 255), thickness=1)
            intersects.append(intersect)
plt.figure(figsize=(10,10))
plt.imshow(out)
<matplotlib.image.AxesImage at 0x7f61dbe95f00>
../_images/e422d328b73a15e73f19210a6e00bef876cc0fceb79cdb2a45524ba66c01bae7.png

交点の中心点をとる#

from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=4)
kmeans.fit(intersects)
centroids = kmeans.cluster_centers_
centroids
array([[ 50.5 , 500.5 ],
       [100.75,  98.  ],
       [452.5 , 500.5 ],
       [402.25,  99.  ]])
values = centroids[:, 0] + centroids[:, 1]
idx = np.argsort(values)
centroids = centroids[idx]
centroids
array([[100.75,  98.  ],
       [402.25,  99.  ],
       [ 50.5 , 500.5 ],
       [452.5 , 500.5 ]])
out = img.copy()
for centroid in centroids:
    cv2.circle(out, tuple(centroid.astype("uint32")), radius=5, color=(0, 0, 255), thickness=2)

plt.imshow(out)
plt.title("Corners Detected by Intersects of lines")
plt.show()
../_images/b3fed2feed838a3ebab67585df98d4ac38693e4cd3365bde1fec5cf207515904.png

Perspective Transform#

pts_src = centroids.astype("float32")
# 変換後の座標(出力画像のサイズを決定)
# width, height = 300, 400  # 出力画像の幅と高さを指定
margin = 50  # 余白
pts_dst = np.array([
    [margin, margin],
    [width - margin - 1, margin],
    [margin, height - margin - 1],
    [width - margin - 1, height - margin - 1]
], dtype='float32')
pts_dst
array([[ 50.,  50.],
       [429.,  50.],
       [ 50., 589.],
       [429., 589.]], dtype=float32)
# 変換行列を計算
matrix = cv2.getPerspectiveTransform(pts_src, pts_dst)
# 変換を適用
transformed_img = cv2.warpPerspective(img, matrix, (width, height))

plt.imshow(transformed_img, cmap="gray")
<matplotlib.image.AxesImage at 0x7f61db9aa980>
../_images/8876fa0b9be794646a337cfea35d19401b0588b91b18f8ceccb254bd4248da60.png

参考#