From 6c718941a09d57e273125efe956eac048887341e Mon Sep 17 00:00:00 2001 From: AlbertDominguez Date: Fri, 2 Feb 2024 18:31:49 +0100 Subject: [PATCH] Initial release --- .github/workflows/test_and_deploy.yml | 79 ++++ .gitignore | 20 + .napari/DESCRIPTION.md | 92 +++++ .pre-commit-config.yaml | 21 ++ LICENSE | 28 ++ MANIFEST.in | 6 + README.md | 38 ++ artwork/spotiflow_logo.png | Bin 0 -> 11822 bytes docs/index.md | 3 + mkdocs.yml | 11 + napari_spotiflow/__init__.py | 13 + napari_spotiflow/_dock_widget.py | 350 ++++++++++++++++++ napari_spotiflow/_io_hooks.py | 97 +++++ napari_spotiflow/_sample_data.py | 9 + napari_spotiflow/napari.yaml | 30 ++ .../resources/spotiflow_transp_small.png | Bin 0 -> 4629 bytes pyproject.toml | 5 + requirements.txt | 5 + setup.cfg | 46 +++ tests/data/test1.csv | 8 + tests/data/test2.csv | 8 + tests/example.py | 31 ++ tests/test_reader.py | 16 + tests/test_widget.py | 9 + tox.ini | 38 ++ 25 files changed, 963 insertions(+) create mode 100644 .github/workflows/test_and_deploy.yml create mode 100644 .gitignore create mode 100644 .napari/DESCRIPTION.md create mode 100644 .pre-commit-config.yaml create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 artwork/spotiflow_logo.png create mode 100644 docs/index.md create mode 100644 mkdocs.yml create mode 100644 napari_spotiflow/__init__.py create mode 100644 napari_spotiflow/_dock_widget.py create mode 100644 napari_spotiflow/_io_hooks.py create mode 100644 napari_spotiflow/_sample_data.py create mode 100644 napari_spotiflow/napari.yaml create mode 100644 napari_spotiflow/resources/spotiflow_transp_small.png create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 tests/data/test1.csv create mode 100644 tests/data/test2.csv create mode 100644 tests/example.py create mode 100644 tests/test_reader.py create mode 100644 tests/test_widget.py create mode 100644 tox.ini diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml new file mode 100644 index 0000000..7885307 --- /dev/null +++ b/.github/workflows/test_and_deploy.yml @@ -0,0 +1,79 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: tests + +on: + push: + branches: + - wheels + release: + types: + - published + +jobs: + test: + name: ${{ matrix.platform }} py${{ matrix.python-version }} + runs-on: ${{ matrix.platform }} + strategy: + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + python-version: [3.9, "3.10", "3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + # these libraries enable testing on Qt on linux + - uses: tlambert03/setup-qt-libs@v1 + + # strategy borrowed from vispy for installing opengl libs on windows + - name: Install Windows OpenGL + if: runner.os == 'Windows' + run: | + git clone --depth 1 https://github.com/pyvista/gl-ci-helpers.git + powershell gl-ci-helpers/appveyor/install_opengl.ps1 + if (Test-Path -Path "C:\Windows\system32\opengl32.dll" -PathType Leaf) {Exit 0} else {Exit 1} + + # note: if you need dependencies from conda, considering using + # setup-miniconda: https://github.com/conda-incubator/setup-miniconda + # and + # tox-conda: https://github.com/tox-dev/tox-conda + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install setuptools tox tox-gh-actions + + # this runs the platform-specific tests declared in tox.ini + - name: Test with tox + uses: aganders3/headless-gui@v1 + with: + run: python -m tox + env: + PLATFORM: ${{ matrix.platform }} + + deploy: + needs: [test] + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' # upload to pypi only on release + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -U setuptools setuptools_scm wheel twine build + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: | + python -m build . + twine upload dist/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4214462 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +*/__pycache__/ +*.py[cod] +.idea + +.DS_Store +.ipynb_checkpoints +.pytest_cache +*.egg-info/ +build/ +dist/ +htmlcov/ +.coverage* + +*.npz +*.tif* +*.hdf5 +*.h5 +*.zip +*.so +*.tfevents.* diff --git a/.napari/DESCRIPTION.md b/.napari/DESCRIPTION.md new file mode 100644 index 0000000..9183c54 --- /dev/null +++ b/.napari/DESCRIPTION.md @@ -0,0 +1,92 @@ + + + + +The developer has not yet provided a napari-hub specific description. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..21a62d0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,21 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.254 + hooks: + - id: ruff + args: [--fix, --fix-only, --exit-non-zero-on-fix] + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black + - repo: https://github.com/mwouts/jupytext + rev: v1.14.0 + hooks: + - id: jupytext + args: [--from, ipynb, --to, "py:percent"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7f32bb8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ + +Copyright (c) 2023, Albert Dominguez Mantes, Martin Weigert +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of napari-spotiflow nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0439384 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include LICENSE +include README.md +include requirements.txt + +recursive-exclude * __pycache__ +recursive-exclude * *.py[co] diff --git a/README.md b/README.md new file mode 100644 index 0000000..79205f9 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +[![License: BSD-3](https://img.shields.io/badge/License-BSD3-blue.svg)](https://www.gnu.org/licenses/bsd3) +[![PyPI](https://img.shields.io/pypi/v/napari-spotiflow.svg?color=green)](https://pypi.org/project/napari-spotiflow) +[![Python Version](https://img.shields.io/pypi/pyversions/napari-spotiflow.svg?color=green)](https://python.org) +[![tests](https://github.com/weigertlab/napari-spotiflow/workflows/tests/badge.svg)](https://github.com/weigertlab/napari-spotiflow/actions) +[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-spotiflow)](https://napari-hub.org/plugins/napari-spotiflow) + +![Logo](artwork/spotiflow_logo.png) +--- + +# napari-spotiflow + +Napari plugin for *Spotiflow*, a deep learning-based, threshold-agnostic, and subpixel-accurate spot detection method for fluorescence microscopy. For the main repo, see [here](https://github.com/weigertlab/spotiflow). + + +https://github.com/weigertlab/napari-spotiflow/assets/11042162/99c09826-bda7-46bc-a9a8-6e0c52b3f9f1 + + +---------------------------------- + +# Usage + +1. Open 2d raw image (or open one of our samples eg `File > Open Sample > napari-spotiflow > HybISS`) +2. Start Plugin `Plugins > napari-spotiflow` +3. Select model (pretrained or custom trained) and optionally adjust other parameter +4. Click `run` + +## Supported input formats +- [x] 2D (YX or YXC) +- [x] 2D+t (TYX or TYXC) + +## Installation + +Clone the repo and install it in an environment with `napari` and `spotiflow`. + +``` +git clone git@github.com:weigertlab/napari-spotiflow.git +pip install -e napari-spotiflow +``` diff --git a/artwork/spotiflow_logo.png b/artwork/spotiflow_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..c38f397083691370559b14dc44e4a4effd369ad2 GIT binary patch literal 11822 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 z z`A$efN?>j9xn3+t2B%>u`DJ0m`-MlGr3Cr{HA~uyd?XWy=UqjMhnutSFv}T^8&Im) z-}J=8+vSGdf=Oa)Dy&abl4j(_7~J%Yhnb^7Gf+y3e2m*$F5y|{{_KU}KvI0{#IcQt3JefKq!8yp5=Z2}a!#*|m;8eT zi^a*`H9qP=>-Z)XpIfTA`;ZqmBgdJ~JT?xLIAMCOnZ^5o$6O=ay?F{Lb)`7>0TVhB!gOI#WhqX2QIr#~EkriZ>EX!rJ$zB@2i; zqnw*abFiU2RX`!?B2^k{iaANS%_X5=_k3J9R6jAfKV_`sU80;G6VqAw)p=e?bvFz( zyAWypq-pEXwnIS@(sY4}qU;csu?Y8j+ek)7pCRl-t?LnNTQJerJZvdjGc4G4seh+G#^Cxl zYOI1$i^(pjw+kq3cBw3jmWg({%X}E&UkUCbpi7C<)}%|f_1r84`?hBmD~F@UYX)*& z5Tu(YtZJVdi^Ujs#5TOe+4l{{ZBZ7I41&Gu`g=O=GM|@)eZN3CV6=fKr0kc z)z{^aU7GxkswgpCNpNU`Sy|qc?7}<(WVb4v)YVHYjA`|JeZXHkJW%=;d4vCmskaXTENq?dt4=Uw-dGkc)fn=?)(Tx`gfMnwFHaY(6^aQ{?L|e z7Rb_*56@3V3N=;rS}>n^o?`KmZ|dPFZ~nJOD;E+J*W%UYt@T@NTj*8ILgGvgoZNhy zqdn|`DYy&#EgH&d!MU}}q+F7!Z0SX)XSk-IWVR$TVCsrhG3FXei$k+n4~JY*n?f)A zMV4Bx+{)f*%}0I{)%>D^rh&>voAin(U#fv?3tHrA=;O0_>dZ{h`{whs}A=neL&JcF2U8?RR&?$L%#yj9PAbRwgK|`G_i&Ys(7*i(Wj0WauE; zcm1PjA^Uj#S98PFxSO?q8`{6{Greko|lYu zrXHcAy%i-vv)fphxqAZB_FG6^aMA%K_6O=bQ^zB>bu*+p^06g7gW=rBlHdCJozCq( z!O`Wnmu&g*iu@&U70J549%fERn?*VI;FRQIh}E8!%MBq!AFW+bstkm&Jo`V$b}3Kj zYG@~oSK;rearz1_&bHh)yOP=4G}9BrdLs%L2|PXUQK`Z1JqoK(SMho$tk#m`3BQ?w zO=1*La2{{S8rc)LQTW*YffUp3P!FbHp*`{$(h*-|7uvnl4xdLPOfwhb$9zzkirZ24 z!`I6w8Lg%7ec({ctkLdm>2J>frl|KQ=PB~PC4Y4;diLGXEPiEY8@h?Y@34fH7R69# z^+h#mg!P){Ygk9f;c-_xF_RiCsZ_Dd1Z}GIu++_Xw|ah^PMh;mG2iU`lhxLWIVxv{ zax`?{JX9KWu90lom&v4@%9Y$IglWp;%C_nG%8CQm#I*fjBl+6)v5OZmpbupah03<< zlyZ~Lq8k1loulqx-?8p*iG}8lKvMJv^Uf*HV-IIa19A~6w}v_Ap}~JjEMQKa*`~ zGs?Szbpm*bjbV7iumGtujy4_WHd6z>pLnF`@V?2_N_tf8?bcs%YiENQ0<$H@J0Wn6>dI&)#7t>yh$+TFQqo z#Cy^fqklx_S{>>L?|a)pO|*Ju#Rgq-;=J`!nCx1)YHUJ5PKa%(M9{`o9ke_a*gUSX zVQU@)lB~^`AMaM3kKqZgxBOdD zPo9^9Y!%wEj)}ogj$pC?Z!sql?fj!}`4g@*)a(WWLp*fbjjM3qMY%o7j`Tx3qSTUe zD|3QB8m`Ft_yjz6eb*(?TWGPfG^k>Du^v``3(rH4hL$&vR(jYPDQP^dQ7*glf32>* zWtWSkOiMXbsP;7A4aK7979^JALjev!{QJ1s`kzX}AT4V#BWJig6xJr6y6O4KmlSq3 z(?p8YtA_U_t8o-XdAX z70)6e$HoRe_d);H@W??KJ}Xv9K2c}Ut)y4Hd?@q-JD!Yi%sBmH~kQalvLM(kN{tQdU~ZhXVJ`1Vp@ypA}SWcbPY>Mf6D;1 zCHC)eOjP08>SFH^I}}>QyVkw~H`gkSbJKXXl1FGrz^kS(5Ovp2*^(HaCr7qW3w>*U z9&Z;FXnN2-qA5ty>fjk5aAs;3kFMO5gH#7Hg60~{XUIYyXHy&}dk@uw6E7}>jTb#%O1Td-T)7qVrfs)#`TDvCmEUk_r8wgm>NukO;s)G?k} zP*Ys3xR#%R`ZFILKjeaCsBvTVijr!mN$j)=+np3|25~5eu?*=GUg&!OPf+PU?mJE= ztjuw2yW!IZh*a>tf%Afh2y<8UG-|gJh>JMj+7b8=7U|*iog0&3b#2a)-m1THN$Q^z zizOC9$`&877iVcNH_LT7j~s_lYxJK{8a^ftj?S4-$1E zRl}@Oc#L9hiTF#)<@)_mR4%UCsE(}9NC_BDqzqTs^_xH4NqW%1EZ=>67xx{|2dxf9 zYP>ic3@q79NijC{X+^ODw7SvRaYO(jT21jYB8;29tes5KA^TC%hO1QBh^z8+8Y2R} zG=%yFK&vCPt$-31e%}YyMLi)2f&&H=pEvqREpc)QGE<+PxVtKN$sdU&acpIT?9{8< zweVgyTG+=kqZ~hspVsIzw}iRQ&ixWwMS@Il9M{Bx!ThXpnGA2I%fc!6cGT)R;4heK zj@7sftCnHtkNrRl?)|h~N2l{X;NP*OFn1^;R*;cghczV3;2x)gYk?Zhf~%7q{Lyrs z5`8+JvIGTFk6E(rU}3Cn4|%oMK(PWB8_d!~W`DvKY?Yoyl03d~wx1dU%5UAxKXSM4 zUi&^BEvLNkOSAdBqPzeby5&eKwEfw?d=u3#rUOG>q~+B%aBgD!QbeF4ejR$wuVUvv zMt46ji5SAjH_H1#4z!#Xpphu*QCAJnGHgMrMgQ9r!LGw(CxMn79OYgo62Ga z;Io)?Bt5%yCYvWv^hg#I$WQAST(&0|bd)4aIb`rQ3ddw+yyWI~lPsIblb18`sz0~v zU)=;@I~f=fqZl28Hs`!4_l1XFGfgEjaS8t0YW=Zp8*#yJs5|HGMzd-xW*XMY&q(J4 z6uq@m(bRT-6WaG7#MbY2=IdO%wV{Q}CzPTARA0=~v_*5 zMQ*`E{32LkQjOw_PrD8>-4*B(i(=ggIjs84;zImLGJ7q*KUfhMsSmcuOfxZyR%$SH zxOn;qPwCTdMJ5*nfNrzAri|%zV1>OxBQUz4hYyTMR#nY^G#t=0%;!tCXe7@H#t%f6D3uwGd~(+xt*tj08$HwO z92puUW-(QDdNX!XhGa4S37Z;$dkA3gsIKMs*?#V5~t_xq7#6ZF`PGEwXSq#S*AKw&>5$$bdX*3%a>1P(~CAX ze~ywOiZz<}v!P@=Sa`7wQ6+W_sZ87qIY|i-!p=ob*ABQ#d|x#5oEh70YMWZhV#efy z5gxmPwX%k1=k9(xCdzV`IH4vMeFjhcL+QENdv9RxsUa+>^1cc`7TD2 z^jw#n7YB>pKJeT{hn%t%H0M!)XfIK}@R0G-c?VT`U(*t0*%3Z;=WlPNrNwZ}^!ptA zEr&{@uUsKwvVCCp8;f8gy#~W-VzG*K^Me1^=lQq4+U%X~%=yOXw5Qkw6N_$`*s8?T znWn~$ncdly`U*=ID%o#0B&bTLsb2+x!!`OfUUm&_?kEat8>7c!%3Nj=F1M9F+mlzh zUAxd#LdlP8&oq&4$>IahhD?`x)OdJ#CcppvF`Imwf9Xcx1{w=@-xBL5q#jaeRbtc$ zyWO*__r*-vTPncj1*u_wy#MYlxQQ|5M)paHdDCMux`)n7Q0j9AU__C9$*;vhf?^6K z49a{MS;E;CE|?{{(U4AEjGsTves~ zC^%j9-zz>;CbyMAV8!Ne9DK z?%+UWdUcTr-!lnT=5-Y9jE!b~jF0M8b8d2FLBlcsMf=#{M9mFa`5tLOl{2{zg`mzA zg)j(gnj@DP&PPDGXd{PBb=rT!(B|xL-v%uQUb z*gJ#lPg{&Ce}2x5U6lTU3D_nI)d}DZ-H?`{hBE_}*7&M4Xf~_empl@u1Ck0bZ(ZoU z3}3rRJwt6`)xM|?#5b88uYLWgpw2FZ=pzsRt7KFoN!_#73+w8iuh{2@8kB1nhK1%W zzS*lF#M>KZP|PIs?G34PBlp7AVX`hKR}oRtsqaP+=~;p zS8}tP9bpUWu)*xQR;|mzDBD8DWR%bZ?t_yxA{MQ6c9|P~zs#mFxgf09tjAQ7o~&P1bJX8z^jpP}Z%9?vI$T>n6_a_O-JR>r z3r>J@b2AE7l&}AKTvc$UOOkRyy`PhF60$3NJhXnz|5_%@n-ll9FVkz|tgC!eR{16# zBVYxI7~{fUb`O~8`Ria!;bI4#(r1NT|3y{B;ga{-2SpHB%wyLE@$Fvaqdkbfuxi*k zkHW9s`*-arcQE}n0{SLmfIGH8;!SoL)jfaYtTJhJ|I*Fb3Ow2%XXbY-=5s8c)xgZY z-Qt%o-TInjs}*@T=Ehm#@u59 zy#j4X4`E1sxfBa*YuH9lCH2BTdt>sPGJ3(N@8%%d6sK6q$$1fD5I)r(!edytQB z>X2T4+jl{08R2Y+i{Fv)>bFuk;4i>3z~4@zrJ=Zm9uA(<5@nUzpU0e=kqPSH^}viILygwrc)u=TKz6OvFlf zCV7GRr6IJq!+8nzZ*1PYesl2+`M*Rx*;N7_#g{Vi(;XPN)LteAq{Ya-&rATW_>1T> z_mtP^Xh(-*`Op1bm|NGoSoSm;JrLKte!bmTQNE7kvFcTZZH=CI%D=t@_{B2B6N#>M z73$Dc1vuRk(nKVzS=;u9t1c|`vhZ66i0X&WzdpMug~pY-P_@Dv&qme(iq|jq!Vqu1 z>RYbf-W81K?j(akFM{Y+?LpsvT!OfPnikpaR8aTS7&4r%_?JI!lJLKNf3UpcFtaPOL_%RHIV~utjDF`1@uM^N4?mRNki%y*8p;Hz~iv0~Se+)$+bJXgv#BE!tW;LyTD z5z6wkN$pZ5UNc<59oigk!v&`R%xLOXMWaGLK^MYEuTy;hkJ`98<#(rF$|Wx!dz!D&x;%ymi;j`owdE6V=;AL z=*)6*Jxb7}fZLM5E-U#>cYI| z#ls;x)~&nG>e~yFnXmhkh| zQ8UHu3RUH=anFM>s8?Ss;tpnS3mZ3y(0W^bZ6e`ifh0b-C3UUatAvNtgGgZ2zR3#^ zQ1j<+d$c&S{9Bw=ugV;iTeW|i{2ssvNi+m9gAA!ZI!o(LZjZEG>b{55*>SI`>9F5I zk5hM8tx{Zl={#!K8G|(Jl+`<{^wJj8w!z%Pn1Vz&E1wsSIWe~;mP>1WJi3x5tL?w7 zq1rVbk=VVgyXseM3RkKBG<5BgmfMGXsz}!MhxnhDY`CFjLW=OUDDC7{i*YxIJReZ# zm5IB}=U6v(?+iW(1GT*B<_ytRQ78m0wFzQ;QnbY+BWsjN`kz8grV0 z9^aJ;?kcbp;&n-Hk-vNGVY8@lJy|!a+VIs3&`^r-1-OkXs&EPcMv^?sKS79xk8y5$ zAj3m<>JlS?jbk~?SevoeK$guHg>@kGsJUO6j9AQGhi+@Q-*_oK-fT$wy8?c#7iOZ$ zk`_qsZzMO zjZD;F%!Iq)nn9_EjaeE;?Yhri{WNH%`?LG3>uNn^PkXu$UdJJ;vepyDzLeOF{T$iZ z@>ON#v>28GO`5BdwfJhEHTZ=0#Y0C>pUPR3*^NXhu)56R3C3}{Tz>koIrU{|Y^kO$ zy%EjwybMc!*nh>fXo%Qt60KrMpGbW89d!JT;vye#{W4 zAex({?>|pBOC6ZLVq%t6XB5P*ZXw&^!;rfY9Ng>zFq+mKkLfi#li#LONmh`2XH!25 zo5U+FF!s`J|MqvMfpy55UW*=3{GGLzzI}b6Zuxx(z*@k_DE}*|OJ!-E;63|;a~E4OT;dEDj<6Ql zx-hvdS{#KK@K8cr?KO(1`PRi`GA)k$M-P&`h0~OOFeg=RC(jS`=nTiJjQ-O@aEa8P69fQp5FHBd-52w_|rkHD9 z{$c*lb}-t}nFkk~xug~NT$7g6mEtnF}Z+>TW~? zuAMQeqOpWZAtX-!4&)B94pXg*g3W%5>N)PU5aR7<)$`S_)xT%{#2WVh$o!DQq%eOw z6|DQa~B-z90fvI{tyN*@d8l5Vp75)UKen^8KY7eiKm0lHGVvP|{G&Db+@qbSYV?L?B-uk#jjt*7&G2p~#m?hw>+W<_ z^XkTx-8Os9p&{I5=+;TSLBUT24{wLN#D*l)Cc|nL3hyn$Bm;34Vi&HBAt$|a+`%Bc zVQunQja|}PY8N#HTid)u>ODa;%J=fQX;Ghu=4s}Fdf*MEc+A0sodwTcCg%7|A|Ygw z*BAkTuAvERVFceym1f8N)d$4AV{+;_YeBN7pVy5hDhbmp)su7cN+I4auX*lmk0Ye9 zqEEi%u5A}XZcTzOQYSUD%2~Ye?%wOV|Fb8*o!^Y;V1JKu<4UtQdVFV*GH}x zH=llLDSBfTolAa5_v_x;=9ml-dZ(YIg@Iei2$XZaH^J^AFPrvrtwnae-S!D9#S}u& zP#+`6dlG!8CW0v{OEn|{+dNHWsfO|lJbt^lGA?>CBm$EA;p4OrffnO?i$qBlCRK@x zVilv?_o(xzquo8zw}vx{^$Q5)yv#9WsA>7j{oxg+;ph5vK4J##rof9*&wWN>1VxA0 z7;m?F!s9SS56df)zFQ3A$=LV8n!jN!&pl->y1x8bRg|iVCl?v(x{&v*y!N*T*upX9 zqK2BV_khp&;R!!j?<&zk)VDSeYF?8O41llKf!ksK%B z0z{T0G*D?aPQ_OeOI0bAi0CFDu9;R854}aq>X6yTZTarA{&kna25 z;Ac3lRmEaZ7~ngiPJX$B@Vz%9el2hT*2l{wo8Q;TI6ie(1p7EZ?vmNXW0FNb8)@&PuVX} z2!rW|Ml>CCC${5-UAx%8E-`jtLVx1hMtuZ()n=-ebT#%W6(#*>R*!m6=at@o(-56k zf~{_KmW$wM-(20q%8oUSfM1Z-i&&4wlefN%G@@GZzW9ieiQHIrSQ$}}S2;+yW=A1_ zJLz$~Sgx5?WPtXKoJ&43PP@%KY{@&7jl{(;RVu}$ zHzw!wjSxi4WPZ&%MCszgwTEvjOK-(aI$&HQP_pI~H(#u*waSi`cut#lR zxK_V*T%VS02kn|cQK-uEN=u3gD%nbtY-EB~HH*aNK$xMs$4}@m?Xr>?5#EndAZOEx z2;iBdTi!HW*@N&TG*P@K$TgN~mMb6Um^I(el5SIkVrPEWT7Z?1acRw4?*xn>P?&3e^mKja0i zGEj#9WjsFKS6RNd)cVw7^^mK?SGscXh%4r~`|eDUsHrZVPqc;F_+Y~C=B@)ziX;4b7w4k0U}e6W22R+C_k!NL4wneMn*aoO*QizW zB8vxha@hX=ZA#`g0O(Tt3Ym=ya}vEvEWf`i5KDz4$UIJ634Xi1TD+@>vojB1&&~Jx z|3garpTgq*ENvX`y}_O4iQ~l~RI9z~qUl^@v|Yd^F6M$}PUi0hz{SDE&Bn>a#>Mf4 zlSh#EqaYU_3kQcF2M1-hmEnH?p!Q&E3(x-tfaiY!4=|m>?*PXCGlGjX)ZE#{1nTgA bV*~^_c>fpkC3e~F9RpC1QIRf_G!FPL|6rkc literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..130505f --- /dev/null +++ b/docs/index.md @@ -0,0 +1,3 @@ +# Welcome to napari-spotiflow + +Napari plugin for Spotiflow diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..d5e0f7b --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,11 @@ +site_name: napari-spotiflow +site_description: Napari plugin for Spotiflow +site_author: Albert Dominguez Mantes, Martin Weigert + +theme: readthedocs + + +repo_url: https://github.com/weigertlab/napari-spotiflow + +pages: +- Home: index.md diff --git a/napari_spotiflow/__init__.py b/napari_spotiflow/__init__.py new file mode 100644 index 0000000..fccf822 --- /dev/null +++ b/napari_spotiflow/__init__.py @@ -0,0 +1,13 @@ +__version__ = "0.0.2" + +_point_layer2d_default_kwargs = dict(size=8, + symbol='ring', + opacity=1, + face_color=[1.,.5,.2], + edge_color=[1.,.5,.2]) + + +# def sample_data_2d(): +# from spotiflow.data import hybiss_data_2d +# +# return [(hybiss_data_2d(), {"name": "HybISS data (2d)"})] diff --git a/napari_spotiflow/_dock_widget.py b/napari_spotiflow/_dock_widget.py new file mode 100644 index 0000000..5fae90c --- /dev/null +++ b/napari_spotiflow/_dock_widget.py @@ -0,0 +1,350 @@ +from magicgui import magicgui +from magicgui import widgets as mw +from magicgui.application import use_app + +import functools +import time +import numpy as np + +from pathlib import Path +from warnings import warn + +import napari +from typing import List, Union +from enum import Enum +from psygnal import Signal + + +def abspath(root, relpath): + from pathlib import Path + root = Path(root) + if root.is_dir(): + path = root/relpath + else: + path = root.parent/relpath + return str(path.absolute()) + + +def change_handler(*widgets, init=True): + """Implementation from https://github.com/stardist/stardist-napari/blob/main/stardist_napari/_dock_widget.py + """ + def decorator_change_handler(handler): + @functools.wraps(handler) + def wrapper(*args): + source = Signal.sender() + emitter = Signal.current_emitter() + return handler(*args) + + for widget in widgets: + widget.changed.connect(wrapper) + if init: + widget.changed(widget.value) + return wrapper + + return decorator_change_handler + + +def plugin_wrapper(): + # delay imports until plugin is requested by user + import torch + from spotiflow.model import Spotiflow + from spotiflow.utils import normalize + from spotiflow.model.pretrained import list_registered + from napari_spotiflow import _point_layer2d_default_kwargs + + def get_data(image): + image = image.data[0] if image.multiscale else image.data + return np.asarray(image) + + models_reg = list_registered() + + if 'general' in models_reg: + models_reg = ['general'] + sorted([m for m in models_reg if m != 'general']) + else: + models_reg = sorted(models_reg) + + model_configs = dict() + model_selected = None + + CUSTOM_MODEL = 'CUSTOM_MODEL' + model_type_choices = [('Pre-trained', Spotiflow), ('Custom', CUSTOM_MODEL)] + peak_mode_choices = ["fast", "skimage"] + + + @functools.lru_cache(maxsize=None) + def get_model(model_type, model, device): + kwargs = dict(inference_mode=True, map_location=device) + + if model_type == CUSTOM_MODEL: + return Spotiflow.from_folder(model, **kwargs) + else: + return model_type.from_pretrained(model, **kwargs) + + # ------------------------------------------------------------------------- + + + DEFAULTS = dict ( + model_type = Spotiflow, + model2d = 'general', + norm_image = True, + perc_low = 1.0, + perc_high = 99.8, + use_optimized = True, + prob_thresh = 0.5, + n_tiles = '1,1', + cnn_output = False, + peak_mode = 'fast', + exclude_border = False, + scale = 1.0, + min_distance = 2, + auto_n_tiles = True, + subpix = True, + ) + + # ------------------------------------------------------------------------- + + logo = abspath(__file__, 'resources/spotiflow_transp_small.png') + + @magicgui ( + label_head = dict(widget_type='Label', label=f'

'), + image = dict(label='Input Image'), + label_nn = dict(widget_type='Label', label='
Neural Network Prediction:'), + model_type = dict(widget_type='RadioButtons', label='Model Type', orientation='horizontal', choices=model_type_choices, value=DEFAULTS['model_type']), + model2d = dict(widget_type='ComboBox', visible=True, label='Pre-trained Model', choices=models_reg, value=DEFAULTS['model2d']), + model_folder = dict(widget_type='FileEdit', visible=True, label='Custom Model', mode='d'), + mode = dict(widget_type='RadioButtons', label='Mode', orientation='horizontal', choices=['2D', '2D+t'], value='2D'), + norm_image = dict(widget_type='CheckBox', text='Normalize Image', value=DEFAULTS['norm_image']), + scale = dict(widget_type='FloatSpinBox', label='Scale factor', min=0.5, max=2, step=0.1, value=DEFAULTS['scale']), + subpix = dict(widget_type='CheckBox', text='Subpixel prediction', value=DEFAULTS['subpix']), + label_nms = dict(widget_type='Label', label='
NMS Postprocessing:'), + perc_low = dict(widget_type='FloatSpinBox', label='Percentile low', min=0.0, max=100.0, step=0.1, value=DEFAULTS['perc_low']), + perc_high = dict(widget_type='FloatSpinBox', label='Percentile high', min=0.0, max=100.0, step=0.1, value=DEFAULTS['perc_high']), + use_optimized = dict(widget_type='CheckBox', text='Use optimized probability threshold', value=DEFAULTS['use_optimized']), + prob_thresh = dict(widget_type='FloatSpinBox', label='Probability/Score Threshold', min=0.0, max= 1.0, step=0.05, value=DEFAULTS['prob_thresh']), + peak_mode = dict(widget_type='RadioButtons', label='Peak extraction mode', orientation='horizontal', choices=peak_mode_choices, value=DEFAULTS['peak_mode']), + exclude_border = dict(widget_type='CheckBox', text='Exclude border', value=DEFAULTS['exclude_border']), + min_distance = dict(widget_type='SpinBox', label='Minimum distance', min=1, max=5, step=1, value=DEFAULTS['min_distance']), + auto_n_tiles = dict(widget_type='Checkbox', text='Automatically infer tiling', value=DEFAULTS['auto_n_tiles']), + n_tiles = dict(widget_type='LiteralEvalLineEdit', label='Number of Tiles', value=DEFAULTS['n_tiles']), + label_adv = dict(widget_type='Label', label='
Advanced Options:'), + cnn_output = dict(widget_type='CheckBox', text='Show CNN Output', value=DEFAULTS['cnn_output']), + progress_bar = dict(label=' ', min=0, max=0, visible=False), + layout = 'vertical', + persist = True, + call_button = True, + ) + def plugin ( + viewer: napari.Viewer, + label_head, + image: napari.layers.Image, + label_nn, + model_type, + model2d, + model_folder, + mode: str, + norm_image, + perc_low, + perc_high, + scale, + subpix, + label_nms, + use_optimized, + prob_thresh, + peak_mode, + exclude_border, + min_distance, + label_adv, + auto_n_tiles, + n_tiles, + cnn_output, + progress_bar: mw.ProgressBar, + ) -> List[napari.types.LayerDataTuple]: + DEVICE_STR = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" + + model = get_model( + model_type, + { + Spotiflow: model2d, + CUSTOM_MODEL: model_folder, + }[model_type], + DEVICE_STR + ) + + model.to(torch.device(DEVICE_STR)) + try: + model = torch.compile(model) + except RuntimeError as _: + warn("Could not compile the model module. Will run without compiling, which can be slightly slower.") + except Exception as e: + raise e + + layers = [] + + assert image is not None, "Please add an image layer to the viewer!" + x = get_data(image) + + if mode == "2D": + assert x.ndim in {2,3}, "Image must be YX or CYX!" # TODO: parametrize axis order + + if x.ndim==3: + x = x.transpose(1,2,0) + + if x.ndim==3 and len(n_tiles)==2: + n_tiles = n_tiles + (1,) + + if norm_image: + print("Normalizing image...") + x = normalize(x, perc_low, perc_high) + + elif mode == "2D+t": + assert x.ndim in {3,4}, "Movie must be TYX or TCYX!" # TODO: parametrize axis order + + if x.ndim==4 and len(n_tiles)==2: + n_tiles = n_tiles + (1,) + if norm_image: + print("Normalizing frames...") + x = np.stack([normalize(_x, perc_low, perc_high) for _x in x]) + + app = use_app() + def progress(size): + def _progress(it, **kwargs): + progress_bar.label = 'Spotiflow Prediction' + progress_bar.range = (0, size) + progress_bar.value = 0 + progress_bar.show() + app.process_events() + for item in it: + yield item + progress_bar.increment(1) + app.process_events() + app.process_events() + return _progress + actual_prob_thresh = prob_thresh if not use_optimized else None + if mode == "2D": + actual_n_tiles = tuple(max(1,s//1024) for s in x.shape) if auto_n_tiles else n_tiles + pred_points, details = model.predict(x, + prob_thresh=actual_prob_thresh, + n_tiles=actual_n_tiles, + peak_mode=peak_mode, + exclude_border=exclude_border, + min_distance=min_distance, + scale=scale, + verbose=True, + progress_bar_wrapper=progress(np.prod(actual_n_tiles)), + device=DEVICE_STR, + subpix=subpix, + ) + + if cnn_output: + details_prob_heatmap = details.heatmap + details_flow = details.flow + + elif mode == "2D+t": + actual_n_tiles = tuple(max(1,s//1024) for s in x.shape[1:]) if auto_n_tiles else n_tiles + pred_points_t, details_t = tuple(zip(*tuple(model.predict(_x, + prob_thresh=actual_prob_thresh, + n_tiles=actual_n_tiles, + peak_mode=peak_mode, + exclude_border=exclude_border, + min_distance=min_distance, + scale=scale, + verbose=True, + device=DEVICE_STR, + subpix=subpix, + ) for _x in progress(x.shape[0])(x)))) + + pred_points = tuple(np.concatenate([[i], p]) + for i,ps in enumerate(pred_points_t) for p in ps) + if cnn_output: + details_prob_heatmap = np.stack([det.heatmap for det in details_t], axis=0) + details_flow = np.stack([det.flow for det in details_t], axis=0) + + if cnn_output: + layers.append((.5*(1+details_flow), dict(name=f'Stereographic flow ({image.name})', + ), 'image')) + layers.append((details_prob_heatmap, dict(name=f'Gaussian heatmap ({image.name})', + colormap='magma'), 'image')) + points_layer_name = f'Spots ({image.name})' + for l in viewer.layers: + if l.name == points_layer_name: + viewer.layers.remove(l) + + layers.append((pred_points, dict(name=f'Spots ({image.name})', + **_point_layer2d_default_kwargs), 'points')) + + + progress_bar.hide() + + return layers + + # # ------------------------------------------------------------------------- + + plugin.n_tiles.value = DEFAULTS['n_tiles'] + plugin.label_head.value = '' + + # make labels prettier (https://doc.qt.io/qt-5/qsizepolicy.html#Policy-enum) + for w in (plugin.label_head, plugin.label_nn, plugin.label_nms, plugin.label_adv): + w.native.setSizePolicy(1|2, 0) + + # ------------------------------------------------------------------------- + + widget_for_modeltype = { + Spotiflow: plugin.model2d, + CUSTOM_MODEL: plugin.model_folder, + } + + def widgets_inactive(*widgets, active): + for widget in widgets: + widget.visible = active + + def widgets_valid(*widgets, valid): + for widget in widgets: + widget.native.setStyleSheet("" if valid else "background-color: lightcoral") + + def select_model(key): + nonlocal model_selected + model_selected = key + config = model_configs.get(key) + print(config) + + # allow some widgets to shrink because their size depends on user input + plugin.image.native.setMinimumWidth(240) + plugin.model2d.native.setMinimumWidth(240) + + plugin.label_head.native.setOpenExternalLinks(True) + + layout = plugin.native.layout() + layout.insertStretch(layout.count()-2) + + @change_handler(plugin.use_optimized) + def _thr_change(active: bool): + widgets_inactive( + plugin.prob_thresh, + active=not active + ) + + @change_handler(plugin.model_type, init=False) + def _model_type_change(model_type: Union[str, type]): + selected = widget_for_modeltype[model_type] + for w in set((plugin.model2d, plugin.model_folder)) - {selected}: + w.hide() + selected.show() + # trigger _model_change + selected.changed(selected.value) + + @change_handler(plugin.norm_image) + def _norm_image_change(active: bool): + widgets_inactive( + plugin.perc_low, plugin.perc_high, active=active + ) + + @change_handler(plugin.auto_n_tiles) + def _auto_n_tiles_change(active: bool): + widgets_inactive( + plugin.n_tiles, + active=not active + ) + + return plugin + + diff --git a/napari_spotiflow/_io_hooks.py b/napari_spotiflow/_io_hooks.py new file mode 100644 index 0000000..a5d0f97 --- /dev/null +++ b/napari_spotiflow/_io_hooks.py @@ -0,0 +1,97 @@ +""" + +Simple csv reader populating a custom points layer + +""" +import logging +from pathlib import Path +from typing import Union + +import numpy as np +import pandas as pd +from napari_builtins.io import napari_get_reader as default_napari_get_reader + +COLUMNS = ('z', 'y', 'x') + + +COLUMNS_NAME_MAP_2D = { + 'axis-0' : 'y', + 'axis-1' : 'x', +} + +COLUMNS_NAME_MAP_3D = { + 'axis-0' : 'z', + 'axis-1' : 'y', + 'axis-2' : 'x', +} + + +def _load_and_parse_csv(path, **kwargs): + df = pd.read_csv(path, **kwargs) + df.columns = df.columns.str.lower() + df.columns = df.columns.str.strip() + if 'axis-2' in df.columns: + df = df.rename(columns = lambda n: COLUMNS_NAME_MAP_3D.get(n,n)) + else: + df = df.rename(columns = lambda n: COLUMNS_NAME_MAP_2D.get(n,n)) + + return df + +def _validate_dataframe(df): + return set(COLUMNS[-2:]).issubset(set(df.columns)) + +def _validate_path(path: Union[str, Path]): + """ checks whether path is a valid csv """ + if isinstance(path, str): + path = Path(path) + check = isinstance(path, Path) and \ + path.suffix == ".csv" and \ + _validate_dataframe(_load_and_parse_csv(path)) + + if not check: + logging.warn(f'napari-spotiflow: failed to validate {path}') + + return check + + + + +def napari_get_reader(path): + print(f"opening {path} with napari-spotiflow") + if _validate_path(path): + return reader_function + else: + return default_napari_get_reader + + +def reader_function(path): + from napari_spotiflow import _point_layer2d_default_kwargs + + if not _validate_path(path): + return None + + df = _load_and_parse_csv(path) + + # if 3d + if set(COLUMNS).issubset(set(df.columns)): + # data = df[list(columns)].to_numpy() + data = df[['z','y','x']].to_numpy() + else: + # data = df[list(columns[-2:])].to_numpy() + data = df[['y','x']].to_numpy() + + kwargs = dict(_point_layer2d_default_kwargs) + + return [(data, kwargs, 'points')] + + + +def napari_write_points(path, data, meta): + if data.shape[-1]==2: + df = pd.DataFrame(data[:,::-1], columns=['x','y']) + elif data.shape[-1]==3: + df = pd.DataFrame(data[:,::-1], columns=['x','y','z']) + else: + return None + df.to_csv(path, index=False) + return path diff --git a/napari_spotiflow/_sample_data.py b/napari_spotiflow/_sample_data.py new file mode 100644 index 0000000..d65b0b4 --- /dev/null +++ b/napari_spotiflow/_sample_data.py @@ -0,0 +1,9 @@ +def _test_image_hybiss_2d(): + from spotiflow import sample_data + + return [(sample_data.test_image_hybiss_2d(), {"name": "hybiss_2d"})] + + +def _test_image_terra_2d(): + from spotiflow import sample_data + return [(sample_data.test_image_terra_2d(), {"name": "terra_2d"})] diff --git a/napari_spotiflow/napari.yaml b/napari_spotiflow/napari.yaml new file mode 100644 index 0000000..dc30f07 --- /dev/null +++ b/napari_spotiflow/napari.yaml @@ -0,0 +1,30 @@ +name: napari-spotiflow +display_name: napari-spotiflow +contributions: + commands: + - id: napari-spotiflow.reader + python_name: napari_spotiflow._io_hooks:napari_get_reader + title: open csv data with napari-spotiflow + - id: napari-spotiflow.widget + python_name: napari_spotiflow._dock_widget:plugin_wrapper + title: Spotiflow + - id: napari-spotiflow.data.hybiss_2d + title: HybISS (2D) sample + python_name: napari_spotiflow._sample_data:_test_image_hybiss_2d + - id: napari-spotiflow.data.terra_2d + title: Terra (2D) sample + python_name: napari_spotiflow._sample_data:_test_image_terra_2d + sample_data: + - key: hybiss + display_name: HybISS + command: napari-spotiflow.data.hybiss_2d + - key: terra + display_name: Terra + command: napari-spotiflow.data.terra_2d + readers: + - command: napari-spotiflow.reader + accepts_directories: false + filename_patterns: ["*.csv"] + widgets: + - command: napari-spotiflow.widget + display_name: Spotiflow widget \ No newline at end of file diff --git a/napari_spotiflow/resources/spotiflow_transp_small.png b/napari_spotiflow/resources/spotiflow_transp_small.png new file mode 100644 index 0000000000000000000000000000000000000000..2656f6a921fa391431470eb6535a292f1480da07 GIT binary patch literal 4629 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2315ce --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e62ec1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# The requirements listed here are for development. +# Requirements of your napari plugin should be listed in setup.cfg +# See also: https://caremad.io/posts/2013/07/setup-vs-requirement/ + +-e . diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3da7c4b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,46 @@ +[metadata] +name = napari-spotiflow +version = 0.1.0 +author = Albert Dominguez Mantes, Martin Weigert +author_email = albert.dominguezmantes@epfl.ch, martin.weigert@epfl.ch +url = https://github.com/weigertlab/napari-spotiflow +license = BSD-3-Clause +description = Napari plugin for Spotiflow +long_description = file: README.md +long_description_content_type = text/markdown +classifiers = + Development Status :: 4 - Beta + Intended Audience :: Developers + Framework :: napari + Topic :: Software Development :: Testing + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Operating System :: OS Independent + License :: OSI Approved :: BSD License + +project_urls = + Bug Tracker = https://github.com/weigertlab/napari-spotiflow/issues + Documentation = https://github.com/weigertlab/napari-spotiflow#README.md + Source Code = https://github.com/weigertlab/napari-spotiflow + User Support = https://github.com/weigertlab/napari-spotiflow/issues + +[options] +packages = find: +python_requires = >=3.9, <3.12 + +install_requires = + spotiflow + npe2 + napari + +[options.package_data] +napari-spotiflow = + napari.yaml + resources/* + +[options.entry_points] +napari.manifest = + napari-spotiflow = napari_spotiflow:napari.yaml diff --git a/tests/data/test1.csv b/tests/data/test1.csv new file mode 100644 index 0000000..e667fce --- /dev/null +++ b/tests/data/test1.csv @@ -0,0 +1,8 @@ +index,axis-0,axis-1 +0.0,56.67851654278793,84.35855659341217 +1.0,59.85682811629277,66.24218062443461 +2.0,108.48499519091675,23.33497438211933 +3.0,53.182373811932614,40.49785687904544 +4.0,41.10478983261424,56.07158358921913 +5.0,47.46141297962391,28.42027289972707 +6.0,14.406972615173618,35.4125583614377 diff --git a/tests/data/test2.csv b/tests/data/test2.csv new file mode 100644 index 0000000..a338185 --- /dev/null +++ b/tests/data/test2.csv @@ -0,0 +1,8 @@ +y,x +56.67851654278793,84.35855659341217 +59.85682811629277,66.24218062443461 +108.48499519091675,23.33497438211933 +53.182373811932614,40.49785687904544 +41.10478983261424,56.07158358921913 +47.46141297962391,28.42027289972707 +14.406972615173618,35.4125583614377 diff --git a/tests/example.py b/tests/example.py new file mode 100644 index 0000000..9ed4316 --- /dev/null +++ b/tests/example.py @@ -0,0 +1,31 @@ +import sys +import numpy as np +import napari + +from csbdeep.utils import normalize +import numpy as np +from tqdm import tqdm +import argparse +from spotiflow.model import SpotNet +from spotiflow.data import hybiss_data_2d + +def example_2d(): + x = hybiss_data_2d() + viewer = napari.Viewer() + viewer.add_image(x) + viewer.window.add_plugin_dock_widget('napari-spotiflow') + + +def example_2d_time(): + x = hybiss_data_2d() + x = np.tile(x, (4,1,1)) + viewer = napari.Viewer() + viewer.add_image(x) + viewer.window.add_plugin_dock_widget('napari-spotiflow') + +if __name__ == '__main__': + + viewer = example_2d() + + napari.run() + diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..1bbce12 --- /dev/null +++ b/tests/test_reader.py @@ -0,0 +1,16 @@ +import pytest +from pathlib import Path +from napari_spotiflow._io_hooks import reader_function + + +def test_reader(): + paths = sorted(Path('data').glob('*.csv')) + layers = tuple(reader_function(path) for path in paths) + + assert not any([lay is None for lay in layers]) + return layers + + +if __name__ == '__main__': + + layers = test_reader() diff --git a/tests/test_widget.py b/tests/test_widget.py new file mode 100644 index 0000000..de36a0c --- /dev/null +++ b/tests/test_widget.py @@ -0,0 +1,9 @@ +import napari +from napari_spotiflow._dock_widget import plugin_wrapper + +if __name__ == "__main__": + dock = plugin_wrapper() + + v = napari.Viewer() + v.window.add_dock_widget(dock, area="right") + napari.run() \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..84de77c --- /dev/null +++ b/tox.ini @@ -0,0 +1,38 @@ +# For more information about tox, see https://tox.readthedocs.io/en/latest/ +[tox] +envlist = py{39,310,311}-{linux,macos,windows} + +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + +[gh-actions:env] +PLATFORM = + ubuntu-latest: linux + macos-latest: macos + windows-latest: windows + +[testenv] +platform = + macos: darwin + linux: linux + windows: win32 +passenv = + CI + GITHUB_ACTIONS + DISPLAY + XAUTHORITY + NUMPY_EXPERIMENTAL_ARRAY_FUNCTION + PYVISTA_OFF_SCREEN +deps = + pytest # https://docs.pytest.org/en/latest/contents.html + pytest-xvfb ; sys_platform == 'linux' + # you can remove these if you don't use them + napari + magicgui + pytest-qt + qtpy + pyqt5 +commands = pytest -v --color=yes