122 lines
3.9 KiB
Python
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()
|