From 4ee3e3a917995b924e80163952ae3d39972bc863 Mon Sep 17 00:00:00 2001 From: Christian <s224389@dtu.dk> Date: Mon, 20 Jan 2025 10:48:43 +0100 Subject: [PATCH] split code into smaller modules --- .../advancedSettingsWidget.cpython-310.pyc | Bin 0 -> 5054 bytes .../circleEditorGraphicsView.cpython-310.pyc | Bin 0 -> 1926 bytes .../circleEditorWidget.cpython-310.pyc | Bin 0 -> 3005 bytes .../circle_edge_kernel.cpython-310.pyc | Bin 0 -> 1017 bytes .../compute_cost_image.cpython-310.pyc | Bin 0 -> 872 bytes .../compute_disk_size.cpython-310.pyc | Bin 0 -> 411 bytes .../draggableCircleItem.cpython-310.pyc | Bin 0 -> 1477 bytes modules/__pycache__/find_path.cpython-310.pyc | Bin 0 -> 559 bytes .../imageGraphicsView.cpython-310.pyc | Bin 0 -> 11904 bytes .../labeledPointItem.cpython-310.pyc | Bin 0 -> 2781 bytes .../__pycache__/load_image.cpython-310.pyc | Bin 0 -> 331 bytes .../__pycache__/mainWindow.cpython-310.pyc | Bin 0 -> 6552 bytes .../panZoomGraphicsView.cpython-310.pyc | Bin 0 -> 2090 bytes .../preprocess_image.cpython-310.pyc | Bin 0 -> 529 bytes modules/advancedSettingsWidget.py | 160 ++++++ modules/circleEditorGraphicsView.py | 49 ++ modules/circleEditorWidget.py | 91 ++++ modules/circle_edge_kernel.py | 38 ++ modules/compute_cost_image.py | 29 ++ modules/compute_disk_size.py | 4 + modules/downscale.py | 30 ++ modules/draggableCircleItem.py | 33 ++ modules/find_path.py | 17 + modules/imageGraphicsView.py | 464 ++++++++++++++++++ modules/labeledPointItem.py | 71 +++ modules/load_image.py | 4 + modules/main.py | 13 + modules/mainWindow.py | 253 ++++++++++ modules/panZoomGraphicsView.py | 47 ++ modules/preprocess_image.py | 11 + 30 files changed, 1314 insertions(+) create mode 100644 modules/__pycache__/advancedSettingsWidget.cpython-310.pyc create mode 100644 modules/__pycache__/circleEditorGraphicsView.cpython-310.pyc create mode 100644 modules/__pycache__/circleEditorWidget.cpython-310.pyc create mode 100644 modules/__pycache__/circle_edge_kernel.cpython-310.pyc create mode 100644 modules/__pycache__/compute_cost_image.cpython-310.pyc create mode 100644 modules/__pycache__/compute_disk_size.cpython-310.pyc create mode 100644 modules/__pycache__/draggableCircleItem.cpython-310.pyc create mode 100644 modules/__pycache__/find_path.cpython-310.pyc create mode 100644 modules/__pycache__/imageGraphicsView.cpython-310.pyc create mode 100644 modules/__pycache__/labeledPointItem.cpython-310.pyc create mode 100644 modules/__pycache__/load_image.cpython-310.pyc create mode 100644 modules/__pycache__/mainWindow.cpython-310.pyc create mode 100644 modules/__pycache__/panZoomGraphicsView.cpython-310.pyc create mode 100644 modules/__pycache__/preprocess_image.cpython-310.pyc create mode 100644 modules/advancedSettingsWidget.py create mode 100644 modules/circleEditorGraphicsView.py create mode 100644 modules/circleEditorWidget.py create mode 100644 modules/circle_edge_kernel.py create mode 100644 modules/compute_cost_image.py create mode 100644 modules/compute_disk_size.py create mode 100644 modules/downscale.py create mode 100644 modules/draggableCircleItem.py create mode 100644 modules/find_path.py create mode 100644 modules/imageGraphicsView.py create mode 100644 modules/labeledPointItem.py create mode 100644 modules/load_image.py create mode 100644 modules/main.py create mode 100644 modules/mainWindow.py create mode 100644 modules/panZoomGraphicsView.py create mode 100644 modules/preprocess_image.py diff --git a/modules/__pycache__/advancedSettingsWidget.cpython-310.pyc b/modules/__pycache__/advancedSettingsWidget.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7236b63a0e28e1ce6261fd142586150be4764839 GIT binary patch literal 5054 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 literal 0 HcmV?d00001 diff --git a/modules/__pycache__/circleEditorGraphicsView.cpython-310.pyc b/modules/__pycache__/circleEditorGraphicsView.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bf701598b85bf4473791e050917cea81199453b3 GIT binary patch literal 1926 zcmZ`)Pj4JG6u0N!&Q4~NkfbyekQPvZtx#_50U-n=RaFT^2`NPyR<fM2vpdP`%wl^I zvYLoPBK5{+*gfVOaOG?G%Bf$0OL_KeLP)4re*64vKilv3-fzlICqmFJ9{fJ|vxU%K zW^l84F!%<#+ycQ6!#Nt^1Y@H+xifMT*XnNWjr_#7x|au|R?@=g8Dc&QE)Wa2d+sJ7 zYwe+4_!>N+9@d@RXM&E0St?&<`~(cEJG4m3a&*h=>?$+4CFo#4_jA6Pi8SZW6(6O> z)6HgXYnprlf};d8lsGJWg$^)tn0wJqT&sJ`zX%i0>H%wk?!Q7FN&>iFTdy}KJY|_G z#m&oXr>=G12%}$h`7R6;I!9AnJ2MDRjcY_4hW5}U-tM{Dm(`dH9TJiinIc5rBdL7{ z;r1AElCc3-u+oz}KhSIcH>-m&6}(WFShopDb1Eevm+0@G(=Yo!N-ku7b0{*YGFtSv zxI9wjxPM5Cii)F3$V&E=plMbNh^$m4xa^NgR^{;D9Cr7?8{<>ahL?rVr3r3~BRu)& zZSmQ-dR^+k+N);vnQmewCm?D(MKh%Ep>yPj$2FR|VDV;XhR>WDVpd_WI%jU}TK$po zLBr>cd{n#BV219_h!0h(c2#%|{#1J!)o{kl*+Xgp5Q#vy2@Bvl+dAGatAarke&DI< z#X8uplqw5huBfB$`GNY*8g!6BihZlJJ1(Ve4XLC`2^}>MnDo<8CbL2+TBKa~uw9sE z>!ndyNxlQ|l~0cWEn%Y9^TdLg^tmx5ahhl85og4lp`A>LMYDg|h<A*503wDi*FY@d zMJL3bvw}ms@+NX)XBkgEe=7>N&i@Y>2F+_Q{(wOP1_XYdx;D5NAb3%`XI||wqd!y* zXbwQ|u=b`dKyYV9tShi7A0T)EJ^`j_3)cCy&)g$VJYimenSU0*{t)af)n;DZs)PIJ z+&K%u8r7jK9CWxajpsU^c4i1T1p}0e0OisgN*MQo$!}42-ku+H)sT1X!@HU64$EUM z0OGFsw^L1qf6+bRUCItCscu+5lU+esR>^CB231a3w>;=hfRKzQ<3AhG0pGY@wJla4 zfC7VfZN(dCy=7szK-@Fpz7gw2yk~^T5gpNDkn=5`E2{CS4w|F2JEFhpNODCQgD||T zSF16jiW8Y<j0<v1^NQ<$rYRpcd8MP1A?LYGv|iihn0Oy{CLPFH;F>wFB#}up0*TLs z_`rDfnZ1Dr4;?3dnKL4^U+@zmZl~g>u-v$mAA<<}2$Iml*Qtn?VY~>m5L^4QGx_AL ziTUqU1$=8zuVwhKpd$`^7!lI25$G`?zf?4DW-I~OdJyI&x-)0Kl;=4iP0850u@z*C zCxCV40BI^Ogz|~KwIyM!+d%J2JEyzq@y4$D$x^Ots&;jJ_v0fBW$E~8{bnh6Lldj) h?#HNM0CUKVJ6#vF4!>%i_qE9sX~2zvP0er3{Ri|U|0Mtb literal 0 HcmV?d00001 diff --git a/modules/__pycache__/circleEditorWidget.cpython-310.pyc b/modules/__pycache__/circleEditorWidget.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15985fdff54132c56f69d348837b1226bb88fa7d GIT binary patch literal 3005 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 literal 0 HcmV?d00001 diff --git a/modules/__pycache__/circle_edge_kernel.cpython-310.pyc b/modules/__pycache__/circle_edge_kernel.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ee86e4ef66d3ce22af22a0a8c402d0374cd819a3 GIT binary patch literal 1017 zcmZuw&2AGh5VpNPjiMGoi=b5n%mIn6)D+rdR3Surt`N{m6%?7+Nw(SSrnZ-MtBq6% zt{`#c1V>(j$MBU?UIB4n)|*yEFp|gfvorp_@j6>tY7&&sYo7*Rd_sO!&UNw7d4@~h zK_!Tw2|1!75T5Wq&?6@3D-wk>%#njxfV#&jdoByOS8ETKzCcw{L35&-B@t|u2ydNC zsUgR-BqixH!-W5lO<75&p7BcZp6-)352yZ>eQz2V^3^Yc(yOXhNa<f%cgZ(84a=~k zBdT_*eVvpH^Fg2Ol9LA~Yb8Ud0slb>X#&cglH5q(FzV{~T*BF<!Ik0#oDMTB;T=zM zsUc2#_%-@ToJy{e!gc7YYz$9;8!$r&J((J*z+|v}6^d0ak?=OlQ^8dM8;XlK*U?tf z)xY41k0lmq=eDlIxpP=;JlwcI1^P7}<j`)M#=;DNrvipD9t=$cS>NFpNtMb39E&Yi zSf2`Q=qDLBn~+URoTWVZt833;eWaoOdJCQxAY`BC307!{95dLcF}1d%$2)TlFQv&< z`WJy3eT||GJNuC4<4I8iszB-?pUBHzoAaqh{{hpIVjx?mj-C`y6RRm5%<vk?toqp^ zY6D@4iHriv(uoburOLDo99bJEo(`n-^@%dkf(>g1Hmq5q?{M0%zCB9|OV6w?Vr{+? zwT$h59roItLyecz*&8a1$2{%qOFc5#q;t&EoU2i;bgnx_@m`z`x;n>^NZlD{B2RFy zC)=&abmtr<g<bwPT)j2DimGvIku7_^x9Ba>`?NuCGIqg!`=PpB@nx!_eGqvzaFbOb bx-R7aUsEfXu#Mds$kS?K8s9i+kQ?j|Dbp0# literal 0 HcmV?d00001 diff --git a/modules/__pycache__/compute_cost_image.cpython-310.pyc b/modules/__pycache__/compute_cost_image.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e755561615f0c50661393a5a4e40992dd71e5376 GIT binary patch literal 872 zcmZWm%Wl*#6t!dLIcX^cLB$_r(*;X}C{mHwu|Y*B8YyyPcWTB?BHO8$l3CGh`3Be^ zb;Ac`hghKg0xPc5hbnO7bB}MVdwtGrH#RZ^>-&>0iwlC#FMqhZActPUw(kHq;#eV# zON>1aRG>*oCQcO9w4@WKD%4>aPCQhRj>>4_k&1O(#=vu)^6U(k37@@1#l|JnMg<<j znl;S|NLIJaNvlr8Qx2#5b*sDHiHb|RtgJj0{)%cDucT%Rf7<Gd=uBG+Yj1PgwKTO7 zmEa3eErn@>y7Y2>e|q@i_0Q9@y_)*I*DHq@!QO{$-vc-_;LmtX9Uc(G&;>&{1Owst zoU9{9h6u9p5U&$Q2g-wUyiQ#>Btyyxr#w8T>ukurM&Hno?I0dKK<nAy<;HJ{U<|EX zXa+)+>?$R-bOkP>D|I8aOpB2oNu}T_)h!oea@h6_XJ&QjUtPq;2NQbm&o+UF*n`(N z63spq`A9qFj>}nZg{chVvbV<1+spvU^w!TYoQy1UtJ~Hc3vcYAOs8>7G&__H^!9v= zuDpcb*F)lCQn%LmpBKnHg3^Bu>Syx<Xlm#C$3|KwSu=kn?9#Q}`~z!xW|qCNy`4M5 zYS}C*+q>QfI|r@3f_O9X>%uVQ-EK8*|DR5vo!tgVf`n|6giw;<Exbi&V0Iu4W*dLU zqyR-)-m}X|>+X?YzU5@xnA=)*t6dmM!_?&9zy40v`ht_dJMb8CWAgm!;V&?p>?S~t Ka(we`27du!<mD~^ literal 0 HcmV?d00001 diff --git a/modules/__pycache__/compute_disk_size.cpython-310.pyc b/modules/__pycache__/compute_disk_size.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3383772746e94d58575ed8f6aec432708e812f0e GIT binary patch literal 411 zcmZWlze@u#6i#w?rCM4Lii&?g;T9J+5f$p(MP16l5t3Nr{Uk3LD&2H;chk{-DXW8Y zc60LO>QL~5_q}|1c`skm42Qb};SXOHub4lB*hYln5(D;;B$2cvb1D-V$@rPhnWWbw z?X9s!W?v^AzApp|`VV29U`#P!f@F!MZ)`;+9g`Iq6Uh$9JE|ibzEMwMXf#`g)-^>+ zBOmG7dRDhS7D|^X^8*LU<c3S_U_Np!2wtlEjtkqE4`Oz(?dL|EX190@vgy6iU^TC^ zD+PsZ+U$YXj+??6aFAKUg{~JlIO~jptZJkyF^i^ZomIKiP~@N=)p5J@d;fOBCqZyK op=J>LQWF%~PHI=ROVdNuRXSf=6h7bg&o=j+hqpqE)F&hM3*oe5od5s; literal 0 HcmV?d00001 diff --git a/modules/__pycache__/draggableCircleItem.cpython-310.pyc b/modules/__pycache__/draggableCircleItem.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf4a7fe166a02e860231ece0675a8968b64f8c61 GIT binary patch literal 1477 zcmZuw&2Hm15GM6Q$&TG-yJ>3#*`ilna%&I8Vo})J2771_*Juj}1_-T4JIczEm!#5l zpqvt5(Mz8pKISp{8oc(DSLmVbjAUbtHWWDIjKtyS`@Sjr{Vsv=$Kmhu-#kM8L}zzE zjC>1IKL;a-pgCF4m{Qz3xwCL%cj3ie<GZ=PU@@cQga}Xgw?z1o-nelf*n~vE17si( zwFl!9l`PM*RDYM}*;30_MlK@9dgC)$ST-K2N}t1b&e)`QWOraWIP}+GB#EgYu_LH- zFFfJQ-1lTgg)6+<PV5U`uv-!{5r`1h;63q396})-`}Czs=JVurE=QS4^QM4@1IT!q zGR(diA*=yR7pA@hW5^A;qXHkNb`Jmnw6(wXDXHC>aL2f{cSEn4@tQTL$(>)bHQ)t1 zRtKA1V8V+I%<hnY-5ZikyjRoO9grIG4#?U;R43%#JyjHLe{jG=ee3ILDU}U5&x*`& zZXK=^)~FC2%d|8IWC&{mWwZM}Pv%--hPaP-ztTUHmxzGfZ{3N^Woj@IKf}mG7D7)_ zS;(_eXC^C)(RotLrEV`Mbrf1p%Y0_(J4;uVsYGOzwtiaXrLt_9C|MZmE}<0vHBx#{ zZHMzTPqgOzp8WN5`pxtwEtQ^*&Q+#OmK4*MQok|fa(a;zRifThN>_SnRFY=Joa@R| zO6uvN6jctZ_&<xs%az)LpF|q<0L&3(G;~7Rg(q}9>ipw5?C*hpb@Ye^j<=Ae2-J39 zE|kFtrk;TTD#;pzuqM|I5C*hE#NEh(HnIS_hNjs0(yg64Zw)d*J0RB{_<rMq1>bK3 z;(?epv@4Wf6vR(7k}xc*LI4Oq0!fxZ#7L!j@I@Q|&lput!BeQ>m@UdnIb20deFCnf zD(;rK;O(inTgYFxdnU5kjK6~}!F8pMFh^sa;mI!K<}I<GgRegV(*r~Yv`6=w=jw~c zaQtvAMdfY>?r}CqBCIvJ0D(vJG@{MVz=9S3rwsKe#J+^7krqa;4j;R-EfcyM8ly-S z5*nk+d8-xJ_c(uBC3*Ws`H;xEN{U7;3X?at*#1VLsmycEcM3VgPY@J^h2wdR1cl@? z8a~<YG|^M&iI2}#WAoMV*!+@-xioFh*}ZM_q{?=BwYLqAN+p}3nmjw#e&8Z}-u}xA OM6kKLNA`h-1Lt4egKcyG literal 0 HcmV?d00001 diff --git a/modules/__pycache__/find_path.cpython-310.pyc b/modules/__pycache__/find_path.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b19c0f67ced5e24602bf64bf5ab8a63a1e30ea1 GIT binary patch literal 559 zcmYk4y^ho{5Xb$I{kZ5B3MjbFZJI3*G$(|R4mxx~0<MT6MJDzpVY7BLwu?Pw8;%xg zUV&FiOLeb+C~ln9p&M()Gx^8A8B4aSRZ7t8+0XhHCFD;m9vZ{q4Z3@VK@mkevZFbr zL@~v`lbosKBgtmBsFIA1PrwGROW)vLH>CuS`zkW-ZH`Iw3v~AagC_&Mp^9!fq@F5v zL%-317VN+d+_QlUY~YHk<VS+vlEZA6`d!{e==IAFwl&@f*Vxc0@kI+My4HDNH{!YI zC-Gc3t(A~Q2`{16e*G!q<IG{<N3M;^A8$g}?aRs<qbsi!%DmHNOs`}Y^g93x5Zh(* zF|DlgWxJEL9_L4XzKHx1s{G{0$8=ou(l;?l;(^};PVxNT_tkmv*=caaMFTiyD^2lM zyREl<@l~22VH>~&S9p+BYwFSkA3(ceXI1EM-?T=R5xwsB<H`SuBiud3aLSf+$<XL= urT>xzJVN0z<N3;Mr@_}X$iA6wG-=ap55fxnAD6F=JzwLaATEh)q~8I`_>}no literal 0 HcmV?d00001 diff --git a/modules/__pycache__/imageGraphicsView.cpython-310.pyc b/modules/__pycache__/imageGraphicsView.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c7704ad73e53c223bbd845ff2e4e55ab3800bcc6 GIT binary patch literal 11904 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 literal 0 HcmV?d00001 diff --git a/modules/__pycache__/labeledPointItem.cpython-310.pyc b/modules/__pycache__/labeledPointItem.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e7ffced62a9d9085d9c896338dd5bb829dd993b GIT binary patch literal 2781 zcmZ`*TW=dh6rS0;FKL~`KuQ9#%B{;KfYgGh5G@2+A)zQC1*DbHYO}MBH(7h#nQ@a? zo2OLj8!!9;=P`eUpD<5IJmrmNo;YXLPTW`>YtA_{JC`%x`R0uCg@r1CQeFLH@6QS$ z|6pUbSkQO~RcwJ^gwc%jXh3^LU{Guunc1@ft7ivx&k3BK8@RgP%)DMDsA${DszLQJ zVK#G~6XqoJ)C_9O+9AGs0jm<<P|fYfJQ{S<Sp1M>=|Chu%A}_jFMFRPM-sbyOIh1n zNnbhJ-|?a7g5JopoGW|#QQnt6RmQg5jB(Z3;!GN%4Msu&%1B@^%8bO^x0uN+W_O$y zq(hm*+~<|RW*)0NCxOGNtOnX;b+!Q7V>j4M&=uBTi=eA)i7kV!y&x6|>Tsf_YWx)K zCRxI^^0cpgR^$Rve8MTn%@|O=EOoG4s3uhL7YIpC$(Vv?;9Fre0bi7bcV<&k(1L_U zVSqFX3#3&zAZ_Us_9;E7NLTA>LB?KDIioOxj4MTTVpR(lEe5?|tuXoTvR;r=T(ee? z{n~h;Fw2o(-;g(%c}lS@8;H)SaZ)R4<3;9Nj#(;dr9ECgBYLkUcxaqyUSMoD^?@%8 zIF_<^vnW0Y>~DD*WfwP~>xuuGvc+(aaOH(z+D~N|DkD6CdJL7TYRHpb{w&(f5{~$B z8${rUL_+)n4|K_Z3RuCfQ8r9qiUgj7DG-Gtq(L1=XHnWm{sha=`abDI!%SjZ6Ne); zIL07wamhSm3JqkFS_(xRWl6cJ%)?G9^~Z3K3%{-`kz^f3j}$#tc7{<`4v$zm6v~b@ zD=U{rQ0^#vrk!*K5l{LuFb8l9dsnmA>cDpAR8_(-&LSbg@SOa&6W?$DA`&jz8(p3X znMVEg_lY==`JlZY^@kBZ7;-TbZONlJ?eB$RD2F@|?Ox7?8EEa&3=!7{$Gib2#nXyA zAl9fe`FVy%tNP~{7U(){=vJM2v|)^HUFY=rG-NT7dRh=m4AllyAwdAKh_D&i(H0UL zDFdV?`$m(T0QvUG*gPXAK<lvu+5~NbwhFVL2bOpbhz^?H1(MUkT*XUZtrDQ4t>@Ch zo{-Q*65>w2xw!{hL;3ds#|!3r{B=+YDYJv|<jdHs@8-ik1KfU2VyW!Ilu1Y@-6Y-X zN@e!azFG)l2vovD4W#ezH(_2-i&Gv0DOeiyl*gk(;At!aM^o=`qPwbIQl}HeGUxn# zm|2Ps&{_e}f-2B4hgyI)qVFNvbO~B@I(law^Eo!Yggz#gSD?=jX^MznS{MT|HZ<nb zC;;jh1xz`BuZfY+HezqDK-{3;g#_eGY)`M@6fPvF>9sdEHRzKN{QbapHToKJ?t_bP z>va%51e<?^9r{kgjjEIYSCaAx2oHlxv&+|U_@^M?>f?L{pQlz=84?nQJid<eComrC zCC(+sk>BNSiH_c$$9XpFF(#5}K^xPMCOF<80EUD_=VY+P2DbOj72Shc!$N_S#^|%H zT%<D1`=TYgNvmX0OXRIoidMN}D;2Fsw(cB#+&cExH+_pQ!XA7H#pfuHxW1`j;;Y!P zhT<BzZb9FDr~<E}V>on;j#lOl36}xW*h>t;yNu~t9}lk%#uqB>7(3*gZu-WRL+7+~ z<K@kjyH~FJ5M~Qp3z$5=`DD<mvAqp5CA@^NIbaHOqs!<Bq=(XegPdtpOS0j=f?fpI z*Ah>;yz+4ZRp1_lIbhBM_qa5!L8fX9@@v$OA=T&l@aXw%I$E29ZD!ycbO@iGjld&x zJA~f_)2F-%ir+(V4X=7CuHLyvFhGOm(9v6SThEU47x2~WN4*4I&1x8yDIe^57(N|F zS^0%~u%I$|!gSu(X@}!|;K&jF1&XgwAYCBcm5)yh_b&`*9DNsu=)8p~1`j9j8S&u# z@NRhv@N+z~VRHNm&*IIk<866weOvyPG6=Nb2&R6IRc-p|@i4u5)UV!PRi6&n$a$jO j=U?J%TzbYM4I+A^ysyine}mu){4d)eD{#3z+BE(Lu-%fe literal 0 HcmV?d00001 diff --git a/modules/__pycache__/load_image.cpython-310.pyc b/modules/__pycache__/load_image.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e867587aa08a678807752452ad7df52ecea128e GIT binary patch literal 331 zcmYjMJxc>Y5Z&E7lMqa>u<$QjV__*G9+x13Ns$o2W8qkCkIN?a#m=rESX%oVwDd1+ zYh`a`<!ltpz<X~V!_4k$N24LZ`Z{?j?@&Ly*)N9X3IWGRl1N&Slt?CnXOdD$mm~^5 zaYE)f_3~G~4-p9h#z=;|(kJ8%dt<mm=TYE-{Nc>?RILjk)1#Z&Vj54f>qUIOOyb*V zL|xbkvwkP~01tf6@^ij|LIY3MT7glb;gbTkrtSDfG*;-X)nI`eEppYA8CYX=0laRd ztx&60$V}CuEKa*eH~hDO|D?}3rn-+3G40*1-@Uy@Jsh~f#qZ5a9}|4e0XbwpjU7ud literal 0 HcmV?d00001 diff --git a/modules/__pycache__/mainWindow.cpython-310.pyc b/modules/__pycache__/mainWindow.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f202a4a0c3b8081acdb334f6de92fcb4b3009718 GIT binary patch literal 6552 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 literal 0 HcmV?d00001 diff --git a/modules/__pycache__/panZoomGraphicsView.cpython-310.pyc b/modules/__pycache__/panZoomGraphicsView.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c0393a74aa13a847ec4d0a2153cb6cfe456c9f3 GIT binary patch literal 2090 zcmZ`)OK%)S5bo}Icy_(sY!rtC3}y%*F$kF);9?;lv6BEwRyIkDG_o`rrrY+|volL~ zPaLl{r&!93->`f1U+OET+&OWgs>hGDmGr7=s$W&zUwvI$EiAMNw5_GTciVG>{DYm@ z=0fLZ5Oo(6C!7{!Kw~;!F{9XLg)?wtmy$<>JKQ}c+!ge-6MNiyN+SO)>>?2}3!9H* zI^4@MwUvuQGr##XABl~s$g`t}8n&r3TsYe>FgEoYC_!S%Nz8cX4cVcbap$-dLpbgo zli0OmKA$@dV$a$EZ-MPwyUpjpo_j-F5(jXk1#@E~E#s;h%$}&9F&nK>Xk&Z_M6H9; z<TaVlF`ZIM#$>{@v+thT9@swGoQ_E{$6-DRU`<Q6_vcZj7AT0!`VXUy@l-t&(gaD8 zm$^=ocwPy;F4Ns76&DgC$9})Uf%q28Jd<gub}BhYbzYVC%51Naaj*G4FJZ@%s#XHr z4b!sB%U#n(D^WU?+AM&B3!o)l$Fv^2gasUyLRwrUiXGz*Qz=S)LQN}4vLaO~NlwUr zTiNRNb0wtOUfYwo(s^2LuM2gctKs&3TGpvNsHLjaww7rI{t{K|S_-v2sCZq#7vfyS zd1ZJcfo8-9Q5{enia#HIi_Yldjd$!g1L{(SFgqw~=;uBtPG2$3#%zhaa>jH*rjV~G zQ1ZZ)H!)*l_+chW-}aobgFRU3ObO)9h5UG<PeVL!n3vyHwGIz+y%!D;-tcfwh+-w& zg+S2${pl@*k3`jv+_;^VyM<U6g-#7Uf(&PAA&j49nHcKGm3U9%i5Gxz+#Nv#NPV)C zW*Sm?&GsSO>45B^XREH`wzduZ0Qcgc#mr>^?a?mnunrw9zYDXAO_xwZb|DEYuUEln zkkWLY9WZ&*G8oZwr^sRtkn&+}4%w{(cmb{E1#kvvIiv5wUw6Vn?C9{%TI;G@4WA<G zLP$F@G5o47fdwk8UI;03&P5rmgwOg>+xX1}i5WEQ$Kc|=^&kTraE6tVEwoy|-!<S^ zB~5RwsFdKp0Ck%Yi4-!|Y<@O?b;@FN!YtbI1DKRuROe92PoVn|h{B53Wj<{K^Lljj z`MVIj;`b7a2%iUHVSEh6nCug|44QIUQ?TfSA%y$%HhIO)dI=PWi8E#vUN-Tj1SzO} z0Cw&aSc+w0bO(VwMe&;uURSA#REtpWiBM&t6f0pr3M^O_CyBQ*t@DM3zLYm$sDY?! zukr-4U=fO(3xtfvk+fHV`V98o0kJ^!SQmg=M4;}x7pQaZmvFj7x4Yo*65V>IbR!cN zNak6R&0^7ztQ$Tk`5721;X}OmZM++DM}CM3Nh6m)nfZHCR)@3vxNa~1yC_7en)}OT zTz-WbU%=ScAPRjUH7qf{eD8f{9Y(YtwXmL-=|Ch26yT&OfM9o$<gYp{ni*U8<Sh)3 zRFnv_#K>mhwCrZ0D3WBR1lVI@9gxU36k7lS(xpMW)9wU|ft~3`zPYw>w5h*e+0=jJ rd{<~?|4W*h-#MFFtE6a<Ltit2&R>ZMerQhrBUU$skE{npdeQkGc<Kj$ literal 0 HcmV?d00001 diff --git a/modules/__pycache__/preprocess_image.cpython-310.pyc b/modules/__pycache__/preprocess_image.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..32055ff7fcd436221cc6f7f3293f35ea7f856c72 GIT binary patch literal 529 zcmZ8ey-ve05VoC!mXh)(78XXNEIa^0fCLg7OT`jJicD-$w@Dp*j>J#cy25*~@IcuR z1L`X<aZW)(oOI{!JKLY{&U!c;5VWn`m;9O%^5Mu|^N^gP!#)N<1T9EOV@e%n1uMPS zL);gESeet<7rhaQRu_1dM6~VayarHw=CHi0G}J~eoaE=@&fCTN!~7(3?AHPWKKdCt z>|wBELL1UBL7&-^XIbMlR4~MT<Ksi#B?=F+LRD#@N@bA+pz@MOo^iVEw!E!*p&n$) z1+VN_fi(;_qM!{_$#ZFX$jl0cc(Y5SnVwn0!AjAFP-<<*QlzTPUx``6V_&1}Fu4M0 zAUPWw)WhzQa|u(ct7O7wH8<1RKn)41WNMbDP+RO8l2VJhz_l{6GCGq0D2pW=RQK)X z|D>A&xU$w+==sbH5ne{jxV&*=ih_6prb~evRah*j>#XhlWV-=$_qw|Ogb=T{{i9_) O$N1^XmqHTKb@mN5(}sTl literal 0 HcmV?d00001 diff --git a/modules/advancedSettingsWidget.py b/modules/advancedSettingsWidget.py new file mode 100644 index 0000000..071bd68 --- /dev/null +++ b/modules/advancedSettingsWidget.py @@ -0,0 +1,160 @@ +from PyQt5.QtWidgets import ( + QPushButton, QVBoxLayout, QWidget, + QSlider, QLabel, QGridLayout, QSizePolicy +) +from PyQt5.QtGui import QPixmap, QImage +from PyQt5.QtCore import Qt +import numpy as np + + + +class AdvancedSettingsWidget(QWidget): + """ + Shows toggle rainbow, circle editor, line smoothing slider, contrast slider, + plus two image previews (contrasted-blurred and cost). + The images should maintain aspect ratio upon resize. + """ + def __init__(self, main_window, parent=None): + super().__init__(parent) + self._main_window = main_window + + self._last_cb_pix = None # store QPixmap for contrasted-blurred + self._last_cost_pix = None # store QPixmap for cost + + main_layout = QVBoxLayout() + self.setLayout(main_layout) + + # A small grid for controls + controls_layout = QGridLayout() + + # 1) Rainbow toggle + self.btn_toggle_rainbow = QPushButton("Toggle Rainbow") + self.btn_toggle_rainbow.clicked.connect(self._on_toggle_rainbow) + controls_layout.addWidget(self.btn_toggle_rainbow, 0, 0) + + # 2) Circle editor + self.btn_circle_editor = QPushButton("Calibrate Kernel Size") + self.btn_circle_editor.clicked.connect(self._main_window.open_circle_editor) + controls_layout.addWidget(self.btn_circle_editor, 0, 1) + + # 3) Line smoothing slider + label + self._lab_smoothing = QLabel("Line smoothing (3)") + controls_layout.addWidget(self._lab_smoothing, 1, 0) + self.line_smoothing_slider = QSlider(Qt.Horizontal) + self.line_smoothing_slider.setRange(3, 51) + self.line_smoothing_slider.setValue(3) + self.line_smoothing_slider.valueChanged.connect(self._on_line_smoothing_slider) + controls_layout.addWidget(self.line_smoothing_slider, 1, 1) + + # 4) Contrast slider + label + self._lab_contrast = QLabel("Contrast (0.01)") + controls_layout.addWidget(self._lab_contrast, 2, 0) + self.contrast_slider = QSlider(Qt.Horizontal) + self.contrast_slider.setRange(1, 20) + self.contrast_slider.setValue(1) # i.e. 0.01 + self.contrast_slider.setSingleStep(1) + self.contrast_slider.valueChanged.connect(self._on_contrast_slider) + controls_layout.addWidget(self.contrast_slider, 2, 1) + + main_layout.addLayout(controls_layout) + + # We'll set a minimum width so that the main window expands + # rather than overlapping the image + self.setMinimumWidth(350) + + # Now a vertical layout for the two images, each with a label above it + images_layout = QVBoxLayout() + + # 1) Contrasted-blurred label + image + self.label_cb_title = QLabel("Contrasted Blurred Image") + self.label_cb_title.setAlignment(Qt.AlignCenter) + images_layout.addWidget(self.label_cb_title) + + self.label_contrasted_blurred = QLabel() + self.label_contrasted_blurred.setAlignment(Qt.AlignCenter) + self.label_contrasted_blurred.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + images_layout.addWidget(self.label_contrasted_blurred) + + # 2) Cost image label + image + self.label_cost_title = QLabel("Current COST IMAGE") + self.label_cost_title.setAlignment(Qt.AlignCenter) + images_layout.addWidget(self.label_cost_title) + + self.label_cost_image = QLabel() + self.label_cost_image.setAlignment(Qt.AlignCenter) + self.label_cost_image.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + images_layout.addWidget(self.label_cost_image) + + main_layout.addLayout(images_layout) + + def showEvent(self, event): + """ When shown, ask parent to resize to accommodate. """ + super().showEvent(event) + if self.parentWidget(): + self.parentWidget().adjustSize() + + def resizeEvent(self, event): + """ + Keep the images at correct aspect ratio by re-scaling + our stored pixmaps to the new label sizes. + """ + super().resizeEvent(event) + self._update_labels() + + def _update_labels(self): + if self._last_cb_pix is not None: + scaled_cb = self._last_cb_pix.scaled( + self.label_contrasted_blurred.size(), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + ) + self.label_contrasted_blurred.setPixmap(scaled_cb) + + if self._last_cost_pix is not None: + scaled_cost = self._last_cost_pix.scaled( + self.label_cost_image.size(), + Qt.KeepAspectRatio, + Qt.SmoothTransformation + ) + self.label_cost_image.setPixmap(scaled_cost) + + def _on_toggle_rainbow(self): + self._main_window.toggle_rainbow() + + def _on_line_smoothing_slider(self, value): + self._lab_smoothing.setText(f"Line smoothing ({value})") + self._main_window.image_view.set_savgol_window_length(value) + + def _on_contrast_slider(self, value): + clip_limit = value / 100.0 + self._lab_contrast.setText(f"Contrast ({clip_limit:.2f})") + self._main_window.update_contrast(clip_limit) + + def update_displays(self, contrasted_img_np, cost_img_np): + """ + Called by main_window to refresh the two images in the advanced panel. + We'll store them as QPixmaps, then do the re-scale in _update_labels(). + """ + cb_pix = self._np_array_to_qpixmap(contrasted_img_np) + cost_pix = self._np_array_to_qpixmap(cost_img_np, normalize=True) + + self._last_cb_pix = cb_pix + self._last_cost_pix = cost_pix + self._update_labels() + + def _np_array_to_qpixmap(self, arr, normalize=False): + if arr is None: + return None + arr_ = arr.copy() + if normalize: + mn, mx = arr_.min(), arr_.max() + if abs(mx - mn) < 1e-12: + arr_[:] = 0 + else: + arr_ = (arr_ - mn) / (mx - mn) + arr_ = np.clip(arr_, 0, 1) + arr_255 = (arr_ * 255).astype(np.uint8) + + h, w = arr_255.shape + qimage = QImage(arr_255.data, w, h, w, QImage.Format_Grayscale8) + return QPixmap.fromImage(qimage) diff --git a/modules/circleEditorGraphicsView.py b/modules/circleEditorGraphicsView.py new file mode 100644 index 0000000..d3a3517 --- /dev/null +++ b/modules/circleEditorGraphicsView.py @@ -0,0 +1,49 @@ +from PyQt5.QtWidgets import QGraphicsView +from panZoomGraphicsView import PanZoomGraphicsView +from PyQt5.QtCore import Qt +from draggableCircleItem import DraggableCircleItem + +# A specialized PanZoomGraphicsView for the circle editor +class CircleEditorGraphicsView(PanZoomGraphicsView): + def __init__(self, circle_editor_widget, parent=None): + super().__init__(parent) + self._circle_editor_widget = circle_editor_widget + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + # Check if user clicked on the circle item + clicked_item = self.itemAt(event.pos()) + if clicked_item is not None: + # climb up parent chain + it = clicked_item + while it is not None and not hasattr(it, "boundingRect"): + it = it.parentItem() + + if isinstance(it, DraggableCircleItem): + # Let normal item-dragging occur, no pan + return QGraphicsView.mousePressEvent(self, event) + super().mousePressEvent(event) + + def wheelEvent(self, event): + """ + If the mouse is hovering over the circle, we adjust the circle's radius + instead of zooming the image. + """ + pos_in_widget = event.pos() + item_under = self.itemAt(pos_in_widget) + if item_under is not None: + it = item_under + while it is not None and not hasattr(it, "boundingRect"): + it = it.parentItem() + + if isinstance(it, DraggableCircleItem): + delta = event.angleDelta().y() + step = 1 if delta > 0 else -1 + old_r = it.radius() + new_r = max(1, old_r + step) + it.set_radius(new_r) + self._circle_editor_widget.update_slider_value(new_r) + event.accept() + return + + super().wheelEvent(event) diff --git a/modules/circleEditorWidget.py b/modules/circleEditorWidget.py new file mode 100644 index 0000000..aef3101 --- /dev/null +++ b/modules/circleEditorWidget.py @@ -0,0 +1,91 @@ +from PyQt5.QtWidgets import ( + QGraphicsScene, QGraphicsPixmapItem, QPushButton, + QHBoxLayout, QVBoxLayout, QWidget, QSlider, QLabel +) +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt, QRectF, QSize +from circleEditorGraphicsView import CircleEditorGraphicsView +from draggableCircleItem import DraggableCircleItem + +class CircleEditorWidget(QWidget): + def __init__(self, pixmap, init_radius=20, done_callback=None, parent=None): + super().__init__(parent) + self._pixmap = pixmap + self._done_callback = done_callback + self._init_radius = init_radius + + layout = QVBoxLayout(self) + self.setLayout(layout) + + # + # 1) ADD A CENTERED LABEL ABOVE THE IMAGE, WITH BIGGER FONT + # + label_instructions = QLabel("Scale the dot to be of the size of your ridge") + label_instructions.setAlignment(Qt.AlignCenter) + big_font = QFont("Arial", 20) + big_font.setBold(True) + label_instructions.setFont(big_font) + layout.addWidget(label_instructions) + + # + # 2) THE SPECIALIZED GRAPHICS VIEW THAT SHOWS THE IMAGE + # + self._graphics_view = CircleEditorGraphicsView(circle_editor_widget=self) + self._scene = QGraphicsScene(self) + self._graphics_view.setScene(self._scene) + layout.addWidget(self._graphics_view) + + # Show the image + self._image_item = QGraphicsPixmapItem(self._pixmap) + self._scene.addItem(self._image_item) + + # Put circle in center + cx = self._pixmap.width() / 2 + cy = self._pixmap.height() / 2 + self._circle_item = DraggableCircleItem(cx, cy, radius=self._init_radius, color=Qt.red) + self._scene.addItem(self._circle_item) + + # Fit in view + self._graphics_view.setSceneRect(QRectF(self._pixmap.rect())) + self._graphics_view.fitInView(self._image_item, Qt.KeepAspectRatio) + + # + # 3) CONTROLS BELOW + # + bottom_layout = QHBoxLayout() + layout.addLayout(bottom_layout) + + # label + slider + self._lbl_size = QLabel(f"size ({self._init_radius})") + bottom_layout.addWidget(self._lbl_size) + + self._slider = QSlider(Qt.Horizontal) + self._slider.setRange(1, 200) + self._slider.setValue(self._init_radius) + bottom_layout.addWidget(self._slider) + + # done button + self._btn_done = QPushButton("Done") + bottom_layout.addWidget(self._btn_done) + + # Connect signals + self._slider.valueChanged.connect(self._on_slider_changed) + self._btn_done.clicked.connect(self._on_done_clicked) + + def _on_slider_changed(self, value): + self._circle_item.set_radius(value) + self._lbl_size.setText(f"size ({value})") + + def _on_done_clicked(self): + final_radius = self._circle_item.radius() + if self._done_callback is not None: + self._done_callback(final_radius) + + def update_slider_value(self, new_radius): + self._slider.blockSignals(True) + self._slider.setValue(new_radius) + self._slider.blockSignals(False) + self._lbl_size.setText(f"size ({new_radius})") + + def sizeHint(self): + return QSize(800, 600) diff --git a/modules/circle_edge_kernel.py b/modules/circle_edge_kernel.py new file mode 100644 index 0000000..4270fba --- /dev/null +++ b/modules/circle_edge_kernel.py @@ -0,0 +1,38 @@ +import numpy as np + +def circle_edge_kernel(k_size=5, radius=None): + """ + Create a k_size x k_size array whose values increase linearly + from 0 at the center to 1 at the circle boundary (radius). + + Parameters + ---------- + k_size : int + The size (width and height) of the kernel array. + radius : float, optional + The circle's radius. By default, set to (k_size-1)/2. + + Returns + ------- + kernel : 2D numpy array of shape (k_size, k_size) + The circle-edge-weighted kernel. + """ + if radius is None: + # By default, let the radius be half the kernel size + radius = (k_size - 1) / 2 + + # Create an empty kernel + kernel = np.zeros((k_size, k_size), dtype=float) + + # Coordinates of the center + center = radius # same as (k_size-1)/2 if radius is default + + # Fill the kernel + for y in range(k_size): + for x in range(k_size): + dist = np.sqrt((x - center)**2 + (y - center)**2) + if dist <= radius: + # Weight = distance / radius => 0 at center, 1 at boundary + kernel[y, x] = dist / radius + + return kernel \ No newline at end of file diff --git a/modules/compute_cost_image.py b/modules/compute_cost_image.py new file mode 100644 index 0000000..1da0a6a --- /dev/null +++ b/modules/compute_cost_image.py @@ -0,0 +1,29 @@ +from skimage.feature import canny +from scipy.signal import convolve2d +from compute_disk_size import compute_disk_size +from load_image import load_image +from preprocess_image import preprocess_image +from circle_edge_kernel import circle_edge_kernel + +def compute_cost_image(path, user_radius, sigma=3, clip_limit=0.01): + + disk_size = compute_disk_size(user_radius) + + ### Load image + image = load_image(path) + + # Apply smoothing + smoothed_img = preprocess_image(image, sigma=sigma, clip_limit=clip_limit) + + # Apply Canny edge detection + canny_img = canny(smoothed_img) + + # Do disk thing + binary_img = canny_img + kernel = circle_edge_kernel(k_size=disk_size) + convolved = convolve2d(binary_img, kernel, mode='same', boundary='fill') + + # Create cost image + cost_img = (convolved.max() - convolved)**4 # Invert edges: higher cost where edges are stronger + + return cost_img \ No newline at end of file diff --git a/modules/compute_disk_size.py b/modules/compute_disk_size.py new file mode 100644 index 0000000..c4cb24d --- /dev/null +++ b/modules/compute_disk_size.py @@ -0,0 +1,4 @@ +import numpy as np + +def compute_disk_size(user_radius, upscale_factor=1.2): + return int(np.ceil(upscale_factor * 2 * user_radius + 1) // 2 * 2 + 1) \ No newline at end of file diff --git a/modules/downscale.py b/modules/downscale.py new file mode 100644 index 0000000..2387e26 --- /dev/null +++ b/modules/downscale.py @@ -0,0 +1,30 @@ +import cv2 + +# Currently not implemented +def downscale(img, points, scale_percent): + """ + Downsample `img` to `scale_percent` size and scale the given points accordingly. + Returns (downsampled_img, (scaled_seed, scaled_target)). + """ + if scale_percent == 100: + return img, (tuple(points[0]), tuple(points[1])) + else: + # Compute new dimensions + width = int(img.shape[1] * scale_percent / 100) + height = int(img.shape[0] * scale_percent / 100) + new_dimensions = (width, height) + + # Downsample + downsampled_img = cv2.resize(img, new_dimensions, interpolation=cv2.INTER_AREA) + + # Scaling factors + scale_x = width / img.shape[1] + scale_y = height / img.shape[0] + + # Scale the points (x, y) + seed_xy = tuple(points[0]) + target_xy = tuple(points[1]) + scaled_seed_xy = (int(seed_xy[0] * scale_x), int(seed_xy[1] * scale_y)) + scaled_target_xy = (int(target_xy[0] * scale_x), int(target_xy[1] * scale_y)) + + return downsampled_img, (scaled_seed_xy, scaled_target_xy) \ No newline at end of file diff --git a/modules/draggableCircleItem.py b/modules/draggableCircleItem.py new file mode 100644 index 0000000..8d0e6fa --- /dev/null +++ b/modules/draggableCircleItem.py @@ -0,0 +1,33 @@ +from PyQt5.QtWidgets import QGraphicsEllipseItem +from PyQt5.QtGui import QPen, QBrush +from PyQt5.QtCore import Qt + +class DraggableCircleItem(QGraphicsEllipseItem): + def __init__(self, x, y, radius=20, color=Qt.red, parent=None): + super().__init__(0, 0, 2*radius, 2*radius, parent) + self._r = radius + + pen = QPen(color) + brush = QBrush(color) + self.setPen(pen) + self.setBrush(brush) + + # Enable item-based dragging + self.setFlags(QGraphicsEllipseItem.ItemIsMovable | + QGraphicsEllipseItem.ItemIsSelectable | + QGraphicsEllipseItem.ItemSendsScenePositionChanges) + + # Position so that (x, y) is the center + self.setPos(x - radius, y - radius) + + def set_radius(self, r): + old_center = self.sceneBoundingRect().center() + self._r = r + self.setRect(0, 0, 2*r, 2*r) + new_center = self.sceneBoundingRect().center() + diff_x = old_center.x() - new_center.x() + diff_y = old_center.y() - new_center.y() + self.moveBy(diff_x, diff_y) + + def radius(self): + return self._r \ No newline at end of file diff --git a/modules/find_path.py b/modules/find_path.py new file mode 100644 index 0000000..426563d --- /dev/null +++ b/modules/find_path.py @@ -0,0 +1,17 @@ +from skimage.graph import route_through_array + +def find_path(cost_image, points): + + if len(points) != 2: + raise ValueError("Points should be a list of 2 points: seed and target.") + + seed_rc, target_rc = points + + path_rc, cost = route_through_array( + cost_image, + start=seed_rc, + end=target_rc, + fully_connected=True + ) + + return path_rc \ No newline at end of file diff --git a/modules/imageGraphicsView.py b/modules/imageGraphicsView.py new file mode 100644 index 0000000..c2c9847 --- /dev/null +++ b/modules/imageGraphicsView.py @@ -0,0 +1,464 @@ +from scipy.signal import savgol_filter +from PyQt5.QtWidgets import QGraphicsScene, QGraphicsPixmapItem +from PyQt5.QtGui import QPixmap, QColor +from PyQt5.QtCore import Qt, QRectF +import math +import numpy as np +from panZoomGraphicsView import PanZoomGraphicsView +from labeledPointItem import LabeledPointItem +from find_path import find_path + + +class ImageGraphicsView(PanZoomGraphicsView): + def __init__(self, parent=None): + super().__init__(parent) + self.scene = QGraphicsScene(self) + self.setScene(self.scene) + + # Image display + self.image_item = QGraphicsPixmapItem() + self.scene.addItem(self.image_item) + + self.anchor_points = [] # List[(x, y)] + self.point_items = [] # LabeledPointItem + self.full_path_points = [] # QGraphicsEllipseItems for path + self._full_path_xy = [] # entire path coords (smoothed) + + self.dot_radius = 4 + self.path_radius = 1 + self.radius_cost_image = 2 + self._img_w = 0 + self._img_h = 0 + + self._mouse_pressed = False + self._press_view_pos = None + self._drag_threshold = 5 + self._was_dragging = False + self._dragging_idx = None + self._drag_offset = (0, 0) + self._drag_counter = 0 + + # Cost images + self.cost_image_original = None + self.cost_image = None + + # Rainbow toggle => start with OFF + self._rainbow_enabled = False + + # Smoothing parameters + self._savgol_window_length = 7 + + def set_rainbow_enabled(self, enabled: bool): + self._rainbow_enabled = enabled + self._rebuild_full_path() + + def toggle_rainbow(self): + self._rainbow_enabled = not self._rainbow_enabled + self._rebuild_full_path() + + def set_savgol_window_length(self, wlen: int): + if wlen < 3: + wlen = 3 + if wlen % 2 == 0: + wlen += 1 + self._savgol_window_length = wlen + + self._rebuild_full_path() + + # -------------------------------------------------------------------- + # LOADING + # -------------------------------------------------------------------- + def load_image(self, path): + pixmap = QPixmap(path) + if not pixmap.isNull(): + self.image_item.setPixmap(pixmap) + self.setSceneRect(QRectF(pixmap.rect())) + + self._img_w = pixmap.width() + self._img_h = pixmap.height() + + self._clear_all_points() + self.resetTransform() + self.fitInView(self.image_item, Qt.KeepAspectRatio) + + # By default, add S/E + s_x, s_y = 0.15 * self._img_w, 0.5 * self._img_h + e_x, e_y = 0.85 * self._img_w, 0.5 * self._img_h + self._insert_anchor_point(-1, s_x, s_y, label="S", removable=False, z_val=100, radius=6) + self._insert_anchor_point(-1, e_x, e_y, label="E", removable=False, z_val=100, radius=6) + + # -------------------------------------------------------------------- + # ANCHOR POINTS + # -------------------------------------------------------------------- + def _insert_anchor_point(self, idx, x, y, label="", removable=True, z_val=0, radius=4): + x_clamped = self._clamp(x, radius, self._img_w - radius) + y_clamped = self._clamp(y, radius, self._img_h - radius) + + if idx < 0: + # Insert before E if there's at least 2 anchors + if len(self.anchor_points) >= 2: + idx = len(self.anchor_points) - 1 + else: + idx = len(self.anchor_points) + + self.anchor_points.insert(idx, (x_clamped, y_clamped)) + color = Qt.green if label in ("S", "E") else Qt.red + item = LabeledPointItem(x_clamped, y_clamped, + label=label, radius=radius, color=color, + removable=removable, z_value=z_val) + self.point_items.insert(idx, item) + self.scene.addItem(item) + + def _add_guide_point(self, x, y): + # Ensure we clamp properly + x_clamped = self._clamp(x, self.dot_radius, self._img_w - self.dot_radius) + y_clamped = self._clamp(y, self.dot_radius, self._img_h - self.dot_radius) + + self._revert_cost_to_original() + + if not self._full_path_xy: + self._insert_anchor_point(-1, x_clamped, y_clamped, + label="", removable=True, z_val=1, radius=self.dot_radius) + else: + self._insert_anchor_between_subpath(x_clamped, y_clamped) + + self._apply_all_guide_points_to_cost() + self._rebuild_full_path() + + def _insert_anchor_between_subpath(self, x_new, y_new): + # If somehow we have no path yet + if not self._full_path_xy: + self._insert_anchor_point(-1, x_new, y_new) + return + + # Find nearest point in the current full path + best_idx = None + best_d2 = float('inf') + for i, (px, py) in enumerate(self._full_path_xy): + dx = px - x_new + dy = py - y_new + d2 = dx*dx + dy*dy + if d2 < best_d2: + best_d2 = d2 + best_idx = i + + if best_idx is None: + self._insert_anchor_point(-1, x_new, y_new) + return + + def approx_equal(xa, ya, xb, yb, tol=1e-3): + return (abs(xa - xb) < tol) and (abs(ya - yb) < tol) + + def is_anchor(coord): + cx, cy = coord + for (ax, ay) in self.anchor_points: + if approx_equal(ax, ay, cx, cy): + return True + return False + + # Walk left + left_anchor_pt = None + iL = best_idx + while iL >= 0: + px, py = self._full_path_xy[iL] + if is_anchor((px, py)): + left_anchor_pt = (px, py) + break + iL -= 1 + + # Walk right + right_anchor_pt = None + iR = best_idx + while iR < len(self._full_path_xy): + px, py = self._full_path_xy[iR] + if is_anchor((px, py)): + right_anchor_pt = (px, py) + break + iR += 1 + + # If we can't find distinct anchors on left & right, + # just insert before E. + if not left_anchor_pt or not right_anchor_pt: + self._insert_anchor_point(-1, x_new, y_new) + return + if left_anchor_pt == right_anchor_pt: + self._insert_anchor_point(-1, x_new, y_new) + return + + # Convert anchor coords -> anchor_points indices + left_idx = None + right_idx = None + for i, (ax, ay) in enumerate(self.anchor_points): + if approx_equal(ax, ay, left_anchor_pt[0], left_anchor_pt[1]): + left_idx = i + if approx_equal(ax, ay, right_anchor_pt[0], right_anchor_pt[1]): + right_idx = i + + if left_idx is None or right_idx is None: + self._insert_anchor_point(-1, x_new, y_new) + return + + # Insert between them + if left_idx < right_idx: + insert_idx = left_idx + 1 + else: + insert_idx = right_idx + 1 + + self._insert_anchor_point(insert_idx, x_new, y_new, label="", removable=True, + z_val=1, radius=self.dot_radius) + + # -------------------------------------------------------------------- + # COST IMAGE + # -------------------------------------------------------------------- + def _revert_cost_to_original(self): + if self.cost_image_original is not None: + self.cost_image = self.cost_image_original.copy() + + def _apply_all_guide_points_to_cost(self): + if self.cost_image is None: + return + for i, (ax, ay) in enumerate(self.anchor_points): + if self.point_items[i].is_removable(): + self._lower_cost_in_circle(ax, ay, self.radius_cost_image) + + def _lower_cost_in_circle(self, x_f, y_f, radius): + if self.cost_image is None: + return + h, w = self.cost_image.shape + row_c = int(round(y_f)) + col_c = int(round(x_f)) + if not (0 <= row_c < h and 0 <= col_c < w): + return + global_min = self.cost_image.min() + r_s = max(0, row_c - radius) + r_e = min(h, row_c + radius + 1) + c_s = max(0, col_c - radius) + c_e = min(w, col_c + radius + 1) + for rr in range(r_s, r_e): + for cc in range(c_s, c_e): + dist = math.sqrt((rr - row_c)**2 + (cc - col_c)**2) + if dist <= radius: + self.cost_image[rr, cc] = global_min + + # -------------------------------------------------------------------- + # PATH BUILDING + # -------------------------------------------------------------------- + def _rebuild_full_path(self): + for item in self.full_path_points: + self.scene.removeItem(item) + self.full_path_points.clear() + self._full_path_xy.clear() + + if len(self.anchor_points) < 2 or self.cost_image is None: + return + + big_xy = [] + for i in range(len(self.anchor_points) - 1): + xA, yA = self.anchor_points[i] + xB, yB = self.anchor_points[i + 1] + sub_xy = self._compute_subpath_xy(xA, yA, xB, yB) + if i == 0: + big_xy.extend(sub_xy) + else: + if len(sub_xy) > 1: + big_xy.extend(sub_xy[1:]) + + if len(big_xy) >= self._savgol_window_length: + arr_xy = np.array(big_xy) + smoothed = savgol_filter( + arr_xy, + window_length=self._savgol_window_length, + polyorder=2, + axis=0 + ) + big_xy = smoothed.tolist() + + self._full_path_xy = big_xy[:] + + n_points = len(big_xy) + for i, (px, py) in enumerate(big_xy): + fraction = i / (n_points - 1) if n_points > 1 else 0 + color = Qt.red + if self._rainbow_enabled: + color = self._rainbow_color(fraction) + + path_item = LabeledPointItem(px, py, label="", + radius=self.path_radius, + color=color, + removable=False, + z_value=0) + self.full_path_points.append(path_item) + self.scene.addItem(path_item) + + # Keep anchor labels on top + for p_item in self.point_items: + if p_item._text_item: + p_item.setZValue(100) + + def _compute_subpath_xy(self, xA, yA, xB, yB): + if self.cost_image is None: + return [] + h, w = self.cost_image.shape + rA, cA = int(round(yA)), int(round(xA)) + rB, cB = int(round(yB)), int(round(xB)) + rA = max(0, min(rA, h - 1)) + cA = max(0, min(cA, w - 1)) + rB = max(0, min(rB, h - 1)) + cB = max(0, min(cB, w - 1)) + try: + path_rc = find_path(self.cost_image, [(rA, cA), (rB, cB)]) + except ValueError as e: + print("Error in find_path:", e) + return [] + # Convert from (row, col) to (x, y) + return [(c, r) for (r, c) in path_rc] + + def _rainbow_color(self, fraction): + hue = int(300 * fraction) + saturation = 255 + value = 255 + return QColor.fromHsv(hue, saturation, value) + + # -------------------------------------------------------------------- + # MOUSE EVENTS + # -------------------------------------------------------------------- + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton: + self._mouse_pressed = True + self._was_dragging = False + self._press_view_pos = event.pos() + + idx = self._find_item_near(event.pos(), threshold=10) + if idx is not None: + self._dragging_idx = idx + self._drag_counter = 0 + scene_pos = self.mapToScene(event.pos()) + px, py = self.point_items[idx].get_pos() + self._drag_offset = (scene_pos.x() - px, scene_pos.y() - py) + self.setCursor(Qt.ClosedHandCursor) + return + + elif event.button() == Qt.RightButton: + self._remove_point_by_click(event.pos()) + + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if self._dragging_idx is not None: + scene_pos = self.mapToScene(event.pos()) + x_new = scene_pos.x() - self._drag_offset[0] + y_new = scene_pos.y() - self._drag_offset[1] + + r = self.point_items[self._dragging_idx]._r + x_clamped = self._clamp(x_new, r, self._img_w - r) + y_clamped = self._clamp(y_new, r, self._img_h - r) + self.point_items[self._dragging_idx].set_pos(x_clamped, y_clamped) + + self._drag_counter += 1 + # Update path every 4 moves + if self._drag_counter >= 4: + self._drag_counter = 0 + self._revert_cost_to_original() + self._apply_all_guide_points_to_cost() + self.anchor_points[self._dragging_idx] = (x_clamped, y_clamped) + self._rebuild_full_path() + else: + if self._mouse_pressed and (event.buttons() & Qt.LeftButton): + dist = (event.pos() - self._press_view_pos).manhattanLength() + if dist > self._drag_threshold: + self._was_dragging = True + + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + super().mouseReleaseEvent(event) + if event.button() == Qt.LeftButton and self._mouse_pressed: + self._mouse_pressed = False + self.setCursor(Qt.ArrowCursor) + + if self._dragging_idx is not None: + idx = self._dragging_idx + self._dragging_idx = None + self._drag_offset = (0, 0) + newX, newY = self.point_items[idx].get_pos() + self.anchor_points[idx] = (newX, newY) + self._revert_cost_to_original() + self._apply_all_guide_points_to_cost() + self._rebuild_full_path() + else: + # No drag => add point + if not self._was_dragging: + scene_pos = self.mapToScene(event.pos()) + x, y = scene_pos.x(), scene_pos.y() + self._add_guide_point(x, y) + + self._was_dragging = False + + def _remove_point_by_click(self, view_pos): + idx = self._find_item_near(view_pos, threshold=10) + if idx is None: + return + if not self.point_items[idx].is_removable(): + return + + self.scene.removeItem(self.point_items[idx]) + self.point_items.pop(idx) + self.anchor_points.pop(idx) + + self._revert_cost_to_original() + self._apply_all_guide_points_to_cost() + self._rebuild_full_path() + + def _find_item_near(self, view_pos, threshold=10): + scene_pos = self.mapToScene(view_pos) + x_click, y_click = scene_pos.x(), scene_pos.y() + + closest_idx = None + min_dist = float('inf') + for i, itm in enumerate(self.point_items): + d = itm.distance_to(x_click, y_click) + if d < min_dist: + min_dist = d + closest_idx = i + if closest_idx is not None and min_dist <= threshold: + return closest_idx + return None + + # -------------------------------------------------------------------- + # UTILS + # -------------------------------------------------------------------- + def _clamp(self, val, mn, mx): + return max(mn, min(val, mx)) + + def _clear_all_points(self): + for it in self.point_items: + self.scene.removeItem(it) + self.point_items.clear() + self.anchor_points.clear() + + for p in self.full_path_points: + self.scene.removeItem(p) + self.full_path_points.clear() + self._full_path_xy.clear() + + def clear_guide_points(self): + i = 0 + while i < len(self.anchor_points): + if self.point_items[i].is_removable(): + self.scene.removeItem(self.point_items[i]) + del self.point_items[i] + del self.anchor_points[i] + else: + i += 1 + + for it in self.full_path_points: + self.scene.removeItem(it) + self.full_path_points.clear() + self._full_path_xy.clear() + + self._revert_cost_to_original() + self._apply_all_guide_points_to_cost() + self._rebuild_full_path() + + def get_full_path_xy(self): + return self._full_path_xy \ No newline at end of file diff --git a/modules/labeledPointItem.py b/modules/labeledPointItem.py new file mode 100644 index 0000000..ff9e263 --- /dev/null +++ b/modules/labeledPointItem.py @@ -0,0 +1,71 @@ +from PyQt5.QtWidgets import QGraphicsEllipseItem, QGraphicsTextItem +from PyQt5.QtGui import QPen, QBrush, QColor, QFont +from PyQt5.QtCore import Qt +import math + +class LabeledPointItem(QGraphicsEllipseItem): + def __init__(self, x, y, label="", radius=4, color=Qt.red, removable=True, z_value=0, parent=None): + super().__init__(0, 0, 2*radius, 2*radius, parent) + self._x = x + self._y = y + self._r = radius + self._removable = removable + + pen = QPen(color) + brush = QBrush(color) + self.setPen(pen) + self.setBrush(brush) + self.setZValue(z_value) + + self._text_item = None + if label: + self._text_item = QGraphicsTextItem(self) + self._text_item.setPlainText(label) + self._text_item.setDefaultTextColor(QColor("black")) + font = QFont("Arial", 14) + font.setBold(True) + self._text_item.setFont(font) + self._scale_text_to_fit() + + self.set_pos(x, y) + + def _scale_text_to_fit(self): + if not self._text_item: + return + self._text_item.setScale(1.0) + circle_diam = 2 * self._r + raw_rect = self._text_item.boundingRect() + text_w = raw_rect.width() + text_h = raw_rect.height() + if text_w > circle_diam or text_h > circle_diam: + scale_factor = min(circle_diam / text_w, circle_diam / text_h) + self._text_item.setScale(scale_factor) + self._center_label() + + def _center_label(self): + if not self._text_item: + return + ellipse_w = 2 * self._r + ellipse_h = 2 * self._r + raw_rect = self._text_item.boundingRect() + scale_factor = self._text_item.scale() + scaled_w = raw_rect.width() * scale_factor + scaled_h = raw_rect.height() * scale_factor + tx = (ellipse_w - scaled_w) * 0.5 + ty = (ellipse_h - scaled_h) * 0.5 + self._text_item.setPos(tx, ty) + + def set_pos(self, x, y): + """Positions the circle so its center is at (x, y).""" + self._x = x + self._y = y + self.setPos(x - self._r, y - self._r) + + def get_pos(self): + return (self._x, self._y) + + def distance_to(self, x_other, y_other): + return math.sqrt((self._x - x_other)**2 + (self._y - y_other)**2) + + def is_removable(self): + return self._removable \ No newline at end of file diff --git a/modules/load_image.py b/modules/load_image.py new file mode 100644 index 0000000..5bded2a --- /dev/null +++ b/modules/load_image.py @@ -0,0 +1,4 @@ +import cv2 + +def load_image(path): + return cv2.imread(path, cv2.IMREAD_GRAYSCALE) \ No newline at end of file diff --git a/modules/main.py b/modules/main.py new file mode 100644 index 0000000..e1f76db --- /dev/null +++ b/modules/main.py @@ -0,0 +1,13 @@ +import sys +from PyQt5.QtWidgets import QApplication +from mainWindow import MainWindow + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec_()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/modules/mainWindow.py b/modules/mainWindow.py new file mode 100644 index 0000000..5ec3e71 --- /dev/null +++ b/modules/mainWindow.py @@ -0,0 +1,253 @@ +import math +import numpy as np +from scipy.signal import savgol_filter +from PyQt5.QtWidgets import ( + QMainWindow, QPushButton, QHBoxLayout, + QVBoxLayout, QWidget, QFileDialog +) +from PyQt5.QtGui import QPixmap, QImage +from compute_cost_image import compute_cost_image +from preprocess_image import preprocess_image +from advancedSettingsWidget import AdvancedSettingsWidget +from imageGraphicsView import ImageGraphicsView +from circleEditorWidget import CircleEditorWidget + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Test GUI") + + self._last_loaded_pixmap = None + self._circle_calibrated_radius = 6 + self._last_loaded_file_path = None + + # For the contrast slider + self._current_clip_limit = 0.01 + + # Outer widget + layout + self._main_widget = QWidget() + self._main_layout = QHBoxLayout(self._main_widget) + + # The "left" part: container for the image area + its controls + self._left_panel = QVBoxLayout() + + # We'll make a container widget for the left panel, so we can set stretches: + self._left_container = QWidget() + self._left_container.setLayout(self._left_panel) + + # Now we add them to the main layout with 70%:30% ratio + self._main_layout.addWidget(self._left_container, 7) # 70% + + # We haven't added the advanced widget yet, but we'll do so with ratio=3 => 30% + self._advanced_widget = AdvancedSettingsWidget(self) + # Hide it initially + self._advanced_widget.hide() + self._main_layout.addWidget(self._advanced_widget, 3) + + self.setCentralWidget(self._main_widget) + + # The image view + self.image_view = ImageGraphicsView() + self._left_panel.addWidget(self.image_view) + + # Button row + btn_layout = QHBoxLayout() + self.btn_load_image = QPushButton("Load Image") + self.btn_load_image.clicked.connect(self.load_image) + btn_layout.addWidget(self.btn_load_image) + + self.btn_export_path = QPushButton("Export Path") + self.btn_export_path.clicked.connect(self.export_path) + btn_layout.addWidget(self.btn_export_path) + + self.btn_clear_points = QPushButton("Clear Points") + self.btn_clear_points.clicked.connect(self.clear_points) + btn_layout.addWidget(self.btn_clear_points) + + # "Advanced Settings" toggle + self.btn_advanced = QPushButton("Advanced Settings") + self.btn_advanced.setCheckable(True) + self.btn_advanced.clicked.connect(self._toggle_advanced_settings) + btn_layout.addWidget(self.btn_advanced) + + self._left_panel.addLayout(btn_layout) + + self.resize(1000, 600) + self._old_central_widget = None + self._editor = None + + def _toggle_advanced_settings(self, checked): + if checked: + self._advanced_widget.show() + else: + self._advanced_widget.hide() + # Force re-layout + self.adjustSize() + + def open_circle_editor(self): + """ Replace central widget with circle editor. """ + if not self._last_loaded_pixmap: + print("No image loaded yet! Cannot open circle editor.") + return + + old_widget = self.takeCentralWidget() + self._old_central_widget = old_widget + + init_radius = self._circle_calibrated_radius + editor = CircleEditorWidget( + pixmap=self._last_loaded_pixmap, + init_radius=init_radius, + done_callback=self._on_circle_editor_done + ) + self._editor = editor + self.setCentralWidget(editor) + + def _on_circle_editor_done(self, final_radius): + self._circle_calibrated_radius = final_radius + print(f"Circle Editor done. Radius = {final_radius}") + + if self._last_loaded_file_path: + cost_img = compute_cost_image( + self._last_loaded_file_path, + self._circle_calibrated_radius, + clip_limit=self._current_clip_limit + ) + self.image_view.cost_image_original = cost_img + self.image_view.cost_image = cost_img.copy() + self.image_view._apply_all_guide_points_to_cost() + self.image_view._rebuild_full_path() + self._update_advanced_images() + + editor_widget = self.takeCentralWidget() + if editor_widget is not None: + editor_widget.setParent(None) + + if self._old_central_widget is not None: + self.setCentralWidget(self._old_central_widget) + self._old_central_widget = None + + if self._editor is not None: + self._editor.deleteLater() + self._editor = None + + def toggle_rainbow(self): + self.image_view.toggle_rainbow() + + def load_image(self): + options = QFileDialog.Options() + file_path, _ = QFileDialog.getOpenFileName( + self, "Open Image", "", + "Images (*.png *.jpg *.jpeg *.bmp *.tif)", + options=options + ) + if file_path: + self.image_view.load_image(file_path) + + cost_img = compute_cost_image( + file_path, + self._circle_calibrated_radius, + clip_limit=self._current_clip_limit + ) + self.image_view.cost_image_original = cost_img + self.image_view.cost_image = cost_img.copy() + + pm = QPixmap(file_path) + if not pm.isNull(): + self._last_loaded_pixmap = pm + + self._last_loaded_file_path = file_path + self._update_advanced_images() + + def update_contrast(self, clip_limit): + self._current_clip_limit = clip_limit + if self._last_loaded_file_path: + cost_img = compute_cost_image( + self._last_loaded_file_path, + self._circle_calibrated_radius, + clip_limit=clip_limit + ) + self.image_view.cost_image_original = cost_img + self.image_view.cost_image = cost_img.copy() + self.image_view._apply_all_guide_points_to_cost() + self.image_view._rebuild_full_path() + + self._update_advanced_images() + + def _update_advanced_images(self): + if not self._last_loaded_pixmap: + return + pm_np = self._qpixmap_to_gray_float(self._last_loaded_pixmap) + contrasted_blurred = preprocess_image( + pm_np, + sigma=3, + clip_limit=self._current_clip_limit + ) + cost_img_np = self.image_view.cost_image + self._advanced_widget.update_displays(contrasted_blurred, cost_img_np) + + def _qpixmap_to_gray_float(self, qpix): + img = qpix.toImage() + img = img.convertToFormat(QImage.Format_ARGB32) + ptr = img.bits() + ptr.setsize(img.byteCount()) + arr = np.frombuffer(ptr, np.uint8).reshape((img.height(), img.width(), 4)) + rgb = arr[..., :3].astype(np.float32) + gray = rgb.mean(axis=2) / 255.0 + return gray + + def export_path(self): + """ + Exports the path as a CSV in the format: x, y, TYPE, + ensuring that each anchor influences exactly one path point. + """ + full_xy = self.image_view.get_full_path_xy() + if not full_xy: + print("No path to export.") + return + + # We'll consider each anchor point as "USER-PLACED". + # But unlike a distance-threshold approach, we assign each anchor + # to exactly one closest path point. + anchor_points = self.image_view.anchor_points + + # For each anchor, find the index of the closest path point + user_placed_indices = set() + for ax, ay in anchor_points: + min_dist = float('inf') + closest_idx = None + for i, (px, py) in enumerate(full_xy): + dist = math.hypot(px - ax, py - ay) + if dist < min_dist: + min_dist = dist + closest_idx = i + if closest_idx is not None: + user_placed_indices.add(closest_idx) + + # Ask user for the CSV filename + options = QFileDialog.Options() + file_path, _ = QFileDialog.getSaveFileName( + self, "Export Path", "", + "CSV Files (*.csv);;All Files (*)", + options=options + ) + if not file_path: + return + + import csv + with open(file_path, 'w', newline='') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["x", "y", "TYPE"]) + + for i, (x, y) in enumerate(full_xy): + ptype = "USER-PLACED" if i in user_placed_indices else "PATH" + writer.writerow([x, y, ptype]) + + print(f"Exported path with {len(full_xy)} points to {file_path}") + + + + def clear_points(self): + self.image_view.clear_guide_points() + + def closeEvent(self, event): + super().closeEvent(event) \ No newline at end of file diff --git a/modules/panZoomGraphicsView.py b/modules/panZoomGraphicsView.py new file mode 100644 index 0000000..85d5e1a --- /dev/null +++ b/modules/panZoomGraphicsView.py @@ -0,0 +1,47 @@ +from PyQt5.QtWidgets import QGraphicsView, QSizePolicy +from PyQt5.QtCore import Qt + +# A pan & zoom QGraphicsView +class PanZoomGraphicsView(QGraphicsView): + def __init__(self, parent=None): + super().__init__(parent) + self.setDragMode(QGraphicsView.NoDrag) # We'll handle panning manually + self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + self._panning = False + self._pan_start = None + + # Let it expand in layouts + self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + + def wheelEvent(self, event): + """ Zoom in/out with mouse wheel. """ + zoom_in_factor = 1.25 + zoom_out_factor = 1 / zoom_in_factor + if event.angleDelta().y() > 0: + self.scale(zoom_in_factor, zoom_in_factor) + else: + self.scale(zoom_out_factor, zoom_out_factor) + event.accept() + + def mousePressEvent(self, event): + """ If left button: Start panning (unless overridden). """ + if event.button() == Qt.LeftButton: + self._panning = True + self._pan_start = event.pos() + self.setCursor(Qt.ClosedHandCursor) + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """ If panning, translate the scene. """ + if self._panning and self._pan_start is not None: + delta = event.pos() - self._pan_start + self._pan_start = event.pos() + self.translate(delta.x(), delta.y()) + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """ End panning. """ + if event.button() == Qt.LeftButton: + self._panning = False + self.setCursor(Qt.ArrowCursor) + super().mouseReleaseEvent(event) diff --git a/modules/preprocess_image.py b/modules/preprocess_image.py new file mode 100644 index 0000000..403323f --- /dev/null +++ b/modules/preprocess_image.py @@ -0,0 +1,11 @@ +from skimage.filters import gaussian +from skimage import exposure + +def preprocess_image(image, sigma=3, clip_limit=0.01): + # Apply histogram equalization + image_contrasted = exposure.equalize_adapthist(image, clip_limit=clip_limit) + + # Apply smoothing + smoothed_img = gaussian(image_contrasted, sigma=sigma) + + return smoothed_img \ No newline at end of file -- GitLab