FLOOR2MODEL / src /preprocessing /skew_corrector.py
Harisri
Purged CV model deployment
fc895f4
"""
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