From e29507a2d5a60d39e4110d18c76182e7952bcc72 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sat, 5 Mar 2022 23:45:06 +1100 Subject: [PATCH 01/25] 2d onto 3d with euclidean, similarity transforms --- requirements.txt | 3 +- setup.cfg | 2 +- src/affinder/affinder.py | 61 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/requirements.txt b/requirements.txt index b02edbd..c8dc646 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ napari-plugin-engine>=0.1.9 napari>=0.4.3 numpy -scikit-image +scikit-image>=0.19.2 magicgui>=0.2.5,!=0.2.7 napari toolz +zarr diff --git a/setup.cfg b/setup.cfg index 1481963..47e3bef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ install_requires = napari>=0.4.12 npe2>=0.1.2 numpy - scikit-image + scikit-image>=0.19.2 magicgui>=0.3.7 toolz python_requires = >=3.7 diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index a40a37e..e460162 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -1,5 +1,5 @@ from typing import Optional - +from napari.layers import Image, Labels, Shapes, Points, Vectors from enum import Enum import pathlib import toolz as tz @@ -58,7 +58,8 @@ def next_layer_callback( # we just added enough points: # estimate transform, go back to layer0 if n0 > ndim: - mat = calculate_transform(pts0, pts1, model_class=model_class) + mat = calculate_transform(pts0, pts1, + ndim, model_class=model_class) moving_image_layer.affine = ( reference_image_layer.affine.affine_matrix @ mat.params ) @@ -73,7 +74,6 @@ def next_layer_callback( viewer.layers.move(viewer.layers.index(reference_points_layer), -1) reset_view(viewer, reference_image_layer) - # make a bindable function to shut things down @magicgui def close_affinder(layers, callback): @@ -81,6 +81,46 @@ def close_affinder(layers, callback): layer.events.data.disconnect(callback) layer.mode = 'pan_zoom' +def ndims(layer): + if isinstance(layer, Image) or isinstance(layer, Labels): + return layer.data.ndim + elif isinstance(layer, Shapes): + # list of s shapes, containing n * D of n points with D dimensions + return layer.data[0].shape[1] + elif isinstance(layer, Points): + # (n, D) array of n points with D dimensions + return layer.data.shape[1] + elif isinstance(layer, Vectors): + # (n, 2, D) of n vectors with start pt and projections in D dimensions + return layer.data.shape[-1] + else: + raise Warning(layer, "layer type is not currently supported - cannot " + "find its ndims.") + +def add_zeros_at_end_of_last_axis(arr): + new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) + new_arr[:, :arr.shape[1]] = arr + return new_arr + +def expand_dims(layer): + if isinstance(layer, Image) or isinstance(layer, Labels): + return np.expand_dims(layer.data, axis=0) + elif isinstance(layer, Shapes): + # list of s shapes, containing n * D of n points with D dimensions + return [add_zeros_at_end_of_last_axis(l) for l in layer.data] + elif isinstance(layer, Points): + # (n, D) array of n points with D dimensions + return add_zeros_at_end_of_last_axis(layer.data) + elif isinstance(layer, Vectors): + # (n, 2, D) of n vectors with start pt and projections in D dimensions + n, b, D = layer.data.shape + new_arr = np.zeros((n, b, D+1)) + new_arr[:,0,:] = add_zeros_at_end_of_last_axis(layer.data[:,0,:]) + new_arr[:,1,:] = add_zeros_at_end_of_last_axis(layer.data[:,1,:]) + return new_arr + else: + raise Warning(layer, "layer type is not currently supported - cannot " + "expand its dimensions.") @magic_factory( call_button='Start', @@ -108,6 +148,16 @@ def start_affinder( # Use C0 and C1 from matplotlib color cycle points_layers_to_add = [(reference, (0.122, 0.467, 0.706, 1.0)), (moving, (1.0, 0.498, 0.055, 1.0))] + + # make no. dimensions the same (so skimage transforms work) + if ndims(reference) != ndims(moving): + size_ordered = lambda l1,l2: (l1,l2) if ndims(l1) < ndims(l2) else (l2,l1) + smaller_layer, larger_layer = size_ordered(reference, moving) + while ndims(smaller_layer) < ndims(larger_layer): + smaller_layer.data = expand_dims(smaller_layer) + + # pad along each dimension so exact pixel dims are same + # make points layer if it was not specified for i in range(len(points_layers)): if points_layers[i] is None: @@ -152,7 +202,7 @@ def start_affinder( start_affinder._call_button.text = 'Start' -def calculate_transform(src, dst, model_class=AffineTransform): +def calculate_transform(src, dst, ndim, model_class=AffineTransform): """Calculate transformation matrix from matched coordinate pairs. Parameters @@ -169,6 +219,7 @@ def calculate_transform(src, dst, model_class=AffineTransform): transform scikit-image Transformation object """ - model = model_class() + # for 3D and higher dimensions, must specific matrix transform + model = model_class(dimensionality=ndim) model.estimate(dst, src) # we want the inverse return model From 069f965721ce66041589442488791dc181ff4d18 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sat, 12 Mar 2022 22:28:03 +1100 Subject: [PATCH 02/25] reference dimensionality from reference layer --- src/affinder/_tests/test_affinder.py | 5 +- src/affinder/affinder.py | 86 +++++++++++++++++++--------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index f46ad61..609d9f8 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -31,7 +31,6 @@ def make_vector_border(layer_pts): % layer_pts.shape[0], :] - layer_pts[n, :] return vectors - vectors0 = make_vector_border(layer0_pts) vectors1 = make_vector_border(layer1_pts) @@ -51,7 +50,11 @@ def make_vector_border(layer_pts): ] # TODO add tracks layer types, after multidim affine support added +# multidimensional image tests +#@pytest.mark.parametrize("reference,moving", [p for p in product(ref, mov)]) + +# 2D onto 2D tests @pytest.mark.parametrize("reference,moving", [p for p in product(ref, mov)]) def test_layer_types(make_napari_viewer, tmp_path, reference, moving): diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index e460162..99f4d5b 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -1,4 +1,5 @@ from typing import Optional +import napari from napari.layers import Image, Labels, Shapes, Points, Vectors from enum import Enum import pathlib @@ -102,25 +103,56 @@ def add_zeros_at_end_of_last_axis(arr): new_arr[:, :arr.shape[1]] = arr return new_arr -def expand_dims(layer): - if isinstance(layer, Image) or isinstance(layer, Labels): - return np.expand_dims(layer.data, axis=0) - elif isinstance(layer, Shapes): - # list of s shapes, containing n * D of n points with D dimensions - return [add_zeros_at_end_of_last_axis(l) for l in layer.data] - elif isinstance(layer, Points): - # (n, D) array of n points with D dimensions - return add_zeros_at_end_of_last_axis(layer.data) - elif isinstance(layer, Vectors): - # (n, 2, D) of n vectors with start pt and projections in D dimensions - n, b, D = layer.data.shape - new_arr = np.zeros((n, b, D+1)) - new_arr[:,0,:] = add_zeros_at_end_of_last_axis(layer.data[:,0,:]) - new_arr[:,1,:] = add_zeros_at_end_of_last_axis(layer.data[:,1,:]) - return new_arr - else: - raise Warning(layer, "layer type is not currently supported - cannot " - "expand its dimensions.") +def expand_dims(layer, target_ndims): + + while ndims(layer) < target_ndims: + if isinstance(layer, Image) or isinstance(layer, Labels): + # add dimension to beginning of dimension list + layer.data = np.expand_dims(layer.data, axis=0) + elif isinstance(layer, Shapes): + # list of s shapes, containing n * D of n points with D dimensions + layer.data = [add_zeros_at_end_of_last_axis(l) for l in layer.data] + elif isinstance(layer, Points): + # (n, D) array of n points with D dimensions + layer.data = add_zeros_at_end_of_last_axis(layer.data) + elif isinstance(layer, Vectors): + # (n, 2, D) of n vectors with start pt and projections in D dimensions + n, b, D = layer.data.shape + new_arr = np.zeros((n, b, D+1)) + new_arr[:,0,:] = add_zeros_at_end_of_last_axis(layer.data[:,0,:]) + new_arr[:,1,:] = add_zeros_at_end_of_last_axis(layer.data[:,1,:]) + layer.data = new_arr + else: + raise Warning(layer, "layer type is not currently supported - cannot " + "expand its dimensions.") + return + + +def extract_ndims(layer, target_ndims): + """ + return the first target_ndims dimensions of the layer + """ + while ndims(layer) > target_ndims: + if isinstance(layer, Image) or isinstance(layer, Labels): + # extract the first value from each of the discarded dimensions + layer.data = np.take(layer.data, 0, axis=0) + elif isinstance(layer, Shapes): + # list of s shapes, containing n * D array of n points with D dimensions + layer.data = [np.take(l, 0, axis=0) for l in layer.data] + elif isinstance(layer, Points): + # (n, D) array of n points with D dimensions + layer.data = np.take(layer.data, 0, axis=0) + elif isinstance(layer, Vectors): + # (n, 2, D) of n vectors with start pt and projections in D dimensions + n, b, D = layer.data.shape + new_arr = np.zeros((n, b, D-1)) + new_arr[:,0,:] = np.take(layer.data[:,0,:], 0, axis=0) + new_arr[:,1,:] = np.take(layer.data[:,1,:], 0, axis=0) + layer.data = new_arr + else: + raise Warning(layer, "layer type is not currently supported - cannot " + "extract its dimensions.") + return @magic_factory( call_button='Start', @@ -141,6 +173,13 @@ def start_affinder( mode = start_affinder._call_button.text # can be "Start" or "Finish" if mode == 'Start': + # make no. dimensions the same (so skimage transforms work) + if ndims(reference) < ndims(moving): + extract_ndims(moving, ndims(reference)) + elif ndims(reference) > ndims(moving): + expand_dims(moving, ndims(reference)) + print("AFTER EQ", ndims(reference), "==?", ndims(moving)) + # focus on the reference layer reset_view(viewer, reference) # set points layer for each image @@ -149,15 +188,6 @@ def start_affinder( points_layers_to_add = [(reference, (0.122, 0.467, 0.706, 1.0)), (moving, (1.0, 0.498, 0.055, 1.0))] - # make no. dimensions the same (so skimage transforms work) - if ndims(reference) != ndims(moving): - size_ordered = lambda l1,l2: (l1,l2) if ndims(l1) < ndims(l2) else (l2,l1) - smaller_layer, larger_layer = size_ordered(reference, moving) - while ndims(smaller_layer) < ndims(larger_layer): - smaller_layer.data = expand_dims(smaller_layer) - - # pad along each dimension so exact pixel dims are same - # make points layer if it was not specified for i in range(len(points_layers)): if points_layers[i] is None: From 8988159500e2b2a40c2eceabd5bb85392dac073c Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sat, 19 Mar 2022 15:29:33 +1100 Subject: [PATCH 03/25] tests pass but failed attempt at 2D 'tiling' onto 3D --- .../_tests/nuclei2D_labels.zarr/.zarray | 22 ++ src/affinder/_tests/nuclei2D_labels.zarr/0.0 | Bin 0 -> 1457 bytes .../nuclei2D_transformed_labels.zarr/.zarray | 22 ++ .../nuclei2D_transformed_labels.zarr/0.0 | Bin 0 -> 1304 bytes .../_tests/nuclei3D_labels.zarr/.zarray | 24 +++ .../_tests/nuclei3D_labels.zarr/0.0.0 | Bin 0 -> 73526 bytes src/affinder/_tests/test_affinder.py | 199 +++++++++++++++++- src/affinder/affinder.py | 152 ++++++++++--- 8 files changed, 384 insertions(+), 35 deletions(-) create mode 100644 src/affinder/_tests/nuclei2D_labels.zarr/.zarray create mode 100644 src/affinder/_tests/nuclei2D_labels.zarr/0.0 create mode 100644 src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray create mode 100644 src/affinder/_tests/nuclei2D_transformed_labels.zarr/0.0 create mode 100644 src/affinder/_tests/nuclei3D_labels.zarr/.zarray create mode 100644 src/affinder/_tests/nuclei3D_labels.zarr/0.0.0 diff --git a/src/affinder/_tests/nuclei2D_labels.zarr/.zarray b/src/affinder/_tests/nuclei2D_labels.zarr/.zarray new file mode 100644 index 0000000..1a5ac69 --- /dev/null +++ b/src/affinder/_tests/nuclei2D_labels.zarr/.zarray @@ -0,0 +1,22 @@ +{ + "chunks": [ + 256, + 256 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": "UcG@>?Z%BzWD#r7y3x&6W>2<(js{OgDMGS&eyTKnSd*)#)0$1YZ zi5->&wmiuTlJT4%U%<8=P)WKGgO_hqEISP0`LlLQYqj`JgOe}R)Pxtfh}%tt_2^!M zr4@Cx;te*(HX8~XxknA4Aq_R;1K{W3y26$lfI?oWmAA-^U@Y>hT4__)VVvPmobPbj bfW*wxyv^`_|7_J;*fH(c!j5S-vWXqPOx`3R literal 0 HcmV?d00001 diff --git a/src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray b/src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray new file mode 100644 index 0000000..c66a6e5 --- /dev/null +++ b/src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray @@ -0,0 +1,22 @@ +{ + "chunks": [ + 246, + 224 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": "5z-S21z`FW?E>dI;D5`-4ctm}ug{xKLMW1VjWB zNmMX#;UUzSO4!2&7dB~TI`6f8^IEOuB3O4IGi!hu0IM3vL*aW$<0zvPsIYm37eqAZ z1Lu)v7|z~tW*!mctdG-Z2d=H1xNVyaaO^hW+1jDkMAGsOl64Sw#9Q{M0gLxh_Nr1< z^oBjEbNprO(k(MJ9A+1(#!)zZgmhy$hVy_qX2|jB+Hwq^GjeO2eAmxoBOxp;$QE6y zS-MaGFLLyX9VvFn*CBFHJf>hX6vHBhr pUMbZB=9b}G{s2Y^l(zr? literal 0 HcmV?d00001 diff --git a/src/affinder/_tests/nuclei3D_labels.zarr/.zarray b/src/affinder/_tests/nuclei3D_labels.zarr/.zarray new file mode 100644 index 0000000..e9831c0 --- /dev/null +++ b/src/affinder/_tests/nuclei3D_labels.zarr/.zarray @@ -0,0 +1,24 @@ +{ + "chunks": [ + 60, + 256, + 256 + ], + "compressor": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "id": "blosc", + "shuffle": 1 + }, + "dtype": "6^8#?)z$Criu1M!9>*ahBmovkD2psuL?|01gv3LNKte(Z3nVLvgd!w1 zH~~dQ0ilQm8&DJ>7Dz!65*r|%_xp_#J9eBgw(-QCmuEaPo*6sCf9~xX_emx}ktb0D z#|uxLy4_t>U!VT_-2dLb6}dZezyg3AzS!n)$34JHdx3A=&s`39;8x&~hk&X5!1wn6 z4<*3UCE(9BK5G|HeGquz^T5~_fM>o09DW%1(>H-Dj{pyT1$g{xz|5n-&z}T#Jr4Zn zyTI@dfIFWCI)BW^9t3{lhfn|r`Py|$9*lhfn|r`Py|$9*zCLJD`f8_KVIA3vl|)_{`7zm7 zPB-0n8kI`e%QL`Oej%=~^$Ny#nw3>KF?WZwOym|(V#_)vc#5SBAVRW7S|+iCB3o84 z$rCMJMY@TpGOzarX`RBl<3rO8O!0EzTc^2Sy>&Yj4BJv^%_L19nrUnrj_q*V&JSfv zEwgxP7o{V8;<>Z#su#%EO+{;77AcU)nwLuFj^|dq=tS=IVkUvF6)yRGa7T)lyjVH+ zk5aZZQCjo@8M(3Ql5d3@EzLv3(YcX&wogQJxHRjiQ7%)>&CSU+Tq-z`ITVLvcb&pH zoJN%z_KVC77H4C5r7IYuaMqVasokB@GMt}BjV&7(jw94A%SFXrY{#RE5V2juIAylt zB{la6dCmkbdVycX1SPfn4j*$94+frqOZV}-B8=v^c^;$ORB!8!1gA_$n>m_tG}AC4 zRWuBT;m`zBWZGdk6af`9vg6yWRfJ6)X!KWqcCEkqPnY#lPv)|6O!rcyIEvm?dijFM zO%@k@q2t^{B`e3qN(;X9ikeC6fn}W>FS>N_Gz9{24^-{0>M@eVrZJ2svNgjliEX(#u@coNZjmoc-TpX{iz=pOmv> zE$1&}1rt285^K5Dn|V&i=UKqrg*-QhB5t0=5I0qJ>_BkZD!FEgrpC=QtaMQ{G8~V! zk{X}5NxH*NK6^UOy2B@XkRv!=%~V%+c*R4c)RwnYcQ9RqlP)Q> zwX&tUed$GbR}ikZ^0o+K!UyzlZz7KkK4W*~oTyzP(_$e@JzgvogbAAIQ!6@0C>y@bUFE>2Bxu%d>`}d2awLV~9to`tEjdpR|l3c$-TY3{94sX=FNNcR&eL8rgBEm1Ozkfkuh^>=}+}h(N&);W!j&8OWLS zhe8l(ogJ=p(GMSKh>z*@N_I zWvFWfgZx7?SN;zkDu=#=>?I7d`cHgNYLnxPC+mJdZ4u)Qh@BN26c4oCPP z_2Ihi;HYVLGegtvW*Vm5ibjUx`4-ad6JdbqnKSN+s0HQvt$59ktjF$nJ{y5QmMr^0 zB03tSWk0M$x!&?I#w(eGkCroO9q!mvLrr754T0Ex60`B z0?U1II~wBMna)H~Bu5nQ4i5N;R{KWJnesi8mu3!C6*}ZOx3k3VOr!E`ctBpKJ-rai z>pRxi?IPbZ;c&QLT00wOBI8!R0uSFK&R6GM6n7c8T1S@*TzNf@gSZDhr{&Q@RStTd zcyaG9XLAAiDc(gN6z-Bk4Ho^F7$tm=5}x-#$&34F-WTcYdPw!n&JAn%$|vPHlXO$g z^RG^+>x}wMzMBb}d^gj`^vX)_K&6o#-)kZH-rPJH1pKETeulsYSKcx&AXp+Jsd8ESlO~k255bFn*{G;lZZJ@t3-r%%bX5&NFnidNU=w>`etf%FXs^41TU-I2m*!VcMd$wawXhD?;3X_zQ0 z8X1l!T1b@NxJ6v*r=B^LWG?laQ8q2>jlyMLU301y=dH@3QFlOI_5#wj5ur zX@VCK54GWZS)R`Ny3$-s*BixjM>-eBaynPg!CJcJM+~FjWp||uh*-bUukNBiSHgKk z!6p$~T`qSG-7J_(zOGJ!uGccpqgUJx?~@dQs#h}4qn|RJ_)@yuD5bs4!7rgf_AO$L zRMLA1njQx?WELa544pOC867u;ZYF37-Ap5sBrBT(l}2{_wRIHw#DRuieQ-O!`b3gR z^tGQGyk5u*kjYY37mw4f6^(Oll)e7&bAxLVea)pFsBL-E?-gr085HU{L}HAuJMuUcMLTtQ4A+zPzRMvvDHdB0=cu(_Z!)LQnb5g$ll%7E7q$W7n|C z=Mjf7%XU`O)gb$E5c%U6+A}~ z=)ja4N*}8Ji|FTxau$z_p6-|P9CAVFXT8$LIhrqf*&o?idEfkl>3j2M*z~=bMkZ)h z%m*rs?0BJd^!<%6C-C5Qa{@oS+cL~jRpgS>OzPnWX`FK%%x?N8bXzVW3Z;H;b*ohG zq3Y*kFAMmpS6Hew3-|@(!`{y7zeLZ9+}pvu#jsb(`u6G>k?*n;>Dj;jHe4)~rE7t5 zv!>TFO|P605iE+Pi#5K8B9+zo;{9;xY&g;e4oiW~9|2N`Bd_Tl1%DO=4q|r5ST8*( z%6A;bHv58H!%k`_SkRETK=%n7`;w zS9VJYUZ~R`e}Ho!Wojcg&v1a1UluZp@Lp87nO*LV{Qa$ocr!;6@n#w(;)+Iw!#s2I z%=uksp1J;Xd_0jUwBl?yw!`sq>y+7MqnM4NQuN({*(i#L37X+h1XR$-j^|rP-#2Y0 zINuDL3Ep}O2C5Iv#}$}-E7FEzI~?YMJ9ELsHa8bseLPwvFx67;gyGl@hl#j}xD92h zPew}xrdsOlFdWE>|C!Rt`+v)VpIiw!?9#gy&s=WX22u!Th`(!w_!(of3@Ynmcc#18aqCOlSU0^Dt*cy)QaM&Fe_{$ak zqSNlUpt55J0=wgaf@tz%I1~XDG_vEdRlhfn|r`Py|%a$PO-*g*LcW znx}E*Kw}}|L)Y#tecEmKTj)7|3q84oMdaShWp`rIcbVqWUhI+`fXO zyiZd(TXB9e$6J$@`~!H)>3p_)r7S)+UR!pupjTt|MH214_Z-;_mTOh2Nq$y@Ijci3MTjeoOvFxzyPy|$9 z*lhfn|r` zPy|$9*lh IK_ff<4bjRDh5!Hn literal 0 HcmV?d00001 diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 609d9f8..b2d6f9e 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -7,6 +7,191 @@ import napari import pytest +nuclei3D_pts = np.array([[ 30. , 68.47649186, 67.08770344], + [ 30. , 85.14195298, 51.81103074], + [ 30. , 104.58499096, 43.94122966], + [ 30. , 154.58137432, 113.38065099]]) + +nuclei2D_3Dpts = np.array([[0, 68.47649186, 67.08770344], + [0, 85.14195298, 51.81103074], + [0, 104.58499096, 43.94122966], + [0, 154.58137432, 113.38065099]]) + +nuclei2D_transformed_3Dpts = np.array([[0, 154.44736842, 18.95499262], + [0, 176.10600098, 24.49557304], + [0, 195.2461879 , 35.57673389], + [0, 160.49163797, 116.67068372]]) +nuclei2D_2Dpts = nuclei2D_3Dpts[:,1:] +nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:,1:] + +# get reference and moving layer types +nuclei2D = data.cells3d()[30,1,:,:] # (256, 256) +nuclei2D_transformed = transform.rotate(nuclei2D[10:, 32:496], 60) # (246, 224) +nuclei3D = data.cells3d()[:,1,:,:] # (60, 256, 256) + +nuclei2D_labels = zarr.open( + './src/affinder/_tests/nuclei2D_labels.zarr', + mode='r')######### +nuclei2D_transformed_labels = zarr.open( + './src/affinder/_tests/nuclei2D_transformed_labels.zarr', + mode='r')######### +nuclei3D_labels = zarr.open( + './src/affinder/_tests/nuclei3D_labels.zarr', + mode='r')######### + +def make_vector_border(layer_pts): + vectors = np.zeros((layer_pts.shape[0], 2, layer_pts.shape[1])) + for n in range(layer_pts.shape[0]): + vectors[n, 0, :] = layer_pts[n, :] + vectors[n, 1, :] = layer_pts[(n+1) + % layer_pts.shape[0], :] - layer_pts[n, :] + return vectors + +def generate_all_layer_types(image, pts, labels): + layers = [ + napari.layers.Image(image), + napari.layers.Shapes(pts), + #napari.layers.Points(pts), + napari.layers.Labels(labels), + #napari.layers.Vectors(make_vector_border(pts)), + ] + + return layers + +nuc2D = generate_all_layer_types(nuclei2D, nuclei2D_2Dpts, nuclei2D_labels) +nuc2D_t = generate_all_layer_types(nuclei2D_transformed, + nuclei2D_transformed_2Dpts, + nuclei2D_transformed_labels) +nuc3D = generate_all_layer_types(nuclei3D, nuclei3D_pts, nuclei3D_labels) + +################ +################ +################ + +# 2D as reference, 2D as moving +@pytest.mark.parametrize("reference,moving", [p for p in product(nuc2D, + nuc2D_t)]) +def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): + + viewer = make_napari_viewer() + + l0 = viewer.add_layer(reference) + viewer.layers[-1].name = "layer0" + viewer.layers[-1].colormap = "green" + + l1 = viewer.add_layer(moving) + viewer.layers[-1].name = "layer1" + viewer.layers[-1].colormap = "magenta" + + my_widget_factory = start_affinder() + my_widget_factory( + viewer=viewer, + reference=l0, + moving=l1, + model=AffineTransformChoices.affine, + output=tmp_path / 'my_affine.txt' + ) + + viewer.layers['layer0_pts'].data = nuclei2D_2Dpts + viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts + + actual_affine = np.asarray(l1.affine) + expected_affine = np.array([[ 0.54048889, 0.8468468 , -30.9685414 ], + [ -0.78297398, 0.52668962, 177.6241674 ], + [ 0. , 0. , 1. ]]) + + np.testing.assert_allclose(actual_affine, expected_affine) + + +# 3D as reference, 2D as moving +@pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D, nuc2D)]) +def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): + + viewer = make_napari_viewer() + + l0 = viewer.add_layer(reference) + viewer.layers[-1].name = "layer0" + viewer.layers[-1].colormap = "green" + + l1 = viewer.add_layer(moving) + viewer.layers[-1].name = "layer1" + viewer.layers[-1].colormap = "magenta" + + my_widget_factory = start_affinder() + my_widget_factory( + viewer=viewer, + reference=l0, + moving=l1, + model=AffineTransformChoices.Euclidean, + output=tmp_path / 'my_affine.txt' + ) + + viewer.layers['layer0_pts'].data = nuclei3D_pts + viewer.layers['layer1_pts'].data = nuclei2D_3Dpts + + actual_affine = np.asarray(l1.affine) + expected_affine = np.array([ + [ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], + [ 0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], + [ 0.00000000e+00, -7.90288925e-18, 1.00000000e+00, 1.42108547e-14], + [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) + + np.testing.assert_allclose(actual_affine, expected_affine) + + + +# 2D as reference, 3D as moving +@pytest.mark.parametrize("reference,moving", [p for p in product(nuc2D, + nuc3D)]) +def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): + + viewer = make_napari_viewer() + + l0 = viewer.add_layer(reference) + viewer.layers[-1].name = "layer0" + viewer.layers[-1].colormap = "green" + + l1 = viewer.add_layer(moving) + viewer.layers[-1].name = "layer1" + viewer.layers[-1].colormap = "magenta" + + my_widget_factory = start_affinder() + my_widget_factory( + viewer=viewer, + reference=l0, + moving=l1, + model=AffineTransformChoices.Euclidean, + output=tmp_path / 'my_affine.txt' + ) + + viewer.layers['layer0_pts'].data = nuclei2D_3Dpts + viewer.layers['layer1_pts'].data = nuclei3D_pts + + actual_affine = np.asarray(l1.affine) + expected_affine = np.array([[1.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00], + [0.000000e+00, 1.000000e+00, 5.780469e-17, 4.107270e-31], + [0.000000e+00, -1.580578e-17, 1.000000e+00, 2.842171e-14], + [0.000000e+00, 0.000000e+00, 0.000000e+00, 1.000000e+00]]) + + """ + np.array([ + [ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], + [ 0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], + [ 0.00000000e+00, -7.90288925e-18, 1.00000000e+00, 1.42108547e-14], + [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) + """ + + np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) + + + + + + + + + +""" layer0_pts = np.array([[140.38371886, 322.5390704], [181.91866481, 319.65803368], [176.15659138, 259.1562627], @@ -15,8 +200,7 @@ 117.37477536], [95.80911919, 152.00358359], [143.16475439, 118.55866623], [131.32584559, 83.33791256]]) - -# get reference and moving layer types + im0 = data.camera() im1 = transform.rotate(im0[100:, 32:496], 60) labels0 = zarr.open('./src/affinder/_tests/labels0.zarr', mode='r') @@ -50,10 +234,6 @@ def make_vector_border(layer_pts): ] # TODO add tracks layer types, after multidim affine support added -# multidimensional image tests -#@pytest.mark.parametrize("reference,moving", [p for p in product(ref, mov)]) - - # 2D onto 2D tests @pytest.mark.parametrize("reference,moving", [p for p in product(ref, mov)]) def test_layer_types(make_napari_viewer, tmp_path, reference, moving): @@ -86,3 +266,10 @@ def test_layer_types(make_napari_viewer, tmp_path, reference, moving): [0., 0., 1.]]) np.testing.assert_allclose(actual_affine, expected_affine) +""" +""" +nuclei2D_3Dpts = np.array([[ 0. , 68.47649186, 67.08770344], + [ 0. , 85.14195298, 51.81103074], + [ 0. , 104.58499096, 43.94122966], + [ 0. , 154.58137432, 113.38065099]]) +""" \ No newline at end of file diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 99f4d5b..05eaa56 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -28,7 +28,6 @@ def reset_view(viewer: 'napari.Viewer', layer: 'napari.layers.Layer'): viewer.camera.center = center viewer.camera.zoom = np.min(viewer._canvas_size) / np.max(size) - @tz.curry def next_layer_callback( value, # we ignore the arguments returned with the event -- we will @@ -40,10 +39,14 @@ def next_layer_callback( moving_points_layer, model_class, output, + align_to_moving_dimensions, ): pts0, pts1 = reference_points_layer.data, moving_points_layer.data n0, n1 = len(pts0), len(pts1) - ndim = pts0.shape[1] + if align_to_moving_dimensions: + ndim = pts1.shape[0] + else: # align to reference layer dimensions + ndim = pts0.shape[1] if reference_points_layer in viewer.layers.selection: if n0 < ndim + 1: return @@ -61,12 +64,9 @@ def next_layer_callback( if n0 > ndim: mat = calculate_transform(pts0, pts1, ndim, model_class=model_class) - moving_image_layer.affine = ( - reference_image_layer.affine.affine_matrix @ mat.params - ) - moving_points_layer.affine = ( - reference_image_layer.affine.affine_matrix @ mat.params - ) + ref_mat = reference_image_layer.affine.affine_matrix + moving_image_layer.affine = (ref_mat @ mat.params) + moving_points_layer.affine = (ref_mat @ mat.params) if output is not None: np.savetxt(output, np.asarray(mat.params), delimiter=',') viewer.layers.selection.active = reference_points_layer @@ -82,6 +82,12 @@ def close_affinder(layers, callback): layer.events.data.disconnect(callback) layer.mode = 'pan_zoom' +""" +class DimensionsFrom(enum): + back = -1 + front = 0 +""" + def ndims(layer): if isinstance(layer, Image) or isinstance(layer, Labels): return layer.data.ndim @@ -98,12 +104,38 @@ def ndims(layer): raise Warning(layer, "layer type is not currently supported - cannot " "find its ndims.") +""" +def ndims(layer_data, layer_type): + if isinstance(layer, Image) or isinstance(layer, Labels): + return layer_data.ndim + elif isinstance(layer, Shapes): + # list of s shapes, containing n * D of n points with D dimensions + return layer_data[0].shape[1] + elif isinstance(layer, Points): + # (n, D) array of n points with D dimensions + return layer_data.shape[1] + elif isinstance(layer, Vectors): + # (n, 2, D) of n vectors with start pt and projections in D dimensions + return layer_data.shape[-1] + else: + raise Warning(layer, "layer type is not currently supported - cannot " + "find its ndims.") +""" +def add_zeros_at_start_of_last_axis(arr): + new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) + new_arr[:, 1:] = arr + return new_arr + +""" def add_zeros_at_end_of_last_axis(arr): new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) new_arr[:, :arr.shape[1]] = arr return new_arr +""" -def expand_dims(layer, target_ndims): +# this will take a long time for vectors and points if lots of dimensions need +# to be padded +def expand_dims(layer, target_ndims, viewer): while ndims(layer) < target_ndims: if isinstance(layer, Image) or isinstance(layer, Labels): @@ -111,24 +143,43 @@ def expand_dims(layer, target_ndims): layer.data = np.expand_dims(layer.data, axis=0) elif isinstance(layer, Shapes): # list of s shapes, containing n * D of n points with D dimensions - layer.data = [add_zeros_at_end_of_last_axis(l) for l in layer.data] + layer.data = [add_zeros_at_start_of_last_axis(l) for l in layer.data] elif isinstance(layer, Points): # (n, D) array of n points with D dimensions - layer.data = add_zeros_at_end_of_last_axis(layer.data) + print("before expand_dims ndim", layer.data.ndim) + #layer.data = add_zeros_at_start_of_last_axis(layer.data) + new_arr = add_zeros_at_start_of_last_axis(layer.data) + new_layer = napari.layers.Points(new_arr, name=layer.name, + properties=layer.properties) + viewer.layers.remove(layer.name) + viewer.add_layer(new_layer) + layer = new_layer + print("within expand_dims ndim", layer.data.ndim) + elif isinstance(layer, Vectors): # (n, 2, D) of n vectors with start pt and projections in D dimensions n, b, D = layer.data.shape new_arr = np.zeros((n, b, D+1)) - new_arr[:,0,:] = add_zeros_at_end_of_last_axis(layer.data[:,0,:]) - new_arr[:,1,:] = add_zeros_at_end_of_last_axis(layer.data[:,1,:]) - layer.data = new_arr + new_arr[:,0,:] = add_zeros_at_start_of_last_axis(layer.data[:,0,:]) + new_arr[:,1,:] = add_zeros_at_start_of_last_axis(layer.data[:,1,:]) + print("before expand_dims ndim", layer.data.ndim) + #layer.data = new_arr + new_layer = napari.layers.Vectors(new_arr, name=layer.name, + properties=layer.properties) + viewer.layers.remove(layer.name) + viewer.add_layer(new_layer) + layer = new_layer + print("within expand_dims ndim", layer.data.ndim) else: raise Warning(layer, "layer type is not currently supported - cannot " "expand its dimensions.") - return + print("layer.extent.world.shape", layer.extent.world.shape) + if isinstance(layer, Vectors) or isinstance(layer, Points): + print("after expand_dims ndim", layer.data.ndim) + return layer -def extract_ndims(layer, target_ndims): +def extract_ndims(layer, target_ndims, viewer): """ return the first target_ndims dimensions of the layer """ @@ -141,18 +192,38 @@ def extract_ndims(layer, target_ndims): layer.data = [np.take(l, 0, axis=0) for l in layer.data] elif isinstance(layer, Points): # (n, D) array of n points with D dimensions - layer.data = np.take(layer.data, 0, axis=0) + new_arr = np.take(layer.data, 0, axis=0) + # napari doesn't let you change D dimensions of points so have to + # create duplicate layer and delete the existing one... + new_layer = napari.layers.Points(new_arr, name=layer.name, + properties=layer.properties) + viewer.layers.remove(layer.name) + viewer.add_layer(new_layer) + layer = new_layer + elif isinstance(layer, Vectors): # (n, 2, D) of n vectors with start pt and projections in D dimensions n, b, D = layer.data.shape new_arr = np.zeros((n, b, D-1)) new_arr[:,0,:] = np.take(layer.data[:,0,:], 0, axis=0) new_arr[:,1,:] = np.take(layer.data[:,1,:], 0, axis=0) - layer.data = new_arr + new_layer = napari.layers.Vectors(new_arr, name=layer.name, + properties=layer.properties) + viewer.layers.remove(layer.name) + viewer.add_layer(new_layer) + layer = new_layer else: raise Warning(layer, "layer type is not currently supported - cannot " "extract its dimensions.") - return + return layer + +def expand_or_extract_ndims(layer, target_ndims, viewer): + new_layer = None + if ndims(layer) < target_ndims: + new_layer = expand_dims(layer, target_ndims, viewer) + elif ndims(layer) > target_ndims: + new_layer = extract_ndims(layer, target_ndims, viewer) + return new_layer @magic_factory( call_button='Start', @@ -168,18 +239,25 @@ def start_affinder( moving: 'napari.layers.Layer', moving_points: Optional['napari.layers.Points'] = None, model: AffineTransformChoices, - output: Optional[pathlib.Path] = None, + align_to_moving_dimensions: bool = False, + output: Optional[pathlib.Path] = None ): mode = start_affinder._call_button.text # can be "Start" or "Finish" if mode == 'Start': - # make no. dimensions the same (so skimage transforms work) - if ndims(reference) < ndims(moving): - extract_ndims(moving, ndims(reference)) - elif ndims(reference) > ndims(moving): - expand_dims(moving, ndims(reference)) - print("AFTER EQ", ndims(reference), "==?", ndims(moving)) - + + if model == AffineTransformChoices.affine: + if ndims(moving) != ndims(reference): + raise ValueError("Choose different model: Affine transform " + "cannot be used if layers have different " + "dimensions. Please choose a different model " + "type") + + if ndims(moving) != ndims(reference) and (not + align_to_moving_dimensions): + # make no. dimensions the same (so skimage transforms work) + moving = expand_or_extract_ndims(moving, ndims(reference), viewer) + # focus on the reference layer reset_view(viewer, reference) # set points layer for each image @@ -211,6 +289,7 @@ def start_affinder( moving_points_layer=pts_layer1, model_class=model.value, output=output, + align_to_moving_dimensions=align_to_moving_dimensions ) pts_layer0.events.data.connect(callback) pts_layer1.events.data.connect(callback) @@ -249,7 +328,22 @@ def calculate_transform(src, dst, ndim, model_class=AffineTransform): transform scikit-image Transformation object """ - # for 3D and higher dimensions, must specific matrix transform + # convert points to correct dimension (from right bottom corner) + # pos_val = lambda x: x if x > 0 else 0 + """ + def convert_pts_ndim(pts_arr, target_ndim): + n_pts, current_ndim = pts_arr.shape + if current_ndim < target_ndim: + new = np.zeros((n_pts, target_ndim)) + i = target_ndim - current_ndim + new[:, i:] = pts_arr + else: + i = current_ndim - target_ndim + new = pts_arr[:, i:] + return new + """ + # do transform model = model_class(dimensionality=ndim) - model.estimate(dst, src) # we want the inverse + model.estimate(dst, src) # we want + # the inverse return model From e79986567eca39d2cda862d08ab41ffe79b00a00 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sat, 19 Mar 2022 16:26:58 +1100 Subject: [PATCH 04/25] fixed 2D-3D bug in reset_view (but badly - might still cause issues for other multidimensional pairings) --- src/affinder/affinder.py | 43 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 35 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 05eaa56..8241b87 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -22,7 +22,10 @@ class AffineTransformChoices(Enum): def reset_view(viewer: 'napari.Viewer', layer: 'napari.layers.Layer'): if viewer.dims.ndisplay != 2: return - extent = layer.extent.world[:, viewer.dims.displayed] + if len(viewer.dims.displayed) == layer.extent.world.shape[1]: + extent = layer.extent.world + else: + extent = layer.extent.world[:, viewer.dims.displayed] size = extent[1] - extent[0] center = extent[0] + size/2 viewer.camera.center = center @@ -38,15 +41,11 @@ def next_layer_callback( moving_image_layer, moving_points_layer, model_class, - output, - align_to_moving_dimensions, + output ): pts0, pts1 = reference_points_layer.data, moving_points_layer.data n0, n1 = len(pts0), len(pts1) - if align_to_moving_dimensions: - ndim = pts1.shape[0] - else: # align to reference layer dimensions - ndim = pts0.shape[1] + ndim = pts0.shape[1] if reference_points_layer in viewer.layers.selection: if n0 < ndim + 1: return @@ -82,12 +81,6 @@ def close_affinder(layers, callback): layer.events.data.disconnect(callback) layer.mode = 'pan_zoom' -""" -class DimensionsFrom(enum): - back = -1 - front = 0 -""" - def ndims(layer): if isinstance(layer, Image) or isinstance(layer, Labels): return layer.data.ndim @@ -104,23 +97,6 @@ def ndims(layer): raise Warning(layer, "layer type is not currently supported - cannot " "find its ndims.") -""" -def ndims(layer_data, layer_type): - if isinstance(layer, Image) or isinstance(layer, Labels): - return layer_data.ndim - elif isinstance(layer, Shapes): - # list of s shapes, containing n * D of n points with D dimensions - return layer_data[0].shape[1] - elif isinstance(layer, Points): - # (n, D) array of n points with D dimensions - return layer_data.shape[1] - elif isinstance(layer, Vectors): - # (n, 2, D) of n vectors with start pt and projections in D dimensions - return layer_data.shape[-1] - else: - raise Warning(layer, "layer type is not currently supported - cannot " - "find its ndims.") -""" def add_zeros_at_start_of_last_axis(arr): new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) new_arr[:, 1:] = arr @@ -239,7 +215,6 @@ def start_affinder( moving: 'napari.layers.Layer', moving_points: Optional['napari.layers.Points'] = None, model: AffineTransformChoices, - align_to_moving_dimensions: bool = False, output: Optional[pathlib.Path] = None ): mode = start_affinder._call_button.text # can be "Start" or "Finish" @@ -253,8 +228,7 @@ def start_affinder( "dimensions. Please choose a different model " "type") - if ndims(moving) != ndims(reference) and (not - align_to_moving_dimensions): + if ndims(moving) != ndims(reference): # make no. dimensions the same (so skimage transforms work) moving = expand_or_extract_ndims(moving, ndims(reference), viewer) @@ -288,8 +262,7 @@ def start_affinder( moving_image_layer=moving, moving_points_layer=pts_layer1, model_class=model.value, - output=output, - align_to_moving_dimensions=align_to_moving_dimensions + output=output ) pts_layer0.events.data.connect(callback) pts_layer1.events.data.connect(callback) From 49d4accaf87cee695ac4d7cc165b4bb9e468fb9b Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Wed, 23 Mar 2022 09:00:12 +1100 Subject: [PATCH 05/25] 2D_2D, 3D_2D tests work for moving Points, Vectors layers --- src/affinder/_tests/test_affinder.py | 140 ++++++--------------------- src/affinder/affinder.py | 8 +- 2 files changed, 29 insertions(+), 119 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index b2d6f9e..5ebd26e 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -21,6 +21,7 @@ [0, 176.10600098, 24.49557304], [0, 195.2461879 , 35.57673389], [0, 160.49163797, 116.67068372]]) +nuclei3D_2Dpts = nuclei3D_pts[:,1:] nuclei2D_2Dpts = nuclei2D_3Dpts[:,1:] nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:,1:] @@ -51,9 +52,9 @@ def generate_all_layer_types(image, pts, labels): layers = [ napari.layers.Image(image), napari.layers.Shapes(pts), - #napari.layers.Points(pts), + napari.layers.Points(pts), napari.layers.Labels(labels), - #napari.layers.Vectors(make_vector_border(pts)), + napari.layers.Vectors(make_vector_border(pts)), ] return layers @@ -95,7 +96,7 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): viewer.layers['layer0_pts'].data = nuclei2D_2Dpts viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts - actual_affine = np.asarray(l1.affine) + actual_affine = np.asarray(viewer.layers['layer1'].affine) expected_affine = np.array([[ 0.54048889, 0.8468468 , -30.9685414 ], [ -0.78297398, 0.52668962, 177.6241674 ], [ 0. , 0. , 1. ]]) @@ -104,7 +105,8 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): # 3D as reference, 2D as moving -@pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D, nuc2D)]) +@pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D, + nuc2D)]) def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): viewer = make_napari_viewer() @@ -126,10 +128,16 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): output=tmp_path / 'my_affine.txt' ) + viewer.layers['layer0_pts'].data = nuclei3D_pts viewer.layers['layer1_pts'].data = nuclei2D_3Dpts - actual_affine = np.asarray(l1.affine) + actual_affine = np.asarray(viewer.layers['layer1'].affine) + # start_affinder currently makes a clone of moving layer when it's of + # type Points of Vectors and not same dimensions as reference layer - so l1 + # is a redundant layer that is no longer used as the real moving layer ( + # this is why we use viewer.layers['layer1] instead of l1 + expected_affine = np.array([ [ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], [ 0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], @@ -138,8 +146,7 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): np.testing.assert_allclose(actual_affine, expected_affine) - - +""" # 2D as reference, 3D as moving @pytest.mark.parametrize("reference,moving", [p for p in product(nuc2D, nuc3D)]) @@ -164,112 +171,21 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): output=tmp_path / 'my_affine.txt' ) - viewer.layers['layer0_pts'].data = nuclei2D_3Dpts - viewer.layers['layer1_pts'].data = nuclei3D_pts - - actual_affine = np.asarray(l1.affine) - expected_affine = np.array([[1.000000e+00, 0.000000e+00, 0.000000e+00, 0.000000e+00], - [0.000000e+00, 1.000000e+00, 5.780469e-17, 4.107270e-31], - [0.000000e+00, -1.580578e-17, 1.000000e+00, 2.842171e-14], - [0.000000e+00, 0.000000e+00, 0.000000e+00, 1.000000e+00]]) - - """ - np.array([ - [ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], - [ 0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], - [ 0.00000000e+00, -7.90288925e-18, 1.00000000e+00, 1.42108547e-14], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) - """ - - np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) - - - - - - - - - -""" -layer0_pts = np.array([[140.38371886, - 322.5390704], [181.91866481, 319.65803368], - [176.15659138, 259.1562627], - [140.14363246, 254.59462124]]) -layer1_pts = np.array([[70.94741072, - 117.37477536], [95.80911919, 152.00358359], - [143.16475439, 118.55866623], - [131.32584559, 83.33791256]]) - -im0 = data.camera() -im1 = transform.rotate(im0[100:, 32:496], 60) -labels0 = zarr.open('./src/affinder/_tests/labels0.zarr', mode='r') -labels1 = zarr.open('./src/affinder/_tests/labels1.zarr', mode='r') - - -def make_vector_border(layer_pts): - vectors = np.zeros((layer_pts.shape[0], 2, layer_pts.shape[1])) - for n in range(layer_pts.shape[0]): - vectors[n, 0, :] = layer_pts[n, :] - vectors[n, 1, :] = layer_pts[(n+1) - % layer_pts.shape[0], :] - layer_pts[n, :] - return vectors - -vectors0 = make_vector_border(layer0_pts) -vectors1 = make_vector_border(layer1_pts) - -ref = [ - napari.layers.Image(im0), - napari.layers.Shapes(layer0_pts), - napari.layers.Points(layer0_pts), - napari.layers.Labels(labels0), - napari.layers.Vectors(vectors0), - ] -mov = [ - napari.layers.Image(im1), - napari.layers.Shapes(layer1_pts), - napari.layers.Points(layer1_pts), - napari.layers.Labels(labels1), - napari.layers.Vectors(vectors1), - ] -# TODO add tracks layer types, after multidim affine support added - -# 2D onto 2D tests -@pytest.mark.parametrize("reference,moving", [p for p in product(ref, mov)]) -def test_layer_types(make_napari_viewer, tmp_path, reference, moving): - - viewer = make_napari_viewer() - - l0 = viewer.add_layer(reference) - viewer.layers[-1].name = "layer0" - viewer.layers[-1].colormap = "green" - - l1 = viewer.add_layer(moving) - viewer.layers[-1].name = "layer1" - viewer.layers[-1].colormap = "magenta" - - my_widget_factory = start_affinder() - my_widget_factory( - viewer=viewer, - reference=l0, - moving=l1, - model=AffineTransformChoices.affine, - output=tmp_path / 'my_affine.txt' - ) - - viewer.layers['layer0_pts'].data = layer0_pts - viewer.layers['layer1_pts'].data = layer1_pts + viewer.layers['layer0_pts'].data = nuclei2D_2Dpts + viewer.layers['layer1_pts'].data = nuclei3D_2Dpts - actual_affine = np.asarray(l1.affine) - expected_affine = np.array([[0.48155037, 0.85804854, 5.43577937], - [-0.88088632, 0.49188026, 328.20642821], - [0., 0., 1.]]) + actual_affine = np.asarray(viewer.layers['layer1'].affine) + # start_affinder currently makes a clone of moving layer when it's of + # type Points of Vectors and not same dimensions as reference layer - so l1 + # is a redundant layer that is no longer used as the real moving layer ( + # this is why we use viewer.layers['layer1] instead of l1 + expected_affine = np.array([[ 1.000000e+00, 2.890235e-17, 0.000000e+00], + [-7.902889e-18, 1.000000e+00, 1.421085e-14], + [ 0.000000e+00, 0.000000e+00, 1.000000e+00]]) np.testing.assert_allclose(actual_affine, expected_affine) + """ -""" -nuclei2D_3Dpts = np.array([[ 0. , 68.47649186, 67.08770344], - [ 0. , 85.14195298, 51.81103074], - [ 0. , 104.58499096, 43.94122966], - [ 0. , 154.58137432, 113.38065099]]) -""" \ No newline at end of file +# check the changed affine for each test type are what you expect (image, +# labels) manually on napari - the tests provide points data straight away, +# so doesn't test the callback diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 8241b87..894bd5b 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -122,7 +122,6 @@ def expand_dims(layer, target_ndims, viewer): layer.data = [add_zeros_at_start_of_last_axis(l) for l in layer.data] elif isinstance(layer, Points): # (n, D) array of n points with D dimensions - print("before expand_dims ndim", layer.data.ndim) #layer.data = add_zeros_at_start_of_last_axis(layer.data) new_arr = add_zeros_at_start_of_last_axis(layer.data) new_layer = napari.layers.Points(new_arr, name=layer.name, @@ -130,7 +129,6 @@ def expand_dims(layer, target_ndims, viewer): viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer - print("within expand_dims ndim", layer.data.ndim) elif isinstance(layer, Vectors): # (n, 2, D) of n vectors with start pt and projections in D dimensions @@ -138,20 +136,16 @@ def expand_dims(layer, target_ndims, viewer): new_arr = np.zeros((n, b, D+1)) new_arr[:,0,:] = add_zeros_at_start_of_last_axis(layer.data[:,0,:]) new_arr[:,1,:] = add_zeros_at_start_of_last_axis(layer.data[:,1,:]) - print("before expand_dims ndim", layer.data.ndim) #layer.data = new_arr new_layer = napari.layers.Vectors(new_arr, name=layer.name, properties=layer.properties) viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer - print("within expand_dims ndim", layer.data.ndim) + else: raise Warning(layer, "layer type is not currently supported - cannot " "expand its dimensions.") - print("layer.extent.world.shape", layer.extent.world.shape) - if isinstance(layer, Vectors) or isinstance(layer, Points): - print("after expand_dims ndim", layer.data.ndim) return layer From 9eb6a240994a7ab89b71d326c21830144d0c2f7b Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sun, 3 Apr 2022 17:29:29 +1000 Subject: [PATCH 06/25] 2D_3D tests work --- src/affinder/_tests/test_affinder.py | 82 ++++++++++++++++++++++++---- src/affinder/affinder.py | 63 +++++++++++---------- 2 files changed, 106 insertions(+), 39 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 5ebd26e..1defbf8 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -6,6 +6,23 @@ import zarr import napari import pytest +from copy import copy + +# TO DO + +#[/] 2D_3D tests + +#[] 3D_3D tests + +# [] do affinder on copy of moving layer if dims are being altered + +#[] add optional slice at parameter for 2D_3D slicing (extracting dims) +# maybe a list in case they are extracting heaps of dims (e.g. 2D_5D) +# need default checking for list to not screw up indexing - then raise error + +#[] check on napari gui that they work +# (Image, Image), (Image, Points), (Image, Vectors) + nuclei3D_pts = np.array([[ 30. , 68.47649186, 67.08770344], [ 30. , 85.14195298, 51.81103074], @@ -30,6 +47,7 @@ nuclei2D_transformed = transform.rotate(nuclei2D[10:, 32:496], 60) # (246, 224) nuclei3D = data.cells3d()[:,1,:,:] # (60, 256, 256) + nuclei2D_labels = zarr.open( './src/affinder/_tests/nuclei2D_labels.zarr', mode='r')######### @@ -103,7 +121,6 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): np.testing.assert_allclose(actual_affine, expected_affine) - # 3D as reference, 2D as moving @pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D, nuc2D)]) @@ -115,7 +132,9 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): viewer.layers[-1].name = "layer0" viewer.layers[-1].colormap = "green" - l1 = viewer.add_layer(moving) + # affinder currently changes the moving layer data when dims are different + # so need to copy + l1 = viewer.add_layer(copy(moving)) viewer.layers[-1].name = "layer1" viewer.layers[-1].colormap = "magenta" @@ -135,7 +154,7 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): actual_affine = np.asarray(viewer.layers['layer1'].affine) # start_affinder currently makes a clone of moving layer when it's of # type Points of Vectors and not same dimensions as reference layer - so l1 - # is a redundant layer that is no longer used as the real moving layer ( + # is a redundant layer that is no longer used as the real moving layer - # this is why we use viewer.layers['layer1] instead of l1 expected_affine = np.array([ @@ -146,7 +165,7 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): np.testing.assert_allclose(actual_affine, expected_affine) -""" + # 2D as reference, 3D as moving @pytest.mark.parametrize("reference,moving", [p for p in product(nuc2D, nuc3D)]) @@ -158,6 +177,51 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): viewer.layers[-1].name = "layer0" viewer.layers[-1].colormap = "green" + # affinder currently changes the moving layer data when dims are different + # so need to copy + l1 = viewer.add_layer(copy(moving)) + viewer.layers[-1].name = "layer1" + viewer.layers[-1].colormap = "magenta" + + my_widget_factory = start_affinder() + my_widget_factory( + viewer=viewer, + reference=l0, + moving=l1, + model=AffineTransformChoices.Euclidean, + output=tmp_path / 'my_affine.txt' + ) + + viewer.layers['layer0_pts'].data = nuclei2D_2Dpts + viewer.layers['layer1_pts'].data = nuclei3D_2Dpts + + + actual_affine = np.asarray(viewer.layers['layer1'].affine) + # start_affinder currently makes a clone of moving layer when it's of + # type Points of Vectors and not same dimensions as reference layer - so l1 + # is a redundant layer that is no longer used as the real moving layer - + # this is why we use viewer.layers['layer1] instead of l1 + expected_affine = np.array([[ 1.000000e+00, 2.890235e-17, 0.000000e+00], + [-7.902889e-18, 1.000000e+00, 1.421085e-14], + [ 0.000000e+00, 0.000000e+00, 1.000000e+00]]) + + np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) + + + +""" + +# 3D as reference, 3D as moving +@pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D_t, + nuc3D)]) +def test_3D_3D(make_napari_viewer, tmp_path, reference, moving): + + viewer = make_napari_viewer() + + l0 = viewer.add_layer(reference) + viewer.layers[-1].name = "layer0" + viewer.layers[-1].colormap = "green" + l1 = viewer.add_layer(moving) viewer.layers[-1].name = "layer1" viewer.layers[-1].colormap = "magenta" @@ -177,15 +241,11 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): actual_affine = np.asarray(viewer.layers['layer1'].affine) # start_affinder currently makes a clone of moving layer when it's of # type Points of Vectors and not same dimensions as reference layer - so l1 - # is a redundant layer that is no longer used as the real moving layer ( + # is a redundant layer that is no longer used as the real moving layer - # this is why we use viewer.layers['layer1] instead of l1 expected_affine = np.array([[ 1.000000e+00, 2.890235e-17, 0.000000e+00], [-7.902889e-18, 1.000000e+00, 1.421085e-14], [ 0.000000e+00, 0.000000e+00, 1.000000e+00]]) - np.testing.assert_allclose(actual_affine, expected_affine) - -""" -# check the changed affine for each test type are what you expect (image, -# labels) manually on napari - the tests provide points data straight away, -# so doesn't test the callback + np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) +""" \ No newline at end of file diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 894bd5b..1cdcb7d 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -111,7 +111,7 @@ def add_zeros_at_end_of_last_axis(arr): # this will take a long time for vectors and points if lots of dimensions need # to be padded -def expand_dims(layer, target_ndims, viewer): +def expand_dims(layer, target_ndims, viewer, extract_index=0): while ndims(layer) < target_ndims: if isinstance(layer, Image) or isinstance(layer, Labels): @@ -149,42 +149,49 @@ def expand_dims(layer, target_ndims, viewer): return layer -def extract_ndims(layer, target_ndims, viewer): +def extract_ndims(layer, target_ndims, viewer, extract_index=-1): """ - return the first target_ndims dimensions of the layer + return the last target_ndims dimensions of the layer """ - while ndims(layer) > target_ndims: - if isinstance(layer, Image) or isinstance(layer, Labels): - # extract the first value from each of the discarded dimensions - layer.data = np.take(layer.data, 0, axis=0) - elif isinstance(layer, Shapes): - # list of s shapes, containing n * D array of n points with D dimensions - layer.data = [np.take(l, 0, axis=0) for l in layer.data] - elif isinstance(layer, Points): - # (n, D) array of n points with D dimensions - new_arr = np.take(layer.data, 0, axis=0) - # napari doesn't let you change D dimensions of points so have to - # create duplicate layer and delete the existing one... - new_layer = napari.layers.Points(new_arr, name=layer.name, - properties=layer.properties) - viewer.layers.remove(layer.name) - viewer.add_layer(new_layer) - layer = new_layer + # get index of dimensions to extract from + if extract_index == -1: + extract_dims_i = list(range(ndims(layer) - target_ndims, ndims(layer))) + elif extract_index == 0: + extract_dims_i = list(0, target_ndims) - elif isinstance(layer, Vectors): - # (n, 2, D) of n vectors with start pt and projections in D dimensions + if isinstance(layer, Image) or isinstance(layer, Labels): + # extract the first value from each of the discarded dimensions + while ndims(layer) > target_ndims: + layer.data = np.take(layer.data, extract_index, axis=0) + elif isinstance(layer, Shapes): + # list of s shapes, containing n * D array of n points with D dimensions + layer.data = [np.take(p, extract_dims_i, axis=0) for s in + layer.data for p in s] + elif isinstance(layer, Points): + # (n, D) array of n points with D dimensions + new_arr = np.take(layer.data, extract_dims_i, axis=1) + # napari doesn't let you change D dimensions of points so have to + # create duplicate layer and delete the existing one... + new_layer = napari.layers.Points(new_arr, name=layer.name, + properties=layer.properties) + viewer.layers.remove(layer.name) + viewer.add_layer(new_layer) + layer = new_layer + elif isinstance(layer, Vectors): + # (n, 2, D) of n vectors with start pt and projections in D dimensions + while ndims(layer) > target_ndims: n, b, D = layer.data.shape new_arr = np.zeros((n, b, D-1)) - new_arr[:,0,:] = np.take(layer.data[:,0,:], 0, axis=0) - new_arr[:,1,:] = np.take(layer.data[:,1,:], 0, axis=0) + new_arr[:,0,:] = np.take(layer.data[:,0,:], extract_dims_i, axis=1) + new_arr[:,1,:] = np.take(layer.data[:,1,:], extract_dims_i, axis=1) new_layer = napari.layers.Vectors(new_arr, name=layer.name, - properties=layer.properties) + properties=layer.properties) viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer - else: - raise Warning(layer, "layer type is not currently supported - cannot " - "extract its dimensions.") + else: + raise Warning(layer, "layer type is not currently supported - cannot " + "extract its dimensions.") return layer def expand_or_extract_ndims(layer, target_ndims, viewer): From f17ce04c6c23d50e88f02248c2997c142d108566 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Tue, 5 Apr 2022 16:19:00 +1000 Subject: [PATCH 07/25] keep original moving image option --- src/affinder/_tests/test_affinder.py | 61 +--------------------------- src/affinder/affinder.py | 11 ++++- 2 files changed, 10 insertions(+), 62 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 1defbf8..ab4798c 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -8,22 +8,6 @@ import pytest from copy import copy -# TO DO - -#[/] 2D_3D tests - -#[] 3D_3D tests - -# [] do affinder on copy of moving layer if dims are being altered - -#[] add optional slice at parameter for 2D_3D slicing (extracting dims) -# maybe a list in case they are extracting heaps of dims (e.g. 2D_5D) -# need default checking for list to not screw up indexing - then raise error - -#[] check on napari gui that they work -# (Image, Image), (Image, Points), (Image, Vectors) - - nuclei3D_pts = np.array([[ 30. , 68.47649186, 67.08770344], [ 30. , 85.14195298, 51.81103074], [ 30. , 104.58499096, 43.94122966], @@ -38,6 +22,7 @@ [0, 176.10600098, 24.49557304], [0, 195.2461879 , 35.57673389], [0, 160.49163797, 116.67068372]]) + nuclei3D_2Dpts = nuclei3D_pts[:,1:] nuclei2D_2Dpts = nuclei2D_3Dpts[:,1:] nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:,1:] @@ -47,7 +32,6 @@ nuclei2D_transformed = transform.rotate(nuclei2D[10:, 32:496], 60) # (246, 224) nuclei3D = data.cells3d()[:,1,:,:] # (60, 256, 256) - nuclei2D_labels = zarr.open( './src/affinder/_tests/nuclei2D_labels.zarr', mode='r')######### @@ -206,46 +190,3 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): [ 0.000000e+00, 0.000000e+00, 1.000000e+00]]) np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) - - - -""" - -# 3D as reference, 3D as moving -@pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D_t, - nuc3D)]) -def test_3D_3D(make_napari_viewer, tmp_path, reference, moving): - - viewer = make_napari_viewer() - - l0 = viewer.add_layer(reference) - viewer.layers[-1].name = "layer0" - viewer.layers[-1].colormap = "green" - - l1 = viewer.add_layer(moving) - viewer.layers[-1].name = "layer1" - viewer.layers[-1].colormap = "magenta" - - my_widget_factory = start_affinder() - my_widget_factory( - viewer=viewer, - reference=l0, - moving=l1, - model=AffineTransformChoices.Euclidean, - output=tmp_path / 'my_affine.txt' - ) - - viewer.layers['layer0_pts'].data = nuclei2D_2Dpts - viewer.layers['layer1_pts'].data = nuclei3D_2Dpts - - actual_affine = np.asarray(viewer.layers['layer1'].affine) - # start_affinder currently makes a clone of moving layer when it's of - # type Points of Vectors and not same dimensions as reference layer - so l1 - # is a redundant layer that is no longer used as the real moving layer - - # this is why we use viewer.layers['layer1] instead of l1 - expected_affine = np.array([[ 1.000000e+00, 2.890235e-17, 0.000000e+00], - [-7.902889e-18, 1.000000e+00, 1.421085e-14], - [ 0.000000e+00, 0.000000e+00, 1.000000e+00]]) - - np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) -""" \ No newline at end of file diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 1cdcb7d..140e781 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -6,6 +6,7 @@ import toolz as tz from magicgui import magicgui, magic_factory import numpy as np +from copy import deepcopy from skimage.transform import ( AffineTransform, EuclideanTransform, @@ -29,7 +30,7 @@ def reset_view(viewer: 'napari.Viewer', layer: 'napari.layers.Layer'): size = extent[1] - extent[0] center = extent[0] + size/2 viewer.camera.center = center - viewer.camera.zoom = np.min(viewer._canvas_size) / np.max(size) + viewer.camera.zoom = np.min(viewer._canvas_size) / np.max(size)##deprecation @tz.curry def next_layer_callback( @@ -216,7 +217,8 @@ def start_affinder( moving: 'napari.layers.Layer', moving_points: Optional['napari.layers.Points'] = None, model: AffineTransformChoices, - output: Optional[pathlib.Path] = None + output: Optional[pathlib.Path] = None, + keep_original_moving_layer = False, ): mode = start_affinder._call_button.text # can be "Start" or "Finish" @@ -232,6 +234,11 @@ def start_affinder( if ndims(moving) != ndims(reference): # make no. dimensions the same (so skimage transforms work) moving = expand_or_extract_ndims(moving, ndims(reference), viewer) + # make copy of moving layer if selected + if keep_original_moving_layer: + og_layer = deepcopy(moving) + og_layer.name = og_layer.name+" original" + viewer.add_layer(og_layer) # focus on the reference layer reset_view(viewer, reference) From 5fbbb6cd75d31ccaac2c6a8420c3f08963a1a183 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Tue, 5 Apr 2022 16:33:07 +1000 Subject: [PATCH 08/25] yapf formatting --- src/affinder/_tests/test_affinder.py | 112 +++++++++++++++------------ src/affinder/affinder.py | 98 +++++++++++++++-------- 2 files changed, 127 insertions(+), 83 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index ab4798c..cefaba7 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -8,39 +8,42 @@ import pytest from copy import copy -nuclei3D_pts = np.array([[ 30. , 68.47649186, 67.08770344], - [ 30. , 85.14195298, 51.81103074], - [ 30. , 104.58499096, 43.94122966], - [ 30. , 154.58137432, 113.38065099]]) - -nuclei2D_3Dpts = np.array([[0, 68.47649186, 67.08770344], - [0, 85.14195298, 51.81103074], - [0, 104.58499096, 43.94122966], +nuclei3D_pts = np.array([[30., 68.47649186, 67.08770344], + [30., 85.14195298, 51.81103074], + [30., 104.58499096, 43.94122966], + [30., 154.58137432, 113.38065099]]) + +nuclei2D_3Dpts = np.array([[0, 68.47649186, 67.08770344], + [0, 85.14195298, 51.81103074], + [0, 104.58499096, 43.94122966], [0, 154.58137432, 113.38065099]]) -nuclei2D_transformed_3Dpts = np.array([[0, 154.44736842, 18.95499262], - [0, 176.10600098, 24.49557304], - [0, 195.2461879 , 35.57673389], +nuclei2D_transformed_3Dpts = np.array([[0, 154.44736842, 18.95499262], + [0, 176.10600098, 24.49557304], + [0, 195.2461879, 35.57673389], [0, 160.49163797, 116.67068372]]) -nuclei3D_2Dpts = nuclei3D_pts[:,1:] -nuclei2D_2Dpts = nuclei2D_3Dpts[:,1:] -nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:,1:] +nuclei3D_2Dpts = nuclei3D_pts[:, 1:] +nuclei2D_2Dpts = nuclei2D_3Dpts[:, 1:] +nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:, 1:] # get reference and moving layer types -nuclei2D = data.cells3d()[30,1,:,:] # (256, 256) -nuclei2D_transformed = transform.rotate(nuclei2D[10:, 32:496], 60) # (246, 224) -nuclei3D = data.cells3d()[:,1,:,:] # (60, 256, 256) +nuclei2D = data.cells3d()[30, 1, :, :] # (256, 256) +nuclei2D_transformed = transform.rotate( + nuclei2D[10:, 32:496], 60 + ) # (246, 224) +nuclei3D = data.cells3d()[:, 1, :, :] # (60, 256, 256) nuclei2D_labels = zarr.open( - './src/affinder/_tests/nuclei2D_labels.zarr', - mode='r')######### + './src/affinder/_tests/nuclei2D_labels.zarr', mode='r' + ) ######### nuclei2D_transformed_labels = zarr.open( - './src/affinder/_tests/nuclei2D_transformed_labels.zarr', - mode='r')######### + './src/affinder/_tests/nuclei2D_transformed_labels.zarr', mode='r' + ) ######### nuclei3D_labels = zarr.open( - './src/affinder/_tests/nuclei3D_labels.zarr', - mode='r')######### + './src/affinder/_tests/nuclei3D_labels.zarr', mode='r' + ) ######### + def make_vector_border(layer_pts): vectors = np.zeros((layer_pts.shape[0], 2, layer_pts.shape[1])) @@ -50,30 +53,35 @@ def make_vector_border(layer_pts): % layer_pts.shape[0], :] - layer_pts[n, :] return vectors + def generate_all_layer_types(image, pts, labels): layers = [ - napari.layers.Image(image), - napari.layers.Shapes(pts), - napari.layers.Points(pts), - napari.layers.Labels(labels), - napari.layers.Vectors(make_vector_border(pts)), - ] + napari.layers.Image(image), + napari.layers.Shapes(pts), + napari.layers.Points(pts), + napari.layers.Labels(labels), + napari.layers.Vectors(make_vector_border(pts)), + ] return layers + nuc2D = generate_all_layer_types(nuclei2D, nuclei2D_2Dpts, nuclei2D_labels) -nuc2D_t = generate_all_layer_types(nuclei2D_transformed, - nuclei2D_transformed_2Dpts, - nuclei2D_transformed_labels) +nuc2D_t = generate_all_layer_types( + nuclei2D_transformed, nuclei2D_transformed_2Dpts, + nuclei2D_transformed_labels + ) nuc3D = generate_all_layer_types(nuclei3D, nuclei3D_pts, nuclei3D_labels) ################ ################ ################ + # 2D as reference, 2D as moving -@pytest.mark.parametrize("reference,moving", [p for p in product(nuc2D, - nuc2D_t)]) +@pytest.mark.parametrize( + "reference,moving", [p for p in product(nuc2D, nuc2D_t)] + ) def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): viewer = make_napari_viewer() @@ -99,15 +107,17 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts actual_affine = np.asarray(viewer.layers['layer1'].affine) - expected_affine = np.array([[ 0.54048889, 0.8468468 , -30.9685414 ], - [ -0.78297398, 0.52668962, 177.6241674 ], - [ 0. , 0. , 1. ]]) + expected_affine = np.array([[0.54048889, 0.8468468, -30.9685414], + [-0.78297398, 0.52668962, 177.6241674], + [0., 0., 1.]]) np.testing.assert_allclose(actual_affine, expected_affine) + # 3D as reference, 2D as moving -@pytest.mark.parametrize("reference,moving", [p for p in product(nuc3D, - nuc2D)]) +@pytest.mark.parametrize( + "reference,moving", [p for p in product(nuc3D, nuc2D)] + ) def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): viewer = make_napari_viewer() @@ -131,7 +141,6 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): output=tmp_path / 'my_affine.txt' ) - viewer.layers['layer0_pts'].data = nuclei3D_pts viewer.layers['layer1_pts'].data = nuclei2D_3Dpts @@ -141,18 +150,20 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): # is a redundant layer that is no longer used as the real moving layer - # this is why we use viewer.layers['layer1] instead of l1 - expected_affine = np.array([ - [ 1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], - [ 0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], - [ 0.00000000e+00, -7.90288925e-18, 1.00000000e+00, 1.42108547e-14], - [ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]]) + expected_affine = np.array( + [[1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], + [0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], + [0.00000000e+00, -7.90288925e-18, 1.00000000e+00, 1.42108547e-14], + [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]] + ) np.testing.assert_allclose(actual_affine, expected_affine) # 2D as reference, 3D as moving -@pytest.mark.parametrize("reference,moving", [p for p in product(nuc2D, - nuc3D)]) +@pytest.mark.parametrize( + "reference,moving", [p for p in product(nuc2D, nuc3D)] + ) def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): viewer = make_napari_viewer() @@ -179,14 +190,13 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): viewer.layers['layer0_pts'].data = nuclei2D_2Dpts viewer.layers['layer1_pts'].data = nuclei3D_2Dpts - actual_affine = np.asarray(viewer.layers['layer1'].affine) # start_affinder currently makes a clone of moving layer when it's of # type Points of Vectors and not same dimensions as reference layer - so l1 # is a redundant layer that is no longer used as the real moving layer - # this is why we use viewer.layers['layer1] instead of l1 - expected_affine = np.array([[ 1.000000e+00, 2.890235e-17, 0.000000e+00], - [-7.902889e-18, 1.000000e+00, 1.421085e-14], - [ 0.000000e+00, 0.000000e+00, 1.000000e+00]]) + expected_affine = np.array([[1.000000e+00, 2.890235e-17, 0.000000e+00], + [-7.902889e-18, 1.000000e+00, 1.421085e-14], + [0.000000e+00, 0.000000e+00, 1.000000e+00]]) np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 140e781..78d83e7 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -30,7 +30,9 @@ def reset_view(viewer: 'napari.Viewer', layer: 'napari.layers.Layer'): size = extent[1] - extent[0] center = extent[0] + size/2 viewer.camera.center = center - viewer.camera.zoom = np.min(viewer._canvas_size) / np.max(size)##deprecation + viewer.camera.zoom = np.min(viewer._canvas_size + ) / np.max(size) ##deprecation + @tz.curry def next_layer_callback( @@ -62,8 +64,9 @@ def next_layer_callback( # we just added enough points: # estimate transform, go back to layer0 if n0 > ndim: - mat = calculate_transform(pts0, pts1, - ndim, model_class=model_class) + mat = calculate_transform( + pts0, pts1, ndim, model_class=model_class + ) ref_mat = reference_image_layer.affine.affine_matrix moving_image_layer.affine = (ref_mat @ mat.params) moving_points_layer.affine = (ref_mat @ mat.params) @@ -75,6 +78,7 @@ def next_layer_callback( viewer.layers.move(viewer.layers.index(reference_points_layer), -1) reset_view(viewer, reference_image_layer) + # make a bindable function to shut things down @magicgui def close_affinder(layers, callback): @@ -82,6 +86,7 @@ def close_affinder(layers, callback): layer.events.data.disconnect(callback) layer.mode = 'pan_zoom' + def ndims(layer): if isinstance(layer, Image) or isinstance(layer, Labels): return layer.data.ndim @@ -95,14 +100,18 @@ def ndims(layer): # (n, 2, D) of n vectors with start pt and projections in D dimensions return layer.data.shape[-1] else: - raise Warning(layer, "layer type is not currently supported - cannot " - "find its ndims.") + raise Warning( + layer, "layer type is not currently supported - cannot " + "find its ndims." + ) + def add_zeros_at_start_of_last_axis(arr): new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) new_arr[:, 1:] = arr return new_arr + """ def add_zeros_at_end_of_last_axis(arr): new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) @@ -110,6 +119,7 @@ def add_zeros_at_end_of_last_axis(arr): return new_arr """ + # this will take a long time for vectors and points if lots of dimensions need # to be padded def expand_dims(layer, target_ndims, viewer, extract_index=0): @@ -120,13 +130,16 @@ def expand_dims(layer, target_ndims, viewer, extract_index=0): layer.data = np.expand_dims(layer.data, axis=0) elif isinstance(layer, Shapes): # list of s shapes, containing n * D of n points with D dimensions - layer.data = [add_zeros_at_start_of_last_axis(l) for l in layer.data] + layer.data = [ + add_zeros_at_start_of_last_axis(l) for l in layer.data + ] elif isinstance(layer, Points): # (n, D) array of n points with D dimensions #layer.data = add_zeros_at_start_of_last_axis(layer.data) new_arr = add_zeros_at_start_of_last_axis(layer.data) - new_layer = napari.layers.Points(new_arr, name=layer.name, - properties=layer.properties) + new_layer = napari.layers.Points( + new_arr, name=layer.name, properties=layer.properties + ) viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer @@ -134,19 +147,26 @@ def expand_dims(layer, target_ndims, viewer, extract_index=0): elif isinstance(layer, Vectors): # (n, 2, D) of n vectors with start pt and projections in D dimensions n, b, D = layer.data.shape - new_arr = np.zeros((n, b, D+1)) - new_arr[:,0,:] = add_zeros_at_start_of_last_axis(layer.data[:,0,:]) - new_arr[:,1,:] = add_zeros_at_start_of_last_axis(layer.data[:,1,:]) + new_arr = np.zeros((n, b, D + 1)) + new_arr[:, 0, :] = add_zeros_at_start_of_last_axis( + layer.data[:, 0, :] + ) + new_arr[:, 1, :] = add_zeros_at_start_of_last_axis( + layer.data[:, 1, :] + ) #layer.data = new_arr - new_layer = napari.layers.Vectors(new_arr, name=layer.name, - properties=layer.properties) + new_layer = napari.layers.Vectors( + new_arr, name=layer.name, properties=layer.properties + ) viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer else: - raise Warning(layer, "layer type is not currently supported - cannot " - "expand its dimensions.") + raise Warning( + layer, "layer type is not currently supported - cannot " + "expand its dimensions." + ) return layer @@ -166,15 +186,18 @@ def extract_ndims(layer, target_ndims, viewer, extract_index=-1): layer.data = np.take(layer.data, extract_index, axis=0) elif isinstance(layer, Shapes): # list of s shapes, containing n * D array of n points with D dimensions - layer.data = [np.take(p, extract_dims_i, axis=0) for s in - layer.data for p in s] + layer.data = [ + np.take(p, extract_dims_i, + axis=0) for s in layer.data for p in s + ] elif isinstance(layer, Points): # (n, D) array of n points with D dimensions new_arr = np.take(layer.data, extract_dims_i, axis=1) # napari doesn't let you change D dimensions of points so have to # create duplicate layer and delete the existing one... - new_layer = napari.layers.Points(new_arr, name=layer.name, - properties=layer.properties) + new_layer = napari.layers.Points( + new_arr, name=layer.name, properties=layer.properties + ) viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer @@ -182,19 +205,27 @@ def extract_ndims(layer, target_ndims, viewer, extract_index=-1): # (n, 2, D) of n vectors with start pt and projections in D dimensions while ndims(layer) > target_ndims: n, b, D = layer.data.shape - new_arr = np.zeros((n, b, D-1)) - new_arr[:,0,:] = np.take(layer.data[:,0,:], extract_dims_i, axis=1) - new_arr[:,1,:] = np.take(layer.data[:,1,:], extract_dims_i, axis=1) - new_layer = napari.layers.Vectors(new_arr, name=layer.name, - properties=layer.properties) + new_arr = np.zeros((n, b, D - 1)) + new_arr[:, 0, :] = np.take( + layer.data[:, 0, :], extract_dims_i, axis=1 + ) + new_arr[:, 1, :] = np.take( + layer.data[:, 1, :], extract_dims_i, axis=1 + ) + new_layer = napari.layers.Vectors( + new_arr, name=layer.name, properties=layer.properties + ) viewer.layers.remove(layer.name) viewer.add_layer(new_layer) layer = new_layer else: - raise Warning(layer, "layer type is not currently supported - cannot " - "extract its dimensions.") + raise Warning( + layer, "layer type is not currently supported - cannot " + "extract its dimensions." + ) return layer + def expand_or_extract_ndims(layer, target_ndims, viewer): new_layer = None if ndims(layer) < target_ndims: @@ -203,6 +234,7 @@ def expand_or_extract_ndims(layer, target_ndims, viewer): new_layer = extract_ndims(layer, target_ndims, viewer) return new_layer + @magic_factory( call_button='Start', layout='vertical', @@ -218,7 +250,7 @@ def start_affinder( moving_points: Optional['napari.layers.Points'] = None, model: AffineTransformChoices, output: Optional[pathlib.Path] = None, - keep_original_moving_layer = False, + keep_original_moving_layer=False, ): mode = start_affinder._call_button.text # can be "Start" or "Finish" @@ -226,10 +258,12 @@ def start_affinder( if model == AffineTransformChoices.affine: if ndims(moving) != ndims(reference): - raise ValueError("Choose different model: Affine transform " - "cannot be used if layers have different " - "dimensions. Please choose a different model " - "type") + raise ValueError( + "Choose different model: Affine transform " + "cannot be used if layers have different " + "dimensions. Please choose a different model " + "type" + ) if ndims(moving) != ndims(reference): # make no. dimensions the same (so skimage transforms work) @@ -237,7 +271,7 @@ def start_affinder( # make copy of moving layer if selected if keep_original_moving_layer: og_layer = deepcopy(moving) - og_layer.name = og_layer.name+" original" + og_layer.name = og_layer.name + " original" viewer.add_layer(og_layer) # focus on the reference layer From dd15f8a04b38b396d42a1cf983d4439ab8b400a0 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:22:41 +1000 Subject: [PATCH 09/25] changed test assert sensitivity --- src/affinder/_tests/test_affinder.py | 9 ++++++--- src/affinder/affinder.py | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index cefaba7..8ace456 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -111,7 +111,8 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): [-0.78297398, 0.52668962, 177.6241674], [0., 0., 1.]]) - np.testing.assert_allclose(actual_affine, expected_affine) + np.testing.assert_allclose(actual_affine, expected_affine, rtol=10, + atol=1e-10) # 3D as reference, 2D as moving @@ -157,7 +158,8 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]] ) - np.testing.assert_allclose(actual_affine, expected_affine) + np.testing.assert_allclose(actual_affine, expected_affine, rtol=10, + atol=1e-10) # 2D as reference, 3D as moving @@ -199,4 +201,5 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): [-7.902889e-18, 1.000000e+00, 1.421085e-14], [0.000000e+00, 0.000000e+00, 1.000000e+00]]) - np.testing.assert_allclose(actual_affine, expected_affine, rtol=1e-06) + np.testing.assert_allclose(actual_affine, expected_affine, rtol=10, + atol=1e-10) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 78d83e7..9b4f1fb 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -119,6 +119,9 @@ def add_zeros_at_end_of_last_axis(arr): return new_arr """ +def add_zeros_at_axis(arr, axis): + ... + # this will take a long time for vectors and points if lots of dimensions need # to be padded From 32a45c27b59a22caba02fd87bf871257fecce0f4 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:37:27 +1000 Subject: [PATCH 10/25] don't assume 2D layer in add_zeros_at_start_of_last_axis --- src/affinder/affinder.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 9b4f1fb..64f0fca 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -105,23 +105,24 @@ def ndims(layer): "find its ndims." ) - def add_zeros_at_start_of_last_axis(arr): - new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) - new_arr[:, 1:] = arr + upsize_last_axis = lambda size: size[:-1]+(size[-1]+1,) + new_arr = np.zeros(upsize_last_axis(arr.shape)) + new_arr[..., 1:] = arr return new_arr +""" +#maybe add option for user to specific which axis to pad (and whether to +#pad from front or back or somewhere in the middle) -""" def add_zeros_at_end_of_last_axis(arr): new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) new_arr[:, :arr.shape[1]] = arr return new_arr -""" - + def add_zeros_at_axis(arr, axis): ... - +""" # this will take a long time for vectors and points if lots of dimensions need # to be padded From 8343a21644ec2986a749eaa10aa562bc215604fd Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Wed, 20 Apr 2022 21:44:45 +1000 Subject: [PATCH 11/25] cleaning comments, formatting --- src/affinder/_tests/test_affinder.py | 15 +++++++++------ src/affinder/affinder.py | 18 +++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 8ace456..45d5845 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -111,8 +111,9 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): [-0.78297398, 0.52668962, 177.6241674], [0., 0., 1.]]) - np.testing.assert_allclose(actual_affine, expected_affine, rtol=10, - atol=1e-10) + np.testing.assert_allclose( + actual_affine, expected_affine, rtol=10, atol=1e-10 + ) # 3D as reference, 2D as moving @@ -158,8 +159,9 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]] ) - np.testing.assert_allclose(actual_affine, expected_affine, rtol=10, - atol=1e-10) + np.testing.assert_allclose( + actual_affine, expected_affine, rtol=10, atol=1e-10 + ) # 2D as reference, 3D as moving @@ -201,5 +203,6 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): [-7.902889e-18, 1.000000e+00, 1.421085e-14], [0.000000e+00, 0.000000e+00, 1.000000e+00]]) - np.testing.assert_allclose(actual_affine, expected_affine, rtol=10, - atol=1e-10) + np.testing.assert_allclose( + actual_affine, expected_affine, rtol=10, atol=1e-10 + ) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 64f0fca..eebfa73 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -105,12 +105,14 @@ def ndims(layer): "find its ndims." ) + def add_zeros_at_start_of_last_axis(arr): - upsize_last_axis = lambda size: size[:-1]+(size[-1]+1,) + upsize_last_axis = lambda size: size[:-1] + (size[-1] + 1,) new_arr = np.zeros(upsize_last_axis(arr.shape)) new_arr[..., 1:] = arr return new_arr + """ #maybe add option for user to specific which axis to pad (and whether to #pad from front or back or somewhere in the middle) @@ -124,6 +126,7 @@ def add_zeros_at_axis(arr, axis): ... """ + # this will take a long time for vectors and points if lots of dimensions need # to be padded def expand_dims(layer, target_ndims, viewer, extract_index=0): @@ -349,18 +352,7 @@ def calculate_transform(src, dst, ndim, model_class=AffineTransform): """ # convert points to correct dimension (from right bottom corner) # pos_val = lambda x: x if x > 0 else 0 - """ - def convert_pts_ndim(pts_arr, target_ndim): - n_pts, current_ndim = pts_arr.shape - if current_ndim < target_ndim: - new = np.zeros((n_pts, target_ndim)) - i = target_ndim - current_ndim - new[:, i:] = pts_arr - else: - i = current_ndim - target_ndim - new = pts_arr[:, i:] - return new - """ + # do transform model = model_class(dimensionality=ndim) model.estimate(dst, src) # we want From be5a2fc5624ba5e5652d30f0978c4c0afda25779 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Wed, 20 Apr 2022 22:16:08 +1000 Subject: [PATCH 12/25] minor edits to affine model error message --- src/affinder/affinder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index eebfa73..9c2e7ce 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -264,12 +264,12 @@ def start_affinder( if mode == 'Start': if model == AffineTransformChoices.affine: - if ndims(moving) != ndims(reference): + if (ndims(moving) != 2) or (ndims(reference) != 2): raise ValueError( "Choose different model: Affine transform " - "cannot be used if layers have different " - "dimensions. Please choose a different model " - "type" + "cannot be used if layers are not both 2D. " + "Please choose a different model " + "type (not \"affine\")" ) if ndims(moving) != ndims(reference): From ac8ff81e02731d62a68d84b62a41fcb202bfc2d9 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Thu, 27 Jul 2023 11:15:12 +1000 Subject: [PATCH 13/25] added required libraries --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c8dc646..e2c6e80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,5 @@ napari>=0.4.3 numpy scikit-image>=0.19.2 magicgui>=0.2.5,!=0.2.7 -napari toolz zarr From d4565c219d9639bf18d8ae94a7d09ed202f7774b Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:25:14 +1100 Subject: [PATCH 14/25] preserve dimensions of moving layer when transforming it onto lower dimension reference layer --- src/affinder/affinder.py | 130 +++++++++++++-------------------------- 1 file changed, 43 insertions(+), 87 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index d3397d8..765c62e 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -14,7 +14,6 @@ SimilarityTransform, ) - class AffineTransformChoices(Enum): affine = AffineTransform Euclidean = EuclideanTransform @@ -71,8 +70,13 @@ def next_layer_callback( pts0, pts1, ndim, model_class=model_class ) ref_mat = reference_image_layer.affine.affine_matrix - moving_image_layer.affine = (ref_mat @ mat.params) moving_points_layer.affine = (ref_mat @ mat.params) + # must pad affine matrix with identity matrix if dims of moving layer different from reference##### + moving_image_layer.affine.affine_matrix = convert_affine_matrix_to_ndims( + moving_points_layer.affine.affine_matrix, ndims(moving_image_layer)) + #TODO currently have to move viewer to get this new affine transform to display when adding additional point pairs after the first set + + if output is not None: np.savetxt(output, np.asarray(mat.params), delimiter=',') viewer.layers.selection.active = reference_points_layer @@ -99,7 +103,7 @@ def ndims(layer): return layer.data[0].shape[1] elif isinstance(layer, Points): # (n, D) array of n points with D dimensions - return layer.data.shape[1] + return layer.data.shape[-1] elif isinstance(layer, Vectors): # (n, 2, D) of n vectors with start pt and projections in D dimensions return layer.data.shape[-1] @@ -109,7 +113,6 @@ def ndims(layer): "find its ndims." ) - def add_zeros_at_start_of_last_axis(arr): upsize_last_axis = lambda size: size[:-1] + (size[-1] + 1,) new_arr = np.zeros(upsize_last_axis(arr.shape)) @@ -117,24 +120,38 @@ def add_zeros_at_start_of_last_axis(arr): return new_arr -""" -#maybe add option for user to specific which axis to pad (and whether to -#pad from front or back or somewhere in the middle) - -def add_zeros_at_end_of_last_axis(arr): - new_arr = np.zeros((arr.shape[0], arr.shape[1] + 1)) - new_arr[:, :arr.shape[1]] = arr - return new_arr - -def add_zeros_at_axis(arr, axis): - ... -""" - +def convert_affine_to_ndims(affine, target_ndims): + if affine.ndim == target_ndims: + return affine + new_affine = deepcopy(affine) + if affine.ndim < target_ndims: + converted_matrix = np.identity(target_ndims+1) + start_i = target_ndims - affine.ndim + converted_matrix[start_i:, start_i:] = affine.affine_matrix + new_affine.affine_matrix = converted_matrix + elif affine.ndim > target_ndims: + new_affine.affine_matrix = affine.affine_matrix[affine.ndim-target_ndims:, affine.ndim-target_ndims:] + + return new_affine + +def convert_affine_matrix_to_ndims(matrix, target_ndims): + affine_ndim = matrix.shape[0]-1 + if affine_ndim < target_ndims: + converted_matrix = np.identity(target_ndims+1) + start_i = target_ndims - affine_ndim + converted_matrix[start_i:, start_i:] = matrix + return converted_matrix + elif affine_ndim > target_ndims: + return matrix[affine_ndim-target_ndims:, affine_ndim-target_ndims:] + else: + return matrix # this will take a long time for vectors and points if lots of dimensions need # to be padded def expand_dims(layer, target_ndims, viewer, extract_index=0): - + """ + will add empty dimensions to layer until its dimensions are target_ndims + """ while ndims(layer) < target_ndims: if isinstance(layer, Image) or isinstance(layer, Labels): # add dimension to beginning of dimension list @@ -181,70 +198,6 @@ def expand_dims(layer, target_ndims, viewer, extract_index=0): return layer -def extract_ndims(layer, target_ndims, viewer, extract_index=-1): - """ - return the last target_ndims dimensions of the layer - """ - # get index of dimensions to extract from - if extract_index == -1: - extract_dims_i = list(range(ndims(layer) - target_ndims, ndims(layer))) - elif extract_index == 0: - extract_dims_i = list(0, target_ndims) - - if isinstance(layer, Image) or isinstance(layer, Labels): - # extract the first value from each of the discarded dimensions - while ndims(layer) > target_ndims: - layer.data = np.take(layer.data, extract_index, axis=0) - elif isinstance(layer, Shapes): - # list of s shapes, containing n * D array of n points with D dimensions - layer.data = [ - np.take(p, extract_dims_i, - axis=0) for s in layer.data for p in s - ] - elif isinstance(layer, Points): - # (n, D) array of n points with D dimensions - new_arr = np.take(layer.data, extract_dims_i, axis=1) - # napari doesn't let you change D dimensions of points so have to - # create duplicate layer and delete the existing one... - new_layer = napari.layers.Points( - new_arr, name=layer.name, properties=layer.properties - ) - viewer.layers.remove(layer.name) - viewer.add_layer(new_layer) - layer = new_layer - elif isinstance(layer, Vectors): - # (n, 2, D) of n vectors with start pt and projections in D dimensions - while ndims(layer) > target_ndims: - n, b, D = layer.data.shape - new_arr = np.zeros((n, b, D - 1)) - new_arr[:, 0, :] = np.take( - layer.data[:, 0, :], extract_dims_i, axis=1 - ) - new_arr[:, 1, :] = np.take( - layer.data[:, 1, :], extract_dims_i, axis=1 - ) - new_layer = napari.layers.Vectors( - new_arr, name=layer.name, properties=layer.properties - ) - viewer.layers.remove(layer.name) - viewer.add_layer(new_layer) - layer = new_layer - else: - raise Warning( - layer, "layer type is not currently supported - cannot " - "extract its dimensions." - ) - return layer - - -def expand_or_extract_ndims(layer, target_ndims, viewer): - new_layer = None - if ndims(layer) < target_ndims: - new_layer = expand_dims(layer, target_ndims, viewer) - elif ndims(layer) > target_ndims: - new_layer = extract_ndims(layer, target_ndims, viewer) - return new_layer - def _update_unique_choices(widget, choice_name): """Update the selected choice in a ComboBox widget to be unique. @@ -310,14 +263,18 @@ def start_affinder( ) if ndims(moving) != ndims(reference): - # make no. dimensions the same (so skimage transforms work) - moving = expand_or_extract_ndims(moving, ndims(reference), viewer) # make copy of moving layer if selected if keep_original_moving_layer: + print("keep og moving layer selected") og_layer = deepcopy(moving) og_layer.name = og_layer.name + " original" viewer.add_layer(og_layer) + # pad dimensions of moving image if it's less than reference + #moving = expand_or_extract_ndims(moving, ndims(reference), viewer) # do not destructively change layers + if ndims(moving) < ndims(reference): + moving = expand_dims(moving, target_ndims=ndims(reference), viewer=viewer) + # focus on the reference layer reset_view(viewer, reference) # set points layer for each image @@ -331,15 +288,14 @@ def start_affinder( if points_layers[i] is None: layer, color = points_layers_to_add[i] new_layer = viewer.add_points( - ndim=layer.ndim, + ndim=reference.ndim, # ndims of all points layers same as reference (so skimage transforms work) name=layer.name + '_pts', - affine=layer.affine, + affine=convert_affine_to_ndims(layer.affine, ndims(reference)), face_color=[color], ) points_layers[i] = new_layer pts_layer0 = points_layers[0] pts_layer1 = points_layers[1] - # make a callback for points added callback = next_layer_callback( viewer=viewer, From 9f1b5cc0b19e94291c6476e74074ff18d4d19a1b Mon Sep 17 00:00:00 2001 From: Thanushi Peiris <54516770+thanushipeiris@users.noreply.github.com> Date: Sun, 26 Nov 2023 16:50:23 +1100 Subject: [PATCH 15/25] updated 3D affine test --- src/affinder/_tests/test_affinder.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index bc1e390..0eb0c82 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -200,9 +200,10 @@ def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): # type Points of Vectors and not same dimensions as reference layer - so l1 # is a redundant layer that is no longer used as the real moving layer - # this is why we use viewer.layers['layer1] instead of l1 - expected_affine = np.array([[1.000000e+00, 2.890235e-17, 0.000000e+00], - [-7.902889e-18, 1.000000e+00, 1.421085e-14], - [0.000000e+00, 0.000000e+00, 1.000000e+00]]) + expected_affine = np.array([[1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], + [0.00000000e+00, 1.000000e+00, 2.890235e-17, 0.000000e+00], + [0.00000000e+00, -7.902889e-18, 1.000000e+00, 1.421085e-14], + [0.00000000e+00, 0.000000e+00, 0.000000e+00, 1.000000e+00]]) np.testing.assert_allclose( actual_affine, expected_affine, rtol=10, atol=1e-10 From 308c0058910a3620c89e2728add2ca53ea2532b7 Mon Sep 17 00:00:00 2001 From: Thanushi Peiris Date: Mon, 11 Dec 2023 16:38:48 +1100 Subject: [PATCH 16/25] affine matrix dimensions will forcefully match minimum dimensions of either ref or moving layer --- src/affinder/_tests/test_affinder.py | 11 ++++----- src/affinder/affinder.py | 35 ++++++++++++++-------------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 0eb0c82..8a3a79f 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -144,8 +144,8 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): output=tmp_path / 'my_affine.txt' ) - viewer.layers['layer0_pts'].data = nuclei3D_pts - viewer.layers['layer1_pts'].data = nuclei2D_3Dpts + viewer.layers['layer0_pts'].data = nuclei3D_2Dpts + viewer.layers['layer1_pts'].data = nuclei2D_2Dpts actual_affine = np.asarray(viewer.layers['layer1'].affine) # start_affinder currently makes a clone of moving layer when it's of @@ -154,10 +154,9 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): # this is why we use viewer.layers['layer1] instead of l1 expected_affine = np.array( - [[1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 3.00000000e+01], - [0.00000000e+00, 1.00000000e+00, 2.89023467e-17, 0.00000000e+00], - [0.00000000e+00, -7.90288925e-18, 1.00000000e+00, 1.42108547e-14], - [0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 1.00000000e+00]] + [[0.00000000e+00, 0.00000000e+00, 3.00000000e+01], + [1.00000000e+00, 2.89023467e-17, 0.00000000e+00], + [-7.90288925e-18, 1.00000000e+00, 1.42108547e-14]] ) np.testing.assert_allclose( diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 765c62e..0c2b6e6 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -70,13 +70,13 @@ def next_layer_callback( pts0, pts1, ndim, model_class=model_class ) ref_mat = reference_image_layer.affine.affine_matrix + # must shrink ndims of affine matrix if dims of image layer is bigger than moving layer ##### + if reference_image_layer.ndim > moving_image_layer.ndim: + ref_mat = convert_affine_matrix_to_ndims(ref_mat, moving_image_layer.ndim) moving_points_layer.affine = (ref_mat @ mat.params) - # must pad affine matrix with identity matrix if dims of moving layer different from reference##### - moving_image_layer.affine.affine_matrix = convert_affine_matrix_to_ndims( + # must pad affine matrix with identity matrix if dims of moving layer smaller ##### + moving_image_layer.affine = convert_affine_matrix_to_ndims( moving_points_layer.affine.affine_matrix, ndims(moving_image_layer)) - #TODO currently have to move viewer to get this new affine transform to display when adding additional point pairs after the first set - - if output is not None: np.savetxt(output, np.asarray(mat.params), delimiter=',') viewer.layers.selection.active = reference_points_layer @@ -253,14 +253,14 @@ def start_affinder( if mode == 'Start': - if model == AffineTransformChoices.affine: - if (ndims(moving) != 2) or (ndims(reference) != 2): - raise ValueError( - "Choose different model: Affine transform " - "cannot be used if layers are not both 2D. " - "Please choose a different model " - "type (not \"affine\")" - ) + #if model == AffineTransformChoices.affine: + # if (ndims(moving) != 2) or (ndims(reference) != 2): + # raise ValueError( + # "Choose different model: Affine transform " + # "cannot be used if layers are not both 2D. " + # "Please choose a different model " + # "type (not \"affine\")" + # ) if ndims(moving) != ndims(reference): # make copy of moving layer if selected @@ -272,8 +272,8 @@ def start_affinder( # pad dimensions of moving image if it's less than reference #moving = expand_or_extract_ndims(moving, ndims(reference), viewer) # do not destructively change layers - if ndims(moving) < ndims(reference): - moving = expand_dims(moving, target_ndims=ndims(reference), viewer=viewer) + #if ndims(moving) < ndims(reference): + # moving = expand_dims(moving, target_ndims=ndims(reference), viewer=viewer) # focus on the reference layer reset_view(viewer, reference) @@ -284,13 +284,14 @@ def start_affinder( (moving, (1.0, 0.498, 0.055, 1.0))] # make points layer if it was not specified + estimation_ndim = min(reference.ndim, moving.ndim) for i in range(len(points_layers)): if points_layers[i] is None: layer, color = points_layers_to_add[i] new_layer = viewer.add_points( - ndim=reference.ndim, # ndims of all points layers same as reference (so skimage transforms work) + ndim=estimation_ndim, # ndims of all points layers same lowest ndim of reference or moving name=layer.name + '_pts', - affine=convert_affine_to_ndims(layer.affine, ndims(reference)), + affine=convert_affine_to_ndims(layer.affine, estimation_ndim), face_color=[color], ) points_layers[i] = new_layer From cd85f711fa97091bb83221abe622ae5127de615f Mon Sep 17 00:00:00 2001 From: Thanushi Peiris Date: Tue, 12 Dec 2023 13:08:03 +1100 Subject: [PATCH 17/25] modified tests to include all affine transform types --- src/affinder/_tests/test_affinder.py | 92 +++++------------------ src/affinder/affinder.py | 105 +-------------------------- 2 files changed, 20 insertions(+), 177 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 7ca0750..9626cbd 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -81,16 +81,12 @@ def generate_all_layer_types(image, pts, labels): ) nuc3D = generate_all_layer_types(nuclei3D, nuclei3D_pts, nuclei3D_labels) -################ -################ -################ - # 2D as reference, 2D as moving @pytest.mark.parametrize( - "reference,moving", [p for p in product(nuc2D, nuc2D_t)] + "reference,moving,model_class", [p for p in product(nuc2D, nuc2D_t, [t for t in AffineTransformChoices])] ) -def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): +def test_2D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): viewer = make_napari_viewer() @@ -107,17 +103,18 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): viewer=viewer, reference=l0, moving=l1, - model=AffineTransformChoices.affine, + model=model_class, output=tmp_path / 'my_affine.txt' ) viewer.layers['layer0_pts'].data = nuclei2D_2Dpts viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts - actual_affine = np.asarray(viewer.layers['layer1'].affine) - expected_affine = np.array([[0.54048889, 0.8468468, -30.9685414], - [-0.78297398, 0.52668962, 177.6241674], - [0., 0., 1.]]) + actual_affine = np.asarray(l1.affine) + + model = model_class.value(dimensionality=2) + model.estimate(viewer.layers['layer1_pts'].data, viewer.layers['layer0_pts'].data) + expected_affine = model.params np.testing.assert_allclose( actual_affine, expected_affine, rtol=10, atol=1e-10 @@ -126,9 +123,9 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving): # 3D as reference, 2D as moving @pytest.mark.parametrize( - "reference,moving", [p for p in product(nuc3D, nuc2D)] + "reference,moving,model_class", [p for p in product(nuc3D, nuc2D_t, [t for t in AffineTransformChoices])] ) -def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): +def test_3D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): viewer = make_napari_viewer() @@ -138,7 +135,7 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): # affinder currently changes the moving layer data when dims are different # so need to copy - l1 = viewer.add_layer(copy(moving)) + l1 = viewer.add_layer(moving) viewer.layers[-1].name = "layer1" viewer.layers[-1].colormap = "magenta" @@ -147,71 +144,18 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving): viewer=viewer, reference=l0, moving=l1, - model=AffineTransformChoices.Euclidean, + model=model_class, output=tmp_path / 'my_affine.txt' ) viewer.layers['layer0_pts'].data = nuclei3D_2Dpts - viewer.layers['layer1_pts'].data = nuclei2D_2Dpts - - actual_affine = np.asarray(viewer.layers['layer1'].affine) - # start_affinder currently makes a clone of moving layer when it's of - # type Points of Vectors and not same dimensions as reference layer - so l1 - # is a redundant layer that is no longer used as the real moving layer - - # this is why we use viewer.layers['layer1] instead of l1 - - expected_affine = np.array( - [[0.00000000e+00, 0.00000000e+00, 3.00000000e+01], - [1.00000000e+00, 2.89023467e-17, 0.00000000e+00], - [-7.90288925e-18, 1.00000000e+00, 1.42108547e-14]] - ) - - np.testing.assert_allclose( - actual_affine, expected_affine, rtol=10, atol=1e-10 - ) - - -# 2D as reference, 3D as moving -@pytest.mark.parametrize( - "reference,moving", [p for p in product(nuc2D, nuc3D)] - ) -def test_2D_3D(make_napari_viewer, tmp_path, reference, moving): - - viewer = make_napari_viewer() - - l0 = viewer.add_layer(reference) - viewer.layers[-1].name = "layer0" - viewer.layers[-1].colormap = "green" - - # affinder currently changes the moving layer data when dims are different - # so need to copy - l1 = viewer.add_layer(copy(moving)) - viewer.layers[-1].name = "layer1" - viewer.layers[-1].colormap = "magenta" + viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts - my_widget_factory = start_affinder() - my_widget_factory( - viewer=viewer, - reference=l0, - moving=l1, - model=AffineTransformChoices.Euclidean, - output=tmp_path / 'my_affine.txt' - ) + actual_affine = np.asarray(l1.affine) - viewer.layers['layer0_pts'].data = nuclei2D_2Dpts - viewer.layers['layer1_pts'].data = nuclei3D_2Dpts - - actual_affine = np.asarray(viewer.layers['layer1'].affine) - # start_affinder currently makes a clone of moving layer when it's of - # type Points of Vectors and not same dimensions as reference layer - so l1 - # is a redundant layer that is no longer used as the real moving layer - - # this is why we use viewer.layers['layer1] instead of l1 - expected_affine = np.array( - [[1.00000000e+00, 0.00000000e+00, 0.00000000e+00, 0.00000000e+00], - [0.00000000e+00, 1.000000e+00, 2.890235e-17, 0.000000e+00], - [0.00000000e+00, -7.902889e-18, 1.000000e+00, 1.421085e-14], - [0.00000000e+00, 0.000000e+00, 0.000000e+00, 1.000000e+00]] - ) + model = model_class.value(dimensionality=2) + model.estimate(viewer.layers['layer1_pts'].data, viewer.layers['layer0_pts'].data) + expected_affine = model.params np.testing.assert_allclose( actual_affine, expected_affine, rtol=10, atol=1e-10 @@ -294,4 +238,4 @@ def test_load_affine(tmp_path): widget = load_affine() widget(layer, affile) - np.testing.assert_allclose(layer.affine, affine) + np.testing.assert_allclose(layer.affine, affine) \ No newline at end of file diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 0c2b6e6..071a984 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -73,10 +73,9 @@ def next_layer_callback( # must shrink ndims of affine matrix if dims of image layer is bigger than moving layer ##### if reference_image_layer.ndim > moving_image_layer.ndim: ref_mat = convert_affine_matrix_to_ndims(ref_mat, moving_image_layer.ndim) - moving_points_layer.affine = (ref_mat @ mat.params) # must pad affine matrix with identity matrix if dims of moving layer smaller ##### moving_image_layer.affine = convert_affine_matrix_to_ndims( - moving_points_layer.affine.affine_matrix, ndims(moving_image_layer)) + (ref_mat @ mat.params), moving_image_layer.ndim) if output is not None: np.savetxt(output, np.asarray(mat.params), delimiter=',') viewer.layers.selection.active = reference_points_layer @@ -94,32 +93,6 @@ def close_affinder(layers, callback): layer.mode = 'pan_zoom' - -def ndims(layer): - if isinstance(layer, Image) or isinstance(layer, Labels): - return layer.data.ndim - elif isinstance(layer, Shapes): - # list of s shapes, containing n * D of n points with D dimensions - return layer.data[0].shape[1] - elif isinstance(layer, Points): - # (n, D) array of n points with D dimensions - return layer.data.shape[-1] - elif isinstance(layer, Vectors): - # (n, 2, D) of n vectors with start pt and projections in D dimensions - return layer.data.shape[-1] - else: - raise Warning( - layer, "layer type is not currently supported - cannot " - "find its ndims." - ) - -def add_zeros_at_start_of_last_axis(arr): - upsize_last_axis = lambda size: size[:-1] + (size[-1] + 1,) - new_arr = np.zeros(upsize_last_axis(arr.shape)) - new_arr[..., 1:] = arr - return new_arr - - def convert_affine_to_ndims(affine, target_ndims): if affine.ndim == target_ndims: return affine @@ -146,57 +119,6 @@ def convert_affine_matrix_to_ndims(matrix, target_ndims): else: return matrix -# this will take a long time for vectors and points if lots of dimensions need -# to be padded -def expand_dims(layer, target_ndims, viewer, extract_index=0): - """ - will add empty dimensions to layer until its dimensions are target_ndims - """ - while ndims(layer) < target_ndims: - if isinstance(layer, Image) or isinstance(layer, Labels): - # add dimension to beginning of dimension list - layer.data = np.expand_dims(layer.data, axis=0) - elif isinstance(layer, Shapes): - # list of s shapes, containing n * D of n points with D dimensions - layer.data = [ - add_zeros_at_start_of_last_axis(l) for l in layer.data - ] - elif isinstance(layer, Points): - # (n, D) array of n points with D dimensions - #layer.data = add_zeros_at_start_of_last_axis(layer.data) - new_arr = add_zeros_at_start_of_last_axis(layer.data) - new_layer = napari.layers.Points( - new_arr, name=layer.name, properties=layer.properties - ) - viewer.layers.remove(layer.name) - viewer.add_layer(new_layer) - layer = new_layer - - elif isinstance(layer, Vectors): - # (n, 2, D) of n vectors with start pt and projections in D dimensions - n, b, D = layer.data.shape - new_arr = np.zeros((n, b, D + 1)) - new_arr[:, 0, :] = add_zeros_at_start_of_last_axis( - layer.data[:, 0, :] - ) - new_arr[:, 1, :] = add_zeros_at_start_of_last_axis( - layer.data[:, 1, :] - ) - #layer.data = new_arr - new_layer = napari.layers.Vectors( - new_arr, name=layer.name, properties=layer.properties - ) - viewer.layers.remove(layer.name) - viewer.add_layer(new_layer) - layer = new_layer - - else: - raise Warning( - layer, "layer type is not currently supported - cannot " - "expand its dimensions." - ) - return layer - def _update_unique_choices(widget, choice_name): """Update the selected choice in a ComboBox widget to be unique. @@ -246,35 +168,12 @@ def start_affinder( moving: 'napari.layers.Layer', moving_points: Optional['napari.layers.Points'] = None, model: AffineTransformChoices, - output: Optional[pathlib.Path] = None, - keep_original_moving_layer=False, + output: Optional[pathlib.Path] = None ): mode = start_affinder._call_button.text # can be "Start" or "Finish" if mode == 'Start': - #if model == AffineTransformChoices.affine: - # if (ndims(moving) != 2) or (ndims(reference) != 2): - # raise ValueError( - # "Choose different model: Affine transform " - # "cannot be used if layers are not both 2D. " - # "Please choose a different model " - # "type (not \"affine\")" - # ) - - if ndims(moving) != ndims(reference): - # make copy of moving layer if selected - if keep_original_moving_layer: - print("keep og moving layer selected") - og_layer = deepcopy(moving) - og_layer.name = og_layer.name + " original" - viewer.add_layer(og_layer) - - # pad dimensions of moving image if it's less than reference - #moving = expand_or_extract_ndims(moving, ndims(reference), viewer) # do not destructively change layers - #if ndims(moving) < ndims(reference): - # moving = expand_dims(moving, target_ndims=ndims(reference), viewer=viewer) - # focus on the reference layer reset_view(viewer, reference) # set points layer for each image From c0a2b935d0b4959dcd509ad82c0d6bcbad9c05d1 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 12 Dec 2023 17:54:00 +1100 Subject: [PATCH 18/25] Use matrices directly, not napari Affine objects --- src/affinder/affinder.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index ee9ea44..e829611 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -105,22 +105,20 @@ def remove_pts_layers(viewer, layers): viewer.layers.remove(layer) -def convert_affine_to_ndims(affine, target_ndims): - if affine.ndim == target_ndims: - return affine - new_affine = deepcopy(affine) - if affine.ndim < target_ndims: - converted_matrix = np.identity(target_ndims + 1) - start_i = target_ndims - affine.ndim - converted_matrix[start_i:, start_i:] = affine.affine_matrix - new_affine.affine_matrix = converted_matrix - elif affine.ndim > target_ndims: - new_affine.affine_matrix = ( - affine.affine_matrix[affine.ndim - target_ndims:, - affine.ndim - target_ndims:] - ) - - return new_affine +def convert_affine_to_ndims(affine, target_ndim): + """Either embed or slice an affine matrix to match the target ndims.""" + affine = np.asarray(affine) + diff = affine.ndim - target_ndim + if diff == 0: + out = affine + if diff < 0: + # target is larger, so embed + out = np.identity(target_ndim + 1) + out[-diff:, -diff:] = affine + else: # diff > 0 + out = affine[diff:, diff:] + + return out def convert_affine_matrix_to_ndims(matrix, target_ndims): From 9bff5c03fbb24e7f30bd4a9a5da23b78a7edb518 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 12 Dec 2023 19:21:34 +1100 Subject: [PATCH 19/25] fix logic in matrix-only version of convert-ndims --- src/affinder/affinder.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index e829611..628b3a4 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -108,10 +108,10 @@ def remove_pts_layers(viewer, layers): def convert_affine_to_ndims(affine, target_ndim): """Either embed or slice an affine matrix to match the target ndims.""" affine = np.asarray(affine) - diff = affine.ndim - target_ndim + diff = affine.ndim + 1 - target_ndim if diff == 0: out = affine - if diff < 0: + elif diff < 0: # target is larger, so embed out = np.identity(target_ndim + 1) out[-diff:, -diff:] = affine From b3fdcaa5ea225d2fc71ce8fcdbbe4c8399d38a08 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 12 Dec 2023 22:29:28 +1100 Subject: [PATCH 20/25] Remove now-redundant matrix-specific function --- src/affinder/affinder.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 628b3a4..8eab29a 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -77,11 +77,11 @@ def next_layer_callback( ref_mat = reference_image_layer.affine.affine_matrix # must shrink ndims of affine matrix if dims of image layer is bigger than moving layer ##### if reference_image_layer.ndim > moving_image_layer.ndim: - ref_mat = convert_affine_matrix_to_ndims( + ref_mat = convert_affine_to_ndims( ref_mat, moving_image_layer.ndim ) # must pad affine matrix with identity matrix if dims of moving layer smaller ##### - moving_image_layer.affine = convert_affine_matrix_to_ndims( + moving_image_layer.affine = convert_affine_to_ndims( (ref_mat @ mat.params), moving_image_layer.ndim ) if output is not None: @@ -121,19 +121,6 @@ def convert_affine_to_ndims(affine, target_ndim): return out -def convert_affine_matrix_to_ndims(matrix, target_ndims): - affine_ndim = matrix.shape[0] - 1 - if affine_ndim < target_ndims: - converted_matrix = np.identity(target_ndims + 1) - start_i = target_ndims - affine_ndim - converted_matrix[start_i:, start_i:] = matrix - return converted_matrix - elif affine_ndim > target_ndims: - return matrix[affine_ndim - target_ndims:, affine_ndim - target_ndims:] - else: - return matrix - - def _update_unique_choices(widget, choice_name): """Update the selected choice in a ComboBox widget to be unique. From 03471e7f00ddc4bcd299a9192b39f809cf027c32 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Tue, 12 Dec 2023 22:35:40 +1100 Subject: [PATCH 21/25] Fix ndim computation wtf --- src/affinder/affinder.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/affinder/affinder.py b/src/affinder/affinder.py index 8eab29a..6a754f8 100644 --- a/src/affinder/affinder.py +++ b/src/affinder/affinder.py @@ -107,16 +107,16 @@ def remove_pts_layers(viewer, layers): def convert_affine_to_ndims(affine, target_ndim): """Either embed or slice an affine matrix to match the target ndims.""" - affine = np.asarray(affine) - diff = affine.ndim + 1 - target_ndim + affine_matrix = np.asarray(affine) + diff = np.shape(affine_matrix)[0] - 1 - target_ndim if diff == 0: - out = affine + out = affine_matrix elif diff < 0: # target is larger, so embed out = np.identity(target_ndim + 1) - out[-diff:, -diff:] = affine + out[-diff:, -diff:] = affine_matrix else: # diff > 0 - out = affine[diff:, diff:] + out = affine_matrix[diff:, diff:] return out From 535f71d2f4658e31023bef4aa74957f980347186 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Wed, 14 Feb 2024 10:13:41 +1100 Subject: [PATCH 22/25] Start cleaning up tests --- src/affinder/_tests/test_affinder.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 87d52ea..c740db8 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -25,6 +25,7 @@ [0, 195.2461879, 35.57673389], [0, 160.49163797, 116.67068372]]) +this_dir = Path(__file__).parent.absolute() nuclei3D_2Dpts = nuclei3D_pts[:, 1:] nuclei2D_2Dpts = nuclei2D_3Dpts[:, 1:] nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:, 1:] @@ -34,21 +35,16 @@ nuclei2D_transformed = transform.rotate( nuclei2D[10:, 32:496], 60 ) # (246, 224) -nuclei3D = data.cells3d()[:, 1, :, :] # (60, 256, 256) - -nuclei2D_labels = zarr.open( - './src/affinder/_tests/nuclei2D_labels.zarr', mode='r' - ) ######### -nuclei2D_transformed_labels = zarr.open( - './src/affinder/_tests/nuclei2D_transformed_labels.zarr', mode='r' - ) ######### -nuclei3D_labels = zarr.open( - './src/affinder/_tests/nuclei3D_labels.zarr', mode='r' - ) ######### +nuclei3d = data.cells3d()[:, 1, :, :] # (60, 256, 256) + +nuclei2d_labels = zarr.open(this_dir / 'nuclei2D_labels.zarr', mode='r') +nuclei2d_labels_transformed = zarr.open( + this_dir / 'nuclei2D_transformed_labels.zarr', mode='r' + ) +nuclei3d_labels = zarr.open(this_dir / 'nuclei3D_labels.zarr', mode='r') im0 = data.camera() im1 = transform.rotate(im0[100:, 32:496], 60) -this_dir = Path(__file__).parent.absolute() labels0 = zarr.open(this_dir / 'labels0.zarr', mode='r') labels1 = zarr.open(this_dir / 'labels1.zarr', mode='r') @@ -74,12 +70,12 @@ def generate_all_layer_types(image, pts, labels): return layers -nuc2D = generate_all_layer_types(nuclei2D, nuclei2D_2Dpts, nuclei2D_labels) +nuc2D = generate_all_layer_types(nuclei2D, nuclei2D_2Dpts, nuclei2d_labels) nuc2D_t = generate_all_layer_types( nuclei2D_transformed, nuclei2D_transformed_2Dpts, - nuclei2D_transformed_labels + nuclei2d_labels_transformed ) -nuc3D = generate_all_layer_types(nuclei3D, nuclei3D_pts, nuclei3D_labels) +nuc3D = generate_all_layer_types(nuclei3d, nuclei3D_pts, nuclei3d_labels) # 2D as reference, 2D as moving From 27455f8b22ecad30bcaad4bae52bb29b7aad0a45 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 15 Feb 2024 17:50:44 +1100 Subject: [PATCH 23/25] Update variable names and simplify assignments --- src/affinder/_tests/test_affinder.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index c740db8..6ba5159 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -90,15 +90,13 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): viewer = make_napari_viewer() l0 = viewer.add_layer(reference) - viewer.layers[-1].name = "layer0" - viewer.layers[-1].colormap = "green" + l0.name = "layer0" l1 = viewer.add_layer(moving) - viewer.layers[-1].name = "layer1" - viewer.layers[-1].colormap = "magenta" + l1.name = "layer1" - my_widget_factory = start_affinder() - my_widget_factory( + affinder_widget = start_affinder() + affinder_widget( viewer=viewer, reference=l0, moving=l1, @@ -134,17 +132,13 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): viewer = make_napari_viewer() l0 = viewer.add_layer(reference) - viewer.layers[-1].name = "layer0" - viewer.layers[-1].colormap = "green" + l0.name = "layer0" - # affinder currently changes the moving layer data when dims are different - # so need to copy l1 = viewer.add_layer(moving) - viewer.layers[-1].name = "layer1" - viewer.layers[-1].colormap = "magenta" + l1.name = "layer1" - my_widget_factory = start_affinder() - my_widget_factory( + affinder_widget = start_affinder() + affinder_widget( viewer=viewer, reference=l0, moving=l1, From 0953f82c0ff3fca8a25b13253959b7784ce3425b Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 15 Feb 2024 17:54:32 +1100 Subject: [PATCH 24/25] Use generated test data instead of .zarr Also, clean up some of the code, including unnecessary list comprehensions --- src/affinder/_test_data.py | 72 ++++++++++++++ src/affinder/_tests/labels0.zarr/.zarray | 22 ----- src/affinder/_tests/labels0.zarr/0.0 | Bin 2839 -> 0 bytes src/affinder/_tests/labels1.zarr/.zarray | 22 ----- src/affinder/_tests/labels1.zarr/0.0 | Bin 2835 -> 0 bytes .../_tests/nuclei2D_labels.zarr/.zarray | 22 ----- src/affinder/_tests/nuclei2D_labels.zarr/0.0 | Bin 1457 -> 0 bytes .../nuclei2D_transformed_labels.zarr/.zarray | 22 ----- .../nuclei2D_transformed_labels.zarr/0.0 | Bin 1304 -> 0 bytes .../_tests/nuclei3D_labels.zarr/.zarray | 24 ----- .../_tests/nuclei3D_labels.zarr/0.0.0 | Bin 73526 -> 0 bytes src/affinder/_tests/test_affinder.py | 90 +++++++----------- 12 files changed, 105 insertions(+), 169 deletions(-) create mode 100644 src/affinder/_test_data.py delete mode 100644 src/affinder/_tests/labels0.zarr/.zarray delete mode 100644 src/affinder/_tests/labels0.zarr/0.0 delete mode 100644 src/affinder/_tests/labels1.zarr/.zarray delete mode 100644 src/affinder/_tests/labels1.zarr/0.0 delete mode 100644 src/affinder/_tests/nuclei2D_labels.zarr/.zarray delete mode 100644 src/affinder/_tests/nuclei2D_labels.zarr/0.0 delete mode 100644 src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray delete mode 100644 src/affinder/_tests/nuclei2D_transformed_labels.zarr/0.0 delete mode 100644 src/affinder/_tests/nuclei3D_labels.zarr/.zarray delete mode 100644 src/affinder/_tests/nuclei3D_labels.zarr/0.0.0 diff --git a/src/affinder/_test_data.py b/src/affinder/_test_data.py new file mode 100644 index 0000000..26b40b5 --- /dev/null +++ b/src/affinder/_test_data.py @@ -0,0 +1,72 @@ +import numpy as np +from scipy import ndimage as ndi +from skimage import data, feature, filters, util, segmentation, morphology, transform +import toolz as tz + +median_filter = tz.curry(ndi.median_filter) +remove_holes = tz.curry(morphology.remove_small_holes) +remove_objects = tz.curry(morphology.remove_small_objects) + + +@tz.curry +def threshold_with(image, method=filters.threshold_li): + return image > method(image) + + +to_origin = np.array([0, -127.5, -127.5]) +c = np.cos(np.radians(60)) +s = np.sin(np.radians(60)) +rot60 = np.array([ + [1, 0, 0], + [0, c, -s], + [0, s, c], + ]) +from_origin = -to_origin +trans = np.array([0, 5, 10]) + +nuclei = data.cells3d()[:, 1, ...] +nuclei_rotated = ndi.rotate(nuclei, 60, axes=(1, 2), reshape=False) +nuclei_rotated_translated = ndi.shift(nuclei_rotated, trans) +nuclei_points = feature.peak_local_max(filters.gaussian(nuclei, 15)) + +nuclei_points_rotated_translated = ((nuclei_points+to_origin) @ rot60.T + + from_origin + trans) + +nuclei_binary = tz.pipe( + nuclei, + median_filter(size=3), + threshold_with(method=filters.threshold_li), + remove_holes(area_threshold=20**3), + remove_objects(min_size=20**3), + ) +nuclei_labels = segmentation.watershed( + filters.farid(nuclei), + markers=util.label_points(nuclei_points, nuclei.shape), + mask=nuclei_binary, + ) +nuclei_labels_rotated = ndi.rotate( + nuclei_labels, 60, axes=(1, 2), reshape=False, order=0 + ) +nuclei_labels_rotated_translated = ndi.shift(nuclei_labels, trans, order=0) + +nuclei2d = nuclei[30] +nuclei2d_points = nuclei_points[:, 1:] # remove z = project onto yx +nuclei2d_rotated = nuclei_rotated[30] +nuclei2d_rotated_translated = nuclei_rotated_translated[30] +nuclei2d_labels = nuclei_labels[30] +nuclei2d_labels_rotated_translated = nuclei_labels_rotated_translated[30] +nuclei2d_points_rotated_translated = nuclei_points_rotated_translated[:, 1:] + +if __name__ == '__main__': + import napari + viewer = napari.Viewer(ndisplay=3) + viewer.add_image(nuclei, blending='additive') + viewer.add_points(nuclei_points) + viewer.add_labels(nuclei_labels, blending='translucent_no_depth') + viewer.add_image(nuclei_rotated_translated, blending='additive') + viewer.add_points(nuclei_points_rotated_translated, face_color='red') + viewer.add_labels(nuclei_labels_rotated, blending='translucent_no_depth') + + viewer.grid.enabled = True + viewer.grid.stride = 3 + napari.run() diff --git a/src/affinder/_tests/labels0.zarr/.zarray b/src/affinder/_tests/labels0.zarr/.zarray deleted file mode 100644 index 31d4d6a..0000000 --- a/src/affinder/_tests/labels0.zarr/.zarray +++ /dev/null @@ -1,22 +0,0 @@ -{ - "chunks": [ - 512, - 512 - ], - "compressor": { - "blocksize": 0, - "clevel": 5, - "cname": "lz4", - "id": "blosc", - "shuffle": 1 - }, - "dtype": "6$Ow500qSWpv)NAuSAYkYS91$xCAaz2|-F!>Ge+~iX=`3@wX304*J4V&LU39lr) zKohJOf7|+pcwziylmC##BE4am+;u(TL&QtNa=9kBEnuZb^o~B=Aq#%{OV2_2>zY1} O^sb&xL#J`fi{k_Jt4LJ< diff --git a/src/affinder/_tests/labels1.zarr/.zarray b/src/affinder/_tests/labels1.zarr/.zarray deleted file mode 100644 index 31d4d6a..0000000 --- a/src/affinder/_tests/labels1.zarr/.zarray +++ /dev/null @@ -1,22 +0,0 @@ -{ - "chunks": [ - 512, - 512 - ], - "compressor": { - "blocksize": 0, - "clevel": 5, - "cname": "lz4", - "id": "blosc", - "shuffle": 1 - }, - "dtype": "vp;Duh!YpKIRN4%P|F>O2S9=gRc|~14*(~gggYfEa6keH5J;^E z6pB!2LZFa_imFiB#(bVlAO{`*LH4rt_kA9FJ)Y4zt~sKVh#VcQ5?zRhPQjf4at2J5 zi3(!U|NQBmM?)s<7)`;X9ivW!_oN+vYR4k(kH7Ek=wD$xL|1QUmz0H_ZqR_)k#M9P z-e(8XIbC;q!GxgYb~!H5xA715rgi$p?{HF@Ysqdv$?0pq%_&frs%z7L=v?1!?+ zFd#yK59AX=L9uwJ_oc@%9B9U8DnCy;xp|Zu6E4ZS*aW$$Z$ps7iH!@Z_GFL& zZpBu8(2U?18SBzXOkY7}U6ggMz{#u{#3evF-tr~3_OQrJIWXeO>B2T0VYm3HHSoAa zU7TF><7atzv#kz)17-^xotj*yS@cUU%YbVI-(*;h=CET%%^5YHk;@O+@Kpr-yzyCw zt71?j*Gknw`Wu330o>W-J@X|>4x7;N343WH!ni)i?1gB5GJtir!O1eaBSD#z;A1(! z{9_+qVzMJT7L-lu`zp42kF{xMcfk4?|2rNBy&42UcG@>?Z%BzWD#r7y3x&6W>2<(js{OgDMGS&eyTKnSd*)#)0$1YZ zi5->&wmiuTlJT4%U%<8=P)WKGgO_hqEISP0`LlLQYqj`JgOe}R)Pxtfh}%tt_2^!M zr4@Cx;te*(HX8~XxknA4Aq_R;1K{W3y26$lfI?oWmAA-^U@Y>hT4__)VVvPmobPbj bfW*wxyv^`_|7_J;*fH(c!j5S-vWXqPOx`3R diff --git a/src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray b/src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray deleted file mode 100644 index c66a6e5..0000000 --- a/src/affinder/_tests/nuclei2D_transformed_labels.zarr/.zarray +++ /dev/null @@ -1,22 +0,0 @@ -{ - "chunks": [ - 246, - 224 - ], - "compressor": { - "blocksize": 0, - "clevel": 5, - "cname": "lz4", - "id": "blosc", - "shuffle": 1 - }, - "dtype": "5z-S21z`FW?E>dI;D5`-4ctm}ug{xKLMW1VjWB zNmMX#;UUzSO4!2&7dB~TI`6f8^IEOuB3O4IGi!hu0IM3vL*aW$<0zvPsIYm37eqAZ z1Lu)v7|z~tW*!mctdG-Z2d=H1xNVyaaO^hW+1jDkMAGsOl64Sw#9Q{M0gLxh_Nr1< z^oBjEbNprO(k(MJ9A+1(#!)zZgmhy$hVy_qX2|jB+Hwq^GjeO2eAmxoBOxp;$QE6y zS-MaGFLLyX9VvFn*CBFHJf>hX6vHBhr pUMbZB=9b}G{s2Y^l(zr? diff --git a/src/affinder/_tests/nuclei3D_labels.zarr/.zarray b/src/affinder/_tests/nuclei3D_labels.zarr/.zarray deleted file mode 100644 index e9831c0..0000000 --- a/src/affinder/_tests/nuclei3D_labels.zarr/.zarray +++ /dev/null @@ -1,24 +0,0 @@ -{ - "chunks": [ - 60, - 256, - 256 - ], - "compressor": { - "blocksize": 0, - "clevel": 5, - "cname": "lz4", - "id": "blosc", - "shuffle": 1 - }, - "dtype": "6^8#?)z$Criu1M!9>*ahBmovkD2psuL?|01gv3LNKte(Z3nVLvgd!w1 zH~~dQ0ilQm8&DJ>7Dz!65*r|%_xp_#J9eBgw(-QCmuEaPo*6sCf9~xX_emx}ktb0D z#|uxLy4_t>U!VT_-2dLb6}dZezyg3AzS!n)$34JHdx3A=&s`39;8x&~hk&X5!1wn6 z4<*3UCE(9BK5G|HeGquz^T5~_fM>o09DW%1(>H-Dj{pyT1$g{xz|5n-&z}T#Jr4Zn zyTI@dfIFWCI)BW^9t3{lhfn|r`Py|$9*lhfn|r`Py|$9*zCLJD`f8_KVIA3vl|)_{`7zm7 zPB-0n8kI`e%QL`Oej%=~^$Ny#nw3>KF?WZwOym|(V#_)vc#5SBAVRW7S|+iCB3o84 z$rCMJMY@TpGOzarX`RBl<3rO8O!0EzTc^2Sy>&Yj4BJv^%_L19nrUnrj_q*V&JSfv zEwgxP7o{V8;<>Z#su#%EO+{;77AcU)nwLuFj^|dq=tS=IVkUvF6)yRGa7T)lyjVH+ zk5aZZQCjo@8M(3Ql5d3@EzLv3(YcX&wogQJxHRjiQ7%)>&CSU+Tq-z`ITVLvcb&pH zoJN%z_KVC77H4C5r7IYuaMqVasokB@GMt}BjV&7(jw94A%SFXrY{#RE5V2juIAylt zB{la6dCmkbdVycX1SPfn4j*$94+frqOZV}-B8=v^c^;$ORB!8!1gA_$n>m_tG}AC4 zRWuBT;m`zBWZGdk6af`9vg6yWRfJ6)X!KWqcCEkqPnY#lPv)|6O!rcyIEvm?dijFM zO%@k@q2t^{B`e3qN(;X9ikeC6fn}W>FS>N_Gz9{24^-{0>M@eVrZJ2svNgjliEX(#u@coNZjmoc-TpX{iz=pOmv> zE$1&}1rt285^K5Dn|V&i=UKqrg*-QhB5t0=5I0qJ>_BkZD!FEgrpC=QtaMQ{G8~V! zk{X}5NxH*NK6^UOy2B@XkRv!=%~V%+c*R4c)RwnYcQ9RqlP)Q> zwX&tUed$GbR}ikZ^0o+K!UyzlZz7KkK4W*~oTyzP(_$e@JzgvogbAAIQ!6@0C>y@bUFE>2Bxu%d>`}d2awLV~9to`tEjdpR|l3c$-TY3{94sX=FNNcR&eL8rgBEm1Ozkfkuh^>=}+}h(N&);W!j&8OWLS zhe8l(ogJ=p(GMSKh>z*@N_I zWvFWfgZx7?SN;zkDu=#=>?I7d`cHgNYLnxPC+mJdZ4u)Qh@BN26c4oCPP z_2Ihi;HYVLGegtvW*Vm5ibjUx`4-ad6JdbqnKSN+s0HQvt$59ktjF$nJ{y5QmMr^0 zB03tSWk0M$x!&?I#w(eGkCroO9q!mvLrr754T0Ex60`B z0?U1II~wBMna)H~Bu5nQ4i5N;R{KWJnesi8mu3!C6*}ZOx3k3VOr!E`ctBpKJ-rai z>pRxi?IPbZ;c&QLT00wOBI8!R0uSFK&R6GM6n7c8T1S@*TzNf@gSZDhr{&Q@RStTd zcyaG9XLAAiDc(gN6z-Bk4Ho^F7$tm=5}x-#$&34F-WTcYdPw!n&JAn%$|vPHlXO$g z^RG^+>x}wMzMBb}d^gj`^vX)_K&6o#-)kZH-rPJH1pKETeulsYSKcx&AXp+Jsd8ESlO~k255bFn*{G;lZZJ@t3-r%%bX5&NFnidNU=w>`etf%FXs^41TU-I2m*!VcMd$wawXhD?;3X_zQ0 z8X1l!T1b@NxJ6v*r=B^LWG?laQ8q2>jlyMLU301y=dH@3QFlOI_5#wj5ur zX@VCK54GWZS)R`Ny3$-s*BixjM>-eBaynPg!CJcJM+~FjWp||uh*-bUukNBiSHgKk z!6p$~T`qSG-7J_(zOGJ!uGccpqgUJx?~@dQs#h}4qn|RJ_)@yuD5bs4!7rgf_AO$L zRMLA1njQx?WELa544pOC867u;ZYF37-Ap5sBrBT(l}2{_wRIHw#DRuieQ-O!`b3gR z^tGQGyk5u*kjYY37mw4f6^(Oll)e7&bAxLVea)pFsBL-E?-gr085HU{L}HAuJMuUcMLTtQ4A+zPzRMvvDHdB0=cu(_Z!)LQnb5g$ll%7E7q$W7n|C z=Mjf7%XU`O)gb$E5c%U6+A}~ z=)ja4N*}8Ji|FTxau$z_p6-|P9CAVFXT8$LIhrqf*&o?idEfkl>3j2M*z~=bMkZ)h z%m*rs?0BJd^!<%6C-C5Qa{@oS+cL~jRpgS>OzPnWX`FK%%x?N8bXzVW3Z;H;b*ohG zq3Y*kFAMmpS6Hew3-|@(!`{y7zeLZ9+}pvu#jsb(`u6G>k?*n;>Dj;jHe4)~rE7t5 zv!>TFO|P605iE+Pi#5K8B9+zo;{9;xY&g;e4oiW~9|2N`Bd_Tl1%DO=4q|r5ST8*( z%6A;bHv58H!%k`_SkRETK=%n7`;w zS9VJYUZ~R`e}Ho!Wojcg&v1a1UluZp@Lp87nO*LV{Qa$ocr!;6@n#w(;)+Iw!#s2I z%=uksp1J;Xd_0jUwBl?yw!`sq>y+7MqnM4NQuN({*(i#L37X+h1XR$-j^|rP-#2Y0 zINuDL3Ep}O2C5Iv#}$}-E7FEzI~?YMJ9ELsHa8bseLPwvFx67;gyGl@hl#j}xD92h zPew}xrdsOlFdWE>|C!Rt`+v)VpIiw!?9#gy&s=WX22u!Th`(!w_!(of3@Ynmcc#18aqCOlSU0^Dt*cy)QaM&Fe_{$ak zqSNlUpt55J0=wgaf@tz%I1~XDG_vEdRlhfn|r`Py|%a$PO-*g*LcW znx}E*Kw}}|L)Y#tecEmKTj)7|3q84oMdaShWp`rIcbVqWUhI+`fXO zyiZd(TXB9e$6J$@`~!H)>3p_)r7S)+UR!pupjTt|MH214_Z-;_mTOh2Nq$y@Ijci3MTjeoOvFxzyPy|$9 z*lhfn|r` zPy|$9*lh IK_ff<4bjRDh5!Hn diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 6ba5159..8e01cbf 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -10,43 +10,7 @@ from copy import copy from scipy import ndimage as ndi -nuclei3D_pts = np.array([[30., 68.47649186, 67.08770344], - [30., 85.14195298, 51.81103074], - [30., 104.58499096, 43.94122966], - [30., 154.58137432, 113.38065099]]) - -nuclei2D_3Dpts = np.array([[0, 68.47649186, 67.08770344], - [0, 85.14195298, 51.81103074], - [0, 104.58499096, 43.94122966], - [0, 154.58137432, 113.38065099]]) - -nuclei2D_transformed_3Dpts = np.array([[0, 154.44736842, 18.95499262], - [0, 176.10600098, 24.49557304], - [0, 195.2461879, 35.57673389], - [0, 160.49163797, 116.67068372]]) - -this_dir = Path(__file__).parent.absolute() -nuclei3D_2Dpts = nuclei3D_pts[:, 1:] -nuclei2D_2Dpts = nuclei2D_3Dpts[:, 1:] -nuclei2D_transformed_2Dpts = nuclei2D_transformed_3Dpts[:, 1:] - -# get reference and moving layer types -nuclei2D = data.cells3d()[30, 1, :, :] # (256, 256) -nuclei2D_transformed = transform.rotate( - nuclei2D[10:, 32:496], 60 - ) # (246, 224) -nuclei3d = data.cells3d()[:, 1, :, :] # (60, 256, 256) - -nuclei2d_labels = zarr.open(this_dir / 'nuclei2D_labels.zarr', mode='r') -nuclei2d_labels_transformed = zarr.open( - this_dir / 'nuclei2D_transformed_labels.zarr', mode='r' - ) -nuclei3d_labels = zarr.open(this_dir / 'nuclei3D_labels.zarr', mode='r') - -im0 = data.camera() -im1 = transform.rotate(im0[100:, 32:496], 60) -labels0 = zarr.open(this_dir / 'labels0.zarr', mode='r') -labels1 = zarr.open(this_dir / 'labels1.zarr', mode='r') +from affinder import _test_data as dat def make_vector_border(layer_pts): @@ -61,7 +25,7 @@ def make_vector_border(layer_pts): def generate_all_layer_types(image, pts, labels): layers = [ napari.layers.Image(image), - napari.layers.Shapes(pts), + #napari.layers.Shapes(pts, shape_type='polygon'), napari.layers.Points(pts), napari.layers.Labels(labels), napari.layers.Vectors(make_vector_border(pts)), @@ -70,22 +34,31 @@ def generate_all_layer_types(image, pts, labels): return layers -nuc2D = generate_all_layer_types(nuclei2D, nuclei2D_2Dpts, nuclei2d_labels) -nuc2D_t = generate_all_layer_types( - nuclei2D_transformed, nuclei2D_transformed_2Dpts, - nuclei2d_labels_transformed +layers2d = generate_all_layer_types( + dat.nuclei2d, dat.nuclei2d_points, dat.nuclei2d_labels + ) +layers2d_transformed = generate_all_layer_types( + dat.nuclei2d_rotated_translated, + dat.nuclei2d_points_rotated_translated, + dat.nuclei2d_labels_rotated_translated, + ) +layers3d = generate_all_layer_types( + dat.nuclei, dat.nuclei_points, dat.nuclei_labels + ) +layers3d_transformed = generate_all_layer_types( + dat.nuclei_rotated_translated, + dat.nuclei_points_rotated_translated, + dat.nuclei_labels_rotated_translated, ) -nuc3D = generate_all_layer_types(nuclei3d, nuclei3D_pts, nuclei3d_labels) # 2D as reference, 2D as moving @pytest.mark.parametrize( - "reference,moving,model_class", [ - p for p in - product(nuc2D, nuc2D_t, [t for t in AffineTransformChoices]) - ] + "reference,moving,model_class", + product(layers2d, layers2d_transformed, AffineTransformChoices) ) -def test_2D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): +def test_2d_2d(make_napari_viewer, tmp_path, reference, moving, model_class): + """Test a 2D reference layer with a 2D moving layer.""" viewer = make_napari_viewer() @@ -104,8 +77,8 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): output=tmp_path / 'my_affine.txt' ) - viewer.layers['layer0_pts'].data = nuclei2D_2Dpts - viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts + viewer.layers['layer0_pts'].data = dat.nuclei2d_points + viewer.layers['layer1_pts'].data = dat.nuclei2d_points_rotated_translated actual_affine = np.asarray(l1.affine) @@ -122,12 +95,15 @@ def test_2D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): # 3D as reference, 2D as moving @pytest.mark.parametrize( - "reference,moving,model_class", [ - p for p in - product(nuc3D, nuc2D_t, [t for t in AffineTransformChoices]) - ] + "reference,moving,model_class", + product(layers3d, layers2d_transformed, AffineTransformChoices) ) -def test_3D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): +def test_3d_2d(make_napari_viewer, tmp_path, reference, moving, model_class): + """Test a 3D reference layer with a 2D moving layer. + + The estimation dimension is always the minimum of the two, so this test + uses 2D points to estimate the transform. + """ viewer = make_napari_viewer() @@ -146,8 +122,8 @@ def test_3D_2D(make_napari_viewer, tmp_path, reference, moving, model_class): output=tmp_path / 'my_affine.txt' ) - viewer.layers['layer0_pts'].data = nuclei3D_2Dpts - viewer.layers['layer1_pts'].data = nuclei2D_transformed_2Dpts + viewer.layers['layer0_pts'].data = dat.nuclei2d_points + viewer.layers['layer1_pts'].data = dat.nuclei2d_points_rotated_translated actual_affine = np.asarray(l1.affine) From 7e7a4d989e67724d4f8e0806773fc1fde613661d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 15 Feb 2024 17:55:02 +1100 Subject: [PATCH 25/25] Add 3d-3d test --- src/affinder/_tests/test_affinder.py | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/affinder/_tests/test_affinder.py b/src/affinder/_tests/test_affinder.py index 8e01cbf..16fbf92 100644 --- a/src/affinder/_tests/test_affinder.py +++ b/src/affinder/_tests/test_affinder.py @@ -138,6 +138,51 @@ def test_3d_2d(make_napari_viewer, tmp_path, reference, moving, model_class): ) +@pytest.mark.parametrize( + "reference,moving,model_class", + product(layers3d, layers3d_transformed, AffineTransformChoices) + ) +def test_3d_3d(make_napari_viewer, tmp_path, reference, moving, model_class): + """Test a 3D reference layer with a 3D moving layer. + + Point clicking in 3D is hard but this should still work, for example if + you combine it with a plugin such as napari-threedee to click on points + in 3D space. + """ + + viewer = make_napari_viewer() + + l0 = viewer.add_layer(reference) + l0.name = "layer0" + + l1 = viewer.add_layer(moving) + l1.name = "layer1" + + affinder_widget = start_affinder() + affinder_widget( + viewer=viewer, + reference=l0, + moving=l1, + model=model_class, + output=tmp_path / 'my_affine.txt' + ) + + viewer.layers['layer0_pts'].data = dat.nuclei_points + viewer.layers['layer1_pts'].data = dat.nuclei_points_rotated_translated + + actual_affine = np.asarray(l1.affine) + + model = model_class.value(dimensionality=3) + model.estimate( + viewer.layers['layer1_pts'].data, viewer.layers['layer0_pts'].data + ) + expected_affine = model.params + + np.testing.assert_allclose( + actual_affine, expected_affine, rtol=10, atol=1e-10 + ) + + def test_ensure_different_layers(make_napari_viewer): viewer = make_napari_viewer() image0 = np.random.random((50, 50))