diff --git a/renderapi/transform/leaf/thin_plate_spline.py b/renderapi/transform/leaf/thin_plate_spline.py index a0714ea6..793fc0f9 100644 --- a/renderapi/transform/leaf/thin_plate_spline.py +++ b/renderapi/transform/leaf/thin_plate_spline.py @@ -394,10 +394,8 @@ def adaptive_mesh_estimate( mn = self.srcPts.min(axis=1) mx = self.srcPts.max(axis=1) - xt, yt = np.meshgrid( - np.linspace(mn[0], mx[0], starting_grid), - np.linspace(mn[1], mx[1], starting_grid)) - new_src = np.vstack((xt.flatten(), yt.flatten())).transpose() + new_src = self.src_array( + mn[0], mn[1], mx[0], mx[1], starting_grid, starting_grid) old_src = self.srcPts.transpose() old_dst = self.tform(old_src) @@ -411,3 +409,82 @@ def adaptive_mesh_estimate( max_iter=max_iter, nworst=nworst, niter=0) + + @staticmethod + def src_array(xmin, ymin, xmax, ymax, nx, ny): + """create N x 2 array of regularly spaced points + + Parameters + ---------- + xmin : float + minimum of x grid + ymin : float + minimum of y grid + xmax : float + maximum of x grid + ymax : float + maximum of y grid + nx : int + number of points in x axis + ny : int + number of points in y axis + + Returns + ------- + src : :class:`numpy.ndarray` + (nx * ny) x 2 array of coordinated. + + """ + src = np.mgrid[xmin:xmax:nx*1j, ymin:ymax:ny*1j].reshape(2, -1).T + return src + + def scale_coordinates( + self, + factor, + ngrid=20, + preserve_srcPts=False): + """estimates a new ThinPlateSplineTransform from the current one + in a scaled transform space. + + Parameters + ---------- + factor : float + the factor by which to scale the space + ngrid : int + number of points per axis for the estimation grid + preserve_srcPts : bool + one might want to keep the original scaled srcPts + for example, if pts were made specially for a mask + or a crack or fold + + Returns + ------- + new_tform : :class:`renderapi.transform.ThinPlateSplineTransform` + the new transform in the scaled space + + """ + + new_tform = ThinPlateSplineTransform() + computeAffine = True + if self.aMtx is None: + computeAffine = False + + mn = self.srcPts.min(axis=1) + mx = self.srcPts.max(axis=1) + src = self.src_array(mn[0], mn[1], mx[0], mx[1], ngrid, ngrid) + + if preserve_srcPts: + # do not repeat close points + dist = scipy.spatial.distance.cdist( + src, + self.srcPts.transpose(), + metric='euclidean') + ind = np.invert(np.any(dist < 1e-3, axis=0)) + src = np.vstack((src, self.srcPts.transpose()[ind])) + + new_tform.estimate( + src * factor, + self.tform(src) * factor, + computeAffine=computeAffine) + + return new_tform diff --git a/test/rendersettings.py b/test/rendersettings.py index b825aa58..2152fdae 100644 --- a/test/rendersettings.py +++ b/test/rendersettings.py @@ -36,6 +36,9 @@ TEST_THINPLATESPLINE_FILE = os.path.join( TEST_FILES_DIR, 'thin_plate_spline.json') +TEST_THINPLATEROUGH_FILE = os.path.join( + TEST_FILES_DIR, + 'thin_plate_from_rough.json') TEST_THINPLATESPLINEAFFINE_FILE = os.path.join( TEST_FILES_DIR, 'thin_plate_spline_affine.json') diff --git a/test/test_files/thin_plate_from_rough.json b/test/test_files/thin_plate_from_rough.json new file mode 100644 index 00000000..775c7b11 --- /dev/null +++ b/test/test_files/thin_plate_from_rough.json @@ -0,0 +1 @@ +{"type": "leaf", "className": "mpicbg.trakem2.transform.ThinPlateSplineTransform", "dataString": "ThinPlateSplineR2LogR 2 25 eJwBMADP/78yLRWQifN2vw/ErHm04Cs/IqdJx765j78ab8sN7GLJwCCjMIEXaI2/9qEQnRU3FUt4FyU= eJw9zFtMkmEYAOCJELrmIcvsoJ10Mipnx4VtfWLlRQ2PYFPCCNKVMv65iYeIiEw8QLFCy5b/nNp086KtRCNyvmOuzJjLuiBLRUGaW2ZYU1cbtnXzf9/lc/M4S2+NVnOM7c5LXS9/KrazhMYXIxPTn+aJHyuGOui6R8Tda+cXeywJxH2OV5z1qWVsJ/MJDe0Gu23Kij9i5iNmPmLmwyaf1UgnNy/tIx82/rDxh40/xuSjh2t+py0GyYeNP2z8YeOPMfmeavs8q6uL5MPGHzb+sPHHGPSsQ9JopwIKg2ZvIC8eien7u09ssgJ/JTaHX2RD1Mm43su5bqgKzeqXzFYAa+GLabRgL8oUNLDd3C0op9zMmz9gRjf+LU3T7gSQ58dy5+yfkVKUd+VJ2DdQJ3HLecUPkaQ47Nwc1YQaabbQwZVBibUSkk31KEMdDpPCCTBIHIG3+iY4Pfb6o89GobsCNeV5p4LrliMhH243QtWfo2lD72kweIOlG8pccCY1sXqN6kcmtsfrf84BnWU0qmOrCxrqJgfDHSEgVm3bGaX5jjSChYDaTqH03IEdIvlFkGplqjxxGdypOP5Vc+wUKvC0RW/Um5CupXe9qnkz4rsiWh1NflRb45CMZKSArLjNrA3UoqKrgsyDpgpU6ffFWFPjIXvlXpleIUf14z9ixuQdKH9Pl6dE3gri9F/jPfv/ogvPsqXU4UgoHBxWDuyKQEZWXOdyZAaSPOjU+brPQknDzMwELxRyNaGixBQdKG++uTablfQfS4+iWQ=="} diff --git a/test/test_transform.py b/test/test_transform.py index ef6d6924..57c3135e 100644 --- a/test/test_transform.py +++ b/test/test_transform.py @@ -5,6 +5,8 @@ import rendersettings import importlib import pytest +from scipy.spatial.distance import cdist + EPSILON = 0.0000000001 EPSILON2 = 0.000000001 @@ -1037,3 +1039,47 @@ def test_polynomial_shear(): assert atf.shear == ptf.shear assert atf.rotation == ptf.rotation assert atf.translation == ptf.translation + + +@pytest.mark.parametrize('ngrid', [5, 20]) +@pytest.mark.parametrize('preserve_srcPts', [True, False]) +@pytest.mark.parametrize('computeAffine', [True, False]) +@pytest.mark.parametrize('factor', [1e-3, 1e3, 1e5]) +def test_scale_thinplate(factor, computeAffine, preserve_srcPts, ngrid): + # use case is to scale a rough alignment thinplate spline + with open(rendersettings.TEST_THINPLATEROUGH_FILE, 'r') as f: + tform = renderapi.transform.load_transform_json( + json.load(f)) + + if not computeAffine: + tform.aMtx = None + tform.bVec = None + + mn = tform.srcPts.min(axis=0) + mx = tform.srcPts.max(axis=0) + + npts = 1000 + src = np.random.rand(npts, 2) + src[:, 0] = src[:, 0] * (mx[0] - mn[0]) + mn[0] + src[:, 1] = src[:, 1] * (mx[1] - mn[1]) + mn[1] + dst = tform.tform(src) + + scaled_tform = tform.scale_coordinates( + factor, ngrid=ngrid, preserve_srcPts=preserve_srcPts) + dst_scaled = scaled_tform.tform(src * factor) + + delta = np.linalg.norm(dst_scaled - dst * factor, axis=1) + scale = dst_scaled.ptp(axis=0).mean() + tol = 1e-4 + if ngrid == 5: + tol = 1e-3 + assert (delta.max() / scale) < tol + + if preserve_srcPts: + dist = cdist( + tform.srcPts.transpose() * factor, + scaled_tform.srcPts.transpose(), + metric='euclidean') + # check that the original srcPts have a close neighbor still + # in the scaled srcPts + assert np.all(np.any(dist < 1e-3, axis=1)) diff --git a/test_requirements.txt b/test_requirements.txt index 2018ec5b..74e88cc7 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,7 +1,7 @@ coverage>=4.1 mock>=2.0.0 pep8>=1.7.0 -pytest>=3.0.5 +pytest>4.6,<5.0 pytest-cov>=2.2.1 pytest-pep8>=1.0.6 pytest-xdist>=1.14