Files
2026-03-12 21:05:35 +01:00

122 lines
3.9 KiB
Python

import numpy as np
from skimage import transform
class ScaleTranslationTransform(transform.SimilarityTransform):
"""
A custom transformation model that only considers uniform scaling and translation.
This class is a simplified version of `skimage.transform.SimilarityTransform`
that ignores rotation and shear. It is used to estimate the transformation
from a set of source and destination points.
"""
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
def test():
# Create synthetic data
# src: square
src = np.array([[0, 0], [10, 0], [10, 10], [0, 10]])
# Case 1: Pure translation
dst_trans = src + [5, 5]
model = ScaleTranslationTransform()
model.estimate(src, dst_trans)
print("Case 1 (Translation):")
print(f"Scale: {model.scale} (Expected 1.0)")
print(f"Translation: {model.translation} (Expected [5. 5.])")
print(f"Rotation: {model.rotation} (Expected 0)")
# Case 2: Pure scale
dst_scale = src * 2.0
model = ScaleTranslationTransform()
model.estimate(src, dst_scale)
print("\nCase 2 (Scale):")
print(f"Scale: {model.scale} (Expected 2.0)")
print(f"Translation: {model.translation} (Expected [0. 0.])")
# Case 3: Scale + Translation
dst_st = src * 0.5 + [10, 20]
model = ScaleTranslationTransform()
model.estimate(src, dst_st)
print("\nCase 3 (Scale + Translation):")
print(f"Scale: {model.scale} (Expected 0.5)")
print(f"Translation: {model.translation} (Expected [10. 20.])")
# Case 4: Rotation (should fail to capture rotation, but find best scale/trans)
# Rotate 90 degrees: (x,y) -> (-y, x)
# src: (0,0), (10,0), (10,10), (0,10)
# dst: (0,0), (0,10), (-10,10), (-10,0)
# Centroids: src(5, 5), dst(-5, 5)
# src_c: (-5,-5), (5,-5), (5,5), (-5,5)
# dst_c: (5,-5), (5,5), (-5,5), (-5,-5)
# num = (-25 + 25) + (25 - 25) + (-25 + 25) + (25 - 25) = 0?
# Let's check.
# (-5)*5 + (-5)*(-5) = -25 + 25 = 0
# 5*5 + (-5)*5 = 25 - 25 = 0
# ...
# So scale should be 0? That makes sense as projection of orthogonal vectors.
dst_rot = np.array([[0, 0], [0, 10], [-10, 10], [-10, 0]])
model = ScaleTranslationTransform()
model.estimate(src, dst_rot)
print("\nCase 4 (Rotation 90deg):")
print(f"Scale: {model.scale}")
print(f"Translation: {model.translation}")
# Case 5: Rotation small (10 deg)
# Should find scale ~1 and translation approx correct
theta = np.radians(10)
c, s = np.cos(theta), np.sin(theta)
rot_mat = np.array([[c, -s], [s, c]])
dst_rot_small = src @ rot_mat.T
model = ScaleTranslationTransform()
model.estimate(src, dst_rot_small)
print("\nCase 5 (Rotation 10deg):")
print(f"Scale: {model.scale}")
print(f"Translation: {model.translation}")
# Compare with SimilarityTransform
sim_model = transform.SimilarityTransform()
sim_model.estimate(src, dst_rot_small)
print(f"Similarity Scale: {sim_model.scale}")
print(f"Similarity Rotation: {sim_model.rotation}")
if __name__ == "__main__":
test()