File size: 4,040 Bytes
fc895f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""

skew_corrector.py

-----------------

Detects and corrects rotational skew in scanned/photographed floor plans.



Strategy:

  1. Detect dominant lines using Probabilistic Hough Transform.

  2. Compute the median angle of near-horizontal and near-vertical lines.

  3. Rotate the image to align the dominant axis.



Floor plans are axis-aligned by design, so the dominant line angle

should be close to 0° or 90°.

"""

import cv2
import numpy as np
from typing import Optional


def detect_skew_angle(binary: np.ndarray, angle_threshold: float = 45.0) -> float:
    """

    Estimate the skew angle of the image using the Hough Line Transform.



    Args:

        binary:           Binary image (uint8, 0/255).

        angle_threshold:  Only consider lines within this many degrees of

                          horizontal (0°) or vertical (90°).



    Returns:

        Estimated skew angle in degrees. Positive = clockwise skew.

        Returns 0.0 if no dominant angle is found.

    """
    # Hough works on edges — use Canny on the binary image
    edges = cv2.Canny(binary, threshold1=50, threshold2=150, apertureSize=3)

    # Probabilistic Hough: faster and more robust for long wall lines
    lines = cv2.HoughLinesP(
        edges,
        rho=1,
        theta=np.pi / 180,
        threshold=80,
        minLineLength=binary.shape[1] // 8,  # at least 1/8 of image width
        maxLineGap=20,
    )

    if lines is None or len(lines) == 0:
        return 0.0

    angles = []
    for line in lines:
        x1, y1, x2, y2 = line[0]
        if x2 == x1:
            continue  # vertical line → 90°, handle separately
        angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))

        # Normalise to [-90, 90]
        if angle > 90:
            angle -= 180
        elif angle < -90:
            angle += 180

        # Keep only near-horizontal lines (close to 0°)
        if abs(angle) <= angle_threshold:
            angles.append(angle)

    if not angles:
        return 0.0

    # Use the median to be robust against outliers
    return float(np.median(angles))


def correct_skew(

    img: np.ndarray,

    angle: Optional[float] = None,

    binary: Optional[np.ndarray] = None,

    background_color: int = 255,

) -> np.ndarray:
    """

    Rotate an image to correct for skew.



    Args:

        img:              Grayscale image to correct.

        angle:            Skew angle in degrees (provide this OR binary).

        binary:           Binary image used to auto-detect angle if angle=None.

        background_color: Fill value for borders created by rotation (0=black, 255=white).



    Returns:

        Rotated (deskewed) grayscale image, same size as input.

    """
    if img is None:
        raise ValueError("img must not be None.")

    if angle is None:
        if binary is None:
            raise ValueError("Provide either 'angle' or 'binary' for auto-detection.")
        angle = detect_skew_angle(binary)

    # If skew is negligible, skip rotation to avoid resampling artifacts
    if abs(angle) < 0.3:
        return img.copy()

    h, w = img.shape[:2]
    center = (w / 2.0, h / 2.0)

    # Rotation matrix — negate angle because OpenCV y-axis is flipped
    M = cv2.getRotationMatrix2D(center, -angle, scale=1.0)

    rotated = cv2.warpAffine(
        img,
        M,
        (w, h),
        flags=cv2.INTER_LINEAR,
        borderMode=cv2.BORDER_CONSTANT,
        borderValue=background_color,
    )

    return rotated


def deskew(img: np.ndarray, binary: np.ndarray) -> tuple[np.ndarray, float]:
    """

    Convenience wrapper: detect angle and correct in one call.



    Args:

        img:    Original grayscale image.

        binary: Binary version used for angle detection.



    Returns:

        (corrected_image, detected_angle_degrees)

    """
    angle = detect_skew_angle(binary)
    corrected = correct_skew(img, angle=angle)
    return corrected, angle