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