Files
Premik-ComfyUI-ImgPatchEditor/image_warper.py
T
2025-11-24 19:56:36 +01:00

115 lines
4.0 KiB
Python

from dataclasses import dataclass
import numpy as np
from PIL import Image
from skimage import transform, feature, measure
class ScaleTranslationTransform(transform.SimilarityTransform):
def estimate(self, src, dst):
# src and dst are (N, 2) arrays
if src.shape[0] < 2:
return False
# 1. Compute centroids
src_mean = src.mean(axis=0)
dst_mean = dst.mean(axis=0)
# 2. Center the points
src_centered = src - src_mean
dst_centered = dst - dst_mean
# 3. Compute scale s
# s = sum(src_centered * dst_centered) / sum(src_centered^2)
# We sum over both dimensions and all points
num = np.sum(src_centered * dst_centered)
den = np.sum(src_centered**2)
if den == 0:
return False
s = num / den
# 4. Compute translation
# t = dst_mean - s * src_mean
t = dst_mean - s * src_mean
# 5. Set parameters
# Update the matrix
self.params = np.array([[s, 0, t[0]], [0, s, t[1]], [0, 0, 1]])
return True
@dataclass
class ImageWarper:
max_keypoints: int = 1000
min_matches: int = 10
ransac_residual_threshold: float = 2.0
def warp(self, original: Image.Image, edited: Image.Image) -> Image.Image:
"""
Aligns the edited image to the original image using feature matching and warping.
If alignment fails, falls back to simple resizing.
"""
# Convert to grayscale for feature detection
img1_gray = np.array(original.convert('L'))
img2_gray = np.array(edited.convert('L'))
# Detect ORB features
orb = feature.ORB(n_keypoints=self.max_keypoints)
orb.detect_and_extract(img1_gray)
keypoints1 = orb.keypoints
descriptors1 = orb.descriptors
orb.detect_and_extract(img2_gray)
keypoints2 = orb.keypoints
descriptors2 = orb.descriptors
# Check if descriptors were found
if descriptors1 is None or descriptors2 is None or len(descriptors1) == 0 or len(descriptors2) == 0:
return edited.resize(original.size, Image.Resampling.LANCZOS)
# Match descriptors
matches12 = feature.match_descriptors(descriptors1, descriptors2, cross_check=True)
if len(matches12) < self.min_matches:
return edited.resize(original.size, Image.Resampling.LANCZOS)
# Extract location of good matches
# keypoints are (row, col)
src_rc = keypoints2[matches12[:, 1]] # edited
dst_rc = keypoints1[matches12[:, 0]] # original
# skimage transforms use (x, y) coordinates, which is (col, row).
# We need to swap the columns to work with the transform model.
src = src_rc[:, ::-1]
dst = dst_rc[:, ::-1]
# Estimate transform using RANSAC
# ScaleTranslationTransform handles translation and uniform scaling (no rotation).
model_robust, inliers = measure.ransac(
(src, dst), ScaleTranslationTransform, min_samples=3, residual_threshold=self.ransac_residual_threshold, max_trials=100
)
if model_robust is None:
print("RANSAC failed to find a model.")
return edited.resize(original.size, Image.Resampling.LANCZOS)
# print(f"Matches: {len(matches12)}")
# print(f"Inliers: {np.sum(inliers)}")
# print(f"Scale: {model_robust.scale:.4f}")
# print(f"Rotation: {model_robust.rotation:.4f} rad")
# print(f"Translation: {model_robust.translation}")
# Warp the edited image
edited_arr = np.array(edited.convert('RGBA'))
# warp output is float. preserve_range=True keeps values in [0, 255] if input was uint8
warped_arr = transform.warp(
edited_arr, model_robust.inverse, output_shape=np.array(original).shape[:2], order=1, mode='constant', cval=0, preserve_range=True
)
warped_img = Image.fromarray(warped_arr.astype(np.uint8), 'RGBA')
return warped_img