diff --git a/.travis.yml b/.travis.yml index edf871ee..1f2fc210 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ addons: - oracle-java8-set-default - maven install: + - pip install codecov - pip install -r requirements.txt - pip install -r test_requirements.txt before_install: @@ -44,4 +45,5 @@ cache: - $HOME/.m2 secure: "x3rK0ICjrT1yAguEgq7/UyNDDCbbr46j7DsHLzEEgoM4l5crRECfdclgws4s4Z7RH6TRrW3+CKvhJuq/71QRIWBUN/ZUK9REEvgC13mOFFN7PQlhQGHTQPSiJ1oUTtXG5KenNBOOgz9q4vJNFnahn+L+GuN2TiAYRJGbyE8G5A5uFLIhXHVb+Xo295XkarFtX8EFKQmJJzdyFgqU+NsKW2gbq0hIASmQi3swJZhmbzQXhaj0gCuLjQfnDR+3qHzE+v5mQfObk4v6FRf3mdnZRVqV4S67yKONDK5LCkuI69C/1EKPiAfEEg5RKcRIzQlDjApgYVFf+jFjrVy6RLU9xOp2dstSFmynL96N+K4HoRAw53F08WokBih5hbsEwGAb/Fat1fLVy1hqmboF4d5Fy42TXrPmHgkqwlMXABPVqKwoWQTo0sANQdOVNWaF6NyMDTkUwtAzD9IG+Qwu/9v1zleT0VQ92Uk0s8wTDlkVny+8XGv4Pi2sNBcgG+huECNAOQKCeFbMn0LfMoGaKDXNCW4OHp59wXXXJQHAaN8xujwGpwQxJKb3iwf6uY9wnBpfKtaaCqOcP3Y/WhuiuKf4477bZPoQXD+2DFAPlC5nVjn+LzzN1HigdPCWn8F+SianZsL/iuxgEqmiW6OFGGzpqD4zHFNACky3fTS7yBLnd+Q=" after_success: + - codecov -t 3f12d985-af62-455d-a11d-9669c039640d - "BRANCHES_TO_MERGE_REGEX='develop' BRANCH_TO_MERGE_INTO=master GITHUB_REPO=cdown/srt .travis/merge_script.sh" \ No newline at end of file diff --git a/README.rst b/README.rst index 4b881551..83ed3ece 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,9 @@ .. image:: https://travis-ci.org/fcollman/render-python.svg?branch=master :target: https://travis-ci.org/fcollman/render-python :alt: Build Status - +.. image:: https://codecov.io/gh/fcollman/render-python/branch/master/graph/badge.svg + :target: https://codecov.io/gh/fcollman/render-python + render-python ############# diff --git a/renderapi/transform.py b/renderapi/transform.py index 28305b13..916bf3ea 100644 --- a/renderapi/transform.py +++ b/renderapi/transform.py @@ -62,8 +62,12 @@ def __init__(self, tforms=None, transformId=None, json=None): self.tforms = tforms self.transformId = transformId - def to_dict(self): + def to_dict(self,prefer_affine=True): """serialization function + Parameters + ---------- + prefer_affine: bool + prefer to represent all linear transform as affine's (True) Returns ------- @@ -72,7 +76,7 @@ def to_dict(self): """ d = {} d['type'] = 'list' - d['specList'] = [tform.to_dict() for tform in self.tforms] + d['specList'] = [tform.to_dict(prefer_affine=True) for tform in self.tforms] if self.transformId is not None: d['id'] = self.transformId return d @@ -88,6 +92,11 @@ def to_json(self): """ return json.dumps(self.to_dict()) + def tform(self,pts): + for tf in self.tforms: + pts = tf.tform(pts) + return pts + def from_dict(self, d): """deserialization function @@ -222,7 +231,7 @@ def __init__(self, a=None, b=None, lambda_=None, json=None): self.b = b self.lambda_ = lambda_ - def to_dict(self): + def to_dict(self,**kwargs): """serialization routine Returns @@ -278,7 +287,7 @@ def __init__(self, refId=None, json=None): else: self.refId = refId - def to_dict(self): + def to_dict(self,**kwargs): """serialization routine Returns @@ -352,7 +361,7 @@ def __init__(self, className=None, dataString=None, self.transformId = transformId self.labels = labels - def to_dict(self): + def to_dict(self,**kwargs): """serialization routine Returns @@ -483,11 +492,10 @@ def __init__(self, M00=1.0, M01=0.0, M10=0.0, M11=1.0, B0=0.0, B1=0.0, self.M11 = M11 self.B0 = B0 self.B1 = B1 - self.className = 'mpicbg.trakem2.transform.AffineModel2D' self.labels = labels self.load_M() self.transformId = transformId - + self.className = 'mpicbg.trakem2.transform.AffineModel2D' @property def dataString(self): """dataString string for this transform""" @@ -770,12 +778,22 @@ class TranslationModel(AffineModel): def __init__(self, *args, **kwargs): super(TranslationModel, self).__init__(*args, **kwargs) + self.className = 'mpicbg.trakem2.transform.TranslationModel2D' + + @property + def dataString(self): + """dataString string for this transform""" + return "%.10f %.10f" % (self.B0, self.B1) def _process_dataString(self, dataString): """expected dataString is 'tx ty'""" - tx, ty = map(float(dataString.split(' '))) + tx, ty = map(float,dataString.split(' ')) self.B0 = tx self.B1 = ty + self.M00 = 1.0 + self.M11 = 1.0 + self.M01 = 0.0 + self.M10 = 0.0 self.load_M() @staticmethod @@ -861,14 +879,18 @@ class RigidModel(AffineModel): def __init__(self, *args, **kwargs): super(RigidModel, self).__init__(*args, **kwargs) + self.className = 'mpicbg.trakem2.transform.RigidModel2D' + @property + def dataString(self): + return "%0.10f %0.10f %0.10f"%(np.arctan2(self.M10,self.M00), self.B0,self.B1) def _process_dataString(self, dataString): """expected datastring is 'theta tx ty'""" - theta, tx, ty = map(float(dataString.split(' '))) + theta, tx, ty = map(float,dataString.split(' ')) self.M00 = np.cos(theta) self.M01 = -np.sin(theta) self.M10 = np.sin(theta) - self.M11 = np.sin(theta) + self.M11 = np.cos(theta) self.B0 = tx self.B1 = ty self.load_M() @@ -990,14 +1012,22 @@ class SimilarityModel(RigidModel): def __init__(self, *args, **kwargs): super(SimilarityModel, self).__init__(*args, **kwargs) + self.className = 'mpicbg.trakem2.transform.SimilarityModel2D' + + @property + def dataString(self): + return "%0.10f %0.10f %0.10f %0.10f"%(np.linalg.det(self.M), + np.arctan2(self.M10,self.M00), + self.B0, + self.B1) def _process_dataString(self, dataString): """expected datastring is 's theta tx ty'""" - s, theta, tx, ty = map(float(dataString.split(' '))) + s, theta, tx, ty = map(float,dataString.split(' ')) self.M00 = s * np.cos(theta) self.M01 = -s * np.sin(theta) self.M10 = s * np.sin(theta) - self.M11 = s * np.sin(theta) + self.M11 = s * np.cos(theta) self.B0 = tx self.B1 = ty self.load_M() @@ -1135,7 +1165,7 @@ def fit(src, dst, order=2): raise EstimationError( 'source has {} points, but dest has {}!'.format( len(src), len(dst))) - if no_coeff > len(src): + if no_coeff > 2*len(src): raise EstimationError( 'order {} is too large to fit {} points!'.format( order, len(src))) @@ -1371,7 +1401,7 @@ def estimate_dstpts(transformlist, src=None): Parameters ---------- - transformlist : :obj:list of :obj:Transform + transformlist : :obj:list of :obj:Transform or :obj:TransformList transforms that have a tform method implemented src : numpy.array a Nx2 array of source points @@ -1382,11 +1412,14 @@ def estimate_dstpts(transformlist, src=None): Nx2 array of destination points """ dstpts = src - for tform in transformlist: - if isinstance(tform, list): - dstpts = estimate_dstpts(tform, dstpts) - else: - dstpts = tform.tform(dstpts) + if isinstance(transformlist,TransformList): + dstpts = transformlist.tform(dstpts) + else: + for tform in transformlist: + if isinstance(tform, list): + dstpts = estimate_dstpts(tform, dstpts) + else: + dstpts = tform.tform(dstpts) return dstpts @@ -1564,6 +1597,10 @@ def estimate_transformsum(transformlist, src=None, order=2): """ def flatten(l): """generator-iterator to flatten deep lists of lists""" + if isinstance(l, TransformList): + for sub in flatten(l.tforms): + yield sub + for i in l: if isinstance(i, Iterable): try: @@ -1573,12 +1610,15 @@ def flatten(l): if notstring: for sub in flatten(i): yield sub + elif isinstance(i, TransformList): + for sub in flatten(i.tforms): + yield sub else: yield i dstpts = estimate_dstpts(transformlist, src) tforms = flatten(transformlist) - if all([(tform.className == AffineModel.className) + if all([isinstance(tform,AffineModel) for tform in tforms]): am = AffineModel() am.estimate(A=src, B=dstpts, return_params=False) diff --git a/test/test_transform.py b/test/test_transform.py index b266de54..c7e5fcb0 100644 --- a/test/test_transform.py +++ b/test/test_transform.py @@ -4,6 +4,7 @@ import scipy.linalg import rendersettings import importlib +import pytest def cross_py23_reload(module): try: @@ -11,6 +12,65 @@ def cross_py23_reload(module): except NameError as e: importlib.reload(module) +@pytest.mark.parametrize(('Class','ds'), + [(renderapi.transform.TranslationModel,"%0.10f %0.10f"%(0,0)), + (renderapi.transform.RigidModel,"%0.10f %0.10f %0.10f"%(0.0,0.0,0.0)), + (renderapi.transform.SimilarityModel,"%0.10f %0.10f %0.10f %0.10f"%(1.0,0.0,0.0,0.0))]) +def test_transform_simple(Class,ds): + tform=Class(transformId="test",labels=['testlabel']) + d = tform.to_dict() + assert(d['dataString']==ds) + assert('testlabel' in d['metaData']['labels']) + tform = Class(json=d) + assert('testlabel' in tform.labels) + + +@pytest.mark.parametrize(('Class','frompts','topts','ds'), + [ (renderapi.transform.TranslationModel, + np.array([[0.0,0.0],[100.0,100.0]]), + np.array([[100.0,0.0],[200.0,100.0]]), + '%0.10f %0.10f'%(100,0)), + (renderapi.transform.RigidModel, + np.array([[0.0,0.0],[100.0,0.0]]), + np.array([[0.0,0.0],[0.0,100.0]]), + '%0.10f %0.10f %0.10f'%(np.pi/2,0,0)), + (renderapi.transform.RigidModel, + np.array([[0.0,0.0],[-100.0,0.0],[-50,0]]), + np.array([[0.0,0.0],[100.0,0.0],[50,0]]), + '%0.10f %0.10f %0.10f'%(np.pi,0,0)), + (renderapi.transform.SimilarityModel, + np.array([[0.0,0.0],[100.0,0.0]]), + np.array([[0.0,0.0],[0.0,200.0]]), + '%0.10f %0.10f %0.10f %0.10f'%(2.0,np.pi/2,0,0)) + ]) +def test_transform_estimate(Class,frompts,topts,ds): + tform=Class() + tform.estimate(frompts,topts) + d = tform.to_dict() + d['dataString']=ds + tform2 = Class(json=d) + print(tform,tform2) + assert(np.allclose(tform.M,tform2.M)) + +@pytest.mark.parametrize(('Class','frompts','topts'), + [ (renderapi.transform.RigidModel, + np.array([[0.0,0.0],[0.0,0.0]]), + np.array([[0.0,0.0],[0.0,0.0]])), + (renderapi.transform.Polynomial2DTransform, + np.array([[0.0,0.0],[1.0,0.0],[2.0,0.0],[0.0,1.0],[0.0,2.0]]), + np.array([[0.0,0.0],[1.0,0.0],[4.0,0.0],[0.0,1.0],[0.0,8.0]])) + ]) +def test_transform_estimate_fail(Class,frompts,topts): + tform=Class() + with pytest.raises(renderapi.transform.EstimationError): + tform.estimate(frompts,topts) + +def test_reference_transform(): + tform=Class(refId="test") + d = tform.to_dict() + assert(d['refId']=="test") + tform = Class(json=d) + def test_affine_rot_90(): am = renderapi.transform.AffineModel() # setup a 90 degree clockwise rotation @@ -104,13 +164,15 @@ def noscipy_import(name, globals=None, locals=None, derived_pt = renderapi.transform.Polynomial2DTransform( src=srcpts, dst=dstpts) assert(np.allclose(derived_pt.params, default_pt.params)) - + assert(derived_pt.coefficients() == 12) if use_numpy: builtins.__import__ = realimport cross_py23_reload(renderapi.transform) assert(renderapi.transform.svd is scipy.linalg.svd) - - + with pytest.raises(renderapi.transform.ConversionError): + derived_pt.asorder(1) + with pytest.raises(renderapi.transform.ConversionError): + derived_pt.fromAffine(derived_pt) def test_Polynomial_estimation_numpy(): test_Polynomial_estimation(use_numpy=True) @@ -343,9 +405,10 @@ def estimate_homography_transform( assert np.allclose(tform.translation, random_translate.translation) if do_rotate: assert np.isclose(tform.rotation, random_rotate.rotation) - # currently forces as affines - am = renderapi.transform.AffineModel(json=tform.to_dict()) - assert am.to_dict() == tform.to_dict() + # # currently forces as affines + # I didn't see the point of this test, and it now fails for TranslationModels + # am = renderapi.transform.AffineModel(json=tform.to_dict()) + # assert am.M == tform.to_dict() def test_estimate_similarity_transform(): @@ -404,7 +467,8 @@ def test_non_linear_transform(): "1.3042150458228838E19 1.0829624014456685E19 9.899910593314652E18 " "9.885988268659214E18 1.1051253229808925E19 1.6002173610912907E19 " "0.0 2048 2048 "), - transformId="testing") + transformId="testing", + labels=["testing"]) ticks = np.arange(0,2048,64,np.float) xx,yy = np.meshgrid(ticks,ticks) @@ -415,4 +479,94 @@ def test_non_linear_transform(): xyp=lens_tform.tform(xy) dv = xyp-xy mean_disp= np.mean(np.sqrt(np.sum(dv**2,axis=1))) - assert((mean_disp-0.7570507)<.01) \ No newline at end of file + assert((mean_disp-0.7570507)<.01) + + +def test_transform_list_simple(): + affine1 = renderapi.transform.AffineModel(M00=2.0,M11=2.0) + affine2 = renderapi.transform.AffineModel(M00=.5,M11=.5) + tform_list = renderapi.transform.TransformList(tforms=[affine1,affine2],transformId='mylist') + points_in = np.array([[1,1]]) + points_out=renderapi.transform.estimate_dstpts(tform_list.tforms,points_in) + mean_disp = np.mean(np.sqrt(np.sum((points_out-points_in)**2,axis=1))) + assert(mean_disp<.001) + d = tform_list.to_dict() + assert(d['id']=='mylist') + assert(len(d['specList'])==2) + + affine1b = renderapi.transform.Transform(json=d['specList'][0]) + +def test_transform_list_fail(): + affine1 = renderapi.transform.AffineModel(M00=2.0,M11=2.0) + affine2 = renderapi.transform.AffineModel(M00=.5,M11=.5) + + with pytest.raises(renderapi.render.RenderError): + tform_list = renderapi.transform.TransformList(tforms=affine1,transformId='mylist') + + with pytest.raises(renderapi.render.RenderError): + tform_list = renderapi.transform.TransformList(tforms=[affine1,affine2],transformId='mylist') + d = tform_list.to_dict() + d['specList'][1]['type']='notatype' + tform_list.from_dict(d) + + with pytest.raises(renderapi.render.RenderError): + renderapi.transform.load_leaf_json(d['specList'][1]) + +def test_affine_fit_fail(): + affine1 = renderapi.transform.AffineModel(M00=2.0,M11=2.0) + points_in = np.array([[1,1],[0,0],[-1,0],[1,0]]) + points_out =np.array([[1,1],[0,0],[-1,0]]) + with pytest.raises(renderapi.transform.EstimationError): + affine1.fit(points_in,points_out) + +def test_affine_convert_point_vector_fail(): + affine1 = renderapi.transform.AffineModel(M00=2.0,M11=2.0) + points_4d = np.array([[1,1,0,0]]) + with pytest.raises(renderapi.transform.ConversionError): + affine1.convert_to_point_vector(points_4d) + +def assert_estimation(tforml): + start_pt = np.array([[0,0]]) + end_pt = renderapi.transform.estimate_dstpts(tforml, + start_pt) + assert(np.allclose(end_pt,np.array([1.0,1.0]))) +def assert_estimation_sum(tforml): + src_pts = 3*np.random.random((10,2)) + tform_sum = renderapi.transform.estimate_transformsum(tforml,src=src_pts,order=1) + tform_expected = renderapi.transform.AffineModel(B0=1.0,B1=1.0) + + assert(np.allclose(tform_sum.M,tform_expected.M)) +def test_tranform_nested_estimate(): + t1 = renderapi.transform.AffineModel(B0=1) + t2 = renderapi.transform.AffineModel(B1=1.0) + t3 = renderapi.transform.AffineModel(B0 = 1.0, B1 = 1.0) + t4 = renderapi.transform.TranslationModel(B0=-1.0,B1=-1.0) + tform_list = renderapi.transform.TransformList(tforms=[t1,t2]) + tform_list2 = renderapi.transform.TransformList(tforms=[t3,t4]) + tform_metalist = renderapi.transform.TransformList(tforms=[tform_list,tform_list2]) + tform_list_list = [[t1,t2],[t3,t4]] + + assert_estimation(tform_metalist) + assert_estimation(tform_list_list) + assert_estimation([tform_list,tform_list2]) + + assert_estimation_sum(tform_metalist) + assert_estimation_sum(tform_list_list) + assert_estimation_sum([tform_list,tform_list2]) + +def test_new_unknown_transform(): + d = { + 'specList':[{ + 'className':'mpicbg.trakem2.transform.StephanCrazyTform', + 'dataString':"3.0 2.0 1.0 0.0", + 'metaData':{ + 'labels':['crazy'] + }, + 'id':"myfirstcrazy" + }] + } + tform_list = renderapi.transform.TransformList(json=d) + tform = tform_list.tforms[0] + assert(tform.className == 'mpicbg.trakem2.transform.StephanCrazyTform') + assert(tform.transformId == 'myfirstcrazy') + assert('crazy' in tform.labels)