From fbd60a231b30c6941f6129f38b8c47e8488eef68 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 27 Jun 2023 14:39:39 +0300 Subject: [PATCH 01/99] Revert "MOD: Add status icons for tracker" This reverts commit e0db774bcf8f3641aad6d0ba834b06b233e61215. --- invesalius/data/viewer_volume.py | 113 +++++++++---------------------- navigation/objects/head.stl | Bin 147134 -> 0 bytes navigation/objects/stylus.stl | Bin 36884 -> 0 bytes 3 files changed, 31 insertions(+), 82 deletions(-) delete mode 100644 navigation/objects/head.stl delete mode 100644 navigation/objects/stylus.stl diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 0466d1068..aa557ddd1 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -255,9 +255,6 @@ def __init__(self, parent): self.aim_actor = None self.dummy_coil_actor = None self.dummy_robot_actor = None - self.dummy_probe_actor = None - self.dummy_ref_actor = None - self.dummy_obj_actor = None self.target_mode = False self.polydata = None self.use_default_object = True @@ -523,89 +520,41 @@ def OnSensors(self, markers_flag): else: colour3 = (1, 0, 0) - self.dummy_probe_actor.GetProperty().SetColor(colour1) - self.dummy_ref_actor.GetProperty().SetColor(colour2) - self.dummy_obj_actor.GetProperty().SetColor(colour3) + self.probe.SetColour(colour1) + self.ref.SetColour(colour2) + self.obj.SetColour(colour3) def CreateSensorID(self): - self.ren_probe = vtkRenderer() - self.ren_probe.SetLayer(1) - - self.interactor.GetRenderWindow().AddRenderer(self.ren_probe) - self.ren_probe.SetViewport(0.01, 0.79, 0.15, 0.99) - filename = os.path.join(inv_paths.OBJ_DIR, "stylus.stl") - - reader = vtkSTLReader() - reader.SetFileName(filename) - mapper = vtkPolyDataMapper() - mapper.SetInputConnection(reader.GetOutputPort()) - - dummy_probe_actor = vtkActor() - dummy_probe_actor.SetMapper(mapper) - dummy_probe_actor.GetProperty().SetColor(1, 1, 1) - dummy_probe_actor.GetProperty().SetOpacity(1.) - self.dummy_probe_actor = dummy_probe_actor - - self.ren_probe.AddActor(dummy_probe_actor) - self.ren_probe.InteractiveOff() - - self.ren_ref = vtkRenderer() - self.ren_ref.SetLayer(1) - - self.interactor.GetRenderWindow().AddRenderer(self.ren_ref) - self.ren_ref.SetViewport(0.01, 0.59, 0.15, 0.79) - filename = os.path.join(inv_paths.OBJ_DIR, "head.stl") - - reader = vtkSTLReader() - reader.SetFileName(filename) - mapper = vtkPolyDataMapper() - mapper.SetInputConnection(reader.GetOutputPort()) - - dummy_ref_actor = vtkActor() - dummy_ref_actor.SetMapper(mapper) - dummy_ref_actor.GetProperty().SetColor(1, 1, 1) - dummy_ref_actor.GetProperty().SetOpacity(1.) - self.dummy_ref_actor = dummy_ref_actor - - self.ren_ref.AddActor(dummy_ref_actor) - self.ren_ref.InteractiveOff() - - self.ren_obj = vtkRenderer() - self.ren_obj.SetLayer(1) - - self.interactor.GetRenderWindow().AddRenderer(self.ren_obj) - self.ren_obj.SetViewport(0.01, 0.39, 0.15, 0.59) - filename = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") - - reader = vtkSTLReader() - reader.SetFileName(filename) - mapper = vtkPolyDataMapper() - mapper.SetInputConnection(reader.GetOutputPort()) - transform = vtkTransform() - transform.RotateZ(180) - transformPD = vtkTransformPolyDataFilter() - transformPD.SetTransform(transform) - transformPD.SetInputConnection(reader.GetOutputPort()) - transformPD.Update() - mapper.SetInputConnection(transformPD.GetOutputPort()) - - dummy_obj_actor = vtkActor() - dummy_obj_actor.SetMapper(mapper) - dummy_obj_actor.GetProperty().SetColor(1, 1, 1) - dummy_obj_actor.GetProperty().SetOpacity(1.) - self.dummy_obj_actor = dummy_obj_actor - - self.ren_obj.AddActor(dummy_obj_actor) - self.ren_obj.InteractiveOff() + probe = vtku.Text() + probe.SetSize(const.TEXT_SIZE_LARGE) + probe.SetPosition((const.X, const.Y)) + probe.ShadowOff() + probe.SetValue("P") + self.probe = probe + self.ren.AddActor(probe.actor) + + ref = vtku.Text() + ref.SetSize(const.TEXT_SIZE_LARGE) + ref.SetPosition((const.X+0.04, const.Y)) + ref.ShadowOff() + ref.SetValue("R") + self.ref = ref + self.ren.AddActor(ref.actor) + + obj = vtku.Text() + obj.SetSize(const.TEXT_SIZE_LARGE) + obj.SetPosition((const.X+0.08, const.Y)) + obj.ShadowOff() + obj.SetValue("O") + self.obj = obj + self.ren.AddActor(obj.actor) + self.UpdateRender() def OnRemoveSensorsID(self): if self.probe: - self.ren_probe.RemoveActor(self.dummy_probe_actor) - self.interactor.GetRenderWindow().RemoveRenderer(self.ren_probe) - self.ren_ref.RemoveActor(self.dummy_ref_actor) - self.interactor.GetRenderWindow().RemoveRenderer(self.ren_ref) - self.ren_obj.RemoveActor(self.dummy_obj_actor) - self.interactor.GetRenderWindow().RemoveRenderer(self.ren_obj) + self.ren.RemoveActor(self.probe.actor) + self.ren.RemoveActor(self.ref.actor) + self.ren.RemoveActor(self.obj.actor) self.probe = self.ref = self.obj = False self.UpdateRender() @@ -1990,7 +1939,7 @@ def ActivateRobotMode(self, robot_mode=None): self.ren_robot.SetLayer(1) self.interactor.GetRenderWindow().AddRenderer(self.ren_robot) - self.ren_robot.SetViewport(0.01, 0.19, 0.15, 0.39) + self.ren_robot.SetViewport(0.02, 0.82, 0.08, 0.92) filename = os.path.join(inv_paths.OBJ_DIR, "robot.stl") reader = vtkSTLReader() diff --git a/navigation/objects/head.stl b/navigation/objects/head.stl deleted file mode 100644 index 70cb941301e10688865c28f4466c45d55ee36ae6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147134 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

nPt1XhZQRgY%V% zpQ=m2F(a1+FSU?}JcpC;<@+i6?7?$Mx9H*S_M&I$xRQpI0f6(xgjuu_PESdvY6oA4 zanNCa6GPCIav|5JEh^Yr`yaBM5u;hnim62o~op=t;*b zaB0?N@JO0SqsCl>^6C|^%DbFyPCPEkW1`b|DEp!b`Z*C4r(hyst2l#`olhTb%oBul zKQ~zZ>oQ%j@u4ClZ38@eoJ@6njzNC6O<=#sleXA?M*0d9r`uGQzyPgV^v2ZxBm(D) zX>m9Fw;KdxT%@rD=M-29TO||X$yu(w^cM4K-H74d8PnKW@jaOEAA7NB6p!8Un&EtL z++-aV@m3K0x*hl_7E+utbjvm%Gy0o->HHFQw1|L75f2o@=lp#Sz$k1fY`uMkPDwwi zz$uuR)i@FwoVen~-OGYF)nE$|lQg)N_b7^YTyc*h@4lv05IX;W0n9&cA*~ds-dhKA z&)s5+MqQQO%oV%(zk9>6%wfEdyS22#i}S@_tm3t4eQh4Q!Gc&Lt?KKeknxs{8{CzJO8|5es-EWAKki) zRwUIcNueKjOt?ZliejL_V=XKj@|v!>bO2Os{h`R`Ce?6@7k)%;T?Zek-ze6aCn<3X zuK9SB5TmolOz;W(Lx0U1K(Q3A$A(3bV01W1F^60fIzm1jVa`tnL*}Y39M@f3Uu9y< zz|}BVoQX4b{B4d?FoA2nxch3g5Nz0M+Pm#CCAI|X!#NR>+A#6}e{!GVeh|k^ z@4YXWemFuq21g4&Y95Y&rPI~;tf=SoQ$z^tc%lQ36^Vdpe6o&^y^29RExj3JuCHU5 zsJXWZ9#{XOQ8CdHEnbvM3WVTg>B^E5o1_xNWg}bFR(&`hkzfol7XEC;spXJaUrlM} zJgLmFPlW90Fc-o;ykIMw+$GO3EfZ6BIYCR42W-1|KMLE92^*9+Y(8eC3*0xh)glb2XLtdZIW_5;@*(W30S z!i|_zmi1w4PB6 zaI+w0o$JZtlk(`7O}#m`1WQ%XTu8Dugt@)`-Z62=^M%^t4Xkmmi{^O616v}me$2}E zhD7%yw#4bU0!v{(@SIV+i(|16nk~7)NZTb8r(goluf=y%Ebq^`Z9a|VDoi~Ex~(`vQ-B9Pw0~bT|Obz1aL%g zdC1XC3cAGf&n)H_k6Te(9#~4QC0h;MV8o6Rc4h5YDOT7Lc^2NxW;vv2rL*ZLTTz^X z2|S+{Gk~uLsqdGMtnX!8>2?jin}aPBcl#V$fX1lj?Dgs)(hVNz=8IIXu8ipnr!vaf zi_v|hTQoRdOp80!L#Khq{uivSAfMux;CnOJC$V2EdbQ?Lo->t zCe4S%o$DFajy;!eB3*xD3nQamF~#m}MK4INrXVmN%dzaw|%T81< zT$eB{6U&C%Laa_trtj8L+SSKVz_fVZ$7&$d&m9O3PbV{cS^!I7-QsmI{T{5#ad+N5 zwwR_2EP+pbGibLz>eBgb`OJ37P#5l)@r3%{&M(DL#5Dfi6{nE&-02pRRs4LM6-zAI z02a6U@}3(tdB0_0kTPQcF9>?TPL{2OXXXR>Vn0(>H!omGDO-Hqy$M!@$TYj{fVQE=7`flm`td2ufWFK329EzRL+ z>IXy*H{(JrU47V^7v*22SZchh*yUgcxQW`ee~3F8tKh3`TXy~D7K(S)vA!LBw!-7M zY`)L@?>oW=PhV$`o;DM2ftkbmt_jfG-Ivc_F-VDbj^+JY&uB9+4s6QZRz0TJUThUU zA0S?6sx^YocgOH6j}sL(W5VH{S`IhbxJR;GCT4u_gtvE(vt#ee6*g{>kbm+hzy3B9 zue|fPP$QYNR8TIgT5q#oAJlBRnZ=3?IS` zDNexzwpOe_s;%Q*lYA9tFKSADV9#+*;syTBmSAmQ$S1ejbOP5BEcNV10u0DZ<{FOG zV!b4NLI9XN4N-jUYCv%cCU8W=2~%+zdu(xM9$eO(51!}(;kq07GVgSzaceVBQ*W+& zDTuw@w*=nY+0V(iKc!-KX}C6ceI3ppd7hF|o&`ri;*nH-ZrLH~b$KrASeVbNlR#LN z5Zw%#dyV72j%7*2ryp?;5g5m-r}U>GjpCr+>12M|@}MBpA5DZ-gPX#Ix_FAEa1B>I z76;ZziG0=ZKtZ^sP3P-(eP&hp;bOn`e|ceACVDA#pi9r*yvgb}l2up-rU_Byts#DO zXKt`jgX8#MLXOqDMnhoGfOjnCoVrBd-Urj-rg!XDYCM7R(j#xBo(%V0gsKVQ!sHjIvy^xT*`AE+*b-ehR%nP6SlLC z&(1TPf(hL75wgwBoPCa)$X8akyWAx|v_Y@OY3ZLW=|B2Mw z0Y2+*VNI`omULh#TsDM^8sGpCv0IqYn$MCSSf5NZw=PiLx*WjMY@0B=Cy4hs<^93% zExP>V&Ifee&A!seY3~#XdW!D+QT_v|E{K1-895nlb3ayitU1SfrugI$K7AqHve-Ti z#$KJxZr)JiSUaZWa~YeOO@iOP>)4^DEu~W-SW3q|2|~B5SB`IJBxV5Lxjl6GF_}%^ zs=V{WWcW7XpfV!z6T<}7C+;*{p9+~R7qf;Kb&m7G1kOpkC)3)ATjg@a!Gtdqr{H{X zPU2+duW?X4Y!4gty``iB6LPGE-CDr+kG7(ncRgUZ3~)V`%Y48eyRh$(1{x3#s3st zrg)DQ?{nduCb3`pVKlUPaGRcTC(^D3Uem{`6yn@c-V`|Vcn~|M)Z{%YyrA;aJ$m-` zF^QIm9+fk=hIlFT_VjRR9S-ZjYgytJ#Dj%!`P6f|C;hSluP5PrW#XMpAw4E;9oh_= zEA3QcZ{=F@!yt)fzO15)>}CMIor&Xy_q@fbns`afO8kXl{Xh$e7<_Uw%#ZY;x(|Z@ z)8d9(<`8I^(2K7N^Pza98T*0#Bc!kY9MBmxS5Yvh5#W6B3L7p>F_RhR0p>-UpWVYvblwfHkPo4Mz+Be?j1x zKK?EdJMM=fXv3ai{M-9(Fim{}IIQyF4u_cZE_@mn_pchilfHW^loNLNg( z)R73R1M3#Os38I88Q!$4-*h17=fIZA6dr%%7GMJF7H@@J*MyA+CQ#?aHXvz7v`pxf zuYoTUby#4kImQ2Eg#W2Ywo3i9Er2+m@_YJk>EEF6=?$3}-K;y;bkc&xT~s-in(wg{ zyg&Ky$8&O|sEe08BW(CUao#6)QX`4Lr$sO=_9iA;Kw!yf#f8m=(tpWdOYq4Nal>uk z0Qhd)j}L9Ok>T^rcvl~vnil&}#qLny7Rjy##z^0sn85nPOlGwcyj%L8^5(dDignB*$-^HtYce~{!nqP89#2-KyiHVDI2-W?Tq_Cw*VC$x=%y= zXWReI-(V>@x;jyY@N!UFexQx2RClo@GEwE>0@of)XQ#@}Q2gQ^e$xrR4=DBr9fp9z zz%&}-V<5d1hTo==-;MHGIKe!p!CldD+j{AxTl|6WNPFZ}O$QQnB3#0Z9VVTxJFxc!gH60xy^;hza z`-=~@u-i|KkKN=-aSGOfb&H;1lMQ4JKg^1I{E!H|6N7b&f9`!aAHIGKWi?kXQJjJa ztWTT+5S}0Gx`TGKE1>waDn7#?w-1LQ?-e^n1oB6Bx0*YM_s(;sT;_WM#(-{iTlf)D z&Tsa1{pa4qnMsRaV&q1erG8TUznV2HYk7wM>8A^<-8AuUlAJq^Rsrwp6uf(#=S#Kz zzP&kaJBq=e0rLb;6G`X&bI}*sFGb=~31BP(v>ScajeYRwkOjw>bfij!n zykt*zhSvb@4T=DbD9s__C^r{-C+Mgzx<@3%r{ygjvC-_@fsqK_*n{UeW{Hndiq;DRBxWa1_Kj zGTRE8eA<=Y)Yz}YDVUHw_pAI*+0ZT!+P{BLierWE$KcWws~@{Q(3SxUchZ8*>*dMzP;(gcP>#iZ!>EV%=meV7e9#teE$dgC-&$^_M)#$*YcD>mlZe# z6IeGP!Sw|+NPRgU(K){qM=_^jA!xri#edun0j!%4txYa;$kR1^y31w-Ca@INEpDZ> zc}P{?x$sRN43xN*xMupnq^l*o)8NHYjS-&fJffeATzJ11t(90RsbLM|o0Rf};miLa z=1zP}&9}PnyBqUMu@v?k*BEi*&}1F-4sFZg^;;@&3MOzA{_ahPfBBnoky;H6E5!ts zIvKMN+%KKtZ8HAeWIbxlsMSVKK1QLT#3`7-F%<9Ov^-6FXL@j*zQ&SOn3jq3cDGtUIc1iPjOZkE@E|YoIl^=WlyvEXew|DCUD%uzOzdJ_y_f1 z^GsSOaSGOla}w*KXEecMp*_Fz<*qrl9ea*x@$Zgx<`C~@!Uufqsl>e~?jLas32||p z3elIg(dQ#oq;rpWl)>LS;yfH%4g=4GGmqyBr7;Qe0ALKJ12>u$*-g*Qm@ER-b zy8?9VU{U0GuJ>}7SQqUYst!8`+j6HOCx%yOvF$h~(FXT^q^1kz^4#+N3`=1hGO_oG zCtN$ag9Ym^m+ofZaaZ=EN3ALU&|IIdw=8A&wp^Wm8YpYRc(L_I>aCXsLpE>acW(R^ zew@*BgwC7Qc-e7vmU3+uWO(o9@!!r!QlH#*!J006c%tJiK`alN#rxU4U~ z$%-#}eFenm81ogHBc(NDtWPG^?w$*-M=F^e50-S`HC@?{+MA=G`iCBmIC zGmjlG@{;dbqVkv5^anm41Fi1r@mT+v45#1{#JdCHW$q*$m^yzbZ~gd;>^a)akk|Cn zNq4XdRqz8V$4UhDNsexQcYiqZz@F7#9jd_hZg88xtw8Jokg<@a(GvDWW=Xe6F@f8P zIDh3}4c7IIpIwYm7Miv28l&)i&pOZ{w6W1rxaZ#hU(0PZ&PGkS1prDe!C? zpCgi2Kc1fdO;fIo0-p&FDNey>mE=BA_riBN;3|dav>NGzDL#{$czP?0s;f5trvE~W zA3Ix%|CjL4;fGWWIbQ$7I|6cxay~s6ZuS1k&h1s@I0X~(zJdL9Ey(!Tz>cW(;!~e5 z6R$LT@`h!F(k>a^Jrj$AMeV`Mhj6QjT{+&b!TT*lUDR){W!%K&?`u{&Z?=W$6~9=7 zmNCaEn2>j6=589mQ_hzuvQ7^JOkgQ&tyrh-k>!0;)yAM4^EtcMQ!QC$T1 zCV@Ebv%?iS2i<0K2R<{$DVV@_1jOCf0s|P=bsYCOU!%Y`6tF&=lUO5LzZ62vkI~={ zgC(o*eFLmd?3job(jtxDu!WyGQ=Ebc*{ZU}1Gs0_a0niKNQqa{uvOSv@vou77sEvR zgY?7CB#QIJ=QrhZuN7wEOs&aqc)RpA#ZvfOr(Aaj{y3zVz0DJ-O0ncQrm=4ERn*B3 zu6r+~%ThN>CrmMceG>Dla~9C$^BMYlaAR&BV*smt^C2@QhvWCb(=XC19{5ckoRheHrsV?j@|=}+XCE`1f(iU` zk2w9Qvlu=NnruGCqgo=c6n^)IkY00FLGH7itjgkDDelAZ>}B4!*-)TYz>8vw6yk*@ ztzP^p?+<5A@1`EX4xpD*!pFF7l=ibeKwaB1OzH+D=&nsk>jXxGRH7a9&sk z&PlvTs$9YAFK?wwyO}dAh39Ruj%#^#;MArizfx7Lz_T#Cf`Dl;_bJwgM}bz{dQ+_e zr(i<s%P!Fl`0RKYtXCAKF0|sd>>mKAE6!*iDJ57tN|WEC{_9$EnM3 zOIW9+2ER%VP=kyW^s3n*7;^Opee1c3&YPJIr@9r>%mM3Y$fUm~YZl%$<()^=~VNYKJ2qRAXzBZ;k?@LtfF4rYus0c?ZtkqTb4*ef2fOh%Kp9}@NmI_y6 zFa8=qUE>-nwj{QJ@@HAFK}Tg|uP!aYI79r0k+ykh`Bn{>;q>=C0M=56Rh}6nR+mk= zX~_a=HLg3Ts2%~VV@%`(+NszGs@@(EM8*0-`i=i!+;<}2670L}Jhk#xgQ9f_fa69; z)gOPR*V~Lw>}DkCz_w$3Vn1q_CCe`x&dcllS|-|m$Wi3bf!uoGFp0oYwK|RI+9zFLN8VAPW5UyG^h1v; zwEI}`PJHYjMZ8BU9h{a2+J!}m`D1R_i+8h%^b4OKp(tj{Z_i+Fm zA8g_4b$99NhY#p1pBO?1)(DtuYRg z1Om4A#EL390E%hp=N<6<(R~_Qa*LjI`0K~-*Y8;CK00vgYA=5Azhc@u@B|HgdQ57m zxCM**IDOmljWId^yIXVIQgPeHIf;5T?GSq!KZ#FWPb31jc}$Bvuhv;?eR*@ZLM$Y! zaKC~hBL4Y(%z5^1MQd2FcZ5V>+c7Op_D8jW58h7nt(`T`>5@@;;~S#}CE4J5v5(^H z{VF=|VIiE1&Mh&CD5PG=IU-hLhjxGy-$zo56f3T~xRvtc?{l=^e3nGuoCvv0o5EYZ ziJmf?!f{?$hr!2Q=EmaR*=k&Jg^q<5t-%+T)At6}9H(Fc*Ed3HwZ75rH*)E?@W~Q^ zrEn_{Z{3?*V7_ff^DUZ9B?3!b-}AuyW%yyLrTzDY8;RIPJFRnosGA>XGkTKF>bQ|+ zjNLEw+Hy~Ax?deU{*SBc4(R!R{x6A&(m*oGN)c%i)%$s6CLx5VP*!$jq*PWSqbNdF zLc_>j)%$ryLZa+dWUs93tlzWWKi_+P{(j!$d3Vow&wJkUp10R>?>qg`WICuz&uD=E z&wXHPA*gs~TWvY{IV7gNWg6i~VAMY$+qeS+7jr^I+zho}+`gK6s zSA>kqZS7fwB~k-;I=aGmDh zI3)+3jk>Sek}P($W;sAtJ8{|`zkoIRV~E!RJR}6Aiv1F=N_IDa78y1B$NtalSam-O zWL6C6YDp24pQ4ODe}=ui><&Jov|-8y5&OC>j*Y!=Sn3;jM)}2Au+T=%*s;_K>8?h3 z&{hyJ!jIOnkC}Dw^W^~&g33Oo9 z3IA$r2F<TeU=IlTu^tB=|~bX`%DQ#<@vo50n4!JU~v?F726Q2i98;&WL-k9JEPaj}*?EX>g3BR9Qc-6AvD3}v%~pf!p2oX-bfpB0V4A@Y&RZfXUa zcw`g%VzFLA(3(W%;Wu7zE$oGA%L!wkElTG(tx3d-GS03)?|?lVETp}UYEDOt$Y!;+ z9wzz>0b6q?PV1tQJpcWJy`O)GEw0!qG=G156eh)&uqm+_j7mk(GGeH86IH{9Q}ENZ zB1Y?_?K3j|6?;E9pS@AlobJ9ya31&XADiUpfs33EGCzlAP}O1wwBC7w&HvIEsvd3o z&uJp<;4-HBWjp`5Y%T z+ZzLTG`P2PnxJ~fh=etJSX;3>yZ`e5+G}*Y(p5ra-;4XshFH1a>Bw)=xt1bmO=4~L zU(aqm)4(v{MWJggU2$X{mTR}Mmgg*Bjkld-m(cZ`TF#2%zHyT(v4tVDs5?Z`oVJ^J z$`_ThO(JXIS99_TDm=s6y7k7E18YmIOYI;TG5OnSHl*G`9=C6TWVg|gPwg(TUL|_7 zp{cJ~{WAfQRJ2}t(i8X2X`}JlrI|c1?jfTigRbXxlfE#w28WsLr_Dl-$TBr6%wur* zy`I!o)Vh~Xkaxa3SMkD$=%#g>(K%t=cO6@Lsva~8IwkoW#4Ylf4BmWmKluKk4$^af z`a=iyVR>yZ(^CJ(daZ4r!GgE-$8XxTr4oveZMc6u&vU;lUzp>Q&neZ>g}YQ9Q{J)h zdU-J0rc{-2;4{lDuJJRR$VOI~JPeCmOBk(-qGd$r=$es_}@GsyZ&ku}Jg}Braxj*yS z@+-1WPEqb8yW`m>d9FcIEm0@AoS9juS!w7Vpu1)FK6hAfaXxeYe|tA-y*os8ugvc> z$`UAoQjOHeWXD?_Vl$fLiq_q?+8w4=Jk#-zu zKfjunubvh?8I0E!OZz!RtN;FC!8u3Rs5(1@R1GJ$Vt?xUK*lj0$=5^s;8nMVsv$qh zS)|=5foQs|2W*&oPra_8IZ$o0GfJ76b3Xg#kONdFVejfQd~n}^_gvTxsfH9mYZCc= zmTKVI{2t(Q$k2%2#m*k7KN*i>IJ;bhXaDDHvZp2gS28)Co&9!TEijgchUyqr0j!yc6bLc9jsXj@qeiY;O#OC(cS};TKH` z<^jDUV4`|ee(30x?4V9l=sYb~s+USDVry&pvg1ty@pYQ`yT-?i*%>#7UEj}1{Af)g zV{h6b_QG~3-tnsiw1nzJTS5HwdwT|p_VdGc6W+4L$^h1Gk113Ip99K|)+GKobgPv= z>PHS!tqXyOjSE@gE)y8v`xMX>ZcmqRHpN#5PS>mH!R+NX7~WgJ-gX`M{jcQ@brE>GUphGr&tl78Y1*678qkw>C&#%C=?TbD{g`$_yhl=gy!4cW^Y zzZfcMPJ2*BOlqsgRI?)SP9HN)c~G4wKSilG$`qEZZlZe=wd4v@{ z@WF=1${1}Qik1^AGrx<0hOT;p4sdcu#VX+@&mj^8enilQk$;X^L4 zgxC)K*nwltQoXc4WFE1<3V2QrYuvl970{AerXSey{l8dPa5iY}Zvb%%%h?ORnzQqu z$p^Wm;ZoInoQafb)5ou@LF-TK;l?bW7Kiwi#Jv%WVH)g%`9O&WMbMfQWnGp9>^d`n zMeXb^tp*f9r4^oSdo$2+OI6(*;UOWYBor+y8m|)z(Gw3L8+*9E_|;?%lUfG zo6Pr_m!vu6N5`1R+|ud*H*Dbu?ZR6k)s{*nBR+IXXImQrMqh6%^&sUT+q(k-mho9z z9l>d6d!(|{U4~lZ;$NA?Hh8_iGhPhccDI&31KN zhcE17{TjQ(-#d=OBYPMUWF`L3{NLV3R~))_iPN@IfvV^1TDU6OPvSxAr8S8=;gk$^ zw?zo_E&i&K?rw<1T41$BHD&ZI7OPRSpKtthopqZs7OGdqFlrT3D>KzXfsLIGGm~i< z0x{6{DNFm{0s|J;md*pzHk1*K;>_XZ$cgNFlnc-jiYRyc#4h^oV}%`SZuP@&USaTF-{yG$(S0JNSDz$ebn*}qaAfG*JUc+Nmk2qHARtQ|} z;Lj)(fw6x5kF!S&g;%w1jFd^Y~GDS7mAw1HU0a zMSrP_d{&na-c{etYwj#g{}mGV25&=o%hfYskwF}ze=T_g>*2gpwcUE<*5SERJMlLQ zsvz{Qm(cwF!H0b2mEm9|@@`V9$m`xXw*F)2(`p@)5zKoQUp#syyvh?iZXDzF?6uuq z#Sh>VO?ivEyGuR!k|wjE&#oDgR8$XIlXx4h+ffzyV?HdoV(*>q&n&Q7T!gQ@Sg;?yg`stxD&Vit>=9s`{ykVcR=m)GjTc`3xKRL_vJEC+`*T z2-cNFz~}3XYim`A);*r-z%o4Rfcp6`q$LzFc7GU@erwO~{;WYXX>gG>{?!?@Zkr-4 zp@1|_B zbNc{L9-H$~{qDS5wsPkeqv1wIm5H(JL06>O{I3H2wIcK2vqt| zbWPyA zVMeE+5D+*5G*gaCS@P(gKf*hhVF2A$)aQS^y`_H?XH7GBz= z1x_1A!Qq~{5)aB-yw#uhmEF~dXCJGFNeD{iYdQ{Yj2s5$BWmIRTHmW;k9S^UwU_jj z5R^*RWBT24zBr*74jIr9DHY`*BXXU#uyt?xtFq_Jf~q7J_?qPfUJiM%X!tqi(tA9Z z*SZ8gS1q8+)&WqksAi=Ovt7$HK1K8Dw$mj9rD`;+3w(Xs8-BRe*l?lirm!aI`}x9k zlcl;A&#VVC{XK!tJp(erA$&FSf3r$$Icbjcs}+@sN-Hwg*ESIuRXW37eJ8Nb7|8l> z8wQ=7FH3sRKc7UFv^L4?s%Nlz<$2-#sQbU4vuM4vXT{BrhWKHl`k$KZ36uJR%1%d& zhy!?WijC-Ugm>2uKw1~&LD9mOF&bc8t{$H<$O7F4Ib$2GrBJ)v7nErO@km@k_rZN8C&2J+Qz4c0Vc)_m|NU%1`ED z^3D_c?l5BWjx>=Zp*qQWOh^G7F}RBTdHs&j{WWjOVk|Dm0;95P5^qJh?%o5F!V=k~ zSYx16R3|E}$Y{NFATFx)i5W*9XAe(g;`JAY;pVbqoT4djMd?0o1Ufc2t5TY1!>1bu zanPE2P|wF7OcoA6E@DT zS$)p;Gvm!|&avjhCrc$1k^T1^t2lKY{7==`qRkXbo>WrE+--w_@}Tv~h&J7e+0{B* z*r{K zLFYW>E&c@<^qUvV{l@;R=?-+nQJrLqY?iUe#6GeSJbmp7w1gt4wBkMI2qXA*!X2z8 zr86o!l|!oOsPp0g>8&dUmopCql_`WO+UC0&|5-KDr(7! z+l&PoSiSlRYh2Vrl8Q<~TUt@juo2$q&=A&jYy`AkI#X#Yh;wZheH?I66LMnP0i~k- zFC!)lsiU%Aki{}LhDtjQ9anPe?v7c+FHb(evP4EVDm&e;=bZounqo)4N$PpF1y?&V{z-h?i? z@7bH*CTM)_7F0iN0KY{q+!y~_i0GZv9;o@Qm+Q;{7UKQg5G;OJrb<2-3Jse-=9QJj zDxZFbp|e;8iz~abMvrT}YSV56q3_HBb&<;}_RlaJ_bocA3P|bz*;ygj%AzR0;~yKT zCUKjQVTtyi;$0t2afeR!?XcT(FMJrb2+B4sRS&N{5vv-X1>KC{81wZ4&pRaYs3?2C z_+h_p8XWWgu!V6GaOIxw{PTq_64mzuBk|#h3;e_En%%j$fePA8Z;y?1ny~S|0x`<} z9Y0dXMMBV;6a~(X$1$4|)DF5&8I_$%LR&;pMyzzlC}E}u52>JFUG=7xq#2;1C({F~!! zSjK|$5?Z{X>+i@;_t9b-Pew?Hy2FJH5qDi(FXz0NY3;%LslDp2x%h2Nxo~wD8$YA2 zxb>VTl~Ba(Sn=+9_hQxPgtG$Sb*}|()yYu}vgn7jgz6;ocpkEY4SrLfm5A)1v|fsk zHGgsQ3qNtE8T%U%AR(T72dLQ7I6uj>2qY`{F@o;=o*2?G|zqE4<#y)Eb3nv^_EnbCUPtk%X0|Tb* zmn+qzC?WeUsuM%B;L^nrQVB&+e&SZYXNKA|F`d0D3kBo*V`0*^a2~e%D%cnIfUBLB zai2j~f&O(MGC@aZVeMAmc-|j-s94bf=0(rv-l0Vhd)g63yRGC(KruM0oMC{w2RFC6 zC3-N^uu^Rw(GCrpxJ&g?DvB2G4LWyaZKI=j`^U48mQa3w>-U3?9a4Gw?lmW2Q~!A0 zHc8+0Y3f`_5=upNs>w_-0k%6l;%#qiRM9gsJ#EW~qigS}SA8)@<43X{6hUZ@i=1( zKmJCm%^nv5tx4FT&cV>4q=;86G2|BdyVx1?wp z@o-uen{u{4yl$!ivu2*>#|PA53I4~y^cd$i$9+&e{BQ!oU9-5G<3`oVTQy$iOL_fR ze1#r9(H?{^@iPB{M$GNTQ3*k75-*E~7-3Ld71P@9CGD1!2dzo0?RyPyN&DxjIJdzP zqOf%&S6=5-TJ8AD4S79jIP-8Se~fvRQs}|pUBUE?hxYL z7@liykffq$Dxt{Ncc4A?pE;Oo4C*emF6BYd;zVD$gg@G!!set-m0Fb6OVJ|l;PTmA zWyJa5L;U)d{p{0=G17>mJmel6vcnJ?%t~b)9`=M~ zxnFp}v!85X#Ae9~>zjX_Z%pHCMS4vXtH;sTeERKdwsO=^7$)+(YU=l5_CIqZelkMK zVUwz`xCVnD4}IUyJ7{NY98QQ?E53tgYu&_iN78qQ3Y74m2r8ktgS=~u zR@+Cg-Us_iBaybWjF>m0CHA{BL!=883A%~3P5H@sye&6!J@BD3e0l0D^*L=Hxpg0! z+~*D29%558h65c9l#2Q_6vexj8LoV_i)~-&BuPajp*4w@_vTI*JW0hTMt71dZ))k& zb`!B`w>(^R1521)y(!>oS)d-Bf0O0J9+PI2ZJ*5iMx|F-Z2jY6v>zL}f@iRMOyw3J zA?Pfk>!Wz*wYh>PX5D5M^ZNs>i`t3uz1oG}mFhY_-Z0xGff9n&OVPsiiSbt11~YiL z%}SEp%lVdi+Sh)}JNc|+mkRspbE0bS+1~JSO>3Z3bX}2oxPEcSH+*f6O&U8PMNp|I zT157{wc)uz@hsPEmb6~cHA?0YSZtkNM=yn~;j@9Z38j+n?iLg{V7P@LGudh{c~7ao zjQTYcrT4fFIKgiu3oz*>`B*7}p1j39PFD|HulrKf@kI;C0-$q6_CUr*+F-%i)ojuD zPLfm!oW6osZ%tnpgE1s14wct2fZ| zKycP@$eAYgEYJHO|KT?L;AC!o<{wY}I1FhCMO2xMh7yw^UiU-I87Hju&in?owIDTg zq;x8x2+CWmS5x$$R{Gccd+x)Ko@?mIl%hq(#ZkbqULk8>Jqak4=k|86rL_)DS$!X5 z#H9_JxoZ_?pQn$N5Y!Jt(c-jiS)c7_uLmoR4*{wnt=DzJG-!YOx%dw8p3uD0g^oPJ zbpnsRFb9+Z9oW&!0LS;c1N@ym7SW!=JDa%N7kyz5I*35ACw;`XKzAGvDcDY zpkrOi?$`IiUYD;y>mRRJSez5qy;mxDEE+mpb!2`AK3_XZLX3<$&rWCd$Esh~B($(a zH!o*Br&wc`7#k_)u37j9*n4*v&i;Hy&VZ|QzGlPW%|;QA8iG_RD#>Jc!eaV$MYB;g zKm9ptXsZjJ0(ToSA82IO^Jx7P z9yLJQ4J~1-nn)%O?uS=&!^KpZ~caKUvZCs64?S6(EPTNr%va_!EhkEM+rGUg!G=;j zz9OgMKR$qKPxtX#XD;#0H%9?IFVj17iWU)#`ak*M_kZ%&E*k^a=k|kL1vk{%H||OG znj4LWoBnxfN6*JXb1Ms1U^KFfdn$`p5`2+D(^6~%Uq!aoOO z@;yZ}r5h!>a#EW}MBRT}sXlt{Cp&s+G!|;Nfa!BOfYJ2h@XfS2?4H;Vvicr`+hI-N z)+bYV?^W~WXXS%dtm@zoZr<7-_LUn$zaKrJD2Pi4T9bGgRykE=+B=h9Sr{r&(P+aK z&ssxlzjMrZPR;qlGIa~@tJ46x8}^anMQJpqjA%Bm2m3w87Cwrfx_ukW0?*1FZ1_6_ zn*WOaQY%JcL2nqc|2S_ouP)NeNO2cyP(Q3^)_hVS_B0{4yu}cD+h`e5p-theBa%L+_TjbpyQR! zDEgbIXshuh>~xARcAI^OQO^za!_Z$*g&r^di1jX(U2*l1cE}&?W`XAA5Pc*TN-xXWcA&}bSeJJm^eyyEw= z3cVdF&43V~S($ouh=tNLKlQ+~Z=`=z6-BGMC5(NOq7MAtM?z2@a;B;3Ppwq&FttlPpp2fC2^-ZQMB-jHZW$b*7U~u)6As5Ua3?v;?KTBek8n* zU${6{>T`;qtswfNNhN>b+bI8ZyKz8QGD`LF+jJ<68>lvT^Hj{JLtC%&=lWjU;*gA> zV~nCj7LU`n`HW^SSl2iGz|DRr+^ouGdmiUX<8|Qbfl%R+%QA-6tZ)|2wcyi<^ZePa z-bhQnX$^YRlZb{qI}-`kWSE&jw{M4~ z)mi4zsa^~2zw9tiQBRZBUAl5=G-?TZN0`9j_!?i|*jLN*UmbbJP8|+H%7fM`Bi3yl z$1i(l@cYB2N>WiCa%9Z~vz6+HE{FLzznMVmq6o@cybaH4hg)23vYA)gN){5eoRSWX z0t4SquAb9x3O%&{nL@)PS3IzNHKQdIL3>mDNAItR{hl9U&3bxBD>dC~X-(pX?sr;f z?3v3tyzL2e7p5bIj%jfxe7GrU&bL*Mp5Z0k<51gB=CNbLAHHw$5B4#ln=}%sRI(mb z_>E5(S;gR-t0WagPzgnroBa*hMXPH5q@O=%bg_bKb-F>qm{Za?miP0Gf1OlKHJ|Yb zmE$D@wG-qX%+ST1q8LWpJS$ua}?; z>efS%zRsg(+?$UssDnJdv5OWpG4y)%zo~nrz2;fR0;RWh6kR!bm)hRt2YYqC2JzmX zvF3|5@%EaPuz39oyLFO`Nb?Mh9fPB*W&DLCIhZnAn7Mo>bUFtc3We0 z%Z`)bq)WKEslOgfjOOt1T6X^H`VHaZv(r*!gve&qU>n~yb*Zr- z=o+3DJ=k$+2y3%R#LAQmm3YwIfR0d+@nl^$W;vlA&IuSIA*du2EzX=!i_hQPhv(=< zNSad#(=1J0pKddR+3q!wXP1=~+;rDLb@}o+5`t3Eb`y5V#~y6*`(?btmryBQl#WE& zZsPa4K{NP3jssdk5i*atGktkaxqs9vzQwA}&5xGi0H}wJo<+nxXYh74*K5EM)lm|HdeSIbQR=FvvXzB@ zSwNHF(usxovUa_h3z}zNb4`~Tf64Revus{>d-!yrx#aPpUM?BoHPRCnAFl_qJ(3tL zsnDAZGYW3<3fF5AZ$&B6>Vzo|oN#|tH#X(kOiXEB3rFS{z^{`*81h>OGn<-0)XX4! zY_5ei^Q?qa3-<-#{GfDx@ZEk!OVaCh$3~qTaZJE8ps_}Z(yeF|+8cD`EyNj}mQVzZ z783E)=F9l#-qpNYxeu&wD*oXamX0|AmtpwJ#!!%Z0?nFU0BfyhEO6X5T;WyYhs)85 z=ZgnzQjd8v2L?Rd${bg(#j4505>?qx3rOOd@V(Amf%w(afz`G9%f4ieg!{E>GbMI0 z9v^p6LeQGTdjpXv#CXPLUT|-WgrE}2Qt5wu#F}jGg43e(q`$A6EmaV8D;Ev+o(K95 zfrzs3*vm@(w8IXDHqy^o6hVK{5|JEkyYnqP>tN!-Q9!Bazgx836lF1%t6e{o@b;d; zlC}~R`FBfE25+cT{}|E=L&x?8+KQ645+ZTdHD-A$o$dQP3h$Y0U}Jx7!!@HWNWB^G zXder^pN2hpT&fxEN!R(~7yJ3v6~0pIQYzZgB5rBZ4gUI84jO~4 zLGcgot<${iBxjtw*$9$8Yk`6KI8JGO9s(;qvbe8@(5iVM+%qp{XWJe^pO-b!ZIyLW zxYO2_I5n}mG>a&L)+F>87=($ZU+}{we^qofaH#gf#mAc9%Rw#vvoajnIs|_On(*u} zU7&M&d7cf{DFa;AI$XN{6t|WRS{O2_CwiUaoFb^U6fN3n(Ll7Y(c&JF4S|+WguIqy zJI{yC`mI>@@yT>>pL4 zb$2@Pz!V)xD$0-6Br@F=_+j&m+Wcpizl>5*o#c2?t1bQzYH&$4sSOn+S+Rhz*W0mn(-_bftMnzSyXy<){3x9*Hm=FIIBjBgeA~(zsy^v( zil9_GbSqix%S`q=r6zx3T;GmxcdjvuT;weApj7l7tjNsW+5&=?^kV7=S1GGXm!(%( zWmyzs2af{fCvLE+E~$4XPR9*@Pe?CW>0L9`N%)q67P6+he0Z2%n8br3C_hD6Ry>=h zEegVh#l@VyMWy%L6fHc$SD)}R-*2dzRF9M504RdK;TCtPU0d^)YqR+(s~J)b5o(di zKK<(Id+Ko~>Z5a^kA$F9Co`L?y|a2FTL4fR8)9S&ufyz{FM;eotS)B7bE|_afqyTJcp24UQ`=WK7{aP|n zdQE=2y9ONXlgzvev!yl^x5#zAFq6Qm?2h6gAt(=u7I89#?P1DWTL#1B7wc3KiWXL% z@aI4OwSkpa^#JOvquwvs!!rDdx%z0>4z~JanBoI=VDdOy`rSs;v~yna8e1uUe87Bv1F0G-Q>V# z&1u7K<SYfr{^;|3>5iVFWkjE`tZJHyiy-y~=Su z{OSLX^@%XSe2v>u><5hr5z$m>vCP-DKDWIx8)*rR5RvcC`>t-FUJ;nZSBp1Tw1i4R zd5iuSaY?npsuoU3@sp&Ydh~AkishMh#9nqaksPVbr>H*!Jk0Y;mivPuWFC)yPGj+B zR=NJth>-N463Pg#Hw|I1mmT(RAuFEB<^kxDIH$B+#YE*M4Gpl=C%}fcLV#W2I2=c|6U-_e4jM8zMT7W&zc;TeWNQpU(f<* z)=Qe}Ue18ov?y71anlJlqQ?v=Q#Q>!P4j??|DC^$#W{U6RadTjW>j99i&xIS+c4?` zb8j~u4~$RX6hWzIwnvd4F7~0iwRfs&^XX`5k1D>T1xjZTz2lxC?JJ71t?H6$z2~p| ztvh0n#`sVK-R;C4)i#PNE7JLp8B?SPA(}~t)+B7W_)W~z#sY4Bv6uF3y1!C8NZ1U? z<~aOh6TTwIO?uTv<1FR4*WgFz)bCbC;zliJM*l@k3LD1K+zN5vswMyVhqvNYT|7J3 z1&{cYvx~1?RQdU5@m56?Jd5A2%3ZS$-L&@rJ%uaEO?bv5ERFGYGi%TY^kj}n2k`3W zgW%c4g%8}Y5z{X0lF*9c_WUL9^2H1-F13YgFAuUTmjl=*hf4@rlURL%uk)v_4p=&` zF|6n~i>1dM#E=O`B(#jEb@U|<|Eq)H54uS8QhwB9A#z7A|H+rHwnwX&x{|z951%KG z)N?d;h+z?hwrK62D?T{{tN1jk~RN6_C9ynsfVLG zIZ6H`+E&!BA@+~s+wv24OoPN0;w5&*1@_G4AQnwHBk`kXu~K)q%FpFF;xdcIVE8zl znU6h;+OtjqMaziR?+SVOyKXor#7OeJPz0?>ykm*4P@P#71+`EAP*E!CxsmlKiMqfO z9u31IVTFwTAx!;#vx^Miw%-LT7_(0F;NScE`QBCmcy1GCw1gt)OcVK*-VMMYewZz3 z{!QXR(J}&b6y6~(49_I3XS810&oW}z@Y;OY*f1Q?Zi9rNeNNHB*XJ;m*WNw}5A8U? zBr4+lk`bMKGFU|UJ-)xYFVc}n`+$y05%psJmX$vXV|nfTkW$f^DkFC3m$EsQPT&}0 zC?V*ar)XjI>144Bb|L8f?vaZA$}F`T`5jt}#MJ9N%ESTxCCMX^jy}py`1+KyeEeuv ztec}Rc?apZqWTGc$=&C?bY=tG{?A_Wa?+JSo`WaO8e+$)Bm5jNX>F$n>Ms?UJfB|V z0qbh>EgdEU-2tf8N3Be8qv)fHSHhjqe(rV2FPh;ukneI!!?2wxl2=vyB^0=Uzb(&X z9gj?aJ6^~6vQew?S->V(wr2y6U%nRKPuVK1uHv@uogof7dW1i+W|CKy>OmzGnJFe{ zVngAxj0h=|q@pV-l~CN#SKQ#2wF1zwAeqteLlN>G^)0Oy7BWM z#SR>JZX?J%Iuz8!$EB{Ax8g3N2)dqAwD3$!(Z<;cE_k5mE~6zBF*<4%_q9yL-#s?h z>;`SMaLjjS{QbRLLQpDMs*@9SF<^l+-sy0i(GrTFesN(lxHrJo5tewtx3<)`>6t!! zMCoSa-T&Kz9Wz$&4%Q{C`Q|Y|>!k>pM^@oAmO6Se%r4VbQ*9}N)}$z8aE%81izR;mWH^jSlN@_;PSn>6y;0(aPqIA3y=HZxC?i<<+t_H>O=28=uL|7&<_j6$>X!R z`Ar)&y}PN_?2n_y^~J<7O{BXJkpuhW7%Z)Gg&Q_r%_tSEm)?blT$5b`aPy54LuP<&PezM-8jFai8aAg1h!UVZASPlwQJ9s{Erp zVPT^&>KWH-yis%9o8gW1n^Xpq`b!8J%}@CW3%SWJUdy7Nt92v!^?(!u`+OdJOnsX_ zQRk)L;c#6O7CA0d^B$v+QqkzxUJ{}*R||LfSh1D2eWVu(l#1#kV(6bX!2BHwSm|^C zT0#*%)+4~g)ll6iq~>)^xq%jzN55j7FFOHk6N;7*hmA+zDZPCx{OvtPUvM1l-VUnN zP54-CIsbr&-mxBunX@`DE3N>gqHm07&nn8WL2lUm#@hTgZ`w<*AZUNch>z2pRHgX} z46pWwkcP3~TRur;yYdOpNMahxE&jp|MBcfDK99?r0(5p#w2b&|SIM8x%2C@j7O^-a zrkV1fEiKmFi79;fQv)8W5hlIIp_D+7jWHSVuGTYtHdSQIZeB-ypt;!83o!rrFCUA^l* z8=weUFGY*L8f`n^n^I&8=UD(Pp$NHk zcl4=?#bs^a&OHO5no8|$eIr?P8*G)dyEBGGM+H#UnDw-3JW=vC*SG}CD$JI0H z*d}&T-a(3{yhZD-8i^)bVpLuJ)d8BXi{9kZ?1CcNaMoI`(fKvs{&xgWAJVJMZYpKt zMNH0|3-XCxXXFR|>Sj~)`_&GA{r$ETLrt zn%`n?6g`}5*BvQ>>Opyn8{SDPRZ&eGu!l)k%zqgQXU=HiP3(nHA0|RZhzS;i*h>;PN5k^1NE9Z>B*%xQS!fPAy12mW8%_rfY_>Xkmmc9%vj26S1UOuW>v9V%Y zUA^51P9AdOfjd1>yE+sM%7>^|)?Y56WyGL5BQQUDHM24Qz$k+9puEN3%k{c~)vpD- zl}meE*M175t#Ney`#AwCZ%v2bR998+y$O8j^`wFkPwsy3C>im1Ye%5@U`QS-2V8@VMykwKDg!pl9Jv2Cff_vsK zlF*{hJFj5z!8!mLUZCNS0xlUT{AS`3sjZ@v6nK$d%0DfgC3xHoUjUkY7O9T<56us& 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 u^}Sz$PmsV|Lha($ From 993b10f2bb58ff0bff4ec68043eec389da44e800 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 3 Jul 2023 16:50:35 +0300 Subject: [PATCH 02/99] ADD: New menu for imports section in NAV mode --- invesalius/gui/task_imports.py | 284 +++++++++++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 invesalius/gui/task_imports.py diff --git a/invesalius/gui/task_imports.py b/invesalius/gui/task_imports.py new file mode 100644 index 000000000..40588620b --- /dev/null +++ b/invesalius/gui/task_imports.py @@ -0,0 +1,284 @@ +#-------------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br +# Contact: invesalius@cti.gov.br +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) +#-------------------------------------------------------------------------- +# Este programa e software livre; voce pode redistribui-lo e/ou +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme +# publicada pela Free Software Foundation; de acordo com a versao 2 +# da Licenca. +# +# Este programa eh distribuido na expectativa de ser util, mas SEM +# QUALQUER GARANTIA; sem mesmo a garantia implicita de +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais +# detalhes. +#-------------------------------------------------------------------------- +import wx +try: + import wx.lib.agw.foldpanelbar as fpb +except ModuleNotFoundError: + import wx.lib.foldpanelbar as fpb +from invesalius.pubsub import pub as Publisher + +import invesalius.constants as const +import invesalius.gui.data_notebook as nb +import invesalius.session as ses +import invesalius.gui.task_exporter as exporter +import invesalius.gui.task_slice as slice_ +import invesalius.gui.task_importer as importer +import invesalius.gui.task_surface as surface +import invesalius.gui.task_tools as tools +import invesalius.gui.task_navigator as navigator + + +def GetCollapsedIconData(): + return \ +b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x01\x8eIDAT8\x8d\xa5\x93-n\xe4@\x10\x85?g\x03\n6lh)\xc4\xd2\x12\xc3\x81\ +\xd6\xa2I\x90\x154\xb9\x81\x8f1G\xc8\x11\x16\x86\xcd\xa0\x99F\xb3A\x91\xa1\ +\xc9J&\x96L"5lX\xcc\x0bl\xf7v\xb2\x7fZ\xa5\x98\xebU\xbdz\xf5\\\x9deW\x9f\xf8\ +H\\\xbfO|{y\x9dT\x15P\x04\x01\x01UPUD\x84\xdb/7YZ\x9f\xa5\n\xce\x97aRU\x8a\ +\xdc`\xacA\x00\x04P\xf0!0\xf6\x81\xa0\xf0p\xff9\xfb\x85\xe0|\x19&T)K\x8b\x18\ +\xf9\xa3\xe4\xbe\xf3\x8c^#\xc9\xd5\n\xa8*\xc5?\x9a\x01\x8a\xd2b\r\x1cN\xc3\ +\x14\t\xce\x97a\xb2F0Ks\xd58\xaa\xc6\xc5\xa6\xf7\xdfya\xe7\xbdR\x13M2\xf9\ +\xf9qKQ\x1fi\xf6-\x00~T\xfac\x1dq#\x82,\xe5q\x05\x91D\xba@\xefj\xba1\xf0\xdc\ +zzW\xcff&\xb8,\x89\xa8@Q\xd6\xaaf\xdfRm,\xee\xb1BDxr#\xae\xf5|\xddo\xd6\xe2H\ +\x18\x15\x84\xa0q@]\xe54\x8d\xa3\xedf\x05M\xe3\xd8Uy\xc4\x15\x8d\xf5\xd7\x8b\ +~\x82\x0fh\x0e"\xb0\xad,\xee\xb8c\xbb\x18\xe7\x8e;6\xa5\x89\x04\xde\xff\x1c\ +\x16\xef\xe0p\xfa>\x19\x11\xca\x8d\x8d\xe0\x93\x1b\x01\xd8m\xf3(;x\xa5\xef=\ +\xb7w\xf3\x1d$\x7f\xc1\xe0\xbd\xa7\xeb\xa0(,"Kc\x12\xc1+\xfd\xe8\tI\xee\xed)\ +\xbf\xbcN\xc1{D\x04k\x05#\x12\xfd\xf2a\xde[\x81\x87\xbb\xdf\x9cr\x1a\x87\xd3\ +0)\xba>\x83\xd5\xb97o\xe0\xaf\x04\xff\x13?\x00\xd2\xfb\xa9`z\xac\x80w\x00\ +\x00\x00\x00IEND\xaeB`\x82' + +def GetCollapsedIconBitmap(): + return wx.Bitmap(GetCollapsedIconImage()) + +def GetCollapsedIconImage(): + from io import BytesIO + stream = BytesIO(GetCollapsedIconData()) + return wx.Image(stream) + +def GetExpandedIconData(): + return \ +b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x10\x00\x00\x00\x10\x08\x06\ +\x00\x00\x00\x1f\xf3\xffa\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\ +\x00\x01\x9fIDAT8\x8d\x95\x93\xa1\x8e\xdc0\x14EO\xb2\xc4\xd0\xd2\x12\xb7(mI\ +\xa4%V\xd1lQT4[4-\x9a\xfe\xc1\xc2|\xc6\xc2~BY\x83:A3E\xd3\xa0*\xa4\xd2\x90H!\ +\x95\x0c\r\r\x1fK\x81g\xb2\x99\x84\xb4\x0fY\xd6\xbb\xc7\xf7>=\'Iz\xc3\xbcv\ +\xfbn\xb8\x9c\x15 \xe7\xf3\xc7\x0fw\xc9\xbc7\x99\x03\x0e\xfbn0\x99F+\x85R\ +\x80RH\x10\x82\x08\xde\x05\x1ef\x90+\xc0\xe1\xd8\ryn\xd0Z-\\A\xb4\xd2\xf7\ +\x9e\xfbwoF\xc8\x088\x1c\xbbae\xb3\xe8y&\x9a\xdf\xf5\xbd\xe7\xfem\x84\xa4\ +\x97\xccYf\x16\x8d\xdb\xb2a]\xfeX\x18\xc9s\xc3\xe1\x18\xe7\x94\x12cb\xcc\xb5\ +\xfa\xb1l8\xf5\x01\xe7\x84\xc7\xb2Y@\xb2\xcc0\x02\xb4\x9a\x88%\xbe\xdc\xb4\ +\x9e\xb6Zs\xaa74\xadg[6\x88<\xb7]\xc6\x14\x1dL\x86\xe6\x83\xa0\x81\xba\xda\ +\x10\x02x/\xd4\xd5\x06\r\x840!\x9c\x1fM\x92\xf4\x86\x9f\xbf\xfe\x0c\xd6\x9ae\ +\xd6u\x8d \xf4\xf5\x165\x9b\x8f\x04\xe1\xc5\xcb\xdb$\x05\x90\xa97@\x04lQas\ +\xcd*7\x14\xdb\x9aY\xcb\xb8\\\xe9E\x10|\xbc\xf2^\xb0E\x85\xc95_\x9f\n\xaa/\ +\x05\x10\x81\xce\xc9\xa8\xf6>\x13\xc0n\xff{PJ\xc5\xfdP\x11""<\xbc\ +\xff\x87\xdf\xf8\xbf\xf5\x17FF\xaf\x8f\x8b\xd3\xe6K\x00\x00\x00\x00IEND\xaeB\ +`\x82' + +def GetExpandedIconBitmap(): + return wx.Bitmap(GetExpandedIconImage()) + +def GetExpandedIconImage(): + from io import BytesIO + stream = BytesIO(GetExpandedIconData()) + return wx.Image(stream) + + +class TaskPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + inner_panel = InnerTaskPanel(self) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(inner_panel, 1, wx.EXPAND|wx.GROW|wx.BOTTOM|wx.RIGHT | + wx.LEFT, 0) + sizer.Fit(self) + + self.SetSizer(sizer) + self.Fit() + self.Update() + self.SetAutoLayout(1) + + +class InnerTaskPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + default_colour = self.GetBackgroundColour() + background_colour = wx.Colour(255,255,255) + self.SetBackgroundColour(background_colour) + + # Create horizontal sizer to represent lines in the panel + txt_sizer = wx.BoxSizer(wx.HORIZONTAL) + + # Fold panel which contains navigation configurations + fold_panel = FoldPanel(self) + fold_panel.SetBackgroundColour(default_colour) + + # Add line sizer into main sizer + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(txt_sizer, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT, 5) + main_sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT, 5) + main_sizer.AddSpacer(5) + main_sizer.Fit(self) + + self.SetSizerAndFit(main_sizer) + self.Update() + self.SetAutoLayout(1) + + self.sizer = main_sizer + + +class FoldPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + inner_panel = InnerFoldPanel(self) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(inner_panel, 0, wx.EXPAND|wx.GROW) + sizer.Fit(self) + + self.SetSizerAndFit(sizer) + self.Update() + self.SetAutoLayout(1) + + +class InnerFoldPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + try: + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, + (10, 330), 0, fpb.FPB_SINGLE_FOLD) + + image_list = wx.ImageList(16,16) + image_list.Add(GetExpandedIconBitmap()) + image_list.Add(GetCollapsedIconBitmap()) + + + self.enable_items = [] + self.overwrite = False + + style = fpb.CaptionBarStyle() + style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) + style.SetFirstColour(default_colour) + style.SetSecondColour(default_colour) + + tasks = [(_("Load data"), importer.TaskPanel), + (_("Select region of interest"), slice_.TaskPanel), + (_("Configure 3D surface"), surface.TaskPanel), + (_("Export data"), exporter.TaskPanel)] + + style = fpb.CaptionBarStyle() + style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) + style.SetFirstColour(default_colour) + style.SetSecondColour(default_colour) + + for i in range(len(tasks)): + (name, panel) = tasks[i] + + # Create panel + item = fold_panel.AddFoldPanel("%d. %s"%(i+1, name), + collapsed=True, + foldIcons=image_list) + fold_panel.ApplyCaptionStyle(item, style) + col = style.GetFirstColour() + + # Add panel to FoldPanel + fold_panel.AddFoldPanelWindow(item, + panel(item), + #Spacing= 0, + leftSpacing=0, + rightSpacing=0) + + # All items, except the first one, should be disabled if + # no data has been imported initially. + # if i != 0: + # self.enable_items.append(item) + + # If it is related to mask, this value should be kept + # It is used as reference to set mouse cursor related to + # slice editor. + if name == _("Select region of interest"): + self.__id_slice = item.GetId() + elif name == _("Configure 3D surface"): + self.__id_surface = item.GetId() + + fold_panel.Expand(fold_panel.GetFoldPanel(0)) + self.fold_panel = fold_panel + self.image_list = image_list + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND) + self.sizer = sizer + self.SetSizerAndFit(sizer) + self.SetStateProjectClose() + self.__bind_events() + + def __bind_events(self): + self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) + Publisher.subscribe(self.OnEnableState, "Enable state project") + Publisher.subscribe(self.OnOverwrite, 'Create surface from index') + Publisher.subscribe(self.OnFoldSurface, 'Fold surface task') + Publisher.subscribe(self.OnFoldExport, 'Fold export task') + + def SetStateProjectClose(self): + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) + for item in self.enable_items: + item.Disable() + + def SetStateProjectOpen(self): + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) + for item in self.enable_items: + item.Enable() + + def OnFoldPressCaption(self, evt): + id = evt.GetTag().GetId() + closed = evt.GetFoldStatus() + + if id == self.__id_slice: + Publisher.sendMessage('Retrieve task slice style') + Publisher.sendMessage('Fold mask page') + elif id == self.__id_surface: + Publisher.sendMessage('Fold surface page') + else: + Publisher.sendMessage('Disable task slice style') + + + evt.Skip() + wx.CallAfter(self.ResizeFPB) + + def ResizeFPB(self): + sizeNeeded = self.fold_panel.GetPanelsLength(0, 0)[2] + self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded )) + self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) + + def OnOverwrite(self, surface_parameters): + self.overwrite = surface_parameters['options']['overwrite'] + + def OnFoldSurface(self): + if not self.overwrite: + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(2)) + + def OnFoldExport(self): + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(3)) + + def OnEnableState(self, state): + if state: + self.SetStateProjectOpen() + else: + self.SetStateProjectClose() From 4319bac5d6030ca2e0653ff18af20fc1a3d2b781 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 3 Jul 2023 16:51:47 +0300 Subject: [PATCH 03/99] MOD: Change current menu items based on new menu --- invesalius/gui/default_tasks.py | 114 +++++++++++++++----------------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index e664cdcde..b44930bdc 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -33,6 +33,7 @@ import invesalius.gui.task_surface as surface import invesalius.gui.task_tools as tools import invesalius.gui.task_navigator as navigator +import invesalius.gui.task_imports as imports FPB_DEFAULT_STYLE = 2621440 @@ -106,10 +107,11 @@ def __init__(self, parent): #sizer = wx.BoxSizer(wx.VERTICAL) gbs = wx.GridBagSizer(5,5) #sizer.Add(UpperTaskPanel(self), 5, wx.EXPAND|wx.GROW) - gbs.Add(UpperTaskPanel(self), (0, 0), flag=wx.EXPAND) - + self.uppertaskpanel = UpperTaskPanel(self) + gbs.Add(self.uppertaskpanel, (0, 0), flag=wx.EXPAND) + self.lowertaskpanel = LowerTaskPanel(self) #sizer.Add(LowerTaskPanel(self), 3, wx.EXPAND|wx.GROW) - gbs.Add(LowerTaskPanel(self), (1, 0), flag=wx.EXPAND | wx.ALIGN_BOTTOM) + gbs.Add(self.lowertaskpanel, (1, 0), flag=wx.EXPAND | wx.ALIGN_BOTTOM) gbs.AddGrowableCol(0) @@ -124,6 +126,30 @@ def __init__(self, parent): #self.SetSizerAndFit(sizer) self.SetSizer(sizer) self.Layout() + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self.SetNavigationMode, "Set navigation mode") + + def SetNavigationMode(self, status): + self.navigation_mode_status = status + session = ses.Session() + if status: + session.SetConfig('mode', const.MODE_NAVIGATOR) + else: + Publisher.sendMessage('Hide target button') + session.SetConfig('mode', const.MODE_RP) + self.gbs.Hide(self.uppertaskpanel) + self.uppertaskpanel.Destroy() + self.uppertaskpanel = UpperTaskPanel(self) + self.gbs.Add(self.uppertaskpanel, (0, 0), flag=wx.EXPAND) + self.Layout() + self.Refresh() + project_status = session.GetConfig('project_status') + if project_status != const.PROJECT_STATUS_CLOSED: + Publisher.sendMessage('Load project data') + Publisher.sendMessage("Enable state project", state=True) + # Lower fold panel @@ -224,7 +250,6 @@ def ResizeFPB(self): self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded )) self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) - # Upper fold panel class UpperTaskPanel(wx.Panel): def __init__(self, parent): @@ -254,19 +279,21 @@ def __init__(self, parent): ] elif mode == const.MODE_NAVIGATOR: - tasks = [(_("Load data"), importer.TaskPanel), - (_("Select region of interest"), slice_.TaskPanel), - (_("Configure 3D surface"), surface.TaskPanel), - (_("Export data"), exporter.TaskPanel), + tasks = [(_("Imports Section"), imports.TaskPanel), (_("Navigation system"), navigator.TaskPanel)] for i in range(len(tasks)): (name, panel) = tasks[i] # Create panel - item = fold_panel.AddFoldPanel("%d. %s"%(i+1, name), - collapsed=True, - foldIcons=image_list) + if mode == const.MODE_RP: + item = fold_panel.AddFoldPanel("%d. %s"%(i+1, name), + collapsed=True, + foldIcons=image_list) + else: + item = fold_panel.AddFoldPanel("%s"%name, + collapsed=True, + foldIcons=image_list) style = fold_panel.GetCaptionStyle(item) col = style.GetFirstColour() @@ -285,10 +312,11 @@ def __init__(self, parent): # If it is related to mask, this value should be kept # It is used as reference to set mouse cursor related to # slice editor. - if name == _("Select region of interest"): - self.__id_slice = item.GetId() - elif name == _("Configure 3D surface"): - self.__id_surface = item.GetId() + if mode == const.MODE_RP: + if name == _("Select region of interest"): + self.__id_slice = item.GetId() + elif name == _("Configure 3D surface"): + self.__id_surface = item.GetId() fold_panel.Expand(fold_panel.GetFoldPanel(0)) self.fold_panel = fold_panel @@ -298,17 +326,18 @@ def __init__(self, parent): sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND) self.sizer = sizer self.SetSizerAndFit(sizer) - - self.SetStateProjectClose() self.__bind_events() def __bind_events(self): - self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) - Publisher.subscribe(self.OnEnableState, "Enable state project") - Publisher.subscribe(self.OnOverwrite, 'Create surface from index') - Publisher.subscribe(self.OnFoldSurface, 'Fold surface task') - Publisher.subscribe(self.OnFoldExport, 'Fold export task') - Publisher.subscribe(self.SetNavigationMode, "Set navigation mode") + session = ses.Session() + mode = session.GetConfig('mode') + if mode == const.MODE_RP: + self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) + Publisher.subscribe(self.OnEnableState, "Enable state project") + Publisher.subscribe(self.OnOverwrite, 'Create surface from index') + Publisher.subscribe(self.OnFoldSurface, 'Fold surface task') + Publisher.subscribe(self.OnFoldExport, 'Fold export task') + # Publisher.subscribe(self.SetNavigationMode, "Set navigation mode") def OnOverwrite(self, surface_parameters): self.overwrite = surface_parameters['options']['overwrite'] @@ -326,45 +355,6 @@ def OnEnableState(self, state): else: self.SetStateProjectClose() - def SetNavigationMode(self, status): - self.navigation_mode_status = status - name = _("Navigation system") - panel = navigator.TaskPanel - session = ses.Session() - if status and (self.fold_panel.GetCount()<=4): - # Create panel - item = self.fold_panel.AddFoldPanel("%d. %s"%(5, name), - collapsed=True, - foldIcons=self.image_list) - style = self.fold_panel.GetCaptionStyle(item) - col = style.GetFirstColour() - - # Add panel to FoldPanel - self.fold_panel.AddFoldPanelWindow(item, - panel(item), - #Spacing=0, - leftSpacing=0, - rightSpacing=0) - self.enable_items.append(item) - if not self.fold_panel.GetFoldPanel(2).IsEnabled(): - item.Disable() - - session.SetConfig('mode', const.MODE_NAVIGATOR) - - elif status and (self.fold_panel.GetCount() > 4): - self.fold_panel.GetFoldPanel(4).Show() - - session.SetConfig('mode', const.MODE_NAVIGATOR) - else: - Publisher.sendMessage('Hide target button') - self.fold_panel.GetFoldPanel(4).Hide() - - # Setting mode to MODE_RP (default) - session.SetConfig('mode', const.MODE_RP) - - self.sizer.Layout() - - def SetStateProjectClose(self): self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) for item in self.enable_items: From 6dc9e82fd24d6f5edebc766dc845a1172972bdd8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 3 Jul 2023 16:52:36 +0300 Subject: [PATCH 04/99] FIX: Load data for new menu --- invesalius/control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invesalius/control.py b/invesalius/control.py index 82d7931ac..bc9bb4a5b 100644 --- a/invesalius/control.py +++ b/invesalius/control.py @@ -127,6 +127,8 @@ def __bind_events(self): Publisher.subscribe(self.enable_mask_preview, 'Enable mask 3D preview') Publisher.subscribe(self.disable_mask_preview, 'Disable mask 3D preview') Publisher.subscribe(self.update_mask_preview, 'Update mask 3D preview') + + Publisher.subscribe(self.LoadProject, 'Load project data') def SetBitmapSpacing(self, spacing): proj = prj.Project() From 6ba7836e4470c32fdcde716ddee3b973d3e26be2 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 3 Jul 2023 16:53:11 +0300 Subject: [PATCH 05/99] FIX: Remove duplicate markers on update --- invesalius/gui/data_notebook.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/data_notebook.py b/invesalius/gui/data_notebook.py index 403c7bdbe..fd290ea44 100644 --- a/invesalius/gui/data_notebook.py +++ b/invesalius/gui/data_notebook.py @@ -578,8 +578,9 @@ def InsertNewItem(self, index=0, label=_("Mask"), threshold="(1000, 4500)", colo def AddMask(self, mask): image_index = len(self.mask_list_index) - self.mask_list_index[image_index] = mask.index - self.InsertNewItem(image_index, mask.name, str(mask.threshold_range), mask.colour) + if mask.index not in self.mask_list_index: + self.mask_list_index[image_index] = mask.index + self.InsertNewItem(image_index, mask.name, str(mask.threshold_range), mask.colour) def EditMaskThreshold(self, index, threshold_range): self.SetItem(index, 2, str(threshold_range)) From 8db6e891c9a6cb455876d3eb2fe018edd541eeed Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:24:53 +0300 Subject: [PATCH 06/99] ADD: head icon for fiducial panel --- icons/head.png | Bin 0 -> 3150 bytes invesalius/gui/task_imports.py | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 icons/head.png diff --git a/icons/head.png b/icons/head.png new file mode 100644 index 0000000000000000000000000000000000000000..44a100dc3d883332da128c3936bff6f30ba7e7f6 GIT binary patch literal 3150 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 literal 0 HcmV?d00001 diff --git a/invesalius/gui/task_imports.py b/invesalius/gui/task_imports.py index 40588620b..c97849677 100644 --- a/invesalius/gui/task_imports.py +++ b/invesalius/gui/task_imports.py @@ -32,6 +32,9 @@ import invesalius.gui.task_surface as surface import invesalius.gui.task_tools as tools import invesalius.gui.task_navigator as navigator +import invesalius.gui.task_tractography as tractography +import invesalius.gui.task_efield as efield + def GetCollapsedIconData(): @@ -181,7 +184,9 @@ def __init__(self, parent): tasks = [(_("Load data"), importer.TaskPanel), (_("Select region of interest"), slice_.TaskPanel), (_("Configure 3D surface"), surface.TaskPanel), - (_("Export data"), exporter.TaskPanel)] + (_("Export data"), exporter.TaskPanel), + (_("Tractography"), tractography.TaskPanel), + (_("E-Field"), efield.TaskPanel)] style = fpb.CaptionBarStyle() style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) From 12abe813604e81298a18401b071776331885b5ab Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:25:08 +0300 Subject: [PATCH 07/99] ADD: new task panels for import secton --- invesalius/gui/task_efield.py | 324 ++++++++++++++++ invesalius/gui/task_tractography.py | 563 ++++++++++++++++++++++++++++ 2 files changed, 887 insertions(+) create mode 100644 invesalius/gui/task_efield.py create mode 100644 invesalius/gui/task_tractography.py diff --git a/invesalius/gui/task_efield.py b/invesalius/gui/task_efield.py new file mode 100644 index 000000000..5dc6a4a2f --- /dev/null +++ b/invesalius/gui/task_efield.py @@ -0,0 +1,324 @@ +#-------------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br +# Contact: invesalius@cti.gov.br +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) +#-------------------------------------------------------------------------- +# Este programa e software livre; voce pode redistribui-lo e/ou +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme +# publicada pela Free Software Foundation; de acordo com a versao 2 +# da Licenca. +# +# Este programa eh distribuido na expectativa de ser util, mas SEM +# QUALQUER GARANTIA; sem mesmo a garantia implicita de +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais +# detalhes. +#-------------------------------------------------------------------------- +import os + +import dataclasses +from functools import partial +import itertools +import time + +import nibabel as nb +import numpy as np +try: + import Trekker + has_trekker = True +except ImportError: + has_trekker = False + +try: + #TODO: the try-except could be done inside the mTMS() method call + from invesalius.navigation.mtms import mTMS + mTMS() + has_mTMS = True +except: + has_mTMS = False + +import wx + +try: + import wx.lib.agw.foldpanelbar as fpb +except ImportError: + import wx.lib.foldpanelbar as fpb + +import wx.lib.colourselect as csel +import wx.lib.masked.numctrl +from invesalius.pubsub import pub as Publisher + +import invesalius.constants as const +import invesalius.data.brainmesh_handler as brain + +import invesalius.data.imagedata_utils as imagedata_utils +import invesalius.data.slice_ as sl +import invesalius.data.tractography as dti +import invesalius.data.record_coords as rec +import invesalius.data.vtk_utils as vtk_utils +import invesalius.data.bases as db +import invesalius.data.coregistration as dcr +import invesalius.gui.dialogs as dlg +import invesalius.project as prj +import invesalius.session as ses + +from invesalius import utils +from invesalius.gui import utils as gui_utils +from invesalius.navigation.iterativeclosestpoint import IterativeClosestPoint +from invesalius.navigation.navigation import Navigation +from invesalius.navigation.image import Image +from invesalius.navigation.tracker import Tracker + +from invesalius.navigation.robot import Robot +from invesalius.data.converters import to_vtk, convert_custom_bin_to_vtk + +from invesalius.net.neuronavigation_api import NeuronavigationApi + +HAS_PEDAL_CONNECTION = True +try: + from invesalius.net.pedal_connection import PedalConnection +except ImportError: + HAS_PEDAL_CONNECTION = False + +from invesalius import inv_paths + +class TaskPanel(wx.Panel): + def __init__(self, parent): + + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None + neuronavigation_api = NeuronavigationApi() + navigation = Navigation( + pedal_connection=pedal_connection, + neuronavigation_api=neuronavigation_api, + ) + + wx.Panel.__init__(self, parent) + + inner_panel = InnerTaskPanel(self, navigation) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(inner_panel, 1, wx.EXPAND | wx.GROW | wx.BOTTOM | wx.RIGHT | + wx.LEFT, 7) + sizer.Fit(self) + + self.SetSizer(sizer) + self.Update() + self.SetAutoLayout(1) + +class InnerTaskPanel(wx.Panel): + def __init__(self, parent, navigation): + wx.Panel.__init__(self, parent) + try: + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.__bind_events() + + self.SetBackgroundColour(default_colour) + self.e_field_loaded = False + self.e_field_brain = None + self.e_field_mesh = None + self.cortex_file = None + self.meshes_file = None + self.multilocus_coil = None + self.coil = None + self.ci = None + self.co = None + self.sleep_nav = const.SLEEP_NAVIGATION + self.navigation = navigation + self.session = ses.Session() + # Check box to enable e-field visualization + enable_efield = wx.CheckBox(self, -1, _('Enable E-field')) + enable_efield.SetValue(False) + enable_efield.Enable(1) + enable_efield.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableEfield, ctrl=enable_efield)) + self.enable_efield = enable_efield + + tooltip2 = wx.ToolTip(_("Load Brain Json config")) + btn_act2 = wx.Button(self, -1, _("Load Config"), size=wx.Size(100, 23)) + btn_act2.SetToolTip(tooltip2) + btn_act2.Enable(1) + btn_act2.Bind(wx.EVT_BUTTON, self.OnAddConfig) + + tooltip = wx.ToolTip(_("Save Efield")) + self.btn_save = wx.Button(self, -1, _("Save Efield"), size=wx.Size(80, -1)) + self.btn_save.SetToolTip(tooltip) + self.btn_save.Bind(wx.EVT_BUTTON, self.OnSaveEfield) + self.btn_save.Enable(False) + + text_sleep = wx.StaticText(self, -1, _("Sleep (s):")) + spin_sleep = wx.SpinCtrlDouble(self, -1, "", size = wx.Size(50,23), inc = 0.01) + spin_sleep.Enable(1) + spin_sleep.SetRange(0.05,10.0) + spin_sleep.SetValue(self.sleep_nav) + spin_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectSleep, ctrl=spin_sleep)) + spin_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectSleep, ctrl=spin_sleep)) + + border = 1 + line_sleep = wx.BoxSizer(wx.VERTICAL) + line_sleep.AddMany([(text_sleep, 1, wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + line_btns = wx.BoxSizer(wx.HORIZONTAL) + line_btns.Add(btn_act2, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + + line_btns_save = wx.BoxSizer(wx.HORIZONTAL) + line_btns_save.Add(self.btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + + # Add line sizers into main sizer + border_last = 5 + txt_surface = wx.StaticText(self, -1, _('Change coil:'), pos=(0,100)) + self.combo_surface_name = wx.ComboBox(self, -1, size=(210, 23), pos=(25, 50), + style=wx.CB_DROPDOWN | wx.CB_READONLY) + # combo_surface_name.SetSelection(0) + self.combo_surface_name.Bind(wx.EVT_COMBOBOX_DROPDOWN, self.OnComboCoilNameClic) + self.combo_surface_name.Bind(wx.EVT_COMBOBOX, self.OnComboCoil) + self.combo_surface_name.Insert('Select coil:',0) + self.combo_surface_name.Enable(False) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(line_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border_last) + main_sizer.Add(enable_efield, 1, wx.LEFT | wx.RIGHT, 2) + main_sizer.Add(line_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(self.combo_surface_name, 1, wx.BOTTOM | wx.ALIGN_RIGHT) + main_sizer.Add(line_btns_save, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border_last) + + main_sizer.SetSizeHints(self) + self.SetSizer(main_sizer) + + def __bind_events(self): + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + Publisher.subscribe(self.OnGetEfieldActor, 'Get Efield actor from json') + Publisher.subscribe(self.OnGetEfieldPaths, 'Get Efield paths') + Publisher.subscribe(self.OnGetMultilocusCoils,'Get multilocus paths from json') + + def OnAddConfig(self, evt): + filename = dlg.LoadConfigEfield() + if filename: + convert_to_inv = dlg.ImportMeshCoordSystem() + Publisher.sendMessage('Update status in GUI', value=50, label="Loading E-field...") + Publisher.sendMessage('Update convert_to_inv flag', convert_to_inv=convert_to_inv) + Publisher.sendMessage('Read json config file for efield', filename=filename, convert_to_inv=convert_to_inv) + self.Init_efield() + + def Init_efield(self): + self.navigation.neuronavigation_api.initialize_efield( + cortex_model_path=self.cortex_file, + mesh_models_paths=self.meshes_file, + coil_model_path=self.coil, + conductivities_inside=self.ci, + conductivities_outside=self.co, + ) + Publisher.sendMessage('Update status in GUI', value=0, label="Ready") + + def OnEnableEfield(self, evt, ctrl): + efield_enabled = ctrl.GetValue() + if efield_enabled: + if self.session.GetConfig('debug_efield'): + debug_efield_enorm = dlg.ShowLoadCSVDebugEfield() + if isinstance(debug_efield_enorm, np.ndarray): + self.navigation.debug_efield_enorm = debug_efield_enorm + else: + dlg.Efield_debug_Enorm_warning() + self.enable_efield.SetValue(False) + self.e_field_loaded = False + self.navigation.e_field_loaded = self.e_field_loaded + return + else: + if not self.navigation.neuronavigation_api.connection: + dlg.Efield_connection_warning() + #self.combo_surface_name.Enable(False) + self.enable_efield.Enable(False) + self.e_field_loaded = False + return + self.e_field_brain = brain.E_field_brain(self.e_field_mesh) + Publisher.sendMessage('Initialize E-field brain', e_field_brain=self.e_field_brain) + + Publisher.sendMessage('Initialize color array') + self.e_field_loaded = True + self.combo_surface_name.Enable(True) + self.btn_save.Enable(True) + else: + Publisher.sendMessage('Recolor again') + self.e_field_loaded = False + #self.combo_surface_name.Enable(True) + self.navigation.e_field_loaded = self.e_field_loaded + + def OnComboNameClic(self, evt): + import invesalius.project as prj + proj = prj.Project() + self.combo_surface_name.Clear() + for n in range(len(proj.surface_dict)): + self.combo_surface_name.Insert(str(proj.surface_dict[n].name), n) + + def OnComboCoilNameClic(self, evt): + self.combo_surface_name.Clear() + if self.multilocus_coil is not None: + for elements in range(len(self.multilocus_coil)): + self.combo_surface_name.Insert(self.multilocus_coil[elements], elements) + + def OnComboCoil(self, evt): + coil_name = evt.GetString() + coil_index = evt.GetSelection() + self.OnChangeCoil(self.multilocus_coil[coil_index]) + #self.e_field_mesh = self.proj.surface_dict[self.surface_index].polydata + #Publisher.sendMessage('Get Actor', surface_index = self.surface_index) + + def OnChangeCoil(self, coil_model_path): + self.navigation.neuronavigation_api.efield_coil( + coil_model_path=coil_model_path, + ) + + def UpdateNavigationStatus(self, nav_status, vis_status): + if nav_status: + self.enable_efield.Enable(False) + else: + self.enable_efield.Enable(True) + + def OnSelectSleep(self, evt, ctrl): + self.sleep_nav = ctrl.GetValue() + # self.tract.seed_offset = ctrl.GetValue() + Publisher.sendMessage('Update sleep', data=self.sleep_nav) + + def OnGetEfieldActor(self, efield_actor, surface_index_cortex): + self.e_field_mesh = efield_actor + self.surface_index= surface_index_cortex + Publisher.sendMessage('Get Actor', surface_index = self.surface_index) + + def OnGetEfieldPaths(self, path_meshes, cortex_file, meshes_file, coil, ci, co): + self.path_meshes = path_meshes + self.cortex_file = cortex_file + self.meshes_file = meshes_file + self.ci = ci + self.co = co + self.coil = coil + + def OnGetMultilocusCoils(self, multilocus_coil_list): + self.multilocus_coil = multilocus_coil_list + + def OnSaveEfield(self, evt): + import invesalius.project as prj + + proj = prj.Project() + timestamp = time.localtime(time.time()) + stamp_date = '{:0>4d}{:0>2d}{:0>2d}'.format(timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday) + stamp_time = '{:0>2d}{:0>2d}{:0>2d}'.format(timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec) + sep = '-' + if self.path_meshes is None: + import os + current_folder_path = os.getcwd() + else: + current_folder_path = self.path_meshes + parts = [current_folder_path,'/',stamp_date, stamp_time, proj.name, 'Efield'] + default_filename = sep.join(parts) + '.txt' + + filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."), + wildcard='(*.txt)|*.txt', + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + default_filename=default_filename) + + if not filename: + return + + Publisher.sendMessage('Save Efield data', filename = filename) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py new file mode 100644 index 000000000..b50520407 --- /dev/null +++ b/invesalius/gui/task_tractography.py @@ -0,0 +1,563 @@ +#-------------------------------------------------------------------------- +# Software: InVesalius - Software de Reconstrucao 3D de Imagens Medicas +# Copyright: (C) 2001 Centro de Pesquisas Renato Archer +# Homepage: http://www.softwarepublico.gov.br +# Contact: invesalius@cti.gov.br +# License: GNU - GPL 2 (LICENSE.txt/LICENCA.txt) +#-------------------------------------------------------------------------- +# Este programa e software livre; voce pode redistribui-lo e/ou +# modifica-lo sob os termos da Licenca Publica Geral GNU, conforme +# publicada pela Free Software Foundation; de acordo com a versao 2 +# da Licenca. +# +# Este programa eh distribuido na expectativa de ser util, mas SEM +# QUALQUER GARANTIA; sem mesmo a garantia implicita de +# COMERCIALIZACAO ou de ADEQUACAO A QUALQUER PROPOSITO EM +# PARTICULAR. Consulte a Licenca Publica Geral GNU para obter mais +# detalhes. +#-------------------------------------------------------------------------- +import os + +import dataclasses +from functools import partial +import itertools +import time + +import nibabel as nb +import numpy as np +try: + import Trekker + has_trekker = True +except ImportError: + has_trekker = False + +try: + #TODO: the try-except could be done inside the mTMS() method call + from invesalius.navigation.mtms import mTMS + mTMS() + has_mTMS = True +except: + has_mTMS = False + +import wx + +try: + import wx.lib.agw.foldpanelbar as fpb +except ImportError: + import wx.lib.foldpanelbar as fpb + +import wx.lib.colourselect as csel +import wx.lib.masked.numctrl +from invesalius.pubsub import pub as Publisher + +import invesalius.constants as const +import invesalius.data.brainmesh_handler as brain + +import invesalius.data.imagedata_utils as imagedata_utils +import invesalius.data.slice_ as sl +import invesalius.data.tractography as dti +import invesalius.data.record_coords as rec +import invesalius.data.vtk_utils as vtk_utils +import invesalius.data.bases as db +import invesalius.data.coregistration as dcr +import invesalius.gui.dialogs as dlg +import invesalius.project as prj +import invesalius.session as ses + +from invesalius import utils +from invesalius.gui import utils as gui_utils +from invesalius.navigation.iterativeclosestpoint import IterativeClosestPoint +from invesalius.navigation.navigation import Navigation +from invesalius.navigation.image import Image +from invesalius.navigation.tracker import Tracker + +from invesalius.navigation.robot import Robot +from invesalius.data.converters import to_vtk, convert_custom_bin_to_vtk + +from invesalius.net.neuronavigation_api import NeuronavigationApi + +HAS_PEDAL_CONNECTION = True +try: + from invesalius.net.pedal_connection import PedalConnection +except ImportError: + HAS_PEDAL_CONNECTION = False + +from invesalius import inv_paths + +class TaskPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + inner_panel = InnerTaskPanel(self) + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(inner_panel, 1, wx.EXPAND | wx.GROW | wx.BOTTOM | wx.RIGHT | + wx.LEFT, 7) + sizer.Fit(self) + + self.SetSizer(sizer) + self.Update() + self.SetAutoLayout(1) + +class InnerTaskPanel(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + try: + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.SetBackgroundColour(default_colour) + + self.affine = np.identity(4) + self.affine_vtk = None + self.trekker = None + self.n_tracts = const.N_TRACTS + self.peel_depth = const.PEEL_DEPTH + self.view_tracts = False + self.seed_offset = const.SEED_OFFSET + self.seed_radius = const.SEED_RADIUS + self.sleep_nav = const.SLEEP_NAVIGATION + self.brain_opacity = const.BRAIN_OPACITY + self.brain_peel = None + self.brain_actor = None + self.n_peels = const.MAX_PEEL_DEPTH + self.p_old = np.array([[0., 0., 0.]]) + self.tracts_run = None + self.trekker_cfg = const.TREKKER_CONFIG + self.nav_status = False + self.peel_loaded = False + self.SetAutoLayout(1) + self.__bind_events() + + # Button for import config coil file + tooltip = wx.ToolTip(_("Load FOD")) + btn_load = wx.Button(self, -1, _("FOD"), size=wx.Size(50, 23)) + btn_load.SetToolTip(tooltip) + btn_load.Enable(1) + btn_load.Bind(wx.EVT_BUTTON, self.OnLinkFOD) + # self.btn_load = btn_load + + # Save button for object registration + tooltip = wx.ToolTip(_(u"Load Trekker configuration parameters")) + btn_load_cfg = wx.Button(self, -1, _(u"Configure"), size=wx.Size(65, 23)) + btn_load_cfg.SetToolTip(tooltip) + btn_load_cfg.Enable(1) + btn_load_cfg.Bind(wx.EVT_BUTTON, self.OnLoadParameters) + # self.btn_load_cfg = btn_load_cfg + + # Button for creating new coil + tooltip = wx.ToolTip(_("Load brain visualization")) + btn_mask = wx.Button(self, -1, _("Brain"), size=wx.Size(50, 23)) + btn_mask.SetToolTip(tooltip) + btn_mask.Enable(1) + btn_mask.Bind(wx.EVT_BUTTON, self.OnLinkBrain) + # self.btn_new = btn_new + + # Button for creating new coil + tooltip = wx.ToolTip(_("Load anatomical labels")) + btn_act = wx.Button(self, -1, _("ACT"), size=wx.Size(50, 23)) + btn_act.SetToolTip(tooltip) + btn_act.Enable(1) + btn_act.Bind(wx.EVT_BUTTON, self.OnLoadACT) + # self.btn_new = btn_new + + # Create a horizontal sizer to represent button save + line_btns = wx.BoxSizer(wx.HORIZONTAL) + line_btns.Add(btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + line_btns.Add(btn_load_cfg, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + line_btns.Add(btn_mask, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + line_btns.Add(btn_act, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + + # Change peeling depth + text_peel_depth = wx.StaticText(self, -1, _("Peeling depth (mm):")) + spin_peel_depth = wx.SpinCtrl(self, -1, "", size=wx.Size(50, 23)) + spin_peel_depth.Enable(1) + spin_peel_depth.SetRange(0, const.MAX_PEEL_DEPTH) + spin_peel_depth.SetValue(const.PEEL_DEPTH) + spin_peel_depth.Bind(wx.EVT_TEXT, partial(self.OnSelectPeelingDepth, ctrl=spin_peel_depth)) + spin_peel_depth.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectPeelingDepth, ctrl=spin_peel_depth)) + + # Change number of tracts + text_ntracts = wx.StaticText(self, -1, _("Number tracts:")) + spin_ntracts = wx.SpinCtrl(self, -1, "", size=wx.Size(50, 23)) + spin_ntracts.Enable(1) + spin_ntracts.SetRange(1, 2000) + spin_ntracts.SetValue(const.N_TRACTS) + spin_ntracts.Bind(wx.EVT_TEXT, partial(self.OnSelectNumTracts, ctrl=spin_ntracts)) + spin_ntracts.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectNumTracts, ctrl=spin_ntracts)) + + # Change seed offset for computing tracts + text_offset = wx.StaticText(self, -1, _("Seed offset (mm):")) + spin_offset = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc = 0.1) + spin_offset.Enable(1) + spin_offset.SetRange(0, 100.0) + spin_offset.SetValue(self.seed_offset) + spin_offset.Bind(wx.EVT_TEXT, partial(self.OnSelectOffset, ctrl=spin_offset)) + spin_offset.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectOffset, ctrl=spin_offset)) + # self.spin_offset = spin_offset + + # Change seed radius for computing tracts + text_radius = wx.StaticText(self, -1, _("Seed radius (mm):")) + spin_radius = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.1) + spin_radius.Enable(1) + spin_radius.SetRange(0, 100.0) + spin_radius.SetValue(self.seed_radius) + spin_radius.Bind(wx.EVT_TEXT, partial(self.OnSelectRadius, ctrl=spin_radius)) + spin_radius.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectRadius, ctrl=spin_radius)) + # self.spin_radius = spin_radius + + # Change sleep pause between navigation loops + text_sleep = wx.StaticText(self, -1, _("Sleep (s):")) + spin_sleep = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.01) + spin_sleep.Enable(1) + spin_sleep.SetRange(0.01, 10.0) + spin_sleep.SetValue(self.sleep_nav) + spin_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectSleep, ctrl=spin_sleep)) + spin_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectSleep, ctrl=spin_sleep)) + + # Change opacity of brain mask visualization + text_opacity = wx.StaticText(self, -1, _("Brain opacity:")) + spin_opacity = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.1) + spin_opacity.Enable(0) + spin_opacity.SetRange(0, 1.0) + spin_opacity.SetValue(self.brain_opacity) + spin_opacity.Bind(wx.EVT_TEXT, partial(self.OnSelectOpacity, ctrl=spin_opacity)) + spin_opacity.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectOpacity, ctrl=spin_opacity)) + self.spin_opacity = spin_opacity + + # Create a horizontal sizer to threshold configs + border = 1 + line_peel_depth = wx.BoxSizer(wx.HORIZONTAL) + line_peel_depth.AddMany([(text_peel_depth, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_peel_depth, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + + line_ntracts = wx.BoxSizer(wx.HORIZONTAL) + line_ntracts.AddMany([(text_ntracts, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_ntracts, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + + line_offset = wx.BoxSizer(wx.HORIZONTAL) + line_offset.AddMany([(text_offset, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_offset, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + + line_radius = wx.BoxSizer(wx.HORIZONTAL) + line_radius.AddMany([(text_radius, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_radius, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + + line_sleep = wx.BoxSizer(wx.HORIZONTAL) + line_sleep.AddMany([(text_sleep, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + + line_opacity = wx.BoxSizer(wx.HORIZONTAL) + line_opacity.AddMany([(text_opacity, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + (spin_opacity, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + + # Check box to enable tract visualization + checktracts = wx.CheckBox(self, -1, _('Enable tracts')) + checktracts.SetValue(False) + checktracts.Enable(0) + checktracts.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableTracts, ctrl=checktracts)) + self.checktracts = checktracts + + # Check box to enable surface peeling + checkpeeling = wx.CheckBox(self, -1, _('Peel surface')) + checkpeeling.SetValue(False) + checkpeeling.Enable(0) + checkpeeling.Bind(wx.EVT_CHECKBOX, partial(self.OnShowPeeling, ctrl=checkpeeling)) + self.checkpeeling = checkpeeling + + # Check box to enable tract visualization + checkACT = wx.CheckBox(self, -1, _('ACT')) + checkACT.SetValue(False) + checkACT.Enable(0) + checkACT.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableACT, ctrl=checkACT)) + self.checkACT = checkACT + + border_last = 1 + line_checks = wx.BoxSizer(wx.HORIZONTAL) + line_checks.Add(checktracts, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, border_last) + line_checks.Add(checkpeeling, 0, wx.ALIGN_CENTER | wx.RIGHT | wx.LEFT, border_last) + line_checks.Add(checkACT, 0, wx.RIGHT | wx.LEFT, border_last) + + # Add line sizers into main sizer + border = 1 + border_last = 10 + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(line_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border_last) + main_sizer.Add(line_peel_depth, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(line_ntracts, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(line_offset, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(line_radius, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(line_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(line_opacity, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + main_sizer.Add(line_checks, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, border_last) + main_sizer.Fit(self) + + self.SetSizer(main_sizer) + self.Update() + + def __bind_events(self): + Publisher.subscribe(self.OnCloseProject, 'Close project data') + Publisher.subscribe(self.OnUpdateTracts, 'Set cross focal point') + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + + def OnSelectPeelingDepth(self, evt, ctrl): + self.peel_depth = ctrl.GetValue() + if self.checkpeeling.GetValue(): + actor = self.brain_peel.get_actor(self.peel_depth) + Publisher.sendMessage('Update peel', flag=True, actor=actor) + Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers, + normals=self.brain_peel.peel_normals) + Publisher.sendMessage('Get init locator', locator=self.brain_peel.locator) + self.peel_loaded = True + def OnSelectNumTracts(self, evt, ctrl): + self.n_tracts = ctrl.GetValue() + # self.tract.n_tracts = ctrl.GetValue() + Publisher.sendMessage('Update number of tracts', data=self.n_tracts) + + def OnSelectOffset(self, evt, ctrl): + self.seed_offset = ctrl.GetValue() + # self.tract.seed_offset = ctrl.GetValue() + Publisher.sendMessage('Update seed offset', data=self.seed_offset) + + def OnSelectRadius(self, evt, ctrl): + self.seed_radius = ctrl.GetValue() + # self.tract.seed_offset = ctrl.GetValue() + Publisher.sendMessage('Update seed radius', data=self.seed_radius) + + def OnSelectSleep(self, evt, ctrl): + self.sleep_nav = ctrl.GetValue() + # self.tract.seed_offset = ctrl.GetValue() + Publisher.sendMessage('Update sleep', data=self.sleep_nav) + + def OnSelectOpacity(self, evt, ctrl): + self.brain_actor.GetProperty().SetOpacity(ctrl.GetValue()) + Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor) + + def OnShowPeeling(self, evt, ctrl): + # self.view_peeling = ctrl.GetValue() + if ctrl.GetValue(): + actor = self.brain_peel.get_actor(self.peel_depth) + self.peel_loaded = True + Publisher.sendMessage('Update peel visualization', data=self.peel_loaded) + else: + actor = None + self.peel_loaded = False + Publisher.sendMessage('Update peel visualization', data= self.peel_loaded) + + Publisher.sendMessage('Update peel', flag=ctrl.GetValue(), actor=actor) + + def OnEnableTracts(self, evt, ctrl): + self.view_tracts = ctrl.GetValue() + Publisher.sendMessage('Update tracts visualization', data=self.view_tracts) + if not self.view_tracts: + Publisher.sendMessage('Remove tracts') + Publisher.sendMessage("Update marker offset state", create=False) + + def OnEnableACT(self, evt, ctrl): + # self.view_peeling = ctrl.GetValue() + # if ctrl.GetValue(): + # act_data = self.brain_peel.get_actor(self.peel_depth) + # else: + # actor = None + Publisher.sendMessage('Enable ACT', data=ctrl.GetValue()) + + def UpdateNavigationStatus(self, nav_status, vis_status): + self.nav_status = nav_status + + def OnLinkBrain(self, event=None): + Publisher.sendMessage('Begin busy cursor') + inv_proj = prj.Project() + peels_dlg = dlg.PeelsCreationDlg(wx.GetApp().GetTopWindow()) + ret = peels_dlg.ShowModal() + method = peels_dlg.method + if ret == wx.ID_OK: + slic = sl.Slice() + ww = slic.window_width + wl = slic.window_level + affine = np.eye(4) + if method == peels_dlg.FROM_FILES: + try: + affine = slic.affine.copy() + except AttributeError: + pass + + self.brain_peel = brain.Brain(self.n_peels, ww, wl, affine, inv_proj) + if method == peels_dlg.FROM_MASK: + choices = [i for i in inv_proj.mask_dict.values()] + mask_index = peels_dlg.cb_masks.GetSelection() + mask = choices[mask_index] + self.brain_peel.from_mask(mask) + else: + mask_path = peels_dlg.mask_path + self.brain_peel.from_mask_file(mask_path) + self.brain_actor = self.brain_peel.get_actor(self.peel_depth) + self.brain_actor.GetProperty().SetOpacity(self.brain_opacity) + Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor) + Publisher.sendMessage('Get peel centers and normals', centers=self.brain_peel.peel_centers, + normals=self.brain_peel.peel_normals) + Publisher.sendMessage('Get init locator', locator=self.brain_peel.locator) + self.checkpeeling.Enable(1) + self.checkpeeling.SetValue(True) + self.spin_opacity.Enable(1) + Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded")) + self.peel_loaded = True + Publisher.sendMessage('Update peel visualization', data= self.peel_loaded) + + peels_dlg.Destroy() + Publisher.sendMessage('End busy cursor') + + def OnLinkFOD(self, event=None): + Publisher.sendMessage('Begin busy cursor') + filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import Trekker FOD")) + # Juuso + # data_dir = os.environ.get('OneDriveConsumer') + '\\data\\dti' + # FOD_path = 'sub-P0_dwi_FOD.nii' + # Baran + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609' + # FOD_path = 'Baran_FOD.nii' + # filename = os.path.join(data_dir, FOD_path) + + if not self.affine_vtk: + slic = sl.Slice() + prj_data = prj.Project() + matrix_shape = tuple(prj_data.matrix_shape) + spacing = tuple(prj_data.spacing) + img_shift = spacing[1] * (matrix_shape[1] - 1) + self.affine = slic.affine.copy() + self.affine[1, -1] -= img_shift + self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine) + + if filename: + Publisher.sendMessage('Update status text in GUI', label=_("Busy")) + sp = dlg.TractographyProgressWindow() + try: + self.trekker = Trekker.initialize(filename.encode('utf-8')) + self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg) + + self.checktracts.Enable(1) + self.checktracts.SetValue(True) + self.view_tracts = True + + Publisher.sendMessage('Update Trekker object', data=self.trekker) + Publisher.sendMessage('Update number of threads', data=n_threads) + Publisher.sendMessage('Update tracts visualization', data=1) + Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized")) + # except: + # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3")) + sp.Close() + except: + Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialization failed.")) + wx.MessageBox(_("Unable to load FOD."), _("InVesalius 3")) + + Publisher.sendMessage('End busy cursor') + + def OnLoadACT(self, event=None): + if self.trekker: + Publisher.sendMessage('Begin busy cursor') + filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import anatomical labels")) + # Baran + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609' + # act_path = 'Baran_trekkerACTlabels_inFODspace.nii' + # filename = os.path.join(data_dir, act_path) + + if not self.affine_vtk: + slic = sl.Slice() + prj_data = prj.Project() + matrix_shape = tuple(prj_data.matrix_shape) + spacing = tuple(prj_data.spacing) + img_shift = spacing[1] * (matrix_shape[1] - 1) + self.affine = slic.affine.copy() + self.affine[1, -1] -= img_shift + self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine) + + try: + Publisher.sendMessage('Update status text in GUI', label=_("Busy")) + if filename: + act_data = nb.squeeze_image(nb.load(filename)) + act_data = nb.as_closest_canonical(act_data) + act_data.update_header() + act_data_arr = act_data.get_fdata() + + self.checkACT.Enable(1) + self.checkACT.SetValue(True) + + # ACT rules should be as follows: + self.trekker.pathway_stop_at_entry(filename.encode('utf-8'), -1) # outside + self.trekker.pathway_discard_if_ends_inside(filename.encode('utf-8'), 1) # wm + self.trekker.pathway_discard_if_enters(filename.encode('utf-8'), 0) # csf + + Publisher.sendMessage('Update ACT data', data=act_data_arr) + Publisher.sendMessage('Enable ACT', data=True) + Publisher.sendMessage('Update status text in GUI', label=_("Trekker ACT loaded")) + except: + Publisher.sendMessage('Update status text in GUI', label=_("ACT initialization failed.")) + wx.MessageBox(_("Unable to load ACT."), _("InVesalius 3")) + + Publisher.sendMessage('End busy cursor') + else: + wx.MessageBox(_("Load FOD image before the ACT."), _("InVesalius 3")) + + def OnLoadParameters(self, event=None): + import json + filename = dlg.ShowLoadSaveDialog(message=_(u"Load Trekker configuration"), + wildcard=_("JSON file (*.json)|*.json")) + try: + # Check if filename exists, read the JSON file and check if all parameters match + # with the required list defined in the constants module + # if a parameter is missing, raise an error + if filename: + with open(filename) as json_file: + self.trekker_cfg = json.load(json_file) + assert all(name in self.trekker_cfg for name in const.TREKKER_CONFIG) + if self.trekker: + self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg) + Publisher.sendMessage('Update Trekker object', data=self.trekker) + Publisher.sendMessage('Update number of threads', data=n_threads) + + Publisher.sendMessage('Update status text in GUI', label=_("Trekker config loaded")) + + except (AssertionError, json.decoder.JSONDecodeError): + # Inform user that file is not compatible + self.trekker_cfg = const.TREKKER_CONFIG + wx.MessageBox(_("File incompatible, using default configuration."), _("InVesalius 3")) + Publisher.sendMessage('Update status text in GUI', label="") + + def OnUpdateTracts(self, position): + """ + Minimal working version of tract computation. Updates when cross sends Pubsub message to update. + Position refers to the coordinates in InVesalius 2D space. To represent the same coordinates in the 3D space, + flip_x the coordinates and multiply the z coordinate by -1. This is all done in the flix_x function. + + :param arg: event for pubsub + :param position: list or array with the x, y, and z coordinates in InVesalius space + """ + # Minimal working version of tract computation + # It updates when cross updates + # pass + if self.view_tracts and not self.nav_status: + # print("Running during navigation") + coord_flip = list(position[:3]) + coord_flip[1] = -coord_flip[1] + dti.compute_and_visualize_tracts(self.trekker, coord_flip, self.affine, self.affine_vtk, + self.n_tracts) + + def OnCloseProject(self): + self.trekker = None + self.trekker_cfg = const.TREKKER_CONFIG + + self.checktracts.SetValue(False) + self.checktracts.Enable(0) + self.checkpeeling.SetValue(False) + self.checkpeeling.Enable(0) + self.checkACT.SetValue(False) + self.checkACT.Enable(0) + + self.spin_opacity.SetValue(const.BRAIN_OPACITY) + self.spin_opacity.Enable(0) + Publisher.sendMessage('Update peel', flag=False, actor=self.brain_actor) + + self.peel_depth = const.PEEL_DEPTH + self.n_tracts = const.N_TRACTS + + Publisher.sendMessage('Remove tracts') + From e07c26bb31bd9fedd4b5c92f9bada7891f837052 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:25:45 +0300 Subject: [PATCH 08/99] MOD: Button labels &ADD: New labels --- invesalius/constants.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 1bbf537ea..2047d4d9b 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -716,24 +716,25 @@ TR3 = wx.NewId() SET = wx.NewId() +FIDUCIAL_LABELS = ["Left Ear: ", "Right Ear: ", "Nose: "] IMAGE_FIDUCIALS = [ { 'button_id': IR1, - 'label': 'LEI', + 'label': 'Left Ear', 'fiducial_name': 'LE', 'fiducial_index': 0, 'tip': _("Select left ear in image"), }, { 'button_id': IR2, - 'label': 'REI', + 'label': 'Right Ear', 'fiducial_name': 'RE', 'fiducial_index': 1, 'tip': _("Select right ear in image"), }, { 'button_id': IR3, - 'label': 'NAI', + 'label': 'Nasion', 'fiducial_name': 'NA', 'fiducial_index': 2, 'tip': _("Select nasion in image"), @@ -743,21 +744,21 @@ TRACKER_FIDUCIALS = [ { 'button_id': TR1, - 'label': 'LET', + 'label': 'Left Ear', 'fiducial_name': 'LE', 'fiducial_index': 0, 'tip': _("Select left ear with spatial tracker"), }, { 'button_id': TR2, - 'label': 'RET', + 'label': 'Right Ear', 'fiducial_name': 'RE', 'fiducial_index': 1, 'tip': _("Select right ear with spatial tracker"), }, { 'button_id': TR3, - 'label': 'NAT', + 'label': 'Nasion', 'fiducial_name': 'NA', 'fiducial_index': 2, 'tip': _("Select nasion with spatial tracker"), From de5c450e8a5abbcad3f3a8935e96c455c1f5f275 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:26:52 +0300 Subject: [PATCH 09/99] ADD: New function to check if individual tracker fiducials are set --- invesalius/navigation/tracker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index 518c2194f..ab12a0545 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -157,7 +157,10 @@ def DisconnectTracker(self): def IsTrackerInitialized(self): return self.tracker_connection and self.tracker_id and self.tracker_connected - + + def IsTrackerFiducialSet(self, fiducial_index): + return not np.isnan(self.tracker_fiducials)[fiducial_index].any() + def AreTrackerFiducialsSet(self): return not np.isnan(self.tracker_fiducials).any() From 5ef2ec49ccc2e6beb0dc73fdc370213a1b57c830 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:27:06 +0300 Subject: [PATCH 10/99] MOD: Minor UI changes --- invesalius/gui/default_tasks.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index b44930bdc..3f5211d26 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -102,7 +102,7 @@ def GetExpandedIconImage(): class Panel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent, pos=wx.Point(5, 5), - size=wx.Size(280, 656)) + size=wx.Size(300, 656)) #sizer = wx.BoxSizer(wx.VERTICAL) gbs = wx.GridBagSizer(5,5) @@ -178,7 +178,7 @@ def __init__(self, parent): image_list.Add(GetCollapsedIconBitmap()) # Fold 1 - Data - item = fold_panel.AddFoldPanel(_("Data"), collapsed=False, + item = fold_panel.AddFoldPanel(_("Data"), collapsed=True, foldIcons=image_list) style = fold_panel.GetCaptionStyle(item) col = style.GetFirstColour() @@ -280,7 +280,7 @@ def __init__(self, parent): elif mode == const.MODE_NAVIGATOR: tasks = [(_("Imports Section"), imports.TaskPanel), - (_("Navigation system"), navigator.TaskPanel)] + (_("Navigation Section"), navigator.TaskPanel)] for i in range(len(tasks)): (name, panel) = tasks[i] From cda5d77bf1c6244b50cb31686220d5f6abb0b9a3 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:27:28 +0300 Subject: [PATCH 11/99] MOD: Navigation Section --- invesalius/gui/task_navigator.py | 1295 ++++++++++++++++++++++++++---- 1 file changed, 1139 insertions(+), 156 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index e4ea1504e..8b0963831 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -40,6 +40,7 @@ has_mTMS = False import wx +import sys try: import wx.lib.agw.foldpanelbar as fpb @@ -86,6 +87,11 @@ BTN_NEW = wx.NewId() BTN_IMPORT_LOCAL = wx.NewId() + +def getBitMapForBackground(): + image_file = os.path.join('head.png') + bmp = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath(image_file)), wx.BITMAP_TYPE_PNG) + return bmp class TaskPanel(wx.Panel): @@ -111,21 +117,12 @@ def __init__(self, parent): background_colour = wx.Colour(255,255,255) self.SetBackgroundColour(background_colour) - txt_nav = wx.StaticText(self, -1, _('Select fiducials and navigate'), - size=wx.Size(90, 20)) - txt_nav.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) - - # Create horizontal sizer to represent lines in the panel - txt_sizer = wx.BoxSizer(wx.HORIZONTAL) - txt_sizer.Add(txt_nav, 1, wx.EXPAND|wx.GROW, 5) - # Fold panel which contains navigation configurations fold_panel = FoldPanel(self) fold_panel.SetBackgroundColour(default_colour) # Add line sizer into main sizer main_sizer = wx.BoxSizer(wx.VERTICAL) - main_sizer.Add(txt_sizer, 0, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT, 5) main_sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND|wx.LEFT|wx.RIGHT, 5) main_sizer.AddSpacer(5) main_sizer.Fit(self) @@ -136,7 +133,6 @@ def __init__(self, parent): self.sizer = main_sizer - class FoldPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -151,7 +147,6 @@ def __init__(self, parent): self.Update() self.SetAutoLayout(1) - class InnerFoldPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -170,8 +165,10 @@ def __init__(self, parent): # Study this. fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, - (10, 330), 0, fpb.FPB_SINGLE_FOLD) - + (10, 600), 0, fpb.FPB_SINGLE_FOLD) + gbs = wx.GridBagSizer(5,5) + gbs.AddGrowableCol(0, 1) + self.gbs = gbs # Initialize Navigation, Tracker, Robot, Image, and PedalConnection objects here to make them # available to several panels. # @@ -199,7 +196,955 @@ def __init__(self, parent): checkcamera.SetValue(const.CAM_MODE) checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCameraCheckbox) self.checkcamera = checkcamera - + + # Checkbox to use serial port to trigger pulse signal and create markers + tooltip = wx.ToolTip(_("Enable serial port communication to trigger pulse and create markers")) + checkbox_serial_port = wx.CheckBox(self, -1, _('Serial port')) + checkbox_serial_port.SetToolTip(tooltip) + checkbox_serial_port.SetValue(False) + checkbox_serial_port.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableSerialPort, ctrl=checkbox_serial_port)) + self.checkbox_serial_port = checkbox_serial_port + + # Checkbox for object position and orientation update in volume rendering during navigation + tooltip = wx.ToolTip(_("Show and track TMS coil")) + checkobj = wx.CheckBox(self, -1, _('Show coil')) + checkobj.SetToolTip(tooltip) + checkobj.SetValue(False) + checkobj.Disable() + checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowCoil) + self.checkobj = checkobj + + # if sys.platform != 'win32': + self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + checkbox_serial_port.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + + # Fold panel style + style = fpb.CaptionBarStyle() + style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) + style.SetFirstColour(default_colour) + style.SetSecondColour(default_colour) + + item = fold_panel.AddFoldPanel(_("Coregistration"), collapsed=True) + ntw = CoregistrationPanel( + parent=item, + navigation=navigation, + tracker=tracker, + robot=robot, + icp=icp, + image=image, + pedal_connection=pedal_connection, + neuronavigation_api=neuronavigation_api, + ) + self.fold_panel = fold_panel + self.__calc_best_size(ntw) + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, + leftSpacing=0, rightSpacing=0) + fold_panel.Expand(fold_panel.GetFoldPanel(0)) + + item = fold_panel.AddFoldPanel(_("Navigation"), collapsed=True) + ntw = NavigationPanel( + parent=item, + navigation=navigation, + tracker=tracker, + robot=robot, + icp=icp, + image=image, + pedal_connection=pedal_connection, + neuronavigation_api=neuronavigation_api, + ) + + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, + leftSpacing=0, rightSpacing=0) + + # Fold 1 - Navigation panel + item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) + ntw = NeuronavigationPanel( + parent=item, + navigation=navigation, + tracker=tracker, + robot=robot, + icp=icp, + image=image, + pedal_connection=pedal_connection, + neuronavigation_api=neuronavigation_api, + ) + + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, + leftSpacing=0, rightSpacing=0) + fold_panel.Expand(fold_panel.GetFoldPanel(0)) + + # Fold 2 - Object registration panel + item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) + otw = ObjectRegistrationPanel( + parent=item, + tracker=tracker, + pedal_connection=pedal_connection, + neuronavigation_api=neuronavigation_api, + ) + + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, otw, spacing=0, + leftSpacing=0, rightSpacing=0) + + # Fold 3 - Markers panel + item = fold_panel.AddFoldPanel(_("Markers"), collapsed=True) + mtw = MarkersPanel(item, navigation, tracker, icp) + + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, mtw, spacing= 0, + leftSpacing=0, rightSpacing=0) + + # Fold 4 - Tractography panel + if has_trekker: + item = fold_panel.AddFoldPanel(_("Tractography"), collapsed=True) + otw = TractographyPanel(item) + + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, otw, spacing=0, + leftSpacing=0, rightSpacing=0) + + # Fold 5 - DBS + self.dbs_item = fold_panel.AddFoldPanel(_("Deep Brain Stimulation"), collapsed=True) + dtw = DbsPanel(self.dbs_item) #Atribuir nova var, criar panel + + fold_panel.ApplyCaptionStyle(self.dbs_item, style) + fold_panel.AddFoldPanelWindow(self.dbs_item, dtw, spacing= 0, + leftSpacing=0, rightSpacing=0) + self.dbs_item.Hide() + + # Fold 6 - Sessions + item = fold_panel.AddFoldPanel(_("Sessions"), collapsed=False) + stw = SessionPanel(item) + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, stw, spacing= 0, + leftSpacing=0, rightSpacing=0) + + # Fold 7 - E-field + + item = fold_panel.AddFoldPanel(_("E-field"), collapsed=True) + etw = E_fieldPanel(item, navigation) + fold_panel.ApplyCaptionStyle(item, style) + fold_panel.AddFoldPanelWindow(item, etw, spacing=0, + leftSpacing=0, rightSpacing=0) + + # Panel sizer for checkboxes + line_sizer = wx.BoxSizer(wx.HORIZONTAL) + line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) + line_sizer.Add(checkbox_serial_port, 0, wx.ALIGN_CENTER) + line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5) + line_sizer.Fit(self) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(gbs, 1, wx.GROW|wx.EXPAND) + self.SetSizer(sizer) + # Panel sizer to expand fold panel + #sizer = wx.BoxSizer(wx.VERTICAL) + + sizer.Fit(self) + + self.track_obj = False + gbs.Add(fold_panel, (0, 0), flag=wx.EXPAND) + gbs.Add(line_sizer, (1, 0), flag=wx.EXPAND) + gbs.Layout() + sizer.Fit(self) + self.Fit() + + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self.OnCheckStatus, 'Navigation status') + Publisher.subscribe(self.OnShowDbs, "Show dbs folder") + Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") + + # Externally check/uncheck and enable/disable checkboxes. + Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') + Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') + + Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') + Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') + + def __calc_best_size(self, panel): + parent = panel.GetParent() + panel.Reparent(self) + + gbs = self.gbs + fold_panel = self.fold_panel + + # Calculating the size + gbs.AddGrowableRow(1, 1) + #gbs.AddGrowableRow(0, 1) + gbs.Add(fold_panel, (0, 0), flag=wx.EXPAND) + gbs.Add(panel, (1, 0), flag=wx.EXPAND) + gbs.Layout() + self.Fit() + size = panel.GetSize() + + gbs.Remove(1) + gbs.Remove(0) + gbs.RemoveGrowableRow(1) + + panel.Reparent(parent) + panel.SetInitialSize(size) + self.SetInitialSize(self.GetSize()) + + def OnShowDbs(self): + self.dbs_item.Show() + + def OnHideDbs(self): + self.dbs_item.Hide() + + def OnCheckStatus(self, nav_status, vis_status): + if nav_status: + self.checkbox_serial_port.Enable(False) + self.checkobj.Enable(False) + else: + self.checkbox_serial_port.Enable(True) + if self.track_obj: + self.checkobj.Enable(True) + + def OnEnableSerialPort(self, evt, ctrl): + if ctrl.GetValue(): + from wx import ID_OK + dlg_port = dlg.SetCOMPort(select_baud_rate=False) + + if dlg_port.ShowModal() != ID_OK: + ctrl.SetValue(False) + return + + com_port = dlg_port.GetCOMPort() + baud_rate = 115200 + + Publisher.sendMessage('Update serial port', serial_port_in_use=True, com_port=com_port, baud_rate=baud_rate) + else: + Publisher.sendMessage('Update serial port', serial_port_in_use=False) + + # 'Show coil' checkbox + + def CheckShowCoil(self, checked=False): + self.checkobj.SetValue(checked) + self.track_obj = checked + + self.OnShowCoil() + + def EnableShowCoil(self, enabled=False): + self.checkobj.Enable(enabled) + + def OnShowCoil(self, evt=None): + checked = self.checkobj.GetValue() + Publisher.sendMessage('Show-coil checked', checked=checked) + + # 'Volume camera' checkbox + + def CheckVolumeCameraCheckbox(self, checked): + self.checkcamera.SetValue(checked) + self.OnVolumeCameraCheckbox() + + def OnVolumeCameraCheckbox(self, evt=None, status=None): + Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue()) + + def EnableVolumeCameraCheckbox(self, enabled): + self.checkcamera.Enable(enabled) + +class CoregistrationPanel(wx.Panel): + def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): + wx.Panel.__init__(self, parent) + try: + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.SetBackgroundColour(default_colour) + + book = wx.Notebook(self, -1,style= wx.BK_DEFAULT) + book.Bind(wx.EVT_BOOKCTRL_PAGE_CHANGING, self.OnPageChanging) + book.Bind(wx.EVT_BOOKCTRL_PAGE_CHANGED, self.OnPageChanged) + if sys.platform != 'win32': + book.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + + self.navigation = navigation + self.tracker = tracker + self.robot = robot + self.icp = icp + self.image = image + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api + + book.AddPage(ImagePage(book, self.image), _("Image")) + book.AddPage(TrackerPage(book, self.icp, self.tracker, self.navigation, self.pedal_connection, self.neuronavigation_api), _("Tracker")) + book.AddPage(RefinePage(book, self.icp, self.tracker, self.image, self.navigation), _("Refine")) + book.AddPage(StimulatorPage(book), _("Stimulator")) + + book.SetSelection(0) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(book, 0, wx.EXPAND) + self.SetSizer(sizer) + + book.Refresh() + self.book = book + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self._FoldTracker, + 'Next to tracker fiducials') + Publisher.subscribe(self._FoldRefine, + 'Next to refine fiducials') + Publisher.subscribe(self._FoldStimulator, + 'Next to stimulator fiducials') + Publisher.subscribe(self._FoldImage, + 'Back to image fiducials') + + + def OnPageChanging(self, evt): + page = evt.GetOldSelection() + + def OnPageChanged(self, evt): + old_page = evt.GetOldSelection() + new_page = evt.GetSelection() + + # old page validations + if old_page == 0: + # Do not allow user to move to other (forward) tabs if image fiducials not done. + if not self.image.AreImageFiducialsSet(): + self.book.SetSelection(0) + wx.MessageBox(_("Select image fiducials first"), _("InVesalius 3")) + if old_page != 2: + # Load data into refine tab + Publisher.sendMessage("Update UI for refine tab") + + # new page validations + if (old_page == 1) and (new_page == 2 or new_page == 3): + # Do not allow user to move to other (forward) tabs if tracker fiducials not done. + if self.image.AreImageFiducialsSet() and not self.tracker.AreTrackerFiducialsSet(): + self.book.SetSelection(1) + wx.MessageBox(_("Select tracker fiducials first"), _("InVesalius 3")) + + def _FoldImage(self): + """ + Fold image notebook page. + """ + self.book.SetSelection(0) + + def _FoldTracker(self): + """ + Fold tracker notebook page. + """ + Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) + self.book.SetSelection(1) + + def _FoldRefine(self): + """ + Fold refine notebook page. + """ + self.book.SetSelection(2) + + def _FoldStimulator(self): + """ + Fold mask notebook page. + """ + self.book.SetSelection(3) + +class ImagePage(wx.Panel): + def __init__(self, parent, image): + wx.Panel.__init__(self, parent) + + self.image = image + self.btns_set_fiducial = [None, None, None] + self.numctrls_fiducial = [[], [], []] + self.current_coord = 0, 0, 0, None, None, None + + self.bg_bmp = getBitMapForBackground() + # Toggle buttons for image fiducials + background = wx.StaticBitmap(self, -1, self.bg_bmp, (0, 0)) + for n, fiducial in enumerate(const.IMAGE_FIDUCIALS): + button_id = fiducial['button_id'] + label = fiducial['label'] + tip = fiducial['tip'] + + ctrl = wx.ToggleButton(self, button_id, label=label, style=wx.BU_EXACTFIT) + ctrl.SetToolTip(wx.ToolTip(tip)) + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnImageFiducials, n)) + ctrl.SetValue(self.image.IsImageFiducialSet(n)) + ctrl.Disable() + + self.btns_set_fiducial[n] = ctrl + + for m in range(len(self.btns_set_fiducial)): + for n in range(3): + value = self.image.GetImageFiducialForUI(m, n) + self.numctrls_fiducial[m].append( + wx.lib.masked.numctrl.NumCtrl(parent=self, integerWidth=4, fractionWidth=1, value=value, ) + ) + self.numctrls_fiducial[m][n].Hide() + + start_button = wx.ToggleButton(self, label="Start Registration") + start_button.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStartRegistration, ctrl=start_button)) + self.start_button = start_button + + next_button = wx.Button(self, label="Next") + next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) + if not self.image.AreImageFiducialsSet(): + next_button.Disable() + self.next_button = next_button + + top_sizer = wx.BoxSizer(wx.HORIZONTAL) + top_sizer.Add(start_button) + + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + bottom_sizer.Add(next_button) + + sizer = wx.GridBagSizer(5, 5) + sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self.btns_set_fiducial[2], wx.GBPosition(0, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_HORIZONTAL) + sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(background, wx.GBPosition(1, 2)) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10), + (sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5), + (bottom_sizer, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 30)]) + self.sizer = main_sizer + self.SetSizerAndFit(main_sizer) + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials') + Publisher.subscribe(self.SetImageFiducial, 'Set image fiducial') + Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point') + Publisher.subscribe(self.OnNextEnable, "Next enable for image fiducials") + Publisher.subscribe(self.OnNextDisable, "Next disable for image fiducials") + + def LoadImageFiducials(self, label, position): + fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', label) + + fiducial_index = fiducial['fiducial_index'] + fiducial_name = fiducial['fiducial_name'] + + if self.btns_set_fiducial[fiducial_index].GetValue(): + print("Fiducial {} already set, not resetting".format(label)) + return + + Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=position) + + self.btns_set_fiducial[fiducial_index].SetValue(True) + for m in [0, 1, 2]: + self.numctrls_fiducial[fiducial_index][m].SetValue(position[m]) + + if self.image.AreImageFiducialsSet(): + self.OnNextEnable() + else: + self.OnNextDisable() + + def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): + found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] + + assert len(found) != 0, "No fiducial found for which {} = {}".format(attribute_name, attribute_value) + return found[0] + + def SetImageFiducial(self, fiducial_name, position): + fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'fiducial_name', fiducial_name) + fiducial_index = fiducial['fiducial_index'] + + self.image.SetImageFiducial(fiducial_index, position) + if self.image.AreImageFiducialsSet(): + self.OnNextEnable() + else: + self.OnNextDisable() + + def UpdateImageCoordinates(self, position): + self.current_coord = position + + for m in [0, 1, 2]: + if not self.btns_set_fiducial[m].GetValue(): + for n in [0, 1, 2]: + self.numctrls_fiducial[m][n].SetValue(float(position[n])) + + def OnImageFiducials(self, n, evt): + fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] + + # XXX: This is still a bit hard to read, could be cleaned up. + label = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] + + if self.btns_set_fiducial[n].GetValue(): + position = self.numctrls_fiducial[n][0].GetValue(),\ + self.numctrls_fiducial[n][1].GetValue(),\ + self.numctrls_fiducial[n][2].GetValue() + orientation = None, None, None + + Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=position) + + colour = (0., 1., 0.) + size = 2 + seed = 3 * [0.] + + Publisher.sendMessage('Create marker', position=position, orientation=orientation, colour=colour, size=size, + label=label, seed=seed) + else: + for m in [0, 1, 2]: + self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) + print(self.numctrls_fiducial) + Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=np.nan) + Publisher.sendMessage('Delete fiducial marker', label=label) + + def OnNext(self, evt): + Publisher.sendMessage("Next to tracker fiducials") + + def OnNextEnable(self): + self.next_button.Enable() + + def OnNextDisable(self): + self.next_button.Disable() + + def OnStartRegistration(self, evt, ctrl): + value = ctrl.GetValue() + if value: + Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS) + for button in self.btns_set_fiducial: + button.Enable() + self.start_button.SetLabel("Stop registration") + else: + self.start_button.SetLabel("Start registration") + Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) + +class TrackerPage(wx.Panel): + def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavigation_api): + wx.Panel.__init__(self, parent) + + self.icp = icp + self.tracker = tracker + self.navigation = navigation + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api + + self.btns_set_fiducial = [None, None, None] + self.numctrls_fiducial = [[], [], []] + self.current_coord = 0, 0, 0, None, None, None + self.tracker_fiducial_being_set = None + for n in [0, 1, 2]: + if not self.tracker.IsTrackerFiducialSet(n): + self.tracker_fiducial_being_set = n + break + + + self.bg_bmp = getBitMapForBackground() + # Toggle buttons for image fiducials + background = wx.StaticBitmap(self, -1, self.bg_bmp, (0, 0)) + for n, fiducial in enumerate(const.TRACKER_FIDUCIALS): + button_id = fiducial['button_id'] + label = fiducial['label'] + tip = fiducial['tip'] + + ctrl = wx.ToggleButton(self, button_id, label=label, style=wx.BU_EXACTFIT) + ctrl.SetToolTip(wx.ToolTip(tip)) + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, i=n, ctrl=ctrl)) + ctrl.SetValue(self.tracker.IsTrackerFiducialSet(n)) + ctrl.Disable() + + self.btns_set_fiducial[n] = ctrl + + for m in range(len(self.btns_set_fiducial)): + for n in range(3): + value = self.tracker.GetTrackerFiducialForUI(m, n) + self.numctrls_fiducial[m].append( + wx.lib.masked.numctrl.NumCtrl(parent=self, integerWidth=4, fractionWidth=1, value=value, ) + ) + self.numctrls_fiducial[m][n].Hide() + + start_button = wx.ToggleButton(self, label="Start Patient Registration") + start_button.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStartRegistration, ctrl=start_button)) + self.start_button = start_button + + reset_button = wx.Button(self, label="Reset", style=wx.BU_EXACTFIT) + reset_button.Bind(wx.EVT_BUTTON, partial(self.OnReset, ctrl=reset_button)) + self.reset_button = reset_button + + next_button = wx.Button(self, label="Next") + next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) + if not self.tracker.AreTrackerFiducialsSet(): + next_button.Disable() + self.next_button = next_button + + top_sizer = wx.BoxSizer(wx.HORIZONTAL) + top_sizer.AddMany([ + (start_button), + (reset_button) + ]) + + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + bottom_sizer.Add(next_button) + + sizer = wx.GridBagSizer(5, 5) + sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self.btns_set_fiducial[2], wx.GBPosition(0, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_HORIZONTAL) + sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(background, wx.GBPosition(1, 2)) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10), + (sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5), + (bottom_sizer, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 30)]) + self.sizer = main_sizer + self.SetSizerAndFit(main_sizer) + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self.SetTrackerFiducial, 'Set tracker fiducial') + Publisher.subscribe(self.OnNextEnable, "Next enable for tracker fiducials") + Publisher.subscribe(self.OnNextDisable, "Next disable for tracker fiducials") + + def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): + found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] + + assert len(found) != 0, "No fiducial found for which {} = {}".format(attribute_name, attribute_value) + return found[0] + + def SetTrackerFiducial(self, fiducial_name): + # if not self.tracker.IsTrackerInitialized(): + # dlg.ShowNavigationTrackerWarning(0, 'choose') + # return + + fiducial = self.GetFiducialByAttribute(const.TRACKER_FIDUCIALS, 'fiducial_name', fiducial_name) + fiducial_index = fiducial['fiducial_index'] + + # XXX: The reference mode is fetched from navigation object, however it seems like not quite + # navigation-related attribute here, as the reference mode used during the fiducial registration + # is more concerned with the calibration than the navigation. + # + ref_mode_id = self.navigation.GetReferenceMode() + self.tracker.SetTrackerFiducial(ref_mode_id, fiducial_index) + + self.ResetICP() + if self.tracker.AreTrackerFiducialsSet(): + self.OnNextEnable() + else: + self.OnNextDisable() + #self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial, self.txtctrl_fre) + + def set_fiducial_callback(self, state, index=None): + if state: + if index is None: + fiducial_name = const.TRACKER_FIDUCIALS[self.tracker_fiducial_being_set]['fiducial_name'] + Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) + else: + fiducial_name = const.TRACKER_FIDUCIALS[index]['fiducial_name'] + Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) + + if self.tracker.AreTrackerFiducialsSet(): + if self.pedal_connection is not None: + self.pedal_connection.remove_callback(name='fiducial') + + if self.neuronavigation_api is not None: + self.neuronavigation_api.remove_pedal_callback(name='fiducial') + else: + for n in [0, 1, 2]: + if not self.tracker.IsTrackerFiducialSet(n): + self.tracker_fiducial_being_set = n + break + + def OnTrackerFiducials(self, evt, i, ctrl): + value = ctrl.GetValue() + self.set_fiducial_callback(True, index=i) + self.btns_set_fiducial[i].SetValue(self.tracker.IsTrackerFiducialSet(i)) + + def ResetICP(self): + self.icp.ResetICP() + #self.checkbox_icp.Enable(False) + #self.checkbox_icp.SetValue(False) + + def OnReset(self, evt, ctrl): + self.tracker.ResetTrackerFiducials() + for button in self.btns_set_fiducial: + button.SetValue(False) + self.start_button.SetValue(False) + self.OnStartRegistration(self.start_button, self.start_button) + + def OnNext(self, evt): + Publisher.sendMessage("Next to refine fiducials") + + def OnNextEnable(self): + self.next_button.Enable() + + def OnNextDisable(self): + self.next_button.Disable() + + def OnStartRegistration(self, evt, ctrl): + value = ctrl.GetValue() + if not value: + for button in self.btns_set_fiducial: + button.Disable() + else: + if not self.tracker.IsTrackerInitialized(): + self.start_button.SetValue(False) + dlg.ShowNavigationTrackerWarning(0, 'choose') + else: + if self.pedal_connection is not None: + self.pedal_connection.add_callback( + name='fiducial', + callback=self.set_fiducial_callback, + remove_when_released=True, + ) + + if self.neuronavigation_api is not None: + self.neuronavigation_api.add_pedal_callback( + name='fiducial', + callback=self.set_fiducial_callback, + remove_when_released=True, + ) + for button in self.btns_set_fiducial: + button.Enable() + +class RefinePage(wx.Panel): + def __init__(self, parent, icp, tracker, image, navigation): + + wx.Panel.__init__(self, parent) + self.icp = icp + self.tracker = tracker + self.image = image + self.navigation = navigation + + self.numctrls_fiducial = [[], [], [], [], [], []] + const_labels = [label for label in const.FIDUCIAL_LABELS] + labels = const_labels + const_labels # duplicate labels for image and tracker + self.labels = [wx.StaticText(self, -1, _(label)) for label in labels] + + for m in range(6): + for n in range(3): + if m <= 2: + value = self.image.GetImageFiducialForUI(m, n) + else: + value = self.tracker.GetTrackerFiducialForUI(m - 3, n) + + self.numctrls_fiducial[m].append( + wx.lib.masked.numctrl.NumCtrl(parent=self, integerWidth=4, fractionWidth=1, value=value)) + + txt_label_image = wx.StaticText(self, -1, _("Image Fiducials:")) + txt_label_image.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + coord_sizer = wx.GridBagSizer(hgap=5, vgap=0) + + for m in range(3): + coord_sizer.Add(self.labels[m], pos=wx.GBPosition(m, 0)) + for n in range(3): + coord_sizer.Add(self.numctrls_fiducial[m][n], pos=wx.GBPosition(m, n+1)) + if m in range(1, 6): + self.numctrls_fiducial[m][n].SetEditable(False) + + txt_label_track = wx.StaticText(self, -1, _("Tracker Fiducials:")) + txt_label_track.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + coord_sizer_track = wx.GridBagSizer(hgap=5, vgap=0) + + for m in range(3, 6): + coord_sizer_track.Add(self.labels[m], pos=wx.GBPosition(m-3, 0)) + for n in range(3): + coord_sizer_track.Add(self.numctrls_fiducial[m][n], pos=wx.GBPosition(m-3, n+1)) + if m in range(1, 6): + self.numctrls_fiducial[m][n].SetEditable(False) + + txt_fre = wx.StaticText(self, -1, _('FRE:')) + tooltip = wx.ToolTip(_("Fiducial registration error")) + txt_fre.SetToolTip(tooltip) + + value = self.icp.GetFreForUI() + txtctrl_fre = wx.TextCtrl(self, value=value, size=wx.Size(60, -1), style=wx.TE_CENTRE) + txtctrl_fre.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + txtctrl_fre.SetBackgroundColour('WHITE') + txtctrl_fre.SetEditable(0) + txtctrl_fre.SetToolTip(tooltip) + self.txtctrl_fre = txtctrl_fre + + self.OnUpdateUI() + + fre_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) + fre_sizer.AddMany([ + (txt_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (txtctrl_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL) + + ]) + + back_button = wx.Button(self, label="Back") + back_button.Bind(wx.EVT_BUTTON, partial(self.OnBack)) + self.back_button = back_button + + refine_button = wx.Button(self, label="Refine") + refine_button.Bind(wx.EVT_BUTTON, partial(self.OnRefine)) + self.refine_button = refine_button + + next_button = wx.Button(self, label="Next") + next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) + self.next_button = next_button + + button_sizer = wx.BoxSizer(wx.HORIZONTAL) + button_sizer.AddMany([ + (back_button, 0, wx.EXPAND), + (refine_button, 0, wx.EXPAND), + (next_button, 0, wx.EXPAND) + ]) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (txt_label_image, 0, wx.EXPAND | wx.ALL, 10), + (coord_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + (txt_label_track, 0, wx.EXPAND | wx.ALL, 10), + (coord_sizer_track, 0, wx.ALIGN_CENTER_HORIZONTAL), + (10, 10, 0), + (fre_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + (button_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 20), + (10, 10, 0) + ]) + self.sizer = main_sizer + self.SetSizerAndFit(main_sizer) + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self.OnUpdateUI, "Update UI for refine tab") + + def OnUpdateUI(self): + if self.tracker.AreTrackerFiducialsSet() and self.image.AreImageFiducialsSet(): + for m in range(6): + for n in range(3): + if m <= 2: + value = self.image.GetImageFiducialForUI(m, n) + else: + value = self.tracker.GetTrackerFiducialForUI(m - 3, n) + + self.numctrls_fiducial[m][n].SetValue(value) + + + self.navigation.EstimateTrackerToInVTransformationMatrix(self.tracker, self.image) + self.navigation.UpdateFiducialRegistrationError(self.tracker, self.image) + fre, fre_ok = self.navigation.GetFiducialRegistrationError(self.icp) + + self.txtctrl_fre.SetValue(str(round(fre, 2))) + if fre_ok: + self.txtctrl_fre.SetBackgroundColour('GREEN') + else: + self.txtctrl_fre.SetBackgroundColour('RED') + + def OnBack(self, evt): + Publisher.sendMessage('Back to image fiducials') + + def OnNext(self, evt): + Publisher.sendMessage('Next to stimulator fiducials') + + def OnRefine(self, evt): + self.icp.RegisterICP(self.navigation, self.tracker) + if self.icp.use_icp: + self.UpdateUI() + +class StimulatorPage(wx.Panel): + def __init__(self, parent): + + wx.Panel.__init__(self, parent) + + lbl_inter = wx.StaticText(self, -1, _("Stimulator Registration ")) + + border = wx.BoxSizer(wx.VERTICAL) + border.Add(lbl_inter, 1, wx.EXPAND | wx.ALL, 10) + self.SetSizerAndFit(border) + self.Layout() + +class NavigationPanel(wx.Panel): + def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): + wx.Panel.__init__(self, parent) + + self.navigation = navigation + self.tracker = tracker + self.robot = robot + self.icp = icp + self.image = image + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api + + top_sizer = wx.BoxSizer(wx.HORIZONTAL) + top_sizer.Add(MarkersPanel(self, self.navigation, self.tracker, self.icp), 0, wx.GROW | wx.EXPAND ) + + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + bottom_sizer.Add(ControlPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.image, self.pedal_connection, self.neuronavigation_api), 0, wx.EXPAND | wx.TOP, 20) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([(top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + (bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL) + ]) + self.sizer = main_sizer + self.SetSizerAndFit(main_sizer) + self.Update() + +class ControlPanel(wx.Panel): + def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): + + wx.Panel.__init__(self, parent) + + # Initialize global variables + self.navigation = navigation + self.tracker = tracker + self.robot = robot + self.icp = icp + self.image = image + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api + + # Toggle button for neuronavigation + tooltip = wx.ToolTip(_("Start navigation")) + btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1)) + btn_nav.SetFont(wx.Font(11, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + btn_nav.SetToolTip(tooltip) + self.btn_nav = btn_nav + btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn_nav=btn_nav)) + + # Toggle button for robot + tooltip = wx.ToolTip(_("Stop robot")) + btn_robot = wx.ToggleButton(self, -1, _("Stop Robot"), size=wx.Size(80, -1)) + btn_robot.SetFont(wx.Font(11, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + btn_robot.SetToolTip(tooltip) + self.btn_robot = btn_robot + btn_robot.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStopRobot, btn_nav=btn_robot)) + + # Label and Checkbox for Tractography + tooltip = wx.ToolTip(_(u"Control Tractography")) + tractography_checkbox = wx.CheckBox(self, -1, _('Enable / Disable Tractography ')) + tractography_checkbox.SetValue(False) + tractography_checkbox.Enable(False) + tractography_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnTractographyCheckbox, ctrl=tractography_checkbox)) + tractography_checkbox.SetToolTip(tooltip) + self.tractography_checkbox = tractography_checkbox + + # Check box to track object or simply the stylus + checkbox_track_object = wx.CheckBox(self, -1, _('Track object')) + checkbox_track_object.SetValue(False) + checkbox_track_object.Enable(0) + checkbox_track_object.Bind(wx.EVT_CHECKBOX, partial(self.OnTrackObjectCheckbox, ctrl=checkbox_track_object)) + self.checkbox_track_object = checkbox_track_object + + # Label and Checkbox for Lock to Target + tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) + lock_to_target_checkbox = wx.CheckBox(self, -1, _('Lock to target:')) + lock_to_target_checkbox.SetValue(False) + lock_to_target_checkbox.Enable(False) + lock_to_target_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnLockToTargetCheckbox, ctrl=lock_to_target_checkbox)) + lock_to_target_checkbox.SetToolTip(tooltip) + self.lock_to_target_checkbox = lock_to_target_checkbox + + # Checkbox for object position and orientation update in volume rendering during navigation + tooltip = wx.ToolTip(_("Show and track TMS coil")) + checkobj = wx.CheckBox(self, -1, _('Show coil')) + checkobj.SetToolTip(tooltip) + checkobj.SetValue(False) + checkobj.Disable() + checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowCoil) + self.checkobj = checkobj + + # Checkbox for camera update in volume rendering during navigation + tooltip = wx.ToolTip(_("Update camera in volume")) + checkcamera = wx.CheckBox(self, -1, _('Vol. camera')) + checkcamera.SetToolTip(tooltip) + checkcamera.SetValue(const.CAM_MODE) + checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCameraCheckbox) + self.checkcamera = checkcamera + # Checkbox to use serial port to trigger pulse signal and create markers tooltip = wx.ToolTip(_("Enable serial port communication to trigger pulse and create markers")) checkbox_serial_port = wx.CheckBox(self, -1, _('Serial port')) @@ -207,167 +1152,173 @@ def __init__(self, parent): checkbox_serial_port.SetValue(False) checkbox_serial_port.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableSerialPort, ctrl=checkbox_serial_port)) self.checkbox_serial_port = checkbox_serial_port + + #Checkbox for Force Sensor + tooltip = wx.ToolTip(_(u"Control Force Sensor")) + force_checkbox = wx.CheckBox(self, -1, _('Enable / Disable Force Sensor ')) + force_checkbox.SetValue(False) + force_checkbox.Enable(False) + force_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnForceSensorCheckbox, ctrl=force_checkbox)) + force_checkbox.SetToolTip(tooltip) + self.force_checkbox = force_checkbox + + + button_sizer = wx.BoxSizer(wx.VERTICAL) + button_sizer.AddMany([ + (btn_nav, 0, wx.EXPAND | wx.GROW), + (btn_robot, 0, wx.EXPAND | wx.GROW) + ]) + + checkbox_sizer = wx.BoxSizer(wx.VERTICAL) + checkbox_sizer.AddMany([ + (tractography_checkbox), + (checkbox_track_object), + (lock_to_target_checkbox), + (checkobj), + (checkcamera), + (checkbox_serial_port), + (force_checkbox) + ]) - # Checkbox for object position and orientation update in volume rendering during navigation - tooltip = wx.ToolTip(_("Show and track TMS coil")) - checkobj = wx.CheckBox(self, -1, _('Show coil')) - checkobj.SetToolTip(tooltip) - checkobj.SetValue(False) - checkobj.Disable() - checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowCoil) - self.checkobj = checkobj + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (button_sizer, 0, wx.EXPAND | wx.ALL, 10), + (checkbox_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP | wx.BOTTOM , 20) + ]) - # if sys.platform != 'win32': - self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) - checkbox_serial_port.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) - checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + self.sizer = main_sizer + self.SetSizerAndFit(main_sizer) - # Fold panel style - style = fpb.CaptionBarStyle() - style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) - style.SetFirstColour(default_colour) - style.SetSecondColour(default_colour) + self.__bind_events() + self.Update() + self.LoadState() - # Fold 1 - Navigation panel - item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) - ntw = NeuronavigationPanel( - parent=item, - navigation=navigation, - tracker=tracker, - robot=robot, - icp=icp, - image=image, - pedal_connection=pedal_connection, - neuronavigation_api=neuronavigation_api, - ) - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, - leftSpacing=0, rightSpacing=0) - fold_panel.Expand(fold_panel.GetFoldPanel(0)) + def __bind_events(self): + Publisher.subscribe(self.OnStartNavigation, 'Start navigation') + Publisher.subscribe(self.OnStopNavigation, 'Stop navigation') + Publisher.subscribe(self.OnCheckStatus, 'Navigation status') + Publisher.subscribe(self.UpdateTarget, 'Update target') + # Externally check/uncheck and enable/disable checkboxes. + Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') + Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') - # Fold 2 - Object registration panel - item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) - otw = ObjectRegistrationPanel( - parent=item, - tracker=tracker, - pedal_connection=pedal_connection, - neuronavigation_api=neuronavigation_api, - ) + Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') + Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, otw, spacing=0, - leftSpacing=0, rightSpacing=0) + def SaveState(self): + track_object = self.checkbox_track_object + state = { + 'track_object': { + 'checked': track_object.IsChecked(), + 'enabled': track_object.IsEnabled(), + } + } - # Fold 3 - Markers panel - item = fold_panel.AddFoldPanel(_("Markers"), collapsed=True) - mtw = MarkersPanel(item, navigation, tracker, icp) + session = ses.Session() + session.SetState('object_registration_panel', state) - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, mtw, spacing= 0, - leftSpacing=0, rightSpacing=0) + def LoadState(self): + session = ses.Session() + state = session.GetState('object_registration_panel') - # Fold 4 - Tractography panel - if has_trekker: - item = fold_panel.AddFoldPanel(_("Tractography"), collapsed=True) - otw = TractographyPanel(item) + if state is None: + return - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, otw, spacing=0, - leftSpacing=0, rightSpacing=0) + track_object = state['track_object'] - # Fold 5 - DBS - self.dbs_item = fold_panel.AddFoldPanel(_("Deep Brain Stimulation"), collapsed=True) - dtw = DbsPanel(self.dbs_item) #Atribuir nova var, criar panel + self.EnableTrackObjectCheckbox(track_object['enabled']) + self.CheckTrackObjectCheckbox(track_object['checked']) - fold_panel.ApplyCaptionStyle(self.dbs_item, style) - fold_panel.AddFoldPanelWindow(self.dbs_item, dtw, spacing= 0, - leftSpacing=0, rightSpacing=0) - self.dbs_item.Hide() - # Fold 6 - Sessions - item = fold_panel.AddFoldPanel(_("Sessions"), collapsed=False) - stw = SessionPanel(item) - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, stw, spacing= 0, - leftSpacing=0, rightSpacing=0) + # Navigation + def OnStartNavigation(self): + if not self.tracker.AreTrackerFiducialsSet() or not self.image.AreImageFiducialsSet(): + wx.MessageBox(_("Invalid fiducials, select all coordinates."), _("InVesalius 3")) - # Fold 7 - E-field + elif not self.tracker.IsTrackerInitialized(): + dlg.ShowNavigationTrackerWarning(0, 'choose') + errors = True - item = fold_panel.AddFoldPanel(_("E-field"), collapsed=True) - etw = E_fieldPanel(item, navigation) - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, etw, spacing=0, - leftSpacing=0, rightSpacing=0) + else: + # Prepare GUI for navigation. + Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS) + Publisher.sendMessage("Hide current mask") - # Panel sizer for checkboxes - line_sizer = wx.BoxSizer(wx.HORIZONTAL) - line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) - line_sizer.Add(checkbox_serial_port, 0, wx.ALIGN_CENTER) - line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5) - line_sizer.Fit(self) + self.navigation.EstimateTrackerToInVTransformationMatrix(self.tracker, self.image) + self.navigation.StartNavigation(self.tracker, self.icp) - # Panel sizer to expand fold panel - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(fold_panel, 0, wx.GROW|wx.EXPAND) - sizer.Add(line_sizer, 1, wx.GROW | wx.EXPAND) - sizer.Fit(self) + def OnNavigate(self, evt, btn_nav): + nav_id = btn_nav.GetValue() + if not nav_id: + wx.CallAfter(Publisher.sendMessage, 'Stop navigation') - self.track_obj = False + tooltip = wx.ToolTip(_("Start neuronavigation")) + btn_nav.SetToolTip(tooltip) + else: + Publisher.sendMessage("Start navigation") - self.SetSizer(sizer) - self.Update() - self.SetAutoLayout(1) - - def __bind_events(self): - Publisher.subscribe(self.OnCheckStatus, 'Navigation status') - Publisher.subscribe(self.OnShowDbs, "Show dbs folder") - Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") + if self.nav_status: + tooltip = wx.ToolTip(_("Stop neuronavigation")) + btn_nav.SetToolTip(tooltip) + else: + btn_nav.SetValue(False) + + def OnStopNavigation(self): + self.navigation.StopNavigation() + if self.tracker.tracker_id == const.ROBOT: + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) - # Externally check/uncheck and enable/disable checkboxes. - Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') - Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') + def UpdateTarget(self, coord): + self.navigation.target = coord - Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') - Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') + if coord is not None: + self.lock_to_target_checkbox.Enable(True) + self.lock_to_target_checkbox.SetValue(True) + self.navigation.SetLockToTarget(True) - def OnShowDbs(self): - self.dbs_item.Show() - def OnHideDbs(self): - self.dbs_item.Hide() + # 'Robot' + def OnStopRobot(self, evt): + pass - def OnCheckStatus(self, nav_status, vis_status): - if nav_status: - self.checkbox_serial_port.Enable(False) - self.checkobj.Enable(False) - else: - self.checkbox_serial_port.Enable(True) - if self.track_obj: - self.checkobj.Enable(True) - def OnEnableSerialPort(self, evt, ctrl): - if ctrl.GetValue(): - from wx import ID_OK - dlg_port = dlg.SetCOMPort(select_baud_rate=False) + # 'Tracktography' + def OnTractographyCheckbox(self, evt): + pass - if dlg_port.ShowModal() != ID_OK: - ctrl.SetValue(False) - return - com_port = dlg_port.GetCOMPort() - baud_rate = 115200 + # 'Track object' checkbox + def EnableTrackObjectCheckbox(self, enabled): + self.checkbox_track_object.Enable(enabled) - Publisher.sendMessage('Update serial port', serial_port_in_use=True, com_port=com_port, baud_rate=baud_rate) - else: - Publisher.sendMessage('Update serial port', serial_port_in_use=False) + def CheckTrackObjectCheckbox(self, checked): + self.checkbox_track_object.SetValue(checked) + self.OnTrackObjectCheckbox() + + def OnTrackObjectCheckbox(self, evt=None, ctrl=None): + checked = self.checkbox_track_object.IsChecked() + Publisher.sendMessage('Track object', enabled=checked) + + # Disable or enable 'Show coil' checkbox, based on if 'Track object' checkbox is checked. + Publisher.sendMessage('Enable show-coil checkbox', enabled=checked) + + # Also, automatically check or uncheck 'Show coil' checkbox. + Publisher.sendMessage('Check show-coil checkbox', checked=checked) + + self.SaveState() - # 'Show coil' checkbox + # 'Lock to Target' checkbox + def OnLockToTargetCheckbox(self, evt, ctrl): + value = ctrl.GetValue() + self.navigation.SetLockToTarget(value) + + # 'Show coil' checkbox def CheckShowCoil(self, checked=False): self.checkobj.SetValue(checked) self.track_obj = checked - self.OnShowCoil() def EnableShowCoil(self, enabled=False): @@ -375,10 +1326,10 @@ def EnableShowCoil(self, enabled=False): def OnShowCoil(self, evt=None): checked = self.checkobj.GetValue() - Publisher.sendMessage('Show-coil checked', checked=checked) + Publisher.sendMessage('Show-coil checked', checked=checked) - # 'Volume camera' checkbox + # 'Volume camera' checkbox def CheckVolumeCameraCheckbox(self, checked): self.checkcamera.SetValue(checked) self.OnVolumeCameraCheckbox() @@ -388,6 +1339,37 @@ def OnVolumeCameraCheckbox(self, evt=None, status=None): def EnableVolumeCameraCheckbox(self, enabled): self.checkcamera.Enable(enabled) + + + # 'Serial Port Com' + def OnCheckStatus(self, nav_status, vis_status): + if nav_status: + self.checkbox_serial_port.Enable(False) + self.checkobj.Enable(False) + else: + self.checkbox_serial_port.Enable(True) + # if self.track_obj: + # self.checkobj.Enable(True) + + def OnEnableSerialPort(self, evt, ctrl): + if ctrl.GetValue(): + from wx import ID_OK + dlg_port = dlg.SetCOMPort(select_baud_rate=False) + + if dlg_port.ShowModal() != ID_OK: + ctrl.SetValue(False) + return + + com_port = dlg_port.GetCOMPort() + baud_rate = 115200 + + Publisher.sendMessage('Update serial port', serial_port_in_use=True, com_port=com_port, baud_rate=baud_rate) + else: + Publisher.sendMessage('Update serial port', serial_port_in_use=False) + + # 'Force Sensor' + def OnForceSensorCheckbox(self): + pass class NeuronavigationPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): @@ -960,7 +1942,6 @@ def OnCloseProject(self): self.tracker.__init__() self.icp.__init__() - class ObjectRegistrationPanel(wx.Panel): def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): wx.Panel.__init__(self, parent) @@ -1305,7 +2286,6 @@ def OnRemoveObject(self): self.obj_name = None self.timestamp = const.TIMESTAMP - class MarkersPanel(wx.Panel): @dataclasses.dataclass class Marker: @@ -1467,22 +2447,24 @@ def __init__(self, parent, navigation, tracker, icp): self.current_session = 1 self.brain_actor = None - # Change marker size - spin_size = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) - spin_size.SetRange(1, 99) - spin_size.SetValue(self.marker_size) - spin_size.Bind(wx.EVT_TEXT, partial(self.OnSelectSize, ctrl=spin_size)) - spin_size.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectSize, ctrl=spin_size)) + # Change session + spin_session = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) + spin_session.SetRange(1, 99) + spin_session.SetValue(self.current_session) + spin_session.SetToolTip("Set session") + spin_session.Bind(wx.EVT_TEXT, partial(self.OnSessionChanged, ctrl=spin_session)) + spin_session.Bind(wx.EVT_SPINCTRL, partial(self.OnSessionChanged, ctrl=spin_session)) # Marker colour select select_colour = csel.ColourSelect(self, -1, colour=[255*s for s in self.marker_colour], size=wx.Size(20, 23)) + select_colour.SetToolTip("Set colour") select_colour.Bind(csel.EVT_COLOURSELECT, partial(self.OnSelectColour, ctrl=select_colour)) btn_create = wx.Button(self, -1, label=_('Create marker'), size=wx.Size(135, 23)) btn_create.Bind(wx.EVT_BUTTON, self.OnCreateMarker) sizer_create = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) - sizer_create.AddMany([(spin_size, 1), + sizer_create.AddMany([(spin_session, 1), (select_colour, 0), (btn_create, 0)]) @@ -1920,7 +2902,11 @@ def OnSendBrainTarget(self, evt): print("The coil is not at the target") else: print("Target not set") - + + def OnSessionChanged(self, evt, ctrl): + value = ctrl.GetValue() + Publisher.sendMessage('Current session changed', new_session_id=value) + def OnDeleteAllMarkers(self, evt=None): if evt is not None: result = dlg.ShowConfirmationDialog(msg=_("Remove all markers? Cannot be undone.")) @@ -2162,7 +3148,6 @@ def CreateMarker(self, position=None, orientation=None, colour=None, size=None, self.marker_list_ctrl.EnsureVisible(num_items) - class DbsPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -2171,7 +3156,6 @@ def __init__(self, parent): except AttributeError: default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) - class TractographyPanel(wx.Panel): def __init__(self, parent): @@ -2872,7 +3856,6 @@ def __init__(self, parent): def OnSessionChanged(self, evt): Publisher.sendMessage('Current session changed', new_session_id=self.__spin_session.GetValue()) - class InputAttributes(object): # taken from https://stackoverflow.com/questions/2466191/set-attributes-from-dictionary-in-python def __init__(self, *initial_data, **kwargs): From 37c172d8c4ebcc3546ac20aede6b454bad2301d8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 7 Jul 2023 16:33:12 +0300 Subject: [PATCH 12/99] REM: Unfinished code --- invesalius/gui/task_tractography.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index b50520407..3031f9dd3 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -429,7 +429,6 @@ def OnLinkFOD(self, event=None): if filename: Publisher.sendMessage('Update status text in GUI', label=_("Busy")) - sp = dlg.TractographyProgressWindow() try: self.trekker = Trekker.initialize(filename.encode('utf-8')) self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg) @@ -444,7 +443,6 @@ def OnLinkFOD(self, event=None): Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized")) # except: # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3")) - sp.Close() except: Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialization failed.")) wx.MessageBox(_("Unable to load FOD."), _("InVesalius 3")) From 6ff0c9d15ea9fafacd340cca917b9e87898868b5 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 10 Jul 2023 11:19:55 +0300 Subject: [PATCH 13/99] MOD: Combine 2D and 3D visualization preferences into one tab --- invesalius/gui/preferences.py | 43 +++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 996d5e4ae..85f25cd7f 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -18,13 +18,13 @@ def __init__( super().__init__(parent, id_, title, style=style) self.book = wx.Notebook(self, -1) - self.pnl_viewer2d = Viewer2D(self.book) + #self.pnl_viewer2d = Viewer2D(self.book) self.pnl_viewer3d = Viewer3D(self.book) # self.pnl_surface = SurfaceCreation(self) self.pnl_language = Language(self.book) - self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) - self.book.AddPage(self.pnl_viewer3d, _("3D Visualization")) + #self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) + self.book.AddPage(self.pnl_viewer3d, _("Visualization")) # self.book.AddPage(self.pnl_surface, _("Surface creation")) self.book.AddPage(self.pnl_language, _("Language")) @@ -49,10 +49,9 @@ def GetPreferences(self): values = {} lang = self.pnl_language.GetSelection() viewer = self.pnl_viewer3d.GetSelection() - viewer2d = self.pnl_viewer2d.GetSelection() + #viewer2d = self.pnl_viewer2d.GetSelection() values.update(lang) values.update(viewer) - values.update(viewer2d) return values @@ -71,7 +70,7 @@ def LoadPreferences(self): const.SLICE_INTERPOLATION: slice_interpolation, } - self.pnl_viewer2d.LoadSelection(values) + #self.pnl_viewer2d.LoadSelection(values) self.pnl_viewer3d.LoadSelection(values) self.pnl_language.LoadSelection(values) @@ -81,8 +80,8 @@ def __init__(self, parent): wx.Panel.__init__(self, parent) - bsizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Surface")) - lbl_inter = wx.StaticText(bsizer.GetStaticBox(), -1, _("Interpolation ")) + bsizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("3D Visualization")) + lbl_inter = wx.StaticText(bsizer.GetStaticBox(), -1, _("Surface Interpolation ")) rb_inter = self.rb_inter = wx.RadioBox( bsizer.GetStaticBox(), -1, @@ -95,23 +94,34 @@ def __init__(self, parent): bsizer.Add(lbl_inter, 0, wx.TOP | wx.LEFT, 10) bsizer.Add(rb_inter, 0, wx.TOP | wx.LEFT, 0) - # box_rendering = wx.StaticBox(self, -1, _("Volume rendering")) - bsizer_ren = wx.StaticBoxSizer(wx.VERTICAL, self, _("Volume rendering")) - lbl_rendering = wx.StaticText(bsizer_ren.GetStaticBox(), -1, _("Rendering")) + lbl_rendering = wx.StaticText(bsizer.GetStaticBox(), -1, _("Volume Rendering")) rb_rendering = self.rb_rendering = wx.RadioBox( - bsizer_ren.GetStaticBox(), + bsizer.GetStaticBox(), -1, choices=["CPU", _(u"GPU (NVidia video cards only)")], majorDimension=2, style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, ) + bsizer.Add(lbl_rendering, 0, wx.TOP | wx.LEFT, 10) + bsizer.Add(rb_rendering, 0, wx.TOP | wx.LEFT, 0) + + bsizer_slices = wx.StaticBoxSizer(wx.VERTICAL, self, _("2D Visualization")) + lbl_inter_sl = wx.StaticText(bsizer_slices.GetStaticBox(), -1, _("Slice Interpolation ")) + rb_inter_sl = self.rb_inter_sl = wx.RadioBox( + bsizer_slices.GetStaticBox(), + -1, + choices=[_("Yes"), _("No")], + majorDimension=3, + style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, + ) - bsizer_ren.Add(lbl_rendering, 0, wx.TOP | wx.LEFT, 10) - bsizer_ren.Add(rb_rendering, 0, wx.TOP | wx.LEFT, 0) + bsizer_slices.Add(lbl_inter_sl, 0, wx.TOP | wx.LEFT, 10) + bsizer_slices.Add(rb_inter_sl, 0, wx.TOP | wx.LEFT, 0) border = wx.BoxSizer(wx.VERTICAL) + border.Add(bsizer_slices, 1, wx.EXPAND | wx.ALL, 10) border.Add(bsizer, 1, wx.EXPAND | wx.ALL, 10) - border.Add(bsizer_ren, 1, wx.EXPAND | wx.ALL, 10) + self.SetSizerAndFit(border) self.Layout() @@ -120,6 +130,7 @@ def GetSelection(self): options = { const.RENDERING: self.rb_rendering.GetSelection(), const.SURFACE_INTERPOLATION: self.rb_inter.GetSelection(), + const.SLICE_INTERPOLATION: self.rb_inter_sl.GetSelection() } return options @@ -127,9 +138,11 @@ def GetSelection(self): def LoadSelection(self, values): rendering = values[const.RENDERING] surface_interpolation = values[const.SURFACE_INTERPOLATION] + slice_interpolation = values[const.SLICE_INTERPOLATION] self.rb_rendering.SetSelection(int(rendering)) self.rb_inter.SetSelection(int(surface_interpolation)) + self.rb_inter_sl.SetSelection(int(slice_interpolation)) class Viewer2D(wx.Panel): From e02f608d4e05fd4eb938f0192d4d619236b6333e Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 11 Jul 2023 14:56:31 +0300 Subject: [PATCH 14/99] MOD: Separate robot from tracker --- invesalius/navigation/robot.py | 51 ++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 4427b6257..fb2b4ca43 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -19,6 +19,7 @@ import threading +from wx import ID_OK import numpy as np import wx @@ -36,6 +37,7 @@ class Robot(): def __init__(self, tracker): self.tracker = tracker + self.robot_ip = None self.matrix_tracker_to_robot = None self.robot_coregistration_dialog = None @@ -68,30 +70,55 @@ def LoadState(self): return True def ConfigureRobot(self): - self.robot_coregistration_dialog = dlg.RobotCoregistrationDialog(self.tracker) - # Show dialog and store relevant output values. - status = self.robot_coregistration_dialog.ShowModal() - matrix_tracker_to_robot = self.robot_coregistration_dialog.GetValue() + if self.tracker.tracker_connection and self.tracker.tracker_connection.IsConnected(): + select_ip_dialog = dlg.SetRobotIP() + status = select_ip_dialog.ShowModal() - # Destroy the dialog. - self.robot_coregistration_dialog.Destroy() + if status == ID_OK: + robot_ip = select_ip_dialog.GetValue() + self.robot_ip = robot_ip + self.configuration = { + 'tracker_id': self.tracker.GetTrackerId(), + 'robot_ip': robot_ip, + 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), + } - if status != wx.ID_OK: - wx.MessageBox(_("Unable to connect to the robot."), _("InVesalius 3")) - return False + self.connection = self.tracker.tracker_connection + select_ip_dialog.Destroy() + else: + select_ip_dialog.Destroy() + return False - self.matrix_tracker_to_robot = matrix_tracker_to_robot + self.robot_coregistration_dialog = dlg.RobotCoregistrationDialog(self.tracker) - self.SaveState() + # Show dialog and store relevant output values. + status = self.robot_coregistration_dialog.ShowModal() + matrix_tracker_to_robot = self.robot_coregistration_dialog.GetValue() + + # Destroy the dialog. + self.robot_coregistration_dialog.Destroy() + + if status != wx.ID_OK: + wx.MessageBox(_("Unable to connect to the robot."), _("InVesalius 3")) + return False + + self.matrix_tracker_to_robot = matrix_tracker_to_robot + + self.SaveState() + return True + + else: + wx.MessageBox(_("Select Tracker first"), _("InVesalius 3")) + return False - return True def AbortRobotConfiguration(self): if self.robot_coregistration_dialog: self.robot_coregistration_dialog.Destroy() def InitializeRobot(self): + Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) From 47b88a9464b84cf6b131bdda301498c1ebd0199a Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 11 Jul 2023 14:56:47 +0300 Subject: [PATCH 15/99] ADD: tracker and robot in preferences --- invesalius/gui/preferences.py | 190 +++++++++++++++++++++++++++------- 1 file changed, 155 insertions(+), 35 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 85f25cd7f..5240392f7 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -1,11 +1,15 @@ import sys +from functools import partial + import invesalius.constants as const import invesalius.session as ses import wx from invesalius.gui.language_dialog import ComboBoxLanguage from invesalius.pubsub import pub as Publisher +from invesalius.navigation.tracker import Tracker +from invesalius.navigation.robot import Robot class Preferences(wx.Dialog): def __init__( @@ -16,16 +20,21 @@ def __init__( style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, ): super().__init__(parent, id_, title, style=style) + self.tracker = Tracker() + self.robot = Robot( + tracker=self.tracker + ) self.book = wx.Notebook(self, -1) #self.pnl_viewer2d = Viewer2D(self.book) self.pnl_viewer3d = Viewer3D(self.book) + self.pnl_tracker = TrackerPage(self.book, self.tracker, self.robot) # self.pnl_surface = SurfaceCreation(self) self.pnl_language = Language(self.book) #self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) self.book.AddPage(self.pnl_viewer3d, _("Visualization")) - # self.book.AddPage(self.pnl_surface, _("Surface creation")) + self.book.AddPage(self.pnl_tracker, _("Tracker")) self.book.AddPage(self.pnl_language, _("Language")) btnsizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL) @@ -91,8 +100,8 @@ def __init__(self, parent): style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, ) - bsizer.Add(lbl_inter, 0, wx.TOP | wx.LEFT, 10) - bsizer.Add(rb_inter, 0, wx.TOP | wx.LEFT, 0) + bsizer.Add(lbl_inter, 0, wx.TOP | wx.LEFT | wx.FIXED_MINSIZE, 10) + bsizer.Add(rb_inter, 0, wx.TOP | wx.LEFT | wx.FIXED_MINSIZE, 0) lbl_rendering = wx.StaticText(bsizer.GetStaticBox(), -1, _("Volume Rendering")) rb_rendering = self.rb_rendering = wx.RadioBox( @@ -102,8 +111,8 @@ def __init__(self, parent): majorDimension=2, style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, ) - bsizer.Add(lbl_rendering, 0, wx.TOP | wx.LEFT, 10) - bsizer.Add(rb_rendering, 0, wx.TOP | wx.LEFT, 0) + bsizer.Add(lbl_rendering, 0, wx.TOP | wx.LEFT | wx.FIXED_MINSIZE, 10) + bsizer.Add(rb_rendering, 0, wx.TOP | wx.LEFT | wx.FIXED_MINSIZE, 0) bsizer_slices = wx.StaticBoxSizer(wx.VERTICAL, self, _("2D Visualization")) lbl_inter_sl = wx.StaticText(bsizer_slices.GetStaticBox(), -1, _("Slice Interpolation ")) @@ -115,12 +124,12 @@ def __init__(self, parent): style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, ) - bsizer_slices.Add(lbl_inter_sl, 0, wx.TOP | wx.LEFT, 10) - bsizer_slices.Add(rb_inter_sl, 0, wx.TOP | wx.LEFT, 0) + bsizer_slices.Add(lbl_inter_sl, 0, wx.TOP | wx.LEFT | wx.FIXED_MINSIZE, 10) + bsizer_slices.Add(rb_inter_sl, 0, wx.TOP | wx.LEFT | wx.FIXED_MINSIZE, 0) border = wx.BoxSizer(wx.VERTICAL) - border.Add(bsizer_slices, 1, wx.EXPAND | wx.ALL, 10) - border.Add(bsizer, 1, wx.EXPAND | wx.ALL, 10) + border.Add(bsizer_slices, 1, wx.EXPAND | wx.ALL | wx.FIXED_MINSIZE, 10) + border.Add(bsizer, 1, wx.EXPAND | wx.ALL | wx.FIXED_MINSIZE, 10) self.SetSizerAndFit(border) self.Layout() @@ -144,39 +153,114 @@ def LoadSelection(self, values): self.rb_inter.SetSelection(int(surface_interpolation)) self.rb_inter_sl.SetSelection(int(slice_interpolation)) - -class Viewer2D(wx.Panel): - def __init__(self, parent): - +class TrackerPage(wx.Panel): + def __init__(self, parent, tracker, robot): wx.Panel.__init__(self, parent) - bsizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Slices")) - lbl_inter = wx.StaticText(bsizer.GetStaticBox(), -1, _("Interpolated ")) - rb_inter = self.rb_inter = wx.RadioBox( - bsizer.GetStaticBox(), - -1, - choices=[_("Yes"), _("No")], - majorDimension=3, - style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, - ) - - bsizer.Add(lbl_inter, 0, wx.TOP | wx.LEFT, 10) - bsizer.Add(rb_inter, 0, wx.TOP | wx.LEFT, 0) + self.__bind_events() - border = wx.BoxSizer(wx.VERTICAL) - border.Add(bsizer, 1, wx.EXPAND | wx.ALL, 10) - self.SetSizerAndFit(border) + self.tracker = tracker + self.robot = robot + + # ComboBox for spatial tracker device selection + tracker_options = [_("Select")] + self.tracker.get_trackers() + select_tracker_elem = wx.ComboBox(self, -1, "", size=(145, -1), + choices=tracker_options, style=wx.CB_DROPDOWN|wx.CB_READONLY) + tooltip = wx.ToolTip(_("Choose the tracking device")) + select_tracker_elem.SetToolTip(tooltip) + select_tracker_elem.SetSelection(self.tracker.tracker_id) + select_tracker_elem.Bind(wx.EVT_COMBOBOX, partial(self.OnChooseTracker, ctrl=select_tracker_elem)) + self.select_tracker_elem = select_tracker_elem + + select_tracker_label = wx.StaticText(self, -1, _('Choose the tracking device: ')) + + + # ComboBox for tracker reference mode + tooltip = wx.ToolTip(_("Choose the navigation reference mode")) + choice_ref = wx.ComboBox(self, -1, "", + choices=const.REF_MODE, style=wx.CB_DROPDOWN|wx.CB_READONLY) + choice_ref.SetSelection(const.DEFAULT_REF_MODE) + choice_ref.SetToolTip(tooltip) + choice_ref.Bind(wx.EVT_COMBOBOX, partial(self.OnChooseReferenceMode, ctrl=select_tracker_elem)) + self.choice_ref = choice_ref + + choice_ref_label = wx.StaticText(self, -1, _('Choose the navigation reference mode: ')) + + ref_sizer = wx.FlexGridSizer(rows=2, cols=2, hgap=5, vgap=5) + ref_sizer.AddMany([ + (select_tracker_label, wx.LEFT), + (select_tracker_elem, wx.RIGHT), + (choice_ref_label, wx.LEFT), + (choice_ref, wx.RIGHT) + ]) + ref_sizer.Layout() + + sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Setup tracker")) + sizer.Add(ref_sizer, 1, wx.ALL | wx.FIXED_MINSIZE, 20) + + lbl_rob = wx.StaticText(self, -1, _("Robot tracking device :")) + btn_rob = wx.Button(self, -1, _("Setup")) + btn_rob.SetToolTip("Setup robot tracking") + btn_rob.Enable(1) + btn_rob.Bind(wx.EVT_BUTTON, self.OnRobot) + self.btn_rob = btn_rob + + rob_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) + rob_sizer.AddMany([ + (lbl_rob, 0, wx.LEFT), + (btn_rob, 0, wx.RIGHT) + ]) + + rob_static_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Setup robot")) + rob_static_sizer.Add(rob_sizer, 1, wx.ALL | wx.FIXED_MINSIZE, 20) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (sizer, 0, wx.ALL | wx.EXPAND, 10), + (rob_static_sizer, 0, wx.ALL | wx.EXPAND, 10) + ]) + self.SetSizerAndFit(main_sizer) self.Layout() - def GetSelection(self): - - options = {const.SLICE_INTERPOLATION: self.rb_inter.GetSelection()} + def __bind_events(self): + pass - return options + def OnChooseTracker(self, evt, ctrl): + self.HideParent() + + Publisher.sendMessage('Update status text in GUI', + label=_("Configuring tracker ...")) + if hasattr(evt, 'GetSelection'): + choice = evt.GetSelection() + else: + choice = None + + #self.DisconnectTracker() + self.tracker.ResetTrackerFiducials() + self.tracker.SetTracker(choice) + + self.ShowParent() + + def OnChooseReferenceMode(self, evt, ctrl): + pass - def LoadSelection(self, values): - value = values[const.SLICE_INTERPOLATION] - self.rb_inter.SetSelection(int(value)) + def HideParent(self): # hide preferences dialog box + self.GetGrandParent().Hide() + + def ShowParent(self): # show preferences dialog box + self.GetGrandParent().Show() + + def OnRobot(self, evt): + self.HideParent() + + success = self.robot.ConfigureRobot() + if success: + self.robot.InitializeRobot() + else: + #self.DisconnectTracker() + pass + + self.ShowParent() class Language(wx.Panel): @@ -213,6 +297,8 @@ def LoadSelection(self, values): self.cmb_lang.SetSelection(int(selection)) + +# Deprecated code class SurfaceCreation(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -234,3 +320,37 @@ def GetSelection(self): def LoadSelection(self, values): pass + +class Viewer2D(wx.Panel): + def __init__(self, parent): + + wx.Panel.__init__(self, parent) + + bsizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Slices")) + lbl_inter = wx.StaticText(bsizer.GetStaticBox(), -1, _("Interpolated ")) + rb_inter = self.rb_inter = wx.RadioBox( + bsizer.GetStaticBox(), + -1, + choices=[_("Yes"), _("No")], + majorDimension=3, + style=wx.RA_SPECIFY_COLS | wx.NO_BORDER, + ) + + bsizer.Add(lbl_inter, 0, wx.TOP | wx.LEFT, 10) + bsizer.Add(rb_inter, 0, wx.TOP | wx.LEFT, 0) + + border = wx.BoxSizer(wx.VERTICAL) + border.Add(bsizer, 1, wx.EXPAND | wx.ALL, 10) + self.SetSizerAndFit(border) + self.Layout() + + def GetSelection(self): + + options = {const.SLICE_INTERPOLATION: self.rb_inter.GetSelection()} + + return options + + def LoadSelection(self, values): + value = values[const.SLICE_INTERPOLATION] + self.rb_inter.SetSelection(int(value)) + From 8042bb2dc7752102357b84beb0263642e019b09a Mon Sep 17 00:00:00 2001 From: Olli-Pekka Kahilakoski Date: Wed, 12 Jul 2023 12:39:30 +0300 Subject: [PATCH 16/99] FIX: robot connection --- invesalius/gui/dialogs.py | 10 +++++-- invesalius/gui/preferences.py | 41 ++++++++++++++----------- invesalius/gui/task_navigator.py | 9 ++++-- invesalius/navigation/robot.py | 51 ++++++++++++++++++-------------- 4 files changed, 66 insertions(+), 45 deletions(-) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index dd20ff25b..c1c66c0c8 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -5695,7 +5695,7 @@ def GetValue(self): return self.robot_ip class RobotCoregistrationDialog(wx.Dialog): - def __init__(self, tracker, title=_("Create transformation matrix to robot space")): + def __init__(self, robot, tracker, title=_("Create transformation matrix to robot space")): wx.Dialog.__init__(self, wx.GetApp().GetTopWindow(), -1, title, #size=wx.Size(1000, 200), style=wx.DEFAULT_DIALOG_STYLE|wx.FRAME_FLOAT_ON_PARENT|wx.STAY_ON_TOP|wx.RESIZE_BORDER) ''' @@ -5705,8 +5705,8 @@ def __init__(self, tracker, title=_("Create transformation matrix to robot space self.matrix_tracker_to_robot = [] self.robot_status = False + self.robot = robot self.tracker = tracker - self.timer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.HandleContinuousAcquisition, self.timer) @@ -5745,8 +5745,12 @@ def _init_gui(self): btn_load = wx.Button(self, -1, label=_('Load'), size=wx.Size(65, 23)) btn_load.Bind(wx.EVT_BUTTON, self.LoadRegistration) - btn_load.Enable(False) self.btn_load = btn_load + if not self.robot.robot_status: + btn_load.Enable(False) + else: + self.UpdateRobotConnectionStatus(True) + # Create a horizontal sizers border = 1 diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 5240392f7..a60bfcbd7 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -20,15 +20,15 @@ def __init__( style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, ): super().__init__(parent, id_, title, style=style) - self.tracker = Tracker() - self.robot = Robot( - tracker=self.tracker + tracker = Tracker() + robot = Robot( + tracker=tracker ) self.book = wx.Notebook(self, -1) #self.pnl_viewer2d = Viewer2D(self.book) self.pnl_viewer3d = Viewer3D(self.book) - self.pnl_tracker = TrackerPage(self.book, self.tracker, self.robot) + self.pnl_tracker = TrackerPage(self.book, tracker, robot) # self.pnl_surface = SurfaceCreation(self) self.pnl_language = Language(self.book) @@ -205,10 +205,17 @@ def __init__(self, parent, tracker, robot): btn_rob.Bind(wx.EVT_BUTTON, self.OnRobot) self.btn_rob = btn_rob - rob_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) + btn_rob_con = wx.Button(self, -1, _("Register")) + btn_rob_con.SetToolTip("register robot tracking") + btn_rob_con.Enable(1) + btn_rob_con.Bind(wx.EVT_BUTTON, self.OnRobotCon) + self.btn_rob_con = btn_rob_con + + rob_sizer = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) rob_sizer.AddMany([ (lbl_rob, 0, wx.LEFT), - (btn_rob, 0, wx.RIGHT) + (btn_rob, 0, wx.RIGHT), + (btn_rob_con, 0, wx.RIGHT) ]) rob_static_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Setup robot")) @@ -223,7 +230,7 @@ def __init__(self, parent, tracker, robot): self.Layout() def __bind_events(self): - pass + Publisher.subscribe(self.ShowParent, "Show preferences dialog") def OnChooseTracker(self, evt, ctrl): self.HideParent() @@ -235,10 +242,11 @@ def OnChooseTracker(self, evt, ctrl): else: choice = None - #self.DisconnectTracker() + self.tracker.DisconnectTracker() + self.robot.DisconnectRobot() self.tracker.ResetTrackerFiducials() self.tracker.SetTracker(choice) - + Publisher.sendMessage('Update status text in GUI', label=_("Ready")) self.ShowParent() def OnChooseReferenceMode(self, evt, ctrl): @@ -252,16 +260,15 @@ def ShowParent(self): # show preferences dialog box def OnRobot(self, evt): self.HideParent() - - success = self.robot.ConfigureRobot() - if success: - self.robot.InitializeRobot() + if self.robot.ConfigureRobot(): + self.ShowParent() else: - #self.DisconnectTracker() - pass - - self.ShowParent() + self.ShowParent() + def OnRobotCon(self, evt): + self.HideParent() + self.robot.RegisterRobot() + self.ShowParent() class Language(wx.Panel): def __init__(self, parent): diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 8b0963831..6fe852da3 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -472,9 +472,9 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.pedal_connection = pedal_connection self.neuronavigation_api = neuronavigation_api - book.AddPage(ImagePage(book, self.image), _("Image")) - book.AddPage(TrackerPage(book, self.icp, self.tracker, self.navigation, self.pedal_connection, self.neuronavigation_api), _("Tracker")) - book.AddPage(RefinePage(book, self.icp, self.tracker, self.image, self.navigation), _("Refine")) + book.AddPage(ImagePage(book, image), _("Image")) + book.AddPage(TrackerPage(book, icp, tracker, navigation, pedal_connection, neuronavigation_api), _("Tracker")) + book.AddPage(RefinePage(book, icp, tracker, image, navigation), _("Refine")) book.AddPage(StimulatorPage(book), _("Stimulator")) book.SetSelection(0) @@ -879,6 +879,7 @@ def OnStartRegistration(self, evt, ctrl): button.Disable() else: if not self.tracker.IsTrackerInitialized(): + print(self.tracker.tracker_connection, self.tracker.tracker_id) self.start_button.SetValue(False) dlg.ShowNavigationTrackerWarning(0, 'choose') else: @@ -1086,6 +1087,8 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.image = image self.pedal_connection = pedal_connection self.neuronavigation_api = neuronavigation_api + self.nav_status = False + self.target_mode = False # Toggle button for neuronavigation tooltip = wx.ToolTip(_("Start navigation")) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index fb2b4ca43..cd7aa5940 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -37,6 +37,7 @@ class Robot(): def __init__(self, tracker): self.tracker = tracker + self.robot_status = None self.robot_ip = None self.matrix_tracker_to_robot = None self.robot_coregistration_dialog = None @@ -49,6 +50,7 @@ def __init__(self, tracker): def __bind_events(self): Publisher.subscribe(self.AbortRobotConfiguration, 'Dialog robot destroy') + Publisher.subscribe(self.OnRobotStatus, 'Robot connection status') def SaveState(self): matrix_tracker_to_robot = self.matrix_tracker_to_robot.tolist() @@ -70,7 +72,6 @@ def LoadState(self): return True def ConfigureRobot(self): - if self.tracker.tracker_connection and self.tracker.tracker_connection.IsConnected(): select_ip_dialog = dlg.SetRobotIP() status = select_ip_dialog.ShowModal() @@ -83,42 +84,48 @@ def ConfigureRobot(self): 'robot_ip': robot_ip, 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), } - self.connection = self.tracker.tracker_connection + Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) select_ip_dialog.Destroy() + return True else: select_ip_dialog.Destroy() return False - - self.robot_coregistration_dialog = dlg.RobotCoregistrationDialog(self.tracker) - - # Show dialog and store relevant output values. - status = self.robot_coregistration_dialog.ShowModal() - matrix_tracker_to_robot = self.robot_coregistration_dialog.GetValue() - - # Destroy the dialog. - self.robot_coregistration_dialog.Destroy() - - if status != wx.ID_OK: - wx.MessageBox(_("Unable to connect to the robot."), _("InVesalius 3")) - return False - - self.matrix_tracker_to_robot = matrix_tracker_to_robot - - self.SaveState() - return True - else: wx.MessageBox(_("Select Tracker first"), _("InVesalius 3")) return False + + def OnRobotStatus(self, data): + if data: + self.robot_status = data + + def RegisterRobot(self): + Publisher.sendMessage('End busy cursor') + if not self.robot_status: + wx.MessageBox(_("Unable to connect to the robot."), _("InVesalius 3")) + return + self.robot_coregistration_dialog = dlg.RobotCoregistrationDialog(self, self.tracker) + + # Show dialog and store relevant output values. + status = self.robot_coregistration_dialog.ShowModal() + matrix_tracker_to_robot = self.robot_coregistration_dialog.GetValue() + + # Destroy the dialog. + self.robot_coregistration_dialog.Destroy() + + if status != wx.ID_OK: + wx.MessageBox(_("Unable to connect to the robot."), _("InVesalius 3")) + return False + self.matrix_tracker_to_robot = matrix_tracker_to_robot + self.SaveState() + self.InitializeRobot() def AbortRobotConfiguration(self): if self.robot_coregistration_dialog: self.robot_coregistration_dialog.Destroy() def InitializeRobot(self): - Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) From 47d6d83a0dd02869ecb5c4f1a501b77893ed1b1c Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 13 Jul 2023 11:03:00 +0300 Subject: [PATCH 17/99] ADD: Object preferences --- invesalius/gui/preferences.py | 245 +++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index a60bfcbd7..935fee3b2 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -1,15 +1,29 @@ import sys +import os from functools import partial - +import nibabel as nb +import numpy as np import invesalius.constants as const import invesalius.session as ses +import invesalius.gui.dialogs as dlg +import invesalius.data.vtk_utils as vtk_utils +from invesalius import inv_paths + import wx +from invesalius import utils from invesalius.gui.language_dialog import ComboBoxLanguage from invesalius.pubsub import pub as Publisher from invesalius.navigation.tracker import Tracker from invesalius.navigation.robot import Robot +from invesalius.net.neuronavigation_api import NeuronavigationApi + +HAS_PEDAL_CONNECTION = True +try: + from invesalius.net.pedal_connection import PedalConnection +except ImportError: + HAS_PEDAL_CONNECTION = False class Preferences(wx.Dialog): def __init__( @@ -25,16 +39,21 @@ def __init__( tracker=tracker ) + pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None + neuronavigation_api = NeuronavigationApi() + self.book = wx.Notebook(self, -1) #self.pnl_viewer2d = Viewer2D(self.book) self.pnl_viewer3d = Viewer3D(self.book) self.pnl_tracker = TrackerPage(self.book, tracker, robot) + self.pnl_object = ObjectPage(self.book, tracker, pedal_connection, neuronavigation_api) # self.pnl_surface = SurfaceCreation(self) self.pnl_language = Language(self.book) #self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) self.book.AddPage(self.pnl_viewer3d, _("Visualization")) self.book.AddPage(self.pnl_tracker, _("Tracker")) + self.book.AddPage(self.pnl_object, _("Object")) self.book.AddPage(self.pnl_language, _("Language")) btnsizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL) @@ -153,6 +172,230 @@ def LoadSelection(self, values): self.rb_inter.SetSelection(int(surface_interpolation)) self.rb_inter_sl.SetSelection(int(slice_interpolation)) +class ObjectPage(wx.Panel): + def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): + wx.Panel.__init__(self, parent) + self.coil_list = const.COIL + + self.tracker = tracker + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api + + self.__bind_events() + self.obj_fiducials = None + self.obj_orients = None + self.obj_ref_mode = None + self.obj_name = None + self.timestamp = const.TIMESTAMP + + # Button for creating new coil + tooltip = wx.ToolTip(_("Create new coil")) + btn_new = wx.Button(self, -1, _("New"), size=wx.Size(65, 23)) + btn_new.SetToolTip(tooltip) + btn_new.Enable(1) + btn_new.Bind(wx.EVT_BUTTON, self.OnCreateNewCoil) + self.btn_new = btn_new + + # Button for loading coil config file + tooltip = wx.ToolTip(_("Load coil configuration file")) + btn_load = wx.Button(self, -1, _("Load"), size=wx.Size(65, 23)) + btn_load.SetToolTip(tooltip) + btn_load.Enable(1) + btn_load.Bind(wx.EVT_BUTTON, self.OnLoadCoil) + self.btn_load = btn_load + + # Save button for saving coil config file + tooltip = wx.ToolTip(_(u"Save coil configuration file")) + btn_save = wx.Button(self, -1, _(u"Save"), size=wx.Size(65, 23)) + btn_save.SetToolTip(tooltip) + btn_save.Enable(1) + btn_save.Bind(wx.EVT_BUTTON, self.OnSaveCoil) + self.btn_save = btn_save + + load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Object registration")) + load_sizer.AddMany([ + (btn_new, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4), + (btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4), + (btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) + ]) + + # Change angles threshold + text_angles = wx.StaticText(self, -1, _("Angle threshold [degrees]:")) + spin_size_angles = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) + spin_size_angles.SetRange(0.1, 99) + spin_size_angles.SetValue(const.COIL_ANGLES_THRESHOLD) + spin_size_angles.Bind(wx.EVT_TEXT, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles)) + spin_size_angles.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles)) + + # Change dist threshold + text_dist = wx.StaticText(self, -1, _("Distance threshold [mm]:")) + spin_size_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) + spin_size_dist.SetRange(0.1, 99) + spin_size_dist.SetValue(const.COIL_ANGLES_THRESHOLD) + spin_size_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist)) + spin_size_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist)) + + # Change timestamp interval + text_timestamp = wx.StaticText(self, -1, _("Timestamp interval [s]:")) + spin_timestamp_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc = 0.1) + spin_timestamp_dist.SetRange(0.5, 60.0) + spin_timestamp_dist.SetValue(self.timestamp) + spin_timestamp_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist)) + spin_timestamp_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist)) + self.spin_timestamp_dist = spin_timestamp_dist + + # Create a horizontal sizer to threshold configs + line_angle_threshold = wx.BoxSizer(wx.HORIZONTAL) + line_angle_threshold.AddMany([ + (text_angles, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_size_angles, 0, wx.ALL | wx.EXPAND | wx.GROW, 5) + ]) + + line_dist_threshold = wx.BoxSizer(wx.HORIZONTAL) + line_dist_threshold.AddMany([ + (text_dist, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_size_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5) + ]) + + line_timestamp = wx.BoxSizer(wx.HORIZONTAL) + line_timestamp.AddMany([ + (text_timestamp, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_timestamp_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5) + ]) + + # Add line sizers into main sizer + conf_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Object configuration")) + conf_sizer.AddMany([ + (line_angle_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 20), + (line_dist_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 20), + (line_timestamp, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 20) + ]) + + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (load_sizer, 0, wx.ALL | wx.EXPAND, 10), + (conf_sizer, 0, wx.ALL | wx.EXPAND, 10) + ]) + self.SetSizerAndFit(main_sizer) + self.Layout() + + def __bind_events(self): + pass + + def OnCreateNewCoil(self, event=None): + if self.tracker.IsTrackerInitialized(): + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection, self.neuronavigation_api) + try: + if dialog.ShowModal() == wx.ID_OK: + self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() + + self.neuronavigation_api.update_coil_mesh(polydata) + + if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all(): + Publisher.sendMessage('Update object registration', + data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) + Publisher.sendMessage('Update status text in GUI', + label=_("Ready")) + Publisher.sendMessage( + 'Configure object', + obj_name=self.obj_name, + polydata=polydata, + use_default_object=use_default_object, + ) + + # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. + Publisher.sendMessage('Enable track-object checkbox', enabled=True) + Publisher.sendMessage('Check track-object checkbox', checked=True) + Publisher.sendMessage('Check volume camera checkbox', checked=False) + + Publisher.sendMessage('Disable target mode') + + except wx._core.PyAssertionError: # TODO FIX: win64 + pass + dialog.Destroy() + else: + dlg.ShowNavigationTrackerWarning(0, 'choose') + + def OnLoadCoil(self, event=None): + filename = dlg.ShowLoadSaveDialog(message=_(u"Load object registration"), + wildcard=_("Registration files (*.obr)|*.obr")) + + try: + if filename: + with open(filename, 'r') as text_file: + data = [s.split('\t') for s in text_file.readlines()] + + registration_coordinates = np.array(data[1:]).astype(np.float32) + self.obj_fiducials = registration_coordinates[:, :3] + self.obj_orients = registration_coordinates[:, 3:] + + self.obj_name = data[0][1].encode(const.FS_ENCODE) + self.obj_ref_mode = int(data[0][-1]) + + if not os.path.exists(self.obj_name): + self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + + polydata = vtk_utils.CreateObjectPolyData(self.obj_name) + if polydata: + self.neuronavigation_api.update_coil_mesh(polydata) + else: + self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + + if os.path.basename(self.obj_name) == "magstim_fig8_coil.stl": + use_default_object = True + else: + use_default_object = False + + Publisher.sendMessage('Update object registration', + data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) + Publisher.sendMessage('Update status text in GUI', + label=_("Object file successfully loaded")) + Publisher.sendMessage( + 'Configure object', + obj_name=self.obj_name, + polydata=polydata, + use_default_object=use_default_object + ) + + # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. + Publisher.sendMessage('Enable track-object checkbox', enabled=True) + Publisher.sendMessage('Check track-object checkbox', checked=True) + Publisher.sendMessage('Check volume camera checkbox', checked=False) + + Publisher.sendMessage('Disable target mode') + if use_default_object: + msg = _("Default object file successfully loaded") + else: + msg = _("Object file successfully loaded") + wx.MessageBox(msg, _("InVesalius 3")) + except: + wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3")) + Publisher.sendMessage('Update status text in GUI', label="") + + def OnSaveCoil(self, evt): + if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any(): + wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error")) + else: + filename = dlg.ShowLoadSaveDialog(message=_(u"Save object registration as..."), + wildcard=_("Registration files (*.obr)|*.obr"), + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + default_filename="object_registration.obr", save_ext="obr") + if filename: + hdr = 'Object' + "\t" + utils.decode(self.obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode) + data = np.hstack([self.obj_fiducials, self.obj_orients]) + np.savetxt(filename, data, fmt='%.4f', delimiter='\t', newline='\n', header=hdr) + wx.MessageBox(_("Object file successfully saved"), _("Save")) + + def OnSelectAngleThreshold(self, evt, ctrl): + Publisher.sendMessage('Update angle threshold', angle=ctrl.GetValue()) + + def OnSelectDistThreshold(self, evt, ctrl): + Publisher.sendMessage('Update dist threshold', dist_threshold=ctrl.GetValue()) + + def OnSelectTimestamp(self, evt, ctrl): + self.timestamp = ctrl.GetValue() + class TrackerPage(wx.Panel): def __init__(self, parent, tracker, robot): wx.Panel.__init__(self, parent) From 846df53ae75a2dae54a9bf2b1d1322c695189a2f Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 09:49:13 +0300 Subject: [PATCH 18/99] MOD: Make controller classes singleton --- invesalius/navigation/robot.py | 3 ++- invesalius/navigation/tracker.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index cd7aa5940..10c5dc0b8 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -27,13 +27,14 @@ import invesalius.gui.dialogs as dlg import invesalius.session as ses from invesalius.pubsub import pub as Publisher +from invesalius.utils import Singleton # XXX: First steps towards decoupling robot and tracker, which were previously # tightly coupled; not fully finished, but whenever possible, robot-related # functionality should be gathered here. -class Robot(): +class Robot(metaclass=Singleton): def __init__(self, tracker): self.tracker = tracker diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index ab12a0545..5b5cd4428 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -27,9 +27,10 @@ import invesalius.gui.dialogs as dlg import invesalius.session as ses from invesalius.pubsub import pub as Publisher +from invesalius.utils import Singleton -class Tracker(): +class Tracker(metaclass=Singleton): def __init__(self): self.tracker_connection = None self.tracker_id = const.DEFAULT_TRACKER From 0c181fff23b620bd9a63e9aa77ccd434789177b5 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 10:40:31 +0300 Subject: [PATCH 19/99] FIX: Navigation bug fix --- invesalius/gui/task_navigator.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 6fe852da3..694c7751d 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1096,7 +1096,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect btn_nav.SetFont(wx.Font(11, wx.DEFAULT, wx.NORMAL, wx.BOLD)) btn_nav.SetToolTip(tooltip) self.btn_nav = btn_nav - btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn_nav=btn_nav)) + self.btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn_nav=self.btn_nav)) # Toggle button for robot tooltip = wx.ToolTip(_("Stop robot")) @@ -1202,6 +1202,7 @@ def __bind_events(self): Publisher.subscribe(self.OnStopNavigation, 'Stop navigation') Publisher.subscribe(self.OnCheckStatus, 'Navigation status') Publisher.subscribe(self.UpdateTarget, 'Update target') + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') # Externally check/uncheck and enable/disable checkboxes. Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') @@ -1258,12 +1259,13 @@ def OnNavigate(self, evt, btn_nav): tooltip = wx.ToolTip(_("Start neuronavigation")) btn_nav.SetToolTip(tooltip) + btn_nav.SetLabelText(_("Start neuronavigation")) else: Publisher.sendMessage("Start navigation") - if self.nav_status: tooltip = wx.ToolTip(_("Stop neuronavigation")) btn_nav.SetToolTip(tooltip) + btn_nav.SetLabelText(_("Stop neuronavigation")) else: btn_nav.SetValue(False) @@ -1281,9 +1283,15 @@ def UpdateTarget(self, coord): self.lock_to_target_checkbox.SetValue(True) self.navigation.SetLockToTarget(True) + def UpdateNavigationStatus(self, nav_status, vis_status): + if not nav_status: + self.nav_status = False + self.current_orientation = None, None, None + else: + self.nav_status = True # 'Robot' - def OnStopRobot(self, evt): + def OnStopRobot(self, evt, ctrl): pass @@ -1599,7 +1607,7 @@ def __bind_events(self): Publisher.subscribe(self.UpdateACTData, 'Update ACT data') Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') Publisher.subscribe(self.UpdateTarget, 'Update target') - Publisher.subscribe(self.OnStartNavigation, 'Start navigation') + #Publisher.subscribe(self.OnStartNavigation, 'Start navigation') Publisher.subscribe(self.OnStopNavigation, 'Stop navigation') def LoadImageFiducials(self, label, position): From 47c084ee413d0fb3ce1c8f905a238b89b7778aa8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 10:52:45 +0300 Subject: [PATCH 20/99] FIX: minor fixes --- invesalius/gui/task_navigator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 694c7751d..c7d7bc28e 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1093,7 +1093,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle button for neuronavigation tooltip = wx.ToolTip(_("Start navigation")) btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1)) - btn_nav.SetFont(wx.Font(11, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + btn_nav.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) btn_nav.SetToolTip(tooltip) self.btn_nav = btn_nav self.btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn_nav=self.btn_nav)) @@ -1101,10 +1101,10 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle button for robot tooltip = wx.ToolTip(_("Stop robot")) btn_robot = wx.ToggleButton(self, -1, _("Stop Robot"), size=wx.Size(80, -1)) - btn_robot.SetFont(wx.Font(11, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + btn_robot.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) btn_robot.SetToolTip(tooltip) self.btn_robot = btn_robot - btn_robot.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStopRobot, btn_nav=btn_robot)) + btn_robot.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStopRobot, ctrl=btn_robot)) # Label and Checkbox for Tractography tooltip = wx.ToolTip(_(u"Control Tractography")) @@ -1124,7 +1124,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Label and Checkbox for Lock to Target tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) - lock_to_target_checkbox = wx.CheckBox(self, -1, _('Lock to target:')) + lock_to_target_checkbox = wx.CheckBox(self, -1, _('Lock to target')) lock_to_target_checkbox.SetValue(False) lock_to_target_checkbox.Enable(False) lock_to_target_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnLockToTargetCheckbox, ctrl=lock_to_target_checkbox)) From 141a43b59dd822e7eb227e09ac9f33f03cfebb9c Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 11:05:34 +0300 Subject: [PATCH 21/99] MOD: move object settings to config --- invesalius/data/viewer_volume.py | 4 ++-- invesalius/navigation/navigation.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 81c86d979..931bdd78f 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -415,11 +415,11 @@ def SaveState(self): } session = ses.Session() - session.SetState('viewer', state) + session.SetConfig('viewer', state) def LoadState(self): session = ses.Session() - state = session.GetState('viewer') + state = session.GetConfig('viewer') if state is None: return diff --git a/invesalius/navigation/navigation.py b/invesalius/navigation/navigation.py index feb11c809..e6d8318c4 100644 --- a/invesalius/navigation/navigation.py +++ b/invesalius/navigation/navigation.py @@ -227,11 +227,11 @@ def SaveState(self): } session = ses.Session() - session.SetState('navigation', state) + session.SetConfig('navigation', state) def LoadState(self): session = ses.Session() - state = session.GetState('navigation') + state = session.GetConfig('navigation') if state is None: return From 8916fe5d908c73b0caeb8f6ab5bf4ec1493c1e6a Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 11:11:21 +0300 Subject: [PATCH 22/99] MOD: move tracker to config --- invesalius/navigation/tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index 5b5cd4428..a622a60c8 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -64,11 +64,11 @@ def SaveState(self): 'configuration': configuration, } session = ses.Session() - session.SetState('tracker', state) + session.SetConfig('tracker', state) def LoadState(self): session = ses.Session() - state = session.GetState('tracker') + state = session.GetConfig('tracker') if state is None: return From 072fd1ce39a0cd48134c7a8e57f00b39123a1b0f Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 13:55:15 +0300 Subject: [PATCH 23/99] FIX: Navigation bug fixes --- invesalius/data/viewer_volume.py | 5 +++++ invesalius/gui/task_navigator.py | 27 ++++++++++++++++----------- invesalius/navigation/navigation.py | 1 - 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 931bdd78f..229dc0502 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -429,7 +429,12 @@ def LoadState(self): self.obj_name = object_path.encode(const.FS_ENCODE) if object_path is not None else None self.use_default_object = use_default_object + # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. + Publisher.sendMessage('Enable track-object checkbox', enabled=True) + Publisher.sendMessage('Check track-object checkbox', checked=True) + Publisher.sendMessage('Check volume camera checkbox', checked=False) + Publisher.sendMessage('Disable target mode') self.polydata = pu.LoadPolydata(path=object_path) if object_path is not None else None def get_vtk_mouse_position(self): diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index c7d7bc28e..7038e8fd8 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1089,6 +1089,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.neuronavigation_api = neuronavigation_api self.nav_status = False self.target_mode = False + self.track_obj = False # Toggle button for neuronavigation tooltip = wx.ToolTip(_("Start navigation")) @@ -1210,6 +1211,10 @@ def __bind_events(self): Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') + # Externally check/uncheck and enable/disable checkboxes. + Publisher.subscribe(self.CheckTrackObjectCheckbox, 'Check track-object checkbox') + Publisher.subscribe(self.EnableTrackObjectCheckbox, 'Enable track-object checkbox') + def SaveState(self): track_object = self.checkbox_track_object state = { @@ -1220,11 +1225,11 @@ def SaveState(self): } session = ses.Session() - session.SetState('object_registration_panel', state) + session.SetConfig('object_registration_panel', state) def LoadState(self): session = ses.Session() - state = session.GetState('object_registration_panel') + state = session.GetConfig('object_registration_panel') if state is None: return @@ -1234,6 +1239,15 @@ def LoadState(self): self.EnableTrackObjectCheckbox(track_object['enabled']) self.CheckTrackObjectCheckbox(track_object['checked']) + def OnCheckStatus(self, nav_status, vis_status): + if nav_status: + self.checkbox_serial_port.Enable(False) + self.checkobj.Enable(False) + else: + self.checkbox_serial_port.Enable(True) + self.checkbox_track_object.Enable(True) + if self.track_obj: + self.checkobj.Enable(True) # Navigation def OnStartNavigation(self): @@ -1353,15 +1367,6 @@ def EnableVolumeCameraCheckbox(self, enabled): # 'Serial Port Com' - def OnCheckStatus(self, nav_status, vis_status): - if nav_status: - self.checkbox_serial_port.Enable(False) - self.checkobj.Enable(False) - else: - self.checkbox_serial_port.Enable(True) - # if self.track_obj: - # self.checkobj.Enable(True) - def OnEnableSerialPort(self, evt, ctrl): if ctrl.GetValue(): from wx import ID_OK diff --git a/invesalius/navigation/navigation.py b/invesalius/navigation/navigation.py index e6d8318c4..a48211f39 100644 --- a/invesalius/navigation/navigation.py +++ b/invesalius/navigation/navigation.py @@ -240,7 +240,6 @@ def LoadState(self): object_orientations = np.array(state['object_orientations']) object_reference_mode = state['object_reference_mode'] object_name = state['object_name'].encode(const.FS_ENCODE) - self.object_registration = (object_fiducials, object_orientations, object_reference_mode, object_name) def CoilAtTarget(self, state): From 70c5c09fa409532beed87d0f7bf8392359201161 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 15:36:22 +0300 Subject: [PATCH 24/99] ADD: stimulator preferences --- invesalius/gui/preferences.py | 84 ++++++++++++++++++++++++++++------- 1 file changed, 67 insertions(+), 17 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 935fee3b2..d88ecf05f 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -53,7 +53,7 @@ def __init__( #self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) self.book.AddPage(self.pnl_viewer3d, _("Visualization")) self.book.AddPage(self.pnl_tracker, _("Tracker")) - self.book.AddPage(self.pnl_object, _("Object")) + self.book.AddPage(self.pnl_object, _("Stimulator")) self.book.AddPage(self.pnl_language, _("Language")) btnsizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL) @@ -90,6 +90,7 @@ def LoadPreferences(self): surface_interpolation = session.GetConfig('surface_interpolation') language = session.GetConfig('language') slice_interpolation = session.GetConfig('slice_interpolation') + self.pnl_object.LoadState() values = { const.RENDERING: rendering, @@ -175,6 +176,7 @@ def LoadSelection(self, values): class ObjectPage(wx.Panel): def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): wx.Panel.__init__(self, parent) + self.coil_list = const.COIL self.tracker = tracker @@ -187,37 +189,70 @@ def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): self.obj_ref_mode = None self.obj_name = None self.timestamp = const.TIMESTAMP + self.state = self.LoadState() - # Button for creating new coil - tooltip = wx.ToolTip(_("Create new coil")) + # Button for creating new stimulator + tooltip = wx.ToolTip(_("Create new stimulator")) btn_new = wx.Button(self, -1, _("New"), size=wx.Size(65, 23)) btn_new.SetToolTip(tooltip) btn_new.Enable(1) btn_new.Bind(wx.EVT_BUTTON, self.OnCreateNewCoil) self.btn_new = btn_new - # Button for loading coil config file - tooltip = wx.ToolTip(_("Load coil configuration file")) + # Button for loading stimulator config file + tooltip = wx.ToolTip(_("Load stimulator configuration file")) btn_load = wx.Button(self, -1, _("Load"), size=wx.Size(65, 23)) btn_load.SetToolTip(tooltip) btn_load.Enable(1) btn_load.Bind(wx.EVT_BUTTON, self.OnLoadCoil) self.btn_load = btn_load - # Save button for saving coil config file - tooltip = wx.ToolTip(_(u"Save coil configuration file")) + # Save button for saving stimulator config file + tooltip = wx.ToolTip(_(u"Save stimulator configuration file")) btn_save = wx.Button(self, -1, _(u"Save"), size=wx.Size(65, 23)) btn_save.SetToolTip(tooltip) btn_save.Enable(1) btn_save.Bind(wx.EVT_BUTTON, self.OnSaveCoil) self.btn_save = btn_save - load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Object registration")) - load_sizer.AddMany([ - (btn_new, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4), - (btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4), - (btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) - ]) + if self.state: + lbl = wx.StaticText(self, -1, _("Current Configuration:")) + lbl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + config_txt = wx.StaticText(self, -1, os.path.basename(self.obj_name)) + + lbl_new = wx.StaticText(self, -1, _("Create a new stimulator registration: ")) + lbl_load = wx.StaticText(self, -1, _("Load a stimulator registration: ")) + lbl_save = wx.StaticText(self, -1, _("Save current stimulator registration: ")) + + load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Stimulator registration")) + inner_load_sizer = wx.FlexGridSizer(2, 4, 5) + inner_load_sizer.AddMany([ + (lbl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (config_txt, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + ]) + load_sizer.Add(inner_load_sizer, 0, wx.ALL | wx.EXPAND, 10) + else: + lbl_new = wx.StaticText(self, -1, _("Create a new stimulator registration: ")) + lbl_load = wx.StaticText(self, -1, _("Load a stimulator registration: ")) + lbl_save = wx.StaticText(self, -1, _("Save current stimulator registration: ")) + + load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Stimulator registration")) + inner_load_sizer = wx.FlexGridSizer(2, 3, 5) + inner_load_sizer.AddMany([ + (lbl_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + ]) + load_sizer.Add(inner_load_sizer, 0, wx.ALL | wx.EXPAND, 10) # Change angles threshold text_angles = wx.StaticText(self, -1, _("Angle threshold [degrees]:")) @@ -264,7 +299,7 @@ def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): ]) # Add line sizers into main sizer - conf_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Object configuration")) + conf_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Stimulator configuration")) conf_sizer.AddMany([ (line_angle_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 20), (line_dist_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 20), @@ -282,6 +317,21 @@ def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): def __bind_events(self): pass + + def LoadState(self): + session = ses.Session() + state = session.GetConfig('navigation') + + if state is None: + return False + + object_fiducials = np.array(state['object_fiducials']) + object_orientations = np.array(state['object_orientations']) + object_reference_mode = state['object_reference_mode'] + object_name = state['object_name'].encode(const.FS_ENCODE) + + self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name = object_fiducials, object_orientations, object_reference_mode, object_name + return True def OnCreateNewCoil(self, event=None): if self.tracker.IsTrackerInitialized(): @@ -420,7 +470,7 @@ def __init__(self, parent, tracker, robot): # ComboBox for tracker reference mode tooltip = wx.ToolTip(_("Choose the navigation reference mode")) - choice_ref = wx.ComboBox(self, -1, "", + choice_ref = wx.ComboBox(self, -1, "", size=(145, -1), choices=const.REF_MODE, style=wx.CB_DROPDOWN|wx.CB_READONLY) choice_ref.SetSelection(const.DEFAULT_REF_MODE) choice_ref.SetToolTip(tooltip) @@ -441,7 +491,7 @@ def __init__(self, parent, tracker, robot): sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Setup tracker")) sizer.Add(ref_sizer, 1, wx.ALL | wx.FIXED_MINSIZE, 20) - lbl_rob = wx.StaticText(self, -1, _("Robot tracking device :")) + lbl_rob = wx.StaticText(self, -1, _("Robot tracking device: ")) btn_rob = wx.Button(self, -1, _("Setup")) btn_rob.SetToolTip("Setup robot tracking") btn_rob.Enable(1) @@ -449,7 +499,7 @@ def __init__(self, parent, tracker, robot): self.btn_rob = btn_rob btn_rob_con = wx.Button(self, -1, _("Register")) - btn_rob_con.SetToolTip("register robot tracking") + btn_rob_con.SetToolTip("Register robot tracking") btn_rob_con.Enable(1) btn_rob_con.Bind(wx.EVT_BUTTON, self.OnRobotCon) self.btn_rob_con = btn_rob_con From 3d95c2ae58753cb47c8298ac62d4951f1102dc5f Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 17:19:32 +0300 Subject: [PATCH 25/99] ADD: External trigger for prefences --- invesalius/gui/frame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 21e111891..7072b8ff7 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -150,6 +150,7 @@ def __bind_events(self): sub(self._SetProjectName, 'Set project name') sub(self._ShowContentPanel, 'Show content panel') sub(self._ShowImportPanel, 'Show import panel in frame') + sub(self.ShowPreferences, 'Open preferences menu') #sub(self._ShowHelpMessage, 'Show help message') sub(self._ShowImportNetwork, 'Show retrieve dicom panel') sub(self._ShowImportBitmap, 'Show import bitmap panel in frame') From 31e77a8a7b4f1241e31de6b473cd82a81e565746 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 18 Jul 2023 17:20:14 +0300 Subject: [PATCH 26/99] MOD: Stimulator page --- invesalius/gui/task_navigator.py | 106 ++++++++++++++++++++++++++-- invesalius/navigation/navigation.py | 3 + 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 7038e8fd8..72a979020 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -185,6 +185,11 @@ def __init__(self, parent): neuronavigation_api=neuronavigation_api, ) + self.tracker = tracker + self.robot = robot + self.image = image + self.navigation = navigation + # TODO: Initialize checkboxes before panels: they are updated by ObjectRegistrationPanel when loading its state. # A better solution would be to have these checkboxes save their own state, independent of the panels, but that's # not implemented yet. @@ -244,6 +249,7 @@ def __init__(self, parent): fold_panel.Expand(fold_panel.GetFoldPanel(0)) item = fold_panel.AddFoldPanel(_("Navigation"), collapsed=True) + self.__id_nav = item.GetId() ntw = NavigationPanel( parent=item, navigation=navigation, @@ -331,6 +337,8 @@ def __init__(self, parent): fold_panel.AddFoldPanelWindow(item, etw, spacing=0, leftSpacing=0, rightSpacing=0) + self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) + # Panel sizer for checkboxes line_sizer = wx.BoxSizer(wx.HORIZONTAL) line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) @@ -366,6 +374,8 @@ def __bind_events(self): Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') + + Publisher.subscribe(self.OpenNavigation, 'Open navigation menu') def __calc_best_size(self, panel): parent = panel.GetParent() @@ -448,7 +458,33 @@ def OnVolumeCameraCheckbox(self, evt=None, status=None): def EnableVolumeCameraCheckbox(self, enabled): self.checkcamera.Enable(enabled) + + def OnFoldPressCaption(self, evt): + id = evt.GetTag().GetId() + expanded = evt.GetFoldStatus() + + if id == self.__id_nav: + status = self.CheckRegistration() + if not status: + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) + wx.MessageBox(_("Complete coregistration first!"), _("InVesalius 3")) + return + if not expanded: + self.fold_panel.Expand(evt.GetTag()) + else: + self.fold_panel.Collapse(evt.GetTag()) + def ResizeFPB(self): + sizeNeeded = self.fold_panel.GetPanelsLength(0, 0)[2] + self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded )) + self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) + + def CheckRegistration(self): + return self.tracker.AreTrackerFiducialsSet() and self.image.AreImageFiducialsSet() and self.navigation.GetObjectRegistration() is not None + + def OpenNavigation(self): + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) + class CoregistrationPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): wx.Panel.__init__(self, parent) @@ -475,7 +511,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect book.AddPage(ImagePage(book, image), _("Image")) book.AddPage(TrackerPage(book, icp, tracker, navigation, pedal_connection, neuronavigation_api), _("Tracker")) book.AddPage(RefinePage(book, icp, tracker, image, navigation), _("Refine")) - book.AddPage(StimulatorPage(book), _("Stimulator")) + book.AddPage(StimulatorPage(book, navigation), _("Stimulator")) book.SetSelection(0) @@ -1037,16 +1073,74 @@ def OnRefine(self, evt): self.UpdateUI() class StimulatorPage(wx.Panel): - def __init__(self, parent): + def __init__(self, parent, navigation): wx.Panel.__init__(self, parent) + self.navigation = navigation + + border = wx.FlexGridSizer(2, 3, 5) + object_reg = self.navigation.GetObjectRegistration() + self.object_reg = object_reg + + lbl = wx.StaticText(self, -1, _("No stimulator selected!")) + lbl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + self.lbl = lbl + + config_txt = wx.StaticText(self, -1, "") + self.config_txt = config_txt + self.config_txt.Hide() + + lbl_edit = wx.StaticText(self, -1, _("Edit Configuration:")) + btn_edit = wx.Button(self, -1, _("Preferences")) + btn_edit.SetToolTip("Open preferences menu") + btn_edit.Bind(wx.EVT_BUTTON, self.OnEditPreferences) + + border.AddMany([ + (lbl, 1, wx.EXPAND | wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10), + (0, 0), + (config_txt, 1, wx.EXPAND | wx.LEFT | wx.ALIGN_CENTER_VERTICAL, 10), + (0, 0), + (lbl_edit, 1, wx.EXPAND | wx.ALL | wx.ALIGN_CENTER_VERTICAL, 10), + (btn_edit, 0, wx.EXPAND | wx.ALL | wx.ALIGN_LEFT, 10) + ]) - lbl_inter = wx.StaticText(self, -1, _("Stimulator Registration ")) + next_button = wx.Button(self, label="Proceed to navigation") + next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) + if self.object_reg is None: + next_button.Disable() + self.next_button = next_button + + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) + bottom_sizer.Add(next_button) - border = wx.BoxSizer(wx.VERTICAL) - border.Add(lbl_inter, 1, wx.EXPAND | wx.ALL, 10) - self.SetSizerAndFit(border) + if self.object_reg is not None: + self.OnObjectUpdate() + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.AddMany([ + (border, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10), + (bottom_sizer, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 20) + ]) + + self.SetSizerAndFit(main_sizer) self.Layout() + self.__bind_events() + + def __bind_events(self): + Publisher.subscribe(self.OnObjectUpdate, 'Update object registration') + + def OnObjectUpdate(self, data=None): + self.object_reg = self.navigation.GetObjectRegistration() + self.lbl.SetLabel("Current Configuration:") + self.config_txt.SetLabelText(os.path.basename(self.object_reg[-1])) + self.lbl.Show() + self.config_txt.Show() + + def OnEditPreferences(self, evt): + Publisher.sendMessage('Open preferences menu') + + def OnNext(self, evt): + Publisher.sendMessage('Open navigation menu') class NavigationPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): diff --git a/invesalius/navigation/navigation.py b/invesalius/navigation/navigation.py index a48211f39..a90b52a0e 100644 --- a/invesalius/navigation/navigation.py +++ b/invesalius/navigation/navigation.py @@ -259,6 +259,9 @@ def UpdateObjectRegistration(self, data=None): self.SaveState() + def GetObjectRegistration(self): + return self.object_registration + def TrackObject(self, enabled=False): self.track_obj = enabled From b45720aa7f5c1d386447f4254343bd5b5cc20a24 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 19 Jul 2023 10:00:53 +0300 Subject: [PATCH 27/99] FIX: minor UI changes --- invesalius/gui/default_tasks.py | 2 +- invesalius/gui/task_navigator.py | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 3f5211d26..fdf303f2d 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -102,7 +102,7 @@ def GetExpandedIconImage(): class Panel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent, pos=wx.Point(5, 5), - size=wx.Size(300, 656)) + size=wx.Size(350, 656)) #sizer = wx.BoxSizer(wx.VERTICAL) gbs = wx.GridBagSizer(5,5) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 72a979020..7ef7f6dee 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -798,6 +798,14 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi reset_button.Bind(wx.EVT_BUTTON, partial(self.OnReset, ctrl=reset_button)) self.reset_button = reset_button + back_button = wx.Button(self, label="Back") + back_button.Bind(wx.EVT_BUTTON, partial(self.OnBack)) + self.back_button = back_button + + preferences_button = wx.Button(self, label="Change tracker") + preferences_button.Bind(wx.EVT_BUTTON, partial(self.OnPreferences)) + self.preferences_button = preferences_button + next_button = wx.Button(self, label="Next") next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) if not self.tracker.AreTrackerFiducialsSet(): @@ -811,7 +819,11 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi ]) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) - bottom_sizer.Add(next_button) + bottom_sizer.AddMany([ + (back_button, 0, wx.EXPAND), + (preferences_button, 0, wx.EXPAND), + (next_button, 0, wx.EXPAND) + ]) sizer = wx.GridBagSizer(5, 5) sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) @@ -823,7 +835,7 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi main_sizer.AddMany([ (top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10), (sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5), - (bottom_sizer, 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.TOP, 30)]) + (bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 20)]) self.sizer = main_sizer self.SetSizerAndFit(main_sizer) self.__bind_events() @@ -901,6 +913,12 @@ def OnReset(self, evt, ctrl): def OnNext(self, evt): Publisher.sendMessage("Next to refine fiducials") + + def OnBack(self, evt): + Publisher.sendMessage('Back to image fiducials') + + def OnPreferences(self, evt): + Publisher.sendMessage("Open preferences menu") def OnNextEnable(self): self.next_button.Enable() @@ -1155,13 +1173,13 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.neuronavigation_api = neuronavigation_api top_sizer = wx.BoxSizer(wx.HORIZONTAL) - top_sizer.Add(MarkersPanel(self, self.navigation, self.tracker, self.icp), 0, wx.GROW | wx.EXPAND ) + top_sizer.Add(MarkersPanel(self, self.navigation, self.tracker, self.icp), 1, wx.GROW | wx.EXPAND ) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) bottom_sizer.Add(ControlPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.image, self.pedal_connection, self.neuronavigation_api), 0, wx.EXPAND | wx.TOP, 20) main_sizer = wx.BoxSizer(wx.VERTICAL) - main_sizer.AddMany([(top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + main_sizer.AddMany([(top_sizer, 1, wx.EXPAND | wx.GROW), (bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL) ]) self.sizer = main_sizer @@ -2639,7 +2657,7 @@ def __init__(self, parent, navigation, tracker, icp): group_sizer.Add(sizer_create, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) group_sizer.Add(sizer_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) group_sizer.Add(sizer_delete, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) - group_sizer.Add(marker_list_ctrl, 0, wx.EXPAND | wx.ALL, 5) + group_sizer.Add(marker_list_ctrl, 1, wx.EXPAND | wx.ALL, 5) group_sizer.Fit(self) self.SetSizer(group_sizer) From d437781ca1cc475dc93cad28ffbc6162027b1f30 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 19 Jul 2023 13:38:39 +0300 Subject: [PATCH 28/99] ADD: Minor UI changes --- invesalius/gui/preferences.py | 8 ++++++-- invesalius/gui/task_navigator.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index d88ecf05f..77e0e9557 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -52,8 +52,11 @@ def __init__( #self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) self.book.AddPage(self.pnl_viewer3d, _("Visualization")) - self.book.AddPage(self.pnl_tracker, _("Tracker")) - self.book.AddPage(self.pnl_object, _("Stimulator")) + session = ses.Session() + mode = session.GetConfig('mode') + if mode == const.MODE_NAVIGATOR: + self.book.AddPage(self.pnl_tracker, _("Tracker")) + self.book.AddPage(self.pnl_object, _("Stimulator")) self.book.AddPage(self.pnl_language, _("Language")) btnsizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL) @@ -540,6 +543,7 @@ def OnChooseTracker(self, evt, ctrl): self.tracker.ResetTrackerFiducials() self.tracker.SetTracker(choice) Publisher.sendMessage('Update status text in GUI', label=_("Ready")) + Publisher.sendMessage("Tracker changed") self.ShowParent() def OnChooseReferenceMode(self, evt, ctrl): diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 7ef7f6dee..ee0d3a885 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -109,7 +109,6 @@ def __init__(self, parent): self.Update() self.SetAutoLayout(1) - class InnerTaskPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -264,7 +263,7 @@ def __init__(self, parent): fold_panel.ApplyCaptionStyle(item, style) fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, leftSpacing=0, rightSpacing=0) - + ''' # Fold 1 - Navigation panel item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) ntw = NeuronavigationPanel( @@ -336,7 +335,7 @@ def __init__(self, parent): fold_panel.ApplyCaptionStyle(item, style) fold_panel.AddFoldPanelWindow(item, etw, spacing=0, leftSpacing=0, rightSpacing=0) - +''' self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) # Panel sizer for checkboxes @@ -812,6 +811,22 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi next_button.Disable() self.next_button = next_button + tracker_status = self.tracker.IsTrackerInitialized() + current_label = wx.StaticText(self, -1, _("Current tracker: ")) + current_label.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + main_label = wx.StaticText(self, -1, _("No tracker selected!")) + + if tracker_status: + main_label.SetLabel(self.tracker.get_trackers()[self.tracker.GetTrackerId() - 1]) + + self.main_label = main_label + + middle_sizer = wx.BoxSizer(wx.HORIZONTAL) + middle_sizer.AddMany([ + (current_label), + (main_label) + ]) + top_sizer = wx.BoxSizer(wx.HORIZONTAL) top_sizer.AddMany([ (start_button), @@ -834,7 +849,8 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.AddMany([ (top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10), - (sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5), + (sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5), + (middle_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5), (bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 20)]) self.sizer = main_sizer self.SetSizerAndFit(main_sizer) @@ -844,6 +860,7 @@ def __bind_events(self): Publisher.subscribe(self.SetTrackerFiducial, 'Set tracker fiducial') Publisher.subscribe(self.OnNextEnable, "Next enable for tracker fiducials") Publisher.subscribe(self.OnNextDisable, "Next disable for tracker fiducials") + Publisher.subscribe(self.OnTrackerChanged, "Tracker changed") def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] @@ -952,6 +969,12 @@ def OnStartRegistration(self, evt, ctrl): ) for button in self.btns_set_fiducial: button.Enable() + + def OnTrackerChanged(self): + if self.tracker.GetTrackerId() != const.DEFAULT_TRACKER: + self.main_label.SetLabel(self.tracker.get_trackers()[self.tracker.GetTrackerId() - 1]) + else: + self.main_label.SetLabel(_("No tracker selected!")) class RefinePage(wx.Panel): def __init__(self, parent, icp, tracker, image, navigation): From 71aadcd7ca347d32b1973e9884b868827006f088 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 19 Jul 2023 18:07:05 +0300 Subject: [PATCH 29/99] ADD: FOD progress window --- invesalius/gui/dialogs.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index c1c66c0c8..8e9d3e398 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -5326,6 +5326,31 @@ def GetValueBrainTarget(self): brain_target_orientation.append(orientation) return brain_target_position, brain_target_orientation +class FODProgressWindow(object): + def __init__(self): + self.title = "InVesalius 3" + self.msg = _("Setting up FOD ...") + self.style = wx.PD_APP_MODAL | wx.PD_APP_MODAL | wx.PD_CAN_ABORT + self.dlg = wx.ProgressDialog(self.title, + self.msg, + parent=None, + style=self.style) + self.running = True + self.error = None + self.dlg.Show() + + def WasCancelled(self): + # print("Cancelled?", self.dlg.WasCancelled()) + return self.dlg.WasCancelled() + + def Update(self, msg=None, value=None): + if msg is None: + self.dlg.Pulse() + else: + self.dlg.Pulse(msg) + + def Close(self): + self.dlg.Destroy() class SurfaceProgressWindow(object): def __init__(self): From 7054231f03ac39020a38db1627910df5435110ea Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 19 Jul 2023 18:07:36 +0300 Subject: [PATCH 30/99] ADD: progress functionality in FOD --- invesalius/gui/task_tractography.py | 65 ++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index 3031f9dd3..588cb8214 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -429,18 +429,36 @@ def OnLinkFOD(self, event=None): if filename: Publisher.sendMessage('Update status text in GUI', label=_("Busy")) + t_init = time.time() try: - self.trekker = Trekker.initialize(filename.encode('utf-8')) - self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg) + import concurrent.futures as mp + from concurrent.futures import ThreadPoolExecutor + import multiprocessing + import functools + import wx.lib.agw.genericmessagedialog as GMD + self.tp = dlg.FODProgressWindow() + + self.trekker = None + file = filename.encode('utf-8') + + with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as exec: + future2 = exec.submit(self.UpdateDialog) + future1 = exec.submit(Trekker.initialize, file) + future1.add_done_callback(self.TrekkerCallback) + future2.add_done_callback(print) + + t_end = time.time() + print("Elapsed time - {}".format(t_end-t_init)) + self.tp.running = False + self.tp.Close() + if self.tp.error: + dlgg = GMD.GenericMessageDialog(None, self.tp.error, + "Exception!", + wx.OK|wx.ICON_ERROR) + dlgg.ShowModal() + del self.tp + wx.MessageBox(_("FOD Import successful"), _("InVesalius 3")) - self.checktracts.Enable(1) - self.checktracts.SetValue(True) - self.view_tracts = True - - Publisher.sendMessage('Update Trekker object', data=self.trekker) - Publisher.sendMessage('Update number of threads', data=n_threads) - Publisher.sendMessage('Update tracts visualization', data=1) - Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized")) # except: # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3")) except: @@ -449,6 +467,33 @@ def OnLinkFOD(self, event=None): Publisher.sendMessage('End busy cursor') + def UpdateDialog(self): + while self.tp.running: + self.tp.dlg.Pulse("Setting up FOD ... ") + wx.Yield() + + def _on_callback_error(self, e, dialog=None): + import invesalius.utils as utl + dialog.running = False + msg = utl.log_traceback(e) + dialog.error = msg + + def TrekkerCallback(self, trekker): + print("Import Complete") + if trekker != None: + self.trekker = trekker.result() + self.trekker, n_threads = dti.set_trekker_parameters(self.trekker, self.trekker_cfg) + + self.checktracts.Enable(1) + self.checktracts.SetValue(True) + self.view_tracts = True + + Publisher.sendMessage('Update Trekker object', data=self.trekker) + Publisher.sendMessage('Update number of threads', data=n_threads) + Publisher.sendMessage('Update tracts visualization', data=1) + Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialized")) + self.tp.running = False + def OnLoadACT(self, event=None): if self.trekker: Publisher.sendMessage('Begin busy cursor') From 4eb1e0de9b0f2b61663729de442ba754871de5dc Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 19 Jul 2023 18:25:32 +0300 Subject: [PATCH 31/99] FIX: bug fix --- invesalius/gui/task_tractography.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index 588cb8214..88556a441 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -479,6 +479,7 @@ def _on_callback_error(self, e, dialog=None): dialog.error = msg def TrekkerCallback(self, trekker): + self.tp.running = False print("Import Complete") if trekker != None: self.trekker = trekker.result() From d4922f30783f3d57d4d8b89cd9375f41fe8080f8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 20 Jul 2023 13:31:46 +0300 Subject: [PATCH 32/99] FIX: pedal bug fixes --- invesalius/gui/task_navigator.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index ee0d3a885..0e4444aaf 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -895,6 +895,7 @@ def set_fiducial_callback(self, state, index=None): if index is None: fiducial_name = const.TRACKER_FIDUCIALS[self.tracker_fiducial_being_set]['fiducial_name'] Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) + self.btns_set_fiducial[self.tracker_fiducial_being_set].SetValue(self.tracker.IsTrackerFiducialSet(self.tracker_fiducial_being_set)) else: fiducial_name = const.TRACKER_FIDUCIALS[index]['fiducial_name'] Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) @@ -923,6 +924,8 @@ def ResetICP(self): def OnReset(self, evt, ctrl): self.tracker.ResetTrackerFiducials() + self.tracker_fiducial_being_set = 0 + self.OnNextDisable() for button in self.btns_set_fiducial: button.SetValue(False) self.start_button.SetValue(False) @@ -959,6 +962,7 @@ def OnStartRegistration(self, evt, ctrl): name='fiducial', callback=self.set_fiducial_callback, remove_when_released=True, + remove_when_released=False, ) if self.neuronavigation_api is not None: @@ -966,6 +970,7 @@ def OnStartRegistration(self, evt, ctrl): name='fiducial', callback=self.set_fiducial_callback, remove_when_released=True, + remove_when_released=False, ) for button in self.btns_set_fiducial: button.Enable() From 3fbd9230d66a6561938254fe8cebb111f1e017c7 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 20 Jul 2023 13:33:46 +0300 Subject: [PATCH 33/99] FIX: pedal bug fix --- invesalius/gui/task_navigator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 0e4444aaf..f855aead4 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -961,7 +961,6 @@ def OnStartRegistration(self, evt, ctrl): self.pedal_connection.add_callback( name='fiducial', callback=self.set_fiducial_callback, - remove_when_released=True, remove_when_released=False, ) @@ -969,7 +968,6 @@ def OnStartRegistration(self, evt, ctrl): self.neuronavigation_api.add_pedal_callback( name='fiducial', callback=self.set_fiducial_callback, - remove_when_released=True, remove_when_released=False, ) for button in self.btns_set_fiducial: From c43bb704b392e7c080fdacad2571ebb5c9a0ba28 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 20 Jul 2023 13:34:32 +0300 Subject: [PATCH 34/99] FIX: Trekker progress bug fix --- invesalius/gui/task_navigator.py | 14 ++++++++++++-- invesalius/gui/task_tractography.py | 16 ++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index f855aead4..4ddc672f4 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1342,6 +1342,9 @@ def __bind_events(self): Publisher.subscribe(self.OnCheckStatus, 'Navigation status') Publisher.subscribe(self.UpdateTarget, 'Update target') Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + + Publisher.subscribe(self.UpdateTractsVisualization, 'Update tracts visualization') + # Externally check/uncheck and enable/disable checkboxes. Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') @@ -1448,9 +1451,16 @@ def OnStopRobot(self, evt, ctrl): # 'Tracktography' - def OnTractographyCheckbox(self, evt): - pass + def OnTractographyCheckbox(self, evt, ctrl): + self.view_tracts = ctrl.GetValue() + Publisher.sendMessage('Update tracts visualization', data=self.view_tracts) + if not self.view_tracts: + Publisher.sendMessage('Remove tracts') + Publisher.sendMessage("Update marker offset state", create=False) + def UpdateTractsVisualization(self, data): + self.tractography_checkbox.Enable() + self.tractography_checkbox.SetValue(1) # 'Track object' checkbox def EnableTrackObjectCheckbox(self, enabled): diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index 88556a441..e49832304 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -432,6 +432,8 @@ def OnLinkFOD(self, event=None): t_init = time.time() try: import concurrent.futures as mp + from concurrent.futures import wait + from concurrent.futures import FIRST_COMPLETED from concurrent.futures import ThreadPoolExecutor import multiprocessing import functools @@ -442,11 +444,11 @@ def OnLinkFOD(self, event=None): file = filename.encode('utf-8') with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as exec: - future2 = exec.submit(self.UpdateDialog) - future1 = exec.submit(Trekker.initialize, file) - future1.add_done_callback(self.TrekkerCallback) - future2.add_done_callback(print) - + futures = [exec.submit(self.UpdateDialog), exec.submit(Trekker.initialize, file)] + done, not_done = wait(futures, return_when=FIRST_COMPLETED) + completed_future = done.pop() + self.TrekkerCallback(completed_future) + t_end = time.time() print("Elapsed time - {}".format(t_end-t_init)) self.tp.running = False @@ -458,7 +460,7 @@ def OnLinkFOD(self, event=None): dlgg.ShowModal() del self.tp wx.MessageBox(_("FOD Import successful"), _("InVesalius 3")) - + Publisher.sendMessage('End busy cursor') # except: # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3")) except: @@ -470,6 +472,8 @@ def OnLinkFOD(self, event=None): def UpdateDialog(self): while self.tp.running: self.tp.dlg.Pulse("Setting up FOD ... ") + if not self.tp.running: + break wx.Yield() def _on_callback_error(self, e, dialog=None): From 212d185b175b2402d591cbd36aa6ecdba1b7ba11 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 26 Jul 2023 13:29:51 +0300 Subject: [PATCH 35/99] ADD: Tool bar in navigation --- icons/camera.png | Bin 0 -> 534 bytes icons/coil.png | Bin 0 -> 351 bytes icons/efield.png | Bin 0 -> 693 bytes icons/lock.png | Bin 0 -> 499 bytes icons/port.png | Bin 0 -> 499 bytes icons/track.png | Bin 0 -> 534 bytes icons/tract.png | Bin 0 -> 534 bytes invesalius/gui/task_navigator.py | 234 ++++++++++++++++++++++++++----- 8 files changed, 196 insertions(+), 38 deletions(-) create mode 100644 icons/camera.png create mode 100644 icons/coil.png create mode 100644 icons/efield.png create mode 100644 icons/lock.png create mode 100644 icons/port.png create mode 100644 icons/track.png create mode 100644 icons/tract.png diff --git a/icons/camera.png b/icons/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..afa1f469d19feb6f68b9652a182e4c51630259ec GIT binary patch literal 534 zcmV+x0_pvUP)P+`&P|prNP`o71f>Mhy*hyH~7+_dR&7c)RCh4)+U(^FHT1 z@Avz>=lOAHcO1^)+l^}&Lnk(}Y&w*Y{3hPx9J(-zmpG9lK+uajxQ)~C*$^t|-xb(H zJkLXZ0blV1kFkkSJi@&_{Ey-bPG%K&i^-Hdf!A2Uw5&z*H0{v(w4RKVTl;bC3@T({xzqNd& Y4}}X%FlNZyLjV8(07*qoM6N<$f<@%tk^lez literal 0 HcmV?d00001 diff --git a/icons/coil.png b/icons/coil.png new file mode 100644 index 0000000000000000000000000000000000000000..e8bcb3e8ff40548dcd800306d704e4fd9ca93d6b GIT binary patch literal 351 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjEX7WqAsj$Z!;#Vf2?- zWgyIWxAwmUkRe&(8c`CQpH@mmtT}V`<;yxP|+Pv7sn8f z&bL!-`3@QIxWx;+;;a&2<2%rBgwv|5ow<-}TG^@z5-(!dRL))KwVt86(&xqb6qX=e zSFgwE^-~|K__=J@(wg}#@_{;-p7$f`gf0*DfZhX%Q|t;KJrAxCJr^NoDR+SJ2D5L!$DBPM z-yA%i74Ih!&~CIMsON9)O7G{%uTJ={7Bk}Y@2FAvUbAe*wLQKMxm7paP5ErL)h@%& o-a;(==Lg zClLfK?M!1E1QA;sn-m5cD+?uHVG#s3v~dAZ5jE}@K}F$Mym{t&b8dJ-1`e0=&p-3e zo3p)?bJmHaloB?_^l9!eFg5Wa;ePDGAhu(9xMy$=cX1O}8U=ICa|YI961g^}W7P3U ztS{!OB~CaI6=rCziE=ws5r<8`VEULfqN#o$Jy%rEuHASv%JtdmYu`K!eBzT*j;5zQCp8q8EbK;6?-qw8<8EqEjsPr4TzON0=b8Ec2=p+HX>4V3or&7o=1(a87@EF|)^KW= puvH>*3|DX@a|gSM&+PZ%$zORcs#_<~syu%l~z)L*F4z?3x4n#Ay9`5SP*2PNE zAyuN&FjDAWD^92naJnk!dkb&TK7*iz9~g@L^^~mM=r4IIOHBEbojvlIiZ<3WL-A%AsT2z9(h2ZwkP3BLOO z1DeAu_JzzlPM(8Ga35=t79WCiH}vP pL>+l~j^kLRcqH8aEzG1k{RJnNUc>4@>&5^8002ovPDHLkV1j^S*gpUO literal 0 HcmV?d00001 diff --git a/icons/track.png b/icons/track.png new file mode 100644 index 0000000000000000000000000000000000000000..fb71ca10be84fbfb27426f6d7625bdb22232f972 GIT binary patch literal 534 zcmV+x0_pvUP);060?P5m41V5dC`@UKJQC_b|gN#IYKj0Tl=S zCJ?Djw2s_R;)zQpIds&vZX<+<<|F*83$+;o4_N-Zr5RL9I8RYiA5GK8E2J_^2NfcR z_y)`otxe?_0PjF-3UG&%p9vB01zhO*2HFb&TX&rJc+GObxM zbHNS;Wf~%b>pzV)0q!XnG5eZ9r$m4bayGl`i0LFPGwf`=v4Q~U+60*`HJ06-y)pwt z^ND62;W(~S)RDoJAk~$3lhGMBx}4!#j#E!Q YKkmbH$^A~`004R> z004l5008;`004mK004C`008P>0026e000+ooVrmw00002VoOIv0063uBQgL0010qN zS#tmY0AK(B0AK*{YeLTe000McNliru=LZ-F4l=l34@>|60eeYAK~y-)tx~ zq$P)HOG8!_cDdi3ia_PXo%c@0jKYpOEAq8Z8FLj`t3FM8d+qgGS5dLkhQL?NdsCiQ zc>XBmXGNZu)lxk}WTZn9Y0oS;;kxx=<)9HQPW!c+T91d$RlTGvyHidtxaN4wM#f~L zV4o?0oK2%OkdrQ!Oef5F6otdqq;b`j_6qp0f-MuE*;5;iMul->MM$n@zfbg+;uV)8 zJA5{vL7?4Xw=9(uZW`2UUIstB7yd0Y`Qn3ddDB(jyx)iij#Q78C{k`aX4cDcm;ZLQ Y0aNgKcHksb>i_@%07*qoM6N<$f(4h|=>Px# literal 0 HcmV?d00001 diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 4ddc672f4..0c4a2c3a0 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -47,6 +47,9 @@ except ImportError: import wx.lib.foldpanelbar as fpb +import wx.lib.buttons as btn +import wx.lib.platebtn as pbtn + import wx.lib.colourselect as csel import wx.lib.masked.numctrl from invesalius.pubsub import pub as Publisher @@ -192,7 +195,7 @@ def __init__(self, parent): # TODO: Initialize checkboxes before panels: they are updated by ObjectRegistrationPanel when loading its state. # A better solution would be to have these checkboxes save their own state, independent of the panels, but that's # not implemented yet. - + ''' # Checkbox for camera update in volume rendering during navigation tooltip = wx.ToolTip(_("Update camera in volume")) checkcamera = wx.CheckBox(self, -1, _('Vol. camera')) @@ -217,11 +220,13 @@ def __init__(self, parent): checkobj.Disable() checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowCoil) self.checkobj = checkobj - + + # if sys.platform != 'win32': self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) checkbox_serial_port.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) + ''' # Fold panel style style = fpb.CaptionBarStyle() @@ -338,12 +343,14 @@ def __init__(self, parent): ''' self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) + ''' # Panel sizer for checkboxes line_sizer = wx.BoxSizer(wx.HORIZONTAL) line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) line_sizer.Add(checkbox_serial_port, 0, wx.ALIGN_CENTER) line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5) line_sizer.Fit(self) + ''' sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(gbs, 1, wx.GROW|wx.EXPAND) @@ -355,7 +362,7 @@ def __init__(self, parent): self.track_obj = False gbs.Add(fold_panel, (0, 0), flag=wx.EXPAND) - gbs.Add(line_sizer, (1, 0), flag=wx.EXPAND) + #gbs.Add(line_sizer, (1, 0), flag=wx.EXPAND) gbs.Layout() sizer.Fit(self) self.Fit() @@ -363,17 +370,18 @@ def __init__(self, parent): self.__bind_events() def __bind_events(self): - Publisher.subscribe(self.OnCheckStatus, 'Navigation status') + #Publisher.subscribe(self.OnCheckStatus, 'Navigation status') Publisher.subscribe(self.OnShowDbs, "Show dbs folder") Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") + ''' # Externally check/uncheck and enable/disable checkboxes. Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') - + ''' Publisher.subscribe(self.OpenNavigation, 'Open navigation menu') def __calc_best_size(self, panel): @@ -861,6 +869,7 @@ def __bind_events(self): Publisher.subscribe(self.OnNextEnable, "Next enable for tracker fiducials") Publisher.subscribe(self.OnNextDisable, "Next disable for tracker fiducials") Publisher.subscribe(self.OnTrackerChanged, "Tracker changed") + Publisher.subscribe(self.OnResetTrackerFiducials, "Reset tracker fiducials") def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] @@ -924,6 +933,8 @@ def ResetICP(self): def OnReset(self, evt, ctrl): self.tracker.ResetTrackerFiducials() + + def OnResetTrackerFiducials(self): self.tracker_fiducial_being_set = 0 self.OnNextDisable() for button in self.btns_set_fiducial: @@ -1082,6 +1093,7 @@ def __init__(self, parent, icp, tracker, image, navigation): def __bind_events(self): Publisher.subscribe(self.OnUpdateUI, "Update UI for refine tab") + Publisher.subscribe(self.OnResetTrackerFiducials, "Reset tracker fiducials") def OnUpdateUI(self): if self.tracker.AreTrackerFiducialsSet() and self.image.AreImageFiducialsSet(): @@ -1104,6 +1116,12 @@ def OnUpdateUI(self): self.txtctrl_fre.SetBackgroundColour('GREEN') else: self.txtctrl_fre.SetBackgroundColour('RED') + + def OnResetTrackerFiducials(self): + for m in range(3): + for n in range(3): + value = self.tracker.GetTrackerFiducialForUI(m, n) + self.numctrls_fiducial[m][n].SetValue(value) def OnBack(self, evt): Publisher.sendMessage('Back to image fiducials') @@ -1199,7 +1217,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.neuronavigation_api = neuronavigation_api top_sizer = wx.BoxSizer(wx.HORIZONTAL) - top_sizer.Add(MarkersPanel(self, self.navigation, self.tracker, self.icp), 1, wx.GROW | wx.EXPAND ) + top_sizer.Add(MarkersPanel(self, self.navigation, self.tracker, self.robot, self.icp), 1, wx.GROW | wx.EXPAND ) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) bottom_sizer.Add(ControlPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.image, self.pedal_connection, self.neuronavigation_api), 0, wx.EXPAND | wx.TOP, 20) @@ -1244,7 +1262,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect btn_robot.SetToolTip(tooltip) self.btn_robot = btn_robot btn_robot.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStopRobot, ctrl=btn_robot)) - + ''' # Label and Checkbox for Tractography tooltip = wx.ToolTip(_(u"Control Tractography")) tractography_checkbox = wx.CheckBox(self, -1, _('Enable / Disable Tractography ')) @@ -1303,14 +1321,110 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect force_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnForceSensorCheckbox, ctrl=force_checkbox)) force_checkbox.SetToolTip(tooltip) self.force_checkbox = force_checkbox + ''' + + # Constants for bitmap parent toggle button + ICON_SIZE = (48, 48) + RED_COLOR = (255, 82, 82) + self.RED_COLOR = RED_COLOR + GREEN_COLOR = (118, 255, 3) + self.GREEN_COLOR = GREEN_COLOR + GREY_COLOR = (217, 217, 217) + self.GREY_COLOR = GREY_COLOR + + # Toggle Button for Tractography + tooltip = wx.ToolTip(_(u"Control Tractography")) + BMP_TRACT = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("tract.png")), wx.BITMAP_TYPE_PNG) + tractography_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + tractography_checkbox.SetBackgroundColour(GREY_COLOR) + tractography_checkbox.SetBitmap(BMP_TRACT) + tractography_checkbox.SetValue(False) + tractography_checkbox.Enable(False) + tractography_checkbox.SetToolTip(tooltip) + tractography_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTractographyCheckbox, ctrl=tractography_checkbox)) + self.tractography_checkbox = tractography_checkbox + + # Toggle Button to track object or simply the stylus + tooltip = wx.ToolTip(_(u"Track the object")) + BMP_TRACK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("track.png")), wx.BITMAP_TYPE_PNG) + checkbox_track_object = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + checkbox_track_object.SetBackgroundColour(GREY_COLOR) + checkbox_track_object.SetBitmap(BMP_TRACK) + checkbox_track_object.SetValue(False) + checkbox_track_object.Enable(False) + checkbox_track_object.SetToolTip(tooltip) + checkbox_track_object.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackObjectCheckbox, ctrl=checkbox_track_object)) + self.checkbox_track_object = checkbox_track_object + + # Toggle Button for Lock to Target + tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) + BMP_LOCK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("lock.png")), wx.BITMAP_TYPE_PNG) + lock_to_target_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + lock_to_target_checkbox.SetBackgroundColour(GREY_COLOR) + lock_to_target_checkbox.SetBitmap(BMP_LOCK) + lock_to_target_checkbox.SetValue(False) + lock_to_target_checkbox.Enable(False) + lock_to_target_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnLockToTargetCheckbox, ctrl=lock_to_target_checkbox)) + lock_to_target_checkbox.SetToolTip(tooltip) + self.lock_to_target_checkbox = lock_to_target_checkbox + + # Toggle Button for object position and orientation update in volume rendering during navigation + tooltip = wx.ToolTip(_("Show and track TMS coil")) + BMP_SHOW = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("coil.png")), wx.BITMAP_TYPE_PNG) + checkobj = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + checkobj.SetBackgroundColour(GREY_COLOR) + checkobj.SetBitmap(BMP_SHOW) + checkobj.SetToolTip(tooltip) + checkobj.SetValue(False) + checkobj.Enable(False) + checkobj.Bind(wx.EVT_TOGGLEBUTTON, self.OnShowCoil) + self.checkobj = checkobj + + # Toggle Button for camera update in volume rendering during navigation + tooltip = wx.ToolTip(_("Update camera in volume")) + BMP_UPDATE = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("camera.png")), wx.BITMAP_TYPE_PNG) + checkcamera = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + checkcamera.SetBitmap(BMP_UPDATE) + checkcamera.SetToolTip(tooltip) + checkcamera.SetValue(const.CAM_MODE) + if checkcamera.IsEnabled(): + checkcamera.SetBackgroundColour(GREEN_COLOR) + else: + checkcamera.SetBackgroundColour(RED_COLOR) + checkcamera.Bind(wx.EVT_TOGGLEBUTTON, self.OnVolumeCameraCheckbox) + self.checkcamera = checkcamera + # Toggle Button to use serial port to trigger pulse signal and create markers + tooltip = wx.ToolTip(_("Enable serial port communication to trigger pulse and create markers")) + BMP_PORT = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("port.png")), wx.BITMAP_TYPE_PNG) + checkbox_serial_port = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + checkbox_serial_port.SetBackgroundColour(RED_COLOR) + checkbox_serial_port.SetBitmap(BMP_PORT) + checkbox_serial_port.SetToolTip(tooltip) + checkbox_serial_port.SetValue(False) + checkbox_serial_port.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnEnableSerialPort, ctrl=checkbox_serial_port)) + self.checkbox_serial_port = checkbox_serial_port + #Toggle Button for Efield + tooltip = wx.ToolTip(_(u"Control E-Field")) + BMP_FIELD = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("efield.png")), wx.BITMAP_TYPE_PNG) + efield_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + efield_checkbox.SetBackgroundColour(GREY_COLOR) + efield_checkbox.SetBitmap(BMP_FIELD) + efield_checkbox.SetValue(False) + efield_checkbox.Enable(False) + efield_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnEfieldCheckbox, ctrl=efield_checkbox)) + efield_checkbox.SetToolTip(tooltip) + self.efield_checkbox = efield_checkbox + + #Sizers button_sizer = wx.BoxSizer(wx.VERTICAL) button_sizer.AddMany([ (btn_nav, 0, wx.EXPAND | wx.GROW), (btn_robot, 0, wx.EXPAND | wx.GROW) ]) + ''' checkbox_sizer = wx.BoxSizer(wx.VERTICAL) checkbox_sizer.AddMany([ (tractography_checkbox), @@ -1319,7 +1433,18 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect (checkobj), (checkcamera), (checkbox_serial_port), - (force_checkbox) + (efield_checkbox) + ]) + ''' + checkbox_sizer = wx.FlexGridSizer(4, 5, 5) + checkbox_sizer.AddMany([ + (tractography_checkbox), + (checkbox_track_object), + (lock_to_target_checkbox), + (checkobj), + (checkcamera), + (checkbox_serial_port), + (efield_checkbox) ]) main_sizer = wx.BoxSizer(wx.VERTICAL) @@ -1360,7 +1485,7 @@ def SaveState(self): track_object = self.checkbox_track_object state = { 'track_object': { - 'checked': track_object.IsChecked(), + 'checked': track_object.GetValue(), 'enabled': track_object.IsEnabled(), } } @@ -1380,15 +1505,37 @@ def LoadState(self): self.EnableTrackObjectCheckbox(track_object['enabled']) self.CheckTrackObjectCheckbox(track_object['checked']) + def UpdateToggleButton(self, ctrl, state=None): + # Changes background colour based on current state of toggle button if state is not set, + # otherwise, uses state to set value. + if state is None: + state = ctrl.GetValue() + + ctrl.SetValue(state) + + if state: + ctrl.SetBackgroundColour(self.GREEN_COLOR) + else: + ctrl.SetBackgroundColour(self.RED_COLOR) + + def EnableToggleButton(self, ctrl, state): + ctrl.Enable(state) + ctrl.SetBackgroundColour(self.GREY_COLOR) + def OnCheckStatus(self, nav_status, vis_status): if nav_status: - self.checkbox_serial_port.Enable(False) - self.checkobj.Enable(False) + self.EnableToggleButton(self.checkbox_serial_port, 0) + self.UpdateToggleButton(self.checkbox_serial_port) + self.EnableToggleButton(self.checkobj, 0) + self.UpdateToggleButton(self.checkobj) else: - self.checkbox_serial_port.Enable(True) - self.checkbox_track_object.Enable(True) + self.EnableToggleButton(self.checkbox_serial_port, 1) + self.UpdateToggleButton(self.checkbox_serial_port) + self.EnableToggleButton(self.checkbox_track_object, 1) + self.UpdateToggleButton(self.checkbox_track_object) if self.track_obj: - self.checkobj.Enable(True) + self.EnableToggleButton(self.checkobj, 1) + self.UpdateToggleButton(self.checkobj) # Navigation def OnStartNavigation(self): @@ -1426,7 +1573,7 @@ def OnNavigate(self, evt, btn_nav): def OnStopNavigation(self): self.navigation.StopNavigation() - if self.tracker.tracker_id == const.ROBOT: + if self.robot.IsConnected(): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) @@ -1434,8 +1581,8 @@ def UpdateTarget(self, coord): self.navigation.target = coord if coord is not None: - self.lock_to_target_checkbox.Enable(True) - self.lock_to_target_checkbox.SetValue(True) + self.EnableToggleButton(self.lock_to_target_checkbox, 1) + self.UpdateToggleButton(self.lock_to_target_checkbox, True) self.navigation.SetLockToTarget(True) def UpdateNavigationStatus(self, nav_status, vis_status): @@ -1453,25 +1600,29 @@ def OnStopRobot(self, evt, ctrl): # 'Tracktography' def OnTractographyCheckbox(self, evt, ctrl): self.view_tracts = ctrl.GetValue() + self.UpdateToggleButton(ctrl) Publisher.sendMessage('Update tracts visualization', data=self.view_tracts) if not self.view_tracts: Publisher.sendMessage('Remove tracts') Publisher.sendMessage("Update marker offset state", create=False) def UpdateTractsVisualization(self, data): - self.tractography_checkbox.Enable() - self.tractography_checkbox.SetValue(1) + self.EnableToggleButton(self.tractography_checkbox, 1) + self.UpdateToggleButton(self.tractography_checkbox, data) # 'Track object' checkbox def EnableTrackObjectCheckbox(self, enabled): - self.checkbox_track_object.Enable(enabled) + self.EnableToggleButton(self.checkbox_track_object, enabled) + self.UpdateToggleButton(self.checkbox_track_object) def CheckTrackObjectCheckbox(self, checked): - self.checkbox_track_object.SetValue(checked) + self.UpdateToggleButton(self.checkbox_track_object, checked) self.OnTrackObjectCheckbox() def OnTrackObjectCheckbox(self, evt=None, ctrl=None): - checked = self.checkbox_track_object.IsChecked() + if ctrl is not None: + self.UpdateToggleButton(ctrl) + checked = self.checkbox_track_object.GetValue() Publisher.sendMessage('Track object', enabled=checked) # Disable or enable 'Show coil' checkbox, based on if 'Track object' checkbox is checked. @@ -1485,43 +1636,49 @@ def OnTrackObjectCheckbox(self, evt=None, ctrl=None): # 'Lock to Target' checkbox def OnLockToTargetCheckbox(self, evt, ctrl): + self.UpdateToggleButton(ctrl) value = ctrl.GetValue() self.navigation.SetLockToTarget(value) # 'Show coil' checkbox def CheckShowCoil(self, checked=False): - self.checkobj.SetValue(checked) + self.UpdateToggleButton(self.checkobj, checked) self.track_obj = checked self.OnShowCoil() def EnableShowCoil(self, enabled=False): - self.checkobj.Enable(enabled) + self.EnableToggleButton(self.checkobj, enabled) + self.UpdateToggleButton(self.checkobj) def OnShowCoil(self, evt=None): + self.UpdateToggleButton(self.checkobj) checked = self.checkobj.GetValue() Publisher.sendMessage('Show-coil checked', checked=checked) # 'Volume camera' checkbox def CheckVolumeCameraCheckbox(self, checked): - self.checkcamera.SetValue(checked) + self.UpdateToggleButton(self.checkcamera, checked) self.OnVolumeCameraCheckbox() def OnVolumeCameraCheckbox(self, evt=None, status=None): + self.UpdateToggleButton(self.checkcamera) Publisher.sendMessage('Update volume camera state', camera_state=self.checkcamera.GetValue()) def EnableVolumeCameraCheckbox(self, enabled): - self.checkcamera.Enable(enabled) + self.EnableToggleButton(self.checkcamera, enabled) + self.UpdateToggleButton(self.checkcamera) # 'Serial Port Com' def OnEnableSerialPort(self, evt, ctrl): + self.UpdateToggleButton(ctrl) if ctrl.GetValue(): from wx import ID_OK dlg_port = dlg.SetCOMPort(select_baud_rate=False) if dlg_port.ShowModal() != ID_OK: - ctrl.SetValue(False) + self.UpdateToggleButton(ctrl, False) return com_port = dlg_port.GetCOMPort() @@ -1531,9 +1688,9 @@ def OnEnableSerialPort(self, evt, ctrl): else: Publisher.sendMessage('Update serial port', serial_port_in_use=False) - # 'Force Sensor' - def OnForceSensorCheckbox(self): - pass + # 'E Field' + def OnEfieldCheckbox(self, evt, ctrl): + self.UpdateToggleButton(ctrl) class NeuronavigationPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): @@ -2575,7 +2732,7 @@ def to_dict(self): } - def __init__(self, parent, navigation, tracker, icp): + def __init__(self, parent, navigation, tracker, robot, icp): wx.Panel.__init__(self, parent) try: default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) @@ -2587,6 +2744,7 @@ def __init__(self, parent, navigation, tracker, icp): self.navigation = navigation self.tracker = tracker + self.robot = robot self.icp = icp if has_mTMS: self.mTMS = mTMS() @@ -2693,7 +2851,7 @@ def __init__(self, parent, navigation, tracker, icp): group_sizer.Add(sizer_create, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) group_sizer.Add(sizer_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) group_sizer.Add(sizer_delete, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) - group_sizer.Add(marker_list_ctrl, 1, wx.EXPAND | wx.ALL, 5) + group_sizer.Add(marker_list_ctrl, 0, wx.EXPAND | wx.ALL, 5) group_sizer.Fit(self) self.SetSizer(group_sizer) @@ -2903,7 +3061,7 @@ def OnMouseRightDown(self, evt): menu_id.AppendSeparator() # Enable "Send target to robot" button only if tracker is robot, if navigation is on and if target is not none - if self.tracker.tracker_id == const.ROBOT: + if self.robot.IsConnected(): send_target_to_robot = menu_id.Append(7, _('Send InVesalius target to robot')) menu_id.Bind(wx.EVT_MENU, self.OnMenuSendTargetToRobot, send_target_to_robot) @@ -2946,7 +3104,7 @@ def OnMenuSetTarget(self, evt): wx.MessageBox(_("No data selected."), _("InVesalius 3")) return - if self.tracker.tracker_id == const.ROBOT: + if self.robot.IsConnected(): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) self.__set_marker_as_target(idx) @@ -2976,7 +3134,7 @@ def OnMenuRemoveTarget(self, evt): self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, "") Publisher.sendMessage('Disable or enable coil tracker', status=False) Publisher.sendMessage('Update target', coord=None) - if self.tracker.tracker_id == const.ROBOT: + if self.robot.IsConnected(): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) #self.__delete_all_brain_targets() @@ -3081,7 +3239,7 @@ def OnDeleteAllMarkers(self, evt=None): Publisher.sendMessage('Disable or enable coil tracker', status=False) if evt is not None: wx.MessageBox(_("Target deleted."), _("InVesalius 3")) - if self.tracker.tracker_id == const.ROBOT: + if self.robot.IsConnected(): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) @@ -3120,7 +3278,7 @@ def OnDeleteMultipleMarkers(self, evt=None, label=None): if self.__find_target_marker() in indexes: Publisher.sendMessage('Disable or enable coil tracker', status=False) Publisher.sendMessage('Update target', coord=None) - if self.tracker.tracker_id == const.ROBOT: + if self.robot.IsConnected(): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) wx.MessageBox(_("Target deleted."), _("InVesalius 3")) @@ -3271,7 +3429,7 @@ def CreateMarker(self, position=None, orientation=None, colour=None, size=None, new_marker.session_id = session_id or self.current_session new_marker.is_brain_target = is_brain_target - if self.tracker.tracker_id == const.ROBOT and self.nav_status: + if self.robot.IsConnected() and self.nav_status: current_head_robot_target_status = True else: current_head_robot_target_status = False From e877b377d5c95e58907d0381801af47fd2c933e0 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 26 Jul 2023 13:30:05 +0300 Subject: [PATCH 36/99] FIX: bug fix --- invesalius/navigation/tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index a622a60c8..d3a6df18c 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -208,6 +208,7 @@ def SetTrackerFiducial(self, ref_mode_id, fiducial_index): self.SaveState() def ResetTrackerFiducials(self): + Publisher.sendMessage("Reset tracker fiducials") for m in range(3): self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan] From ba222ad1bd3ec763503155ed3a922949c6c9f6ed Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 26 Jul 2023 13:30:27 +0300 Subject: [PATCH 37/99] REM: Robot tracker --- invesalius/constants.py | 7 +++---- invesalius/data/coordinates.py | 1 - invesalius/data/tracker_connection.py | 1 - invesalius/gui/dialogs.py | 1 - 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 2047d4d9b..aedb343b2 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -683,9 +683,8 @@ POLARIS = 6 POLARISP4 = 7 OPTITRACK = 8 -ROBOT = 9 -DEBUGTRACKRANDOM = 10 -DEBUGTRACKAPPROACH = 11 +DEBUGTRACKRANDOM = 9 +DEBUGTRACKAPPROACH = 10 DEFAULT_TRACKER = SELECT NDICOMPORT = b'COM1' @@ -695,7 +694,7 @@ _("Polhemus FASTRAK"), _("Polhemus ISOTRAK II"), _("Polhemus PATRIOT"), _("Camera tracker"), _("NDI Polaris"), _("NDI Polaris P4"), - _("Optitrack"), _("Robot tracker"), + _("Optitrack"), _("Debug tracker (random)"), _("Debug tracker (approach)")] STATIC_REF = 0 diff --git a/invesalius/data/coordinates.py b/invesalius/data/coordinates.py index 14a43f0a5..43eacc8c3 100644 --- a/invesalius/data/coordinates.py +++ b/invesalius/data/coordinates.py @@ -85,7 +85,6 @@ def GetCoordinatesForThread(tracker_connection, tracker_id, ref_mode): const.POLARIS: PolarisCoord, const.POLARISP4: PolarisP4Coord, const.OPTITRACK: OptitrackCoord, - const.ROBOT: RobotCoord, const.DEBUGTRACKRANDOM: DebugCoordRandom, const.DEBUGTRACKAPPROACH: DebugCoordRandom} coord, markers_flag = getcoord[tracker_id](tracker_connection, tracker_id, ref_mode) diff --git a/invesalius/data/tracker_connection.py b/invesalius/data/tracker_connection.py index 4fe0ec720..1152f23fb 100644 --- a/invesalius/data/tracker_connection.py +++ b/invesalius/data/tracker_connection.py @@ -636,7 +636,6 @@ def Disconnect(self): const.POLARIS: PolarisTrackerConnection, const.POLARISP4: PolarisP4TrackerConnection, const.OPTITRACK: OptitrackTrackerConnection, - const.ROBOT: RobotTrackerConnection, const.DEBUGTRACKRANDOM: DebugTrackerRandomConnection, const.DEBUGTRACKAPPROACH: DebugTrackerApproachConnection, } diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 8e9d3e398..179edffab 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -927,7 +927,6 @@ def ShowNavigationTrackerWarning(trck_id, lib_mode): const.POLARIS: 'NDI Polaris', const.POLARISP4: 'NDI Polaris P4', const.OPTITRACK: 'Optitrack', - const.ROBOT: 'Robotic navigation', const.DEBUGTRACKRANDOM: 'Debug tracker device (random)', const.DEBUGTRACKAPPROACH: 'Debug tracker device (approach)'} From b84bb2ecc472123c25d46c9c8f07630d6f7939e0 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 26 Jul 2023 13:30:53 +0300 Subject: [PATCH 38/99] ADD: native robot connection from preferences --- invesalius/gui/preferences.py | 42 +++++++++++++++++++++++++++++----- invesalius/navigation/robot.py | 3 +++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 77e0e9557..76254d574 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -457,6 +457,7 @@ def __init__(self, parent, tracker, robot): self.tracker = tracker self.robot = robot + self.robot_IP = None # ComboBox for spatial tracker device selection tracker_options = [_("Select")] + self.tracker.get_trackers() @@ -493,12 +494,24 @@ def __init__(self, parent, tracker, robot): sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Setup tracker")) sizer.Add(ref_sizer, 1, wx.ALL | wx.FIXED_MINSIZE, 20) + + lbl_rob = wx.StaticText(self, -1, _("Select IP for robot device: ")) - lbl_rob = wx.StaticText(self, -1, _("Robot tracking device: ")) - btn_rob = wx.Button(self, -1, _("Setup")) - btn_rob.SetToolTip("Setup robot tracking") + # ComboBox for spatial tracker device selection + tooltip = wx.ToolTip(_("Choose or type the robot IP")) + robot_ip_options = [_("Select robot IP:")] + const.ROBOT_ElFIN_IP + choice_IP = wx.ComboBox(self, -1, "", + choices=robot_ip_options, style=wx.CB_DROPDOWN | wx.TE_PROCESS_ENTER) + choice_IP.SetToolTip(tooltip) + choice_IP.SetSelection(const.DEFAULT_TRACKER) + choice_IP.Bind(wx.EVT_COMBOBOX, partial(self.OnChoiceIP, ctrl=choice_IP)) + choice_IP.Bind(wx.EVT_TEXT, partial(self.OnTxt_Ent, ctrl=choice_IP)) + self.choice_IP = choice_IP + + btn_rob = wx.Button(self, -1, _("Connect")) + btn_rob.SetToolTip("Connect to IP") btn_rob.Enable(1) - btn_rob.Bind(wx.EVT_BUTTON, self.OnRobot) + btn_rob.Bind(wx.EVT_BUTTON, self.OnRobotConnect) self.btn_rob = btn_rob btn_rob_con = wx.Button(self, -1, _("Register")) @@ -507,10 +520,11 @@ def __init__(self, parent, tracker, robot): btn_rob_con.Bind(wx.EVT_BUTTON, self.OnRobotCon) self.btn_rob_con = btn_rob_con - rob_sizer = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) + rob_sizer = wx.FlexGridSizer(rows=2, cols=3, hgap=5, vgap=5) rob_sizer.AddMany([ (lbl_rob, 0, wx.LEFT), - (btn_rob, 0, wx.RIGHT), + (choice_IP, 1, wx.EXPAND), + (btn_rob, 0, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15), (btn_rob_con, 0, wx.RIGHT) ]) @@ -555,6 +569,22 @@ def HideParent(self): # hide preferences dialog box def ShowParent(self): # show preferences dialog box self.GetGrandParent().Show() + def OnTxt_Ent(self, evt, ctrl): + self.robot_ip = str(ctrl.GetValue()) + + def OnChoiceIP(self, evt, ctrl): + self.robot_ip = ctrl.GetStringSelection() + + def OnRobotConnect(self, evt): + if self.robot_ip is not None: + self.configuration = { + 'tracker_id': self.tracker.GetTrackerId(), + 'robot_ip': self.robot_ip, + 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), + } + self.connection = self.tracker.tracker_connection + Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) + def OnRobot(self, evt): self.HideParent() if self.robot.ConfigureRobot(): diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 10c5dc0b8..34f9a9a9f 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -126,6 +126,9 @@ def AbortRobotConfiguration(self): if self.robot_coregistration_dialog: self.robot_coregistration_dialog.Destroy() + def IsConnected(self): + return self.robot_status + def InitializeRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) From 0968974e3ebfff9796cb40cc647943e796e1d19e Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 27 Jul 2023 12:51:36 +0300 Subject: [PATCH 39/99] FIX: Bug Fix --- invesalius/gui/task_navigator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 0c4a2c3a0..66ced8758 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1524,10 +1524,12 @@ def EnableToggleButton(self, ctrl, state): def OnCheckStatus(self, nav_status, vis_status): if nav_status: - self.EnableToggleButton(self.checkbox_serial_port, 0) self.UpdateToggleButton(self.checkbox_serial_port) - self.EnableToggleButton(self.checkobj, 0) + self.EnableToggleButton(self.checkbox_serial_port, 0) self.UpdateToggleButton(self.checkobj) + self.EnableToggleButton(self.checkobj, 0) + self.UpdateToggleButton(self.checkbox_track_object) + self.EnableToggleButton(self.checkbox_track_object, 0) else: self.EnableToggleButton(self.checkbox_serial_port, 1) self.UpdateToggleButton(self.checkbox_serial_port) From 71311d09c86602993cb38a33a2063b799094093c Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 1 Aug 2023 14:43:10 +0300 Subject: [PATCH 40/99] FIX: Tracker fiducial recording --- invesalius/gui/task_navigator.py | 118 ++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 24 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 66ced8758..2c0b02c00 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -774,6 +774,16 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi self.bg_bmp = getBitMapForBackground() + RED_COLOR = (255, 82, 82) + self.RED_COLOR = RED_COLOR + GREEN_COLOR = (118, 255, 3) + self.GREEN_COLOR = GREEN_COLOR + YELLOW_COLOR = (255, 196, 0) + self.YELLOW_COLOR = YELLOW_COLOR + + TEXT_COLOR = (250, 250, 250) + self.TEXT_COLOR = TEXT_COLOR + # Toggle buttons for image fiducials background = wx.StaticBitmap(self, -1, self.bg_bmp, (0, 0)) for n, fiducial in enumerate(const.TRACKER_FIDUCIALS): @@ -781,11 +791,21 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi label = fiducial['label'] tip = fiducial['tip'] - ctrl = wx.ToggleButton(self, button_id, label=label, style=wx.BU_EXACTFIT) + # ctrl = wx.ToggleButton(self, button_id, label=label, style=wx.BU_EXACTFIT) + # ctrl.SetToolTip(wx.ToolTip(tip)) + # ctrl.SetBackgroundColour((255, 0, 0)) + # ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, i=n, ctrl=ctrl)) + # ctrl.SetValue(self.tracker.IsTrackerFiducialSet(n)) + # ctrl.Disable() + w, h = wx.ScreenDC().GetTextExtent("M"*len(label)) + ctrl = wx.StaticText(self, button_id, label=label, style=wx.TE_READONLY | wx.BORDER_NONE | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL, size=(55, h+5)) ctrl.SetToolTip(wx.ToolTip(tip)) - ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, i=n, ctrl=ctrl)) - ctrl.SetValue(self.tracker.IsTrackerFiducialSet(n)) - ctrl.Disable() + if self.tracker.IsTrackerFiducialSet(n): + ctrl.SetBackgroundColour(GREEN_COLOR) + else: + ctrl.SetBackgroundColour(RED_COLOR) + #ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, i=n, ctrl=ctrl)) + self.btns_set_fiducial[n] = ctrl @@ -797,6 +817,11 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi ) self.numctrls_fiducial[m][n].Hide() + register_button = wx.Button(self, label="Record Fiducial") + register_button.Bind(wx.EVT_BUTTON, partial(self.OnRegister, ctrl=register_button)) + register_button.Disable() + self.register_button = register_button + start_button = wx.ToggleButton(self, label="Start Patient Registration") start_button.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStartRegistration, ctrl=start_button)) self.start_button = start_button @@ -829,18 +854,18 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi self.main_label = main_label - middle_sizer = wx.BoxSizer(wx.HORIZONTAL) - middle_sizer.AddMany([ - (current_label), - (main_label) - ]) - top_sizer = wx.BoxSizer(wx.HORIZONTAL) top_sizer.AddMany([ (start_button), (reset_button) ]) + middle_sizer = wx.BoxSizer(wx.HORIZONTAL) + middle_sizer.AddMany([ + (current_label), + (main_label) + ]) + bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) bottom_sizer.AddMany([ (back_button, 0, wx.EXPAND), @@ -853,15 +878,19 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi sizer.Add(self.btns_set_fiducial[2], wx.GBPosition(0, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_HORIZONTAL) sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) sizer.Add(background, wx.GBPosition(1, 2)) + sizer.Add(register_button, wx.GBPosition(2, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.AddMany([ (top_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 10), (sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT, 5), - (middle_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 5), - (bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 20)]) + (middle_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.TOP, 20), + (5, 5), + (bottom_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.LEFT | wx.RIGHT | wx.BOTTOM, 20)]) + self.sizer = main_sizer self.SetSizerAndFit(main_sizer) + self.Layout() self.__bind_events() def __bind_events(self): @@ -871,6 +900,16 @@ def __bind_events(self): Publisher.subscribe(self.OnTrackerChanged, "Tracker changed") Publisher.subscribe(self.OnResetTrackerFiducials, "Reset tracker fiducials") + def LabelHandler(self, ctrl, n=None): + if self.tracker.IsTrackerFiducialSet(n): + ctrl.SetBackgroundColour(self.GREEN_COLOR) + elif n == self.tracker_fiducial_being_set: + ctrl.SetBackgroundColour(self.YELLOW_COLOR) + else: + ctrl.SetBackgroundColour(self.RED_COLOR) + + ctrl.Refresh() + def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] @@ -895,16 +934,19 @@ def SetTrackerFiducial(self, fiducial_name): self.ResetICP() if self.tracker.AreTrackerFiducialsSet(): self.OnNextEnable() + self.OnRegisterDisable() else: self.OnNextDisable() + self.OnRegisterEnable() #self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial, self.txtctrl_fre) def set_fiducial_callback(self, state, index=None): if state: if index is None: - fiducial_name = const.TRACKER_FIDUCIALS[self.tracker_fiducial_being_set]['fiducial_name'] + index = self.tracker_fiducial_being_set + fiducial_name = const.TRACKER_FIDUCIALS[index]['fiducial_name'] Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) - self.btns_set_fiducial[self.tracker_fiducial_being_set].SetValue(self.tracker.IsTrackerFiducialSet(self.tracker_fiducial_being_set)) + self.LabelHandler(self.btns_set_fiducial[index], index) else: fiducial_name = const.TRACKER_FIDUCIALS[index]['fiducial_name'] Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) @@ -915,16 +957,31 @@ def set_fiducial_callback(self, state, index=None): if self.neuronavigation_api is not None: self.neuronavigation_api.remove_pedal_callback(name='fiducial') + + self.tracker_fiducial_being_set = None else: for n in [0, 1, 2]: if not self.tracker.IsTrackerFiducialSet(n): self.tracker_fiducial_being_set = n + self.LabelHandler(self.btns_set_fiducial[n], n) break + else: + self.tracker_fiducial_being_set = None def OnTrackerFiducials(self, evt, i, ctrl): value = ctrl.GetValue() self.set_fiducial_callback(True, index=i) self.btns_set_fiducial[i].SetValue(self.tracker.IsTrackerFiducialSet(i)) + if self.tracker.AreTrackerFiducialsSet(): + if self.start_button.GetValue(): + self.start_button.SetValue(False) + self.OnStartRegistration(self.start_button, self.start_button) + + def OnRegister(self, evt, ctrl): + self.set_fiducial_callback(True) + if self.tracker.AreTrackerFiducialsSet(): + if self.start_button.GetValue(): + self.start_button.SetValue(False) def ResetICP(self): self.icp.ResetICP() @@ -933,12 +990,14 @@ def ResetICP(self): def OnReset(self, evt, ctrl): self.tracker.ResetTrackerFiducials() + self.OnResetTrackerFiducials() def OnResetTrackerFiducials(self): - self.tracker_fiducial_being_set = 0 + self.tracker_fiducial_being_set = None self.OnNextDisable() - for button in self.btns_set_fiducial: - button.SetValue(False) + self.OnRegisterDisable() + for i, button in enumerate(self.btns_set_fiducial): + self.LabelHandler(button, i) self.start_button.SetValue(False) self.OnStartRegistration(self.start_button, self.start_button) @@ -951,6 +1010,12 @@ def OnBack(self, evt): def OnPreferences(self, evt): Publisher.sendMessage("Open preferences menu") + def OnRegisterEnable(self): + self.register_button.Enable() + + def OnRegisterDisable(self): + self.register_button.Disable() + def OnNextEnable(self): self.next_button.Enable() @@ -959,10 +1024,11 @@ def OnNextDisable(self): def OnStartRegistration(self, evt, ctrl): value = ctrl.GetValue() - if not value: - for button in self.btns_set_fiducial: - button.Disable() - else: + for n in [0, 1, 2]: + if not self.tracker.IsTrackerFiducialSet(n): + self.tracker_fiducial_being_set = n + break + if value: if not self.tracker.IsTrackerInitialized(): print(self.tracker.tracker_connection, self.tracker.tracker_id) self.start_button.SetValue(False) @@ -981,9 +1047,13 @@ def OnStartRegistration(self, evt, ctrl): callback=self.set_fiducial_callback, remove_when_released=False, ) - for button in self.btns_set_fiducial: - button.Enable() - + # for button in self.btns_set_fiducial: + # button.Enable() + if self.tracker_fiducial_being_set < 3: + self.LabelHandler(self.btns_set_fiducial[self.tracker_fiducial_being_set], self.tracker_fiducial_being_set) + if not self.tracker.AreTrackerFiducialsSet(): + self.OnRegisterEnable() + def OnTrackerChanged(self): if self.tracker.GetTrackerId() != const.DEFAULT_TRACKER: self.main_label.SetLabel(self.tracker.get_trackers()[self.tracker.GetTrackerId() - 1]) From 17aceac2db850679ca41429c93934198fc800bd5 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 3 Aug 2023 10:42:21 +0300 Subject: [PATCH 41/99] FIX: robot bugs --- invesalius/gui/preferences.py | 5 ++++- invesalius/navigation/robot.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 76254d574..de7af968d 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -503,7 +503,10 @@ def __init__(self, parent, tracker, robot): choice_IP = wx.ComboBox(self, -1, "", choices=robot_ip_options, style=wx.CB_DROPDOWN | wx.TE_PROCESS_ENTER) choice_IP.SetToolTip(tooltip) - choice_IP.SetSelection(const.DEFAULT_TRACKER) + if self.robot.robot_ip is not None: + choice_IP.SetSelection(robot_ip_options.index(self.robot.robot_ip)) + else: + choice_IP.SetSelection(0) choice_IP.Bind(wx.EVT_COMBOBOX, partial(self.OnChoiceIP, ctrl=choice_IP)) choice_IP.Bind(wx.EVT_TEXT, partial(self.OnTxt_Ent, ctrl=choice_IP)) self.choice_IP = choice_IP diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 34f9a9a9f..7070488bf 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -60,11 +60,11 @@ def SaveState(self): 'tracker_to_robot': matrix_tracker_to_robot, } session = ses.Session() - session.SetState('robot', state) + session.SetConfig('robot', state) def LoadState(self): session = ses.Session() - state = session.GetState('robot') + state = session.GetConfig('robot') if state is None: return False From 4c365808a0eca79cf1398640a275499588281df8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 14:09:21 +0300 Subject: [PATCH 42/99] ADD: Robot saving to config --- invesalius/navigation/robot.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 7070488bf..429432416 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -45,6 +45,7 @@ def __init__(self, tracker): success = self.LoadState() if success: + self.ConnectToRobot() self.InitializeRobot() self.__bind_events() @@ -57,7 +58,8 @@ def SaveState(self): matrix_tracker_to_robot = self.matrix_tracker_to_robot.tolist() state = { - 'tracker_to_robot': matrix_tracker_to_robot, + 'robot_ip': self.robot_ip, + 'tracker_to_robot': matrix_tracker_to_robot } session = ses.Session() session.SetConfig('robot', state) @@ -69,7 +71,9 @@ def LoadState(self): if state is None: return False + self.robot_ip = state['robot_ip'] self.matrix_tracker_to_robot = np.array(state['tracker_to_robot']) + return True def ConfigureRobot(self): @@ -129,9 +133,13 @@ def AbortRobotConfiguration(self): def IsConnected(self): return self.robot_status + def ConnectToRobot(self): + Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) + def InitializeRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) + wx.MessageBox(_("Connected to Robot!"), _("InVesalius 3")) def DisconnectRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=False) From 70fb1d252a02f0a465775feaf7d6cb684c35659a Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 14:09:28 +0300 Subject: [PATCH 43/99] FIX: Robot connection --- invesalius/gui/preferences.py | 72 ++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index de7af968d..a17cc0544 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -45,8 +45,6 @@ def __init__( self.book = wx.Notebook(self, -1) #self.pnl_viewer2d = Viewer2D(self.book) self.pnl_viewer3d = Viewer3D(self.book) - self.pnl_tracker = TrackerPage(self.book, tracker, robot) - self.pnl_object = ObjectPage(self.book, tracker, pedal_connection, neuronavigation_api) # self.pnl_surface = SurfaceCreation(self) self.pnl_language = Language(self.book) @@ -55,6 +53,8 @@ def __init__( session = ses.Session() mode = session.GetConfig('mode') if mode == const.MODE_NAVIGATOR: + self.pnl_tracker = TrackerPage(self.book, tracker, robot) + self.pnl_object = ObjectPage(self.book, tracker, pedal_connection, neuronavigation_api) self.book.AddPage(self.pnl_tracker, _("Tracker")) self.book.AddPage(self.pnl_object, _("Stimulator")) self.book.AddPage(self.pnl_language, _("Language")) @@ -93,7 +93,10 @@ def LoadPreferences(self): surface_interpolation = session.GetConfig('surface_interpolation') language = session.GetConfig('language') slice_interpolation = session.GetConfig('slice_interpolation') - self.pnl_object.LoadState() + session = ses.Session() + mode = session.GetConfig('mode') + if mode == const.MODE_NAVIGATOR: + self.pnl_object.LoadState() values = { const.RENDERING: rendering, @@ -457,7 +460,9 @@ def __init__(self, parent, tracker, robot): self.tracker = tracker self.robot = robot - self.robot_IP = None + self.robot_ip = None + self.matrix_tracker_to_robot = None + self.state = self.LoadState() # ComboBox for spatial tracker device selection tracker_options = [_("Select")] + self.tracker.get_trackers() @@ -517,17 +522,36 @@ def __init__(self, parent, tracker, robot): btn_rob.Bind(wx.EVT_BUTTON, self.OnRobotConnect) self.btn_rob = btn_rob + status_text = wx.StaticText(self, -1, "Status") + if self.robot.IsConnected(): + status_text.SetLabelText("Robot is connected!") + else: + status_text.SetLabelText("Robot is not connected!") + self.status_text = status_text + btn_rob_con = wx.Button(self, -1, _("Register")) btn_rob_con.SetToolTip("Register robot tracking") btn_rob_con.Enable(1) btn_rob_con.Bind(wx.EVT_BUTTON, self.OnRobotCon) + if self.robot.IsConnected(): + if self.matrix_tracker_to_robot is None: + btn_rob_con.Show() + else: + btn_rob_con.SetLabel("Register Again") + btn_rob_con.Show() + else: + btn_rob_con.Hide() self.btn_rob_con = btn_rob_con + + rob_sizer = wx.FlexGridSizer(rows=2, cols=3, hgap=5, vgap=5) rob_sizer.AddMany([ (lbl_rob, 0, wx.LEFT), (choice_IP, 1, wx.EXPAND), (btn_rob, 0, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15), + (status_text), + (0, 0), (btn_rob_con, 0, wx.RIGHT) ]) @@ -544,6 +568,20 @@ def __init__(self, parent, tracker, robot): def __bind_events(self): Publisher.subscribe(self.ShowParent, "Show preferences dialog") + Publisher.subscribe(self.OnRobotStatus, "Robot connection status") + Publisher.subscribe(self.OnTransformationMatrix, "Load robot transformation matrix") + + def LoadState(self): + session = ses.Session() + state = session.GetConfig('robot') + + if state is None: + return False + + self.robot_ip = state['robot_ip'] + self.matrix_tracker_to_robot = np.array(state['tracker_to_robot']) + + return True def OnChooseTracker(self, evt, ctrl): self.HideParent() @@ -564,6 +602,9 @@ def OnChooseTracker(self, evt, ctrl): self.ShowParent() def OnChooseReferenceMode(self, evt, ctrl): + # Probably need to refactor object registration as a whole to use the + # OnChooseReferenceMode function which was used earlier. It can be found in + # the deprecated code in ObjectRegistrationPanel in task_navigator.py. pass def HideParent(self): # hide preferences dialog box @@ -580,12 +621,12 @@ def OnChoiceIP(self, evt, ctrl): def OnRobotConnect(self, evt): if self.robot_ip is not None: - self.configuration = { - 'tracker_id': self.tracker.GetTrackerId(), - 'robot_ip': self.robot_ip, - 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), - } - self.connection = self.tracker.tracker_connection + # self.configuration = { + # 'tracker_id': self.tracker.GetTrackerId(), + # 'robot_ip': self.robot_ip, + # 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), + # } + # self.connection = self.tracker.tracker_connection Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) def OnRobot(self, evt): @@ -599,6 +640,17 @@ def OnRobotCon(self, evt): self.HideParent() self.robot.RegisterRobot() self.ShowParent() + + def OnRobotStatus(self, data): + if data: + self.status_text.SetLabelText("Setup robot transformation matrix:") + self.btn_rob_con.Show() + + def OnTransformationMatrix(self, data): + if data: + self.status_text.SetLabelText("Robot is fully setup!") + self.btn_rob_con.SetLabel("Register Again") + self.btn_rob_con.Show() class Language(wx.Panel): def __init__(self, parent): From d8afe6e9c4f1a0d1623d0b478964970272803549 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 14:48:15 +0300 Subject: [PATCH 44/99] FIX: UI bug fix --- invesalius/gui/task_navigator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 2c0b02c00..0303f7cbf 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1267,6 +1267,7 @@ def OnObjectUpdate(self, data=None): self.config_txt.SetLabelText(os.path.basename(self.object_reg[-1])) self.lbl.Show() self.config_txt.Show() + self.next_button.Enable() def OnEditPreferences(self, evt): Publisher.sendMessage('Open preferences menu') From 2188c6fb77ad4f0960fc05b41520e72077fb4866 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 14:49:32 +0300 Subject: [PATCH 45/99] ADD: External setting of robot IP --- invesalius/navigation/robot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 429432416..e6f2389d3 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -133,6 +133,10 @@ def AbortRobotConfiguration(self): def IsConnected(self): return self.robot_status + def SetRobotIP(self, data): + if data is not None: + self.robot_ip = data + def ConnectToRobot(self): Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) From e82d227780b8800418f2e2dffa021263b64fa496 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 14:49:53 +0300 Subject: [PATCH 46/99] FIX: robot connection bug fix --- invesalius/gui/preferences.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index a17cc0544..763a2e3da 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -525,6 +525,8 @@ def __init__(self, parent, tracker, robot): status_text = wx.StaticText(self, -1, "Status") if self.robot.IsConnected(): status_text.SetLabelText("Robot is connected!") + if self.robot.matrix_tracker_to_robot is not None: + status_text.SetLabelText("Robot is fully setup!") else: status_text.SetLabelText("Robot is not connected!") self.status_text = status_text @@ -550,7 +552,7 @@ def __init__(self, parent, tracker, robot): (lbl_rob, 0, wx.LEFT), (choice_IP, 1, wx.EXPAND), (btn_rob, 0, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15), - (status_text), + (status_text, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15), (0, 0), (btn_rob_con, 0, wx.RIGHT) ]) @@ -627,6 +629,7 @@ def OnRobotConnect(self, evt): # 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), # } # self.connection = self.tracker.tracker_connection + self.robot.SetRobotIP(self.robot_ip) Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) def OnRobot(self, evt): @@ -651,6 +654,7 @@ def OnTransformationMatrix(self, data): self.status_text.SetLabelText("Robot is fully setup!") self.btn_rob_con.SetLabel("Register Again") self.btn_rob_con.Show() + self.btn_rob_con.Layout() class Language(wx.Panel): def __init__(self, parent): From d83cfa9ef6852a1f387d8d0331716c4dfcd4a2fc Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 14:56:59 +0300 Subject: [PATCH 47/99] FIX: Undesired error message --- invesalius/gui/dialogs.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 179edffab..2a0c57ede 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -5769,11 +5769,14 @@ def _init_gui(self): btn_load = wx.Button(self, -1, label=_('Load'), size=wx.Size(65, 23)) btn_load.Bind(wx.EVT_BUTTON, self.LoadRegistration) - self.btn_load = btn_load + if not self.robot.robot_status: btn_load.Enable(False) else: - self.UpdateRobotConnectionStatus(True) + self.btn_load.Enable(True) + if self.GetAcquiredPoints() >= 3: + self.btn_apply_reg.Enable(True) + self.btn_load = btn_load # Create a horizontal sizers From 806ca20d0b4e8fa0716b4cc20c3f51f4bf8076fc Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 15:06:25 +0300 Subject: [PATCH 48/99] ADD: busy cursor for tracker setup --- invesalius/navigation/tracker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index d3a6df18c..c38d91f02 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -90,6 +90,7 @@ def LoadState(self): ) def SetTracker(self, tracker_id, configuration=None): + Publisher.sendMessage('Begin busy cursor') if tracker_id: self.tracker_connection = tc.CreateTrackerConnection(tracker_id) @@ -128,6 +129,8 @@ def SetTracker(self, tracker_id, configuration=None): self.thread_coord.start() self.SaveState() + Publisher.sendMessage('End busy cursor') + def DisconnectTracker(self): if self.tracker_connected: From 8b9e1f71d25cfd013a5b96dc05384ce514eda455 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 15:06:38 +0300 Subject: [PATCH 49/99] FIX: Minor UI Bug Fix --- invesalius/gui/preferences.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 763a2e3da..ba32ebe75 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -601,6 +601,7 @@ def OnChooseTracker(self, evt, ctrl): self.tracker.SetTracker(choice) Publisher.sendMessage('Update status text in GUI', label=_("Ready")) Publisher.sendMessage("Tracker changed") + ctrl.SetSelection(self.tracker.tracker_id) self.ShowParent() def OnChooseReferenceMode(self, evt, ctrl): From 3419c6efad26c7bc9bd644e22690e7f21328422b Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 4 Aug 2023 16:39:53 +0300 Subject: [PATCH 50/99] ADD: Target Mode in icon bar --- invesalius/gui/task_navigator.py | 125 +++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 23 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 0303f7cbf..64d28830c 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1318,6 +1318,12 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.target_mode = False self.track_obj = False + self.navigation_status = False + + self.target_selected = False + self.show_coil_checked = False + + # Toggle button for neuronavigation tooltip = wx.ToolTip(_("Start navigation")) btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1)) @@ -1328,11 +1334,14 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle button for robot tooltip = wx.ToolTip(_("Stop robot")) - btn_robot = wx.ToggleButton(self, -1, _("Stop Robot"), size=wx.Size(80, -1)) + btn_robot = wx.Button(self, -1, _("Stop Robot"), size=wx.Size(80, -1)) btn_robot.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) btn_robot.SetToolTip(tooltip) + btn_robot.Bind(wx.EVT_BUTTON, partial(self.OnStopRobot, ctrl=btn_robot)) + if not self.robot.IsConnected(): + btn_robot.Hide() self.btn_robot = btn_robot - btn_robot.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStopRobot, ctrl=btn_robot)) + ''' # Label and Checkbox for Tractography tooltip = wx.ToolTip(_(u"Control Tractography")) @@ -1488,6 +1497,18 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect efield_checkbox.SetToolTip(tooltip) self.efield_checkbox = efield_checkbox + #Toggle Button for Target Mode + tooltip = wx.ToolTip(_(u"Control Target Mode")) + BMP_TARGET = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("track.png")), wx.BITMAP_TYPE_PNG) + target_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) + target_checkbox.SetBackgroundColour(GREY_COLOR) + target_checkbox.SetBitmap(BMP_TARGET) + target_checkbox.SetValue(False) + target_checkbox.Enable(False) + target_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTargetCheckbox, ctrl=target_checkbox)) + target_checkbox.SetToolTip(tooltip) + self.target_checkbox = target_checkbox + #Sizers button_sizer = wx.BoxSizer(wx.VERTICAL) button_sizer.AddMany([ @@ -1515,7 +1536,8 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect (checkobj), (checkcamera), (checkbox_serial_port), - (efield_checkbox) + (efield_checkbox), + (target_checkbox) ]) main_sizer = wx.BoxSizer(wx.VERTICAL) @@ -1539,6 +1561,8 @@ def __bind_events(self): Publisher.subscribe(self.UpdateTarget, 'Update target') Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + Publisher.subscribe(self.OnRobotStatus, "Robot connection status") + Publisher.subscribe(self.UpdateTractsVisualization, 'Update tracts visualization') # Externally check/uncheck and enable/disable checkboxes. @@ -1552,6 +1576,15 @@ def __bind_events(self): Publisher.subscribe(self.CheckTrackObjectCheckbox, 'Check track-object checkbox') Publisher.subscribe(self.EnableTrackObjectCheckbox, 'Enable track-object checkbox') + Publisher.subscribe(self.ShowTargetButton, 'Show target button') + Publisher.subscribe(self.HideTargetButton, 'Hide target button') + Publisher.subscribe(self.DisableTargetMode, 'Disable target mode') + + # Conditions for enabling target button: + Publisher.subscribe(self.ShowCoilChecked, 'Show-coil checked') + Publisher.subscribe(self.TargetSelected, 'Target selected') + + # State def SaveState(self): track_object = self.checkbox_track_object state = { @@ -1576,6 +1609,7 @@ def LoadState(self): self.EnableTrackObjectCheckbox(track_object['enabled']) self.CheckTrackObjectCheckbox(track_object['checked']) + # Toggle Button Helpers def UpdateToggleButton(self, ctrl, state=None): # Changes background colour based on current state of toggle button if state is not set, # otherwise, uses state to set value. @@ -1593,23 +1627,6 @@ def EnableToggleButton(self, ctrl, state): ctrl.Enable(state) ctrl.SetBackgroundColour(self.GREY_COLOR) - def OnCheckStatus(self, nav_status, vis_status): - if nav_status: - self.UpdateToggleButton(self.checkbox_serial_port) - self.EnableToggleButton(self.checkbox_serial_port, 0) - self.UpdateToggleButton(self.checkobj) - self.EnableToggleButton(self.checkobj, 0) - self.UpdateToggleButton(self.checkbox_track_object) - self.EnableToggleButton(self.checkbox_track_object, 0) - else: - self.EnableToggleButton(self.checkbox_serial_port, 1) - self.UpdateToggleButton(self.checkbox_serial_port) - self.EnableToggleButton(self.checkbox_track_object, 1) - self.UpdateToggleButton(self.checkbox_track_object) - if self.track_obj: - self.EnableToggleButton(self.checkobj, 1) - self.UpdateToggleButton(self.checkobj) - # Navigation def OnStartNavigation(self): if not self.tracker.AreTrackerFiducialsSet() or not self.image.AreImageFiducialsSet(): @@ -1631,7 +1648,7 @@ def OnNavigate(self, evt, btn_nav): nav_id = btn_nav.GetValue() if not nav_id: wx.CallAfter(Publisher.sendMessage, 'Stop navigation') - + Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) tooltip = wx.ToolTip(_("Start neuronavigation")) btn_nav.SetToolTip(tooltip) btn_nav.SetLabelText(_("Start neuronavigation")) @@ -1665,12 +1682,34 @@ def UpdateNavigationStatus(self, nav_status, vis_status): else: self.nav_status = True + def OnCheckStatus(self, nav_status, vis_status): + if nav_status: + self.UpdateToggleButton(self.checkbox_serial_port) + self.EnableToggleButton(self.checkbox_serial_port, 0) + self.UpdateToggleButton(self.checkobj) + self.EnableToggleButton(self.checkobj, 0) + self.UpdateToggleButton(self.checkbox_track_object) + self.EnableToggleButton(self.checkbox_track_object, 0) + else: + self.EnableToggleButton(self.checkbox_serial_port, 1) + self.UpdateToggleButton(self.checkbox_serial_port) + self.EnableToggleButton(self.checkbox_track_object, 1) + self.UpdateToggleButton(self.checkbox_track_object) + if self.track_obj: + self.EnableToggleButton(self.checkobj, 1) + self.UpdateToggleButton(self.checkobj) + # 'Robot' + def OnRobotStatus(self, data): + if data: + self.btn_robot.Show() + def OnStopRobot(self, evt, ctrl): - pass + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) - # 'Tracktography' + # 'Tractography' def OnTractographyCheckbox(self, evt, ctrl): self.view_tracts = ctrl.GetValue() self.UpdateToggleButton(ctrl) @@ -1764,6 +1803,46 @@ def OnEnableSerialPort(self, evt, ctrl): # 'E Field' def OnEfieldCheckbox(self, evt, ctrl): self.UpdateToggleButton(ctrl) + + # 'Target Button' + def ShowCoilChecked(self, checked): + self.show_coil_checked = checked + self.UpdateTargetButton() + + def TargetSelected(self, status): + self.target_selected = status + self.UpdateTargetButton() + + def ShowTargetButton(self): + self.target_checkbox.Show() + + def HideTargetButton(self): + self.target_checkbox.Hide() + + def DisableTargetMode(self): + self.OnTargetCheckbox(False) + self.UpdateToggleButton(self.target_checkbox, False) + + def UpdateTargetButton(self): + if self.target_selected and self.show_coil_checked: + self.EnableToggleButton(self.target_checkbox, True) + else: + self.DisableTargetMode() + self.EnableToggleButton(self.target_checkbox, False) + + def OnTargetCheckbox(self, evt): + if not self.target_checkbox.GetValue() and evt is not False: + self.UpdateToggleButton(self.target_checkbox, True) + Publisher.sendMessage('Target navigation mode', target_mode=self.target_checkbox.GetValue()) + Publisher.sendMessage('Check volume camera checkbox', checked=False) + Publisher.sendMessage('Enable volume camera checkbox', enabled=False) + + elif self.target_checkbox.GetValue() or evt is False: + self.UpdateToggleButton(self.target_checkbox, False) + Publisher.sendMessage('Target navigation mode', target_mode=self.target_checkbox.GetValue()) + Publisher.sendMessage('Enable volume camera checkbox', enabled=True) + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) class NeuronavigationPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): From 0d50a77e89172aabed3d19bc9f4fba79107c0619 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:48:00 +0300 Subject: [PATCH 51/99] CLP: clean tracker --- invesalius/navigation/tracker.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index c38d91f02..62e42d5d8 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -29,7 +29,8 @@ from invesalius.pubsub import pub as Publisher from invesalius.utils import Singleton - +# Only one tracker will be initialized per time. Therefore, we use +# Singleton design pattern for implementing it class Tracker(metaclass=Singleton): def __init__(self): self.tracker_connection = None @@ -47,9 +48,9 @@ def __init__(self): self.TrackerCoordinates = dco.TrackerCoordinates() - self.LoadState() + self.LoadConfig() - def SaveState(self): + def SaveConfig(self): tracker_id = self.tracker_id tracker_fiducials = self.tracker_fiducials.tolist() tracker_fiducials_raw = self.tracker_fiducials_raw.tolist() @@ -66,7 +67,7 @@ def SaveState(self): session = ses.Session() session.SetConfig('tracker', state) - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('tracker') @@ -128,7 +129,7 @@ def SetTracker(self, tracker_id, configuration=None): self.event_coord) self.thread_coord.start() - self.SaveState() + self.SaveConfig() Publisher.sendMessage('End busy cursor') @@ -208,14 +209,14 @@ def SetTrackerFiducial(self, ref_mode_id, fiducial_index): print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3])) - self.SaveState() + self.SaveConfig() def ResetTrackerFiducials(self): Publisher.sendMessage("Reset tracker fiducials") for m in range(3): self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan] - self.SaveState() + self.SaveConfig() def GetTrackerFiducials(self): return self.tracker_fiducials, self.tracker_fiducials_raw @@ -237,6 +238,13 @@ def GetMatrixTrackerFiducials(self): def GetTrackerId(self): return self.tracker_id + def get_trackers(self): + return const.TRACKERS + + +''' +Deprecated Code + def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre): if self.tracker_connected: selection_ctrl.SetSelection(self.tracker_id) @@ -253,5 +261,4 @@ def UpdateUI(self, selection_ctrl, numctrls_fiducial, txtctrl_fre): txtctrl_fre.SetValue('') txtctrl_fre.SetBackgroundColour('WHITE') - def get_trackers(self): - return const.TRACKERS +''' \ No newline at end of file From 84958ec6379459352b8d78c26d7fc5b16e749b3a Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:48:15 +0300 Subject: [PATCH 52/99] CLP: clean robot --- invesalius/navigation/robot.py | 76 ++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index e6f2389d3..72866913a 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -28,22 +28,20 @@ import invesalius.session as ses from invesalius.pubsub import pub as Publisher from invesalius.utils import Singleton +from invesalius.navigation.tracker import Tracker - -# XXX: First steps towards decoupling robot and tracker, which were previously -# tightly coupled; not fully finished, but whenever possible, robot-related -# functionality should be gathered here. - +# Only one robot will be initialized per time. Therefore, we use +# Singleton design pattern for implementing it class Robot(metaclass=Singleton): - def __init__(self, tracker): - self.tracker = tracker + def __init__(self): + self.tracker = Tracker() self.robot_status = None self.robot_ip = None self.matrix_tracker_to_robot = None self.robot_coregistration_dialog = None - success = self.LoadState() + success = self.LoadConfig() if success: self.ConnectToRobot() self.InitializeRobot() @@ -54,7 +52,7 @@ def __bind_events(self): Publisher.subscribe(self.AbortRobotConfiguration, 'Dialog robot destroy') Publisher.subscribe(self.OnRobotStatus, 'Robot connection status') - def SaveState(self): + def SaveConfig(self): matrix_tracker_to_robot = self.matrix_tracker_to_robot.tolist() state = { @@ -64,7 +62,7 @@ def SaveState(self): session = ses.Session() session.SetConfig('robot', state) - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('robot') @@ -75,30 +73,6 @@ def LoadState(self): self.matrix_tracker_to_robot = np.array(state['tracker_to_robot']) return True - - def ConfigureRobot(self): - if self.tracker.tracker_connection and self.tracker.tracker_connection.IsConnected(): - select_ip_dialog = dlg.SetRobotIP() - status = select_ip_dialog.ShowModal() - - if status == ID_OK: - robot_ip = select_ip_dialog.GetValue() - self.robot_ip = robot_ip - self.configuration = { - 'tracker_id': self.tracker.GetTrackerId(), - 'robot_ip': robot_ip, - 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), - } - self.connection = self.tracker.tracker_connection - Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) - select_ip_dialog.Destroy() - return True - else: - select_ip_dialog.Destroy() - return False - else: - wx.MessageBox(_("Select Tracker first"), _("InVesalius 3")) - return False def OnRobotStatus(self, data): if data: @@ -123,7 +97,7 @@ def RegisterRobot(self): return False self.matrix_tracker_to_robot = matrix_tracker_to_robot - self.SaveState() + self.SaveConfig() self.InitializeRobot() def AbortRobotConfiguration(self): @@ -139,11 +113,41 @@ def SetRobotIP(self, data): def ConnectToRobot(self): Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) + wx.MessageBox(_("Connected to Robot!"), _("InVesalius 3")) def InitializeRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) - wx.MessageBox(_("Connected to Robot!"), _("InVesalius 3")) + wx.MessageBox(_("Robot Initialized!"), _("InVesalius 3")) def DisconnectRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=False) + + +''' +Deprecated Code + + def ConfigureRobot(self): + if self.tracker.tracker_connection and self.tracker.tracker_connection.IsConnected(): + select_ip_dialog = dlg.SetRobotIP() + status = select_ip_dialog.ShowModal() + + if status == ID_OK: + robot_ip = select_ip_dialog.GetValue() + self.robot_ip = robot_ip + self.configuration = { + 'tracker_id': self.tracker.GetTrackerId(), + 'robot_ip': robot_ip, + 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), + } + self.connection = self.tracker.tracker_connection + Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) + select_ip_dialog.Destroy() + return True + else: + select_ip_dialog.Destroy() + return False + else: + wx.MessageBox(_("Select Tracker first"), _("InVesalius 3")) + return False +''' \ No newline at end of file From a6fc7707c73060c93b59a4455947b29a4dd2f78b Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:48:29 +0300 Subject: [PATCH 53/99] CLP: clean navigation --- invesalius/navigation/navigation.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invesalius/navigation/navigation.py b/invesalius/navigation/navigation.py index a90b52a0e..cddfb07fe 100644 --- a/invesalius/navigation/navigation.py +++ b/invesalius/navigation/navigation.py @@ -201,7 +201,7 @@ def __init__(self, pedal_connection, neuronavigation_api): self.lock_to_target = False self.coil_at_target = False - self.LoadState() + self.LoadConfig() self.__bind_events() @@ -211,7 +211,7 @@ def __bind_events(self): Publisher.subscribe(self.UpdateObjectRegistration, 'Update object registration') Publisher.subscribe(self.TrackObject, 'Track object') - def SaveState(self): + def SaveConfig(self): # XXX: This shouldn't be needed, but task_navigator.py currently calls UpdateObjectRegistration with # None parameter when the project is closed, crashing without this checks. if self.object_registration is None: @@ -229,7 +229,7 @@ def SaveState(self): session = ses.Session() session.SetConfig('navigation', state) - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('navigation') @@ -257,7 +257,7 @@ def UpdateSerialPort(self, serial_port_in_use, com_port=None, baud_rate=None): def UpdateObjectRegistration(self, data=None): self.object_registration = data - self.SaveState() + self.SaveConfig() def GetObjectRegistration(self): return self.object_registration From 54e2a5ee204b5486a1edd19bced3ab522518bec3 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:48:46 +0300 Subject: [PATCH 54/99] CLP: clean tractography --- invesalius/gui/task_tractography.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index e49832304..671eb1e80 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -460,9 +460,6 @@ def OnLinkFOD(self, event=None): dlgg.ShowModal() del self.tp wx.MessageBox(_("FOD Import successful"), _("InVesalius 3")) - Publisher.sendMessage('End busy cursor') - # except: - # wx.MessageBox(_("Unable to initialize Trekker, check FOD and config files."), _("InVesalius 3")) except: Publisher.sendMessage('Update status text in GUI', label=_("Trekker initialization failed.")) wx.MessageBox(_("Unable to load FOD."), _("InVesalius 3")) @@ -476,12 +473,6 @@ def UpdateDialog(self): break wx.Yield() - def _on_callback_error(self, e, dialog=None): - import invesalius.utils as utl - dialog.running = False - msg = utl.log_traceback(e) - dialog.error = msg - def TrekkerCallback(self, trekker): self.tp.running = False print("Import Complete") From 4ea834de1efba7661f9259bbda996de31b04d96c Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:49:10 +0300 Subject: [PATCH 55/99] CLP: clean navigator task --- invesalius/gui/task_navigator.py | 3102 ++++++++++++++---------------- 1 file changed, 1455 insertions(+), 1647 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 64d28830c..7d3c9573d 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -91,7 +91,7 @@ BTN_NEW = wx.NewId() BTN_IMPORT_LOCAL = wx.NewId() -def getBitMapForBackground(): +def GetBitMapForBackground(): image_file = os.path.join('head.png') bmp = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath(image_file)), wx.BITMAP_TYPE_PNG) return bmp @@ -171,13 +171,12 @@ def __init__(self, parent): gbs = wx.GridBagSizer(5,5) gbs.AddGrowableCol(0, 1) self.gbs = gbs + # Initialize Navigation, Tracker, Robot, Image, and PedalConnection objects here to make them # available to several panels. - # + tracker = Tracker() - robot = Robot( - tracker=tracker - ) + robot = Robot() image = Image() pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None icp = IterativeClosestPoint() @@ -186,48 +185,11 @@ def __init__(self, parent): pedal_connection=pedal_connection, neuronavigation_api=neuronavigation_api, ) - self.tracker = tracker self.robot = robot self.image = image self.navigation = navigation - # TODO: Initialize checkboxes before panels: they are updated by ObjectRegistrationPanel when loading its state. - # A better solution would be to have these checkboxes save their own state, independent of the panels, but that's - # not implemented yet. - ''' - # Checkbox for camera update in volume rendering during navigation - tooltip = wx.ToolTip(_("Update camera in volume")) - checkcamera = wx.CheckBox(self, -1, _('Vol. camera')) - checkcamera.SetToolTip(tooltip) - checkcamera.SetValue(const.CAM_MODE) - checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCameraCheckbox) - self.checkcamera = checkcamera - - # Checkbox to use serial port to trigger pulse signal and create markers - tooltip = wx.ToolTip(_("Enable serial port communication to trigger pulse and create markers")) - checkbox_serial_port = wx.CheckBox(self, -1, _('Serial port')) - checkbox_serial_port.SetToolTip(tooltip) - checkbox_serial_port.SetValue(False) - checkbox_serial_port.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableSerialPort, ctrl=checkbox_serial_port)) - self.checkbox_serial_port = checkbox_serial_port - - # Checkbox for object position and orientation update in volume rendering during navigation - tooltip = wx.ToolTip(_("Show and track TMS coil")) - checkobj = wx.CheckBox(self, -1, _('Show coil')) - checkobj.SetToolTip(tooltip) - checkobj.SetValue(False) - checkobj.Disable() - checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowCoil) - self.checkobj = checkobj - - - # if sys.platform != 'win32': - self.checkcamera.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) - checkbox_serial_port.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) - checkobj.SetWindowVariant(wx.WINDOW_VARIANT_SMALL) - ''' - # Fold panel style style = fpb.CaptionBarStyle() style.SetCaptionStyle(fpb.CAPTIONBAR_GRADIENT_V) @@ -268,101 +230,15 @@ def __init__(self, parent): fold_panel.ApplyCaptionStyle(item, style) fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, leftSpacing=0, rightSpacing=0) - ''' - # Fold 1 - Navigation panel - item = fold_panel.AddFoldPanel(_("Neuronavigation"), collapsed=True) - ntw = NeuronavigationPanel( - parent=item, - navigation=navigation, - tracker=tracker, - robot=robot, - icp=icp, - image=image, - pedal_connection=pedal_connection, - neuronavigation_api=neuronavigation_api, - ) - - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, ntw, spacing=0, - leftSpacing=0, rightSpacing=0) - fold_panel.Expand(fold_panel.GetFoldPanel(0)) - - # Fold 2 - Object registration panel - item = fold_panel.AddFoldPanel(_("Object registration"), collapsed=True) - otw = ObjectRegistrationPanel( - parent=item, - tracker=tracker, - pedal_connection=pedal_connection, - neuronavigation_api=neuronavigation_api, - ) - - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, otw, spacing=0, - leftSpacing=0, rightSpacing=0) - - # Fold 3 - Markers panel - item = fold_panel.AddFoldPanel(_("Markers"), collapsed=True) - mtw = MarkersPanel(item, navigation, tracker, icp) - - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, mtw, spacing= 0, - leftSpacing=0, rightSpacing=0) - - # Fold 4 - Tractography panel - if has_trekker: - item = fold_panel.AddFoldPanel(_("Tractography"), collapsed=True) - otw = TractographyPanel(item) - - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, otw, spacing=0, - leftSpacing=0, rightSpacing=0) - - # Fold 5 - DBS - self.dbs_item = fold_panel.AddFoldPanel(_("Deep Brain Stimulation"), collapsed=True) - dtw = DbsPanel(self.dbs_item) #Atribuir nova var, criar panel - - fold_panel.ApplyCaptionStyle(self.dbs_item, style) - fold_panel.AddFoldPanelWindow(self.dbs_item, dtw, spacing= 0, - leftSpacing=0, rightSpacing=0) - self.dbs_item.Hide() - - # Fold 6 - Sessions - item = fold_panel.AddFoldPanel(_("Sessions"), collapsed=False) - stw = SessionPanel(item) - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, stw, spacing= 0, - leftSpacing=0, rightSpacing=0) - - # Fold 7 - E-field - - item = fold_panel.AddFoldPanel(_("E-field"), collapsed=True) - etw = E_fieldPanel(item, navigation) - fold_panel.ApplyCaptionStyle(item, style) - fold_panel.AddFoldPanelWindow(item, etw, spacing=0, - leftSpacing=0, rightSpacing=0) -''' self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) - - ''' - # Panel sizer for checkboxes - line_sizer = wx.BoxSizer(wx.HORIZONTAL) - line_sizer.Add(checkcamera, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) - line_sizer.Add(checkbox_serial_port, 0, wx.ALIGN_CENTER) - line_sizer.Add(checkobj, 0, wx.RIGHT | wx.LEFT, 5) - line_sizer.Fit(self) - ''' sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(gbs, 1, wx.GROW|wx.EXPAND) self.SetSizer(sizer) - # Panel sizer to expand fold panel - #sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Fit(self) self.track_obj = False gbs.Add(fold_panel, (0, 0), flag=wx.EXPAND) - #gbs.Add(line_sizer, (1, 0), flag=wx.EXPAND) gbs.Layout() sizer.Fit(self) self.Fit() @@ -370,18 +246,8 @@ def __init__(self, parent): self.__bind_events() def __bind_events(self): - #Publisher.subscribe(self.OnCheckStatus, 'Navigation status') Publisher.subscribe(self.OnShowDbs, "Show dbs folder") Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") - - ''' - # Externally check/uncheck and enable/disable checkboxes. - Publisher.subscribe(self.CheckShowCoil, 'Check show-coil checkbox') - Publisher.subscribe(self.CheckVolumeCameraCheckbox, 'Check volume camera checkbox') - - Publisher.subscribe(self.EnableShowCoil, 'Enable show-coil checkbox') - Publisher.subscribe(self.EnableVolumeCameraCheckbox, 'Enable volume camera checkbox') - ''' Publisher.subscribe(self.OpenNavigation, 'Open navigation menu') def __calc_best_size(self, panel): @@ -492,6 +358,7 @@ def CheckRegistration(self): def OpenNavigation(self): self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) + class CoregistrationPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): wx.Panel.__init__(self, parent) @@ -599,7 +466,7 @@ def __init__(self, parent, image): self.numctrls_fiducial = [[], [], []] self.current_coord = 0, 0, 0, None, None, None - self.bg_bmp = getBitMapForBackground() + self.bg_bmp = GetBitMapForBackground() # Toggle buttons for image fiducials background = wx.StaticBitmap(self, -1, self.bg_bmp, (0, 0)) for n, fiducial in enumerate(const.IMAGE_FIDUCIALS): @@ -773,7 +640,7 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi break - self.bg_bmp = getBitMapForBackground() + self.bg_bmp = GetBitMapForBackground() RED_COLOR = (255, 82, 82) self.RED_COLOR = RED_COLOR GREEN_COLOR = (118, 255, 3) @@ -1204,6 +1071,7 @@ def OnRefine(self, evt): if self.icp.use_icp: self.UpdateUI() + class StimulatorPage(wx.Panel): def __init__(self, parent, navigation): @@ -1314,6 +1182,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.image = image self.pedal_connection = pedal_connection self.neuronavigation_api = neuronavigation_api + self.nav_status = False self.target_mode = False self.track_obj = False @@ -1326,7 +1195,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle button for neuronavigation tooltip = wx.ToolTip(_("Start navigation")) - btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1)) + btn_nav = wx.ToggleButton(self, -1, _("Start neuronavigation"), size=wx.Size(80, -1)) btn_nav.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) btn_nav.SetToolTip(tooltip) self.btn_nav = btn_nav @@ -1341,67 +1210,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect if not self.robot.IsConnected(): btn_robot.Hide() self.btn_robot = btn_robot - - ''' - # Label and Checkbox for Tractography - tooltip = wx.ToolTip(_(u"Control Tractography")) - tractography_checkbox = wx.CheckBox(self, -1, _('Enable / Disable Tractography ')) - tractography_checkbox.SetValue(False) - tractography_checkbox.Enable(False) - tractography_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnTractographyCheckbox, ctrl=tractography_checkbox)) - tractography_checkbox.SetToolTip(tooltip) - self.tractography_checkbox = tractography_checkbox - - # Check box to track object or simply the stylus - checkbox_track_object = wx.CheckBox(self, -1, _('Track object')) - checkbox_track_object.SetValue(False) - checkbox_track_object.Enable(0) - checkbox_track_object.Bind(wx.EVT_CHECKBOX, partial(self.OnTrackObjectCheckbox, ctrl=checkbox_track_object)) - self.checkbox_track_object = checkbox_track_object - # Label and Checkbox for Lock to Target - tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) - lock_to_target_checkbox = wx.CheckBox(self, -1, _('Lock to target')) - lock_to_target_checkbox.SetValue(False) - lock_to_target_checkbox.Enable(False) - lock_to_target_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnLockToTargetCheckbox, ctrl=lock_to_target_checkbox)) - lock_to_target_checkbox.SetToolTip(tooltip) - self.lock_to_target_checkbox = lock_to_target_checkbox - - # Checkbox for object position and orientation update in volume rendering during navigation - tooltip = wx.ToolTip(_("Show and track TMS coil")) - checkobj = wx.CheckBox(self, -1, _('Show coil')) - checkobj.SetToolTip(tooltip) - checkobj.SetValue(False) - checkobj.Disable() - checkobj.Bind(wx.EVT_CHECKBOX, self.OnShowCoil) - self.checkobj = checkobj - - # Checkbox for camera update in volume rendering during navigation - tooltip = wx.ToolTip(_("Update camera in volume")) - checkcamera = wx.CheckBox(self, -1, _('Vol. camera')) - checkcamera.SetToolTip(tooltip) - checkcamera.SetValue(const.CAM_MODE) - checkcamera.Bind(wx.EVT_CHECKBOX, self.OnVolumeCameraCheckbox) - self.checkcamera = checkcamera - - # Checkbox to use serial port to trigger pulse signal and create markers - tooltip = wx.ToolTip(_("Enable serial port communication to trigger pulse and create markers")) - checkbox_serial_port = wx.CheckBox(self, -1, _('Serial port')) - checkbox_serial_port.SetToolTip(tooltip) - checkbox_serial_port.SetValue(False) - checkbox_serial_port.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableSerialPort, ctrl=checkbox_serial_port)) - self.checkbox_serial_port = checkbox_serial_port - - #Checkbox for Force Sensor - tooltip = wx.ToolTip(_(u"Control Force Sensor")) - force_checkbox = wx.CheckBox(self, -1, _('Enable / Disable Force Sensor ')) - force_checkbox.SetValue(False) - force_checkbox.Enable(False) - force_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnForceSensorCheckbox, ctrl=force_checkbox)) - force_checkbox.SetToolTip(tooltip) - self.force_checkbox = force_checkbox - ''' # Constants for bitmap parent toggle button ICON_SIZE = (48, 48) @@ -1516,18 +1325,6 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect (btn_robot, 0, wx.EXPAND | wx.GROW) ]) - ''' - checkbox_sizer = wx.BoxSizer(wx.VERTICAL) - checkbox_sizer.AddMany([ - (tractography_checkbox), - (checkbox_track_object), - (lock_to_target_checkbox), - (checkobj), - (checkcamera), - (checkbox_serial_port), - (efield_checkbox) - ]) - ''' checkbox_sizer = wx.FlexGridSizer(4, 5, 5) checkbox_sizer.AddMany([ (tractography_checkbox), @@ -1551,7 +1348,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.__bind_events() self.Update() - self.LoadState() + self.LoadConfig() def __bind_events(self): @@ -1584,8 +1381,8 @@ def __bind_events(self): Publisher.subscribe(self.ShowCoilChecked, 'Show-coil checked') Publisher.subscribe(self.TargetSelected, 'Target selected') - # State - def SaveState(self): + # Config + def SaveConfig(self): track_object = self.checkbox_track_object state = { 'track_object': { @@ -1597,7 +1394,7 @@ def SaveState(self): session = ses.Session() session.SetConfig('object_registration_panel', state) - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('object_registration_panel') @@ -1743,7 +1540,7 @@ def OnTrackObjectCheckbox(self, evt=None, ctrl=None): # Also, automatically check or uncheck 'Show coil' checkbox. Publisher.sendMessage('Check show-coil checkbox', checked=checked) - self.SaveState() + self.SaveConfig() # 'Lock to Target' checkbox @@ -1800,10 +1597,12 @@ def OnEnableSerialPort(self, evt, ctrl): else: Publisher.sendMessage('Update serial port', serial_port_in_use=False) + # 'E Field' def OnEfieldCheckbox(self, evt, ctrl): self.UpdateToggleButton(ctrl) + # 'Target Button' def ShowCoilChecked(self, checked): self.show_coil_checked = checked @@ -1844,1783 +1643,1790 @@ def OnTargetCheckbox(self, evt): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) -class NeuronavigationPanel(wx.Panel): - def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): - wx.Panel.__init__(self, parent) - try: - default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) - except AttributeError: - default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) - self.SetBackgroundColour(default_colour) +class MarkersPanel(wx.Panel): + @dataclasses.dataclass + class Marker: + """Class for storing markers. @dataclass decorator simplifies + setting default values, serialization, etc.""" + x : float = 0 + y : float = 0 + z : float = 0 + alpha : float = dataclasses.field(default = None) + beta : float = dataclasses.field(default = None) + gamma : float = dataclasses.field(default = None) + r : float = 0 + g : float = 1 + b : float = 0 + size : float = 2 + label : str = '*' + x_seed : float = 0 + y_seed : float = 0 + z_seed : float = 0 + is_target : bool = False + session_id : int = 1 + is_brain_target : bool = False - self.SetAutoLayout(1) + # x, y, z can be jointly accessed as position + @property + def position(self): + return list((self.x, self.y, self.z)) - self.__bind_events() + @position.setter + def position(self, new_position): + self.x, self.y, self.z = new_position - # Initialize global variables - self.pedal_connection = pedal_connection - self.neuronavigation_api = neuronavigation_api + # alpha, beta, gamma can be jointly accessed as orientation + @property + def orientation(self): + return list((self.alpha, self.beta, self.gamma)) - self.navigation = navigation - self.icp = icp - self.tracker = tracker - self.robot = robot - self.image = image + @orientation.setter + def orientation(self, new_orientation): + self.alpha, self.beta, self.gamma = new_orientation - self.nav_status = False - self.tracker_fiducial_being_set = None - self.current_coord = 0, 0, 0, None, None, None + # alpha, beta, gamma can be jointly accessed as orientation + @property + def coordinate(self): + return list((self.x, self.y, self.z, self.alpha, self.beta, self.gamma)) - # Initialize list of buttons and numctrls for wx objects - self.btns_set_fiducial = [None, None, None, None, None, None] - self.numctrls_fiducial = [[], [], [], [], [], []] + # r, g, b can be jointly accessed as colour + @property + def colour(self): + return list((self.r, self.g, self.b),) - # ComboBox for spatial tracker device selection - tracker_options = [_("Select tracker:")] + self.tracker.get_trackers() - select_tracker_elem = wx.ComboBox(self, -1, "", size=(145, -1), - choices=tracker_options, style=wx.CB_DROPDOWN|wx.CB_READONLY) + @colour.setter + def colour(self, new_colour): + self.r, self.g, self.b = new_colour - tooltip = wx.ToolTip(_("Choose the tracking device")) - select_tracker_elem.SetToolTip(tooltip) + # x_seed, y_seed, z_seed can be jointly accessed as seed + @property + def seed(self): + return list((self.x_seed, self.y_seed, self.z_seed),) - select_tracker_elem.SetSelection(self.tracker.tracker_id) - select_tracker_elem.Bind(wx.EVT_COMBOBOX, partial(self.OnChooseTracker, ctrl=select_tracker_elem)) - self.select_tracker_elem = select_tracker_elem + @seed.setter + def seed(self, new_seed): + self.x_seed, self.y_seed, self.z_seed = new_seed - # ComboBox for tracker reference mode - tooltip = wx.ToolTip(_("Choose the navigation reference mode")) - choice_ref = wx.ComboBox(self, -1, "", - choices=const.REF_MODE, style=wx.CB_DROPDOWN|wx.CB_READONLY) - choice_ref.SetSelection(const.DEFAULT_REF_MODE) - choice_ref.SetToolTip(tooltip) - choice_ref.Bind(wx.EVT_COMBOBOX, partial(self.OnChooseReferenceMode, ctrl=select_tracker_elem)) - self.choice_ref = choice_ref + @classmethod + def to_string_headers(cls): + """Return the string containing tab-separated list of field names (headers).""" + res = [field.name for field in dataclasses.fields(cls)] + res.extend(['x_world', 'y_world', 'z_world', 'alpha_world', 'beta_world', 'gamma_world']) + return '\t'.join(map(lambda x: '\"%s\"' % x, res)) - # Toggle buttons for image fiducials - for n, fiducial in enumerate(const.IMAGE_FIDUCIALS): - button_id = fiducial['button_id'] - label = fiducial['label'] - tip = fiducial['tip'] + def to_string(self): + """Serialize to excel-friendly tab-separated string""" + res = '' + for field in dataclasses.fields(self.__class__): + if field.type is str: + res += ('\"%s\"\t' % getattr(self, field.name)) + else: + res += ('%s\t' % str(getattr(self, field.name))) - ctrl = wx.ToggleButton(self, button_id, label=label) - ctrl.SetMinSize((gui_utils.calc_width_needed(ctrl, 3), -1)) - ctrl.SetToolTip(wx.ToolTip(tip)) - ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnImageFiducials, n)) - ctrl.SetValue(self.image.IsImageFiducialSet(n)) + if self.alpha is not None and self.beta is not None and self.gamma is not None: + # Add world coordinates (in addition to the internal ones). + position_world, orientation_world = imagedata_utils.convert_invesalius_to_world( + position=[self.x, self.y, self.z], + orientation=[self.alpha, self.beta, self.gamma], + ) - self.btns_set_fiducial[n] = ctrl + else: + position_world, orientation_world = imagedata_utils.convert_invesalius_to_world( + position=[self.x, self.y, self.z], + orientation=[0,0,0], + ) - # Push buttons for tracker fiducials - for n, fiducial in enumerate(const.TRACKER_FIDUCIALS): - button_id = fiducial['button_id'] - label = fiducial['label'] - tip = fiducial['tip'] + res += '\t'.join(map(lambda x: 'N/A' if x is None else str(x), (*position_world, *orientation_world))) + return res - ctrl = wx.ToggleButton(self, button_id, label=label) - ctrl.SetMinSize((gui_utils.calc_width_needed(ctrl, 3), -1)) - ctrl.SetToolTip(wx.ToolTip(tip)) - ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, n, ctrl=ctrl)) + def from_string(self, inp_str): + """Deserialize from a tab-separated string. If the string is not + properly formatted, might throw an exception and leave the object + in an inconsistent state.""" + for field, str_val in zip(dataclasses.fields(self.__class__), inp_str.split('\t')): + if field.type is float and str_val != 'None': + setattr(self, field.name, float(str_val)) + if field.type is float and str_val == 'None': + setattr(self, field.name, None) + if field.type is float and str_val != 'None': + setattr(self, field.name, float(str_val)) + if field.type is str: + setattr(self, field.name, str_val[1:-1]) # remove the quotation marks + if field.type is bool: + setattr(self, field.name, str_val=='True') - self.btns_set_fiducial[n + 3] = ctrl + def to_dict(self): + return { + 'position': self.position, + 'orientation': self.orientation, + 'colour': self.colour, + 'size': self.size, + 'label': self.label, + 'is_target': self.is_target, + 'seed': self.seed, + 'session_id': self.session_id, + } - # TODO: Find a better alignment between FRE, text and navigate button - # Fiducial registration error text and checkbox - txt_fre = wx.StaticText(self, -1, _('FRE:')) - tooltip = wx.ToolTip(_("Fiducial registration error")) + def __init__(self, parent, navigation, tracker, robot, icp): + wx.Panel.__init__(self, parent) + try: + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.SetBackgroundColour(default_colour) - # XXX: Currently always shows ICP corrected FRE (fiducial registration error) initially - # in the FRE textbox. This is a compromise, done due to corrected and non-corrected FRE values - # being split between Navigation and IterativeClosestPoint classes, and hence it being - # difficult to access both at this stage. This could be improved, e.g., by creating - # a separate class, which would hold both FRE values and would also know whether ICP - # corrected or non-corrected value is being used. - # - value = self.icp.GetFreForUI() + self.SetAutoLayout(1) - txtctrl_fre = wx.TextCtrl(self, value=value, size=wx.Size(60, -1), style=wx.TE_CENTRE) - txtctrl_fre.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) - txtctrl_fre.SetBackgroundColour('WHITE') - txtctrl_fre.SetEditable(0) - txtctrl_fre.SetToolTip(tooltip) - self.txtctrl_fre = txtctrl_fre + self.navigation = navigation + self.tracker = tracker + self.robot = robot + self.icp = icp + if has_mTMS: + self.mTMS = mTMS() + else: + self.mTMS = None - # Toggle button for neuronavigation - tooltip = wx.ToolTip(_("Start navigation")) - btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1)) - btn_nav.SetToolTip(tooltip) - btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn_nav=btn_nav)) + self.__bind_events() - # "Refine" text and checkbox - txt_icp = wx.StaticText(self, -1, _('Refine:')) - tooltip = wx.ToolTip(_(u"Refine the coregistration")) - checkbox_icp = wx.CheckBox(self, -1, _(' ')) - checkbox_icp.SetValue(self.icp.use_icp) - checkbox_icp.Enable(False) - checkbox_icp.Bind(wx.EVT_CHECKBOX, partial(self.OnCheckboxICP, ctrl=checkbox_icp)) - checkbox_icp.SetToolTip(tooltip) - self.checkbox_icp = checkbox_icp + self.session = ses.Session() - # "Pedal pressed" text and an indicator (checkbox) for pedal press - if (pedal_connection is not None and pedal_connection.in_use) or neuronavigation_api is not None: - txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) - tooltip = wx.ToolTip(_(u"Is the pedal pressed")) - checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) - checkbox_pedal_pressed.SetValue(False) - checkbox_pedal_pressed.Enable(False) - checkbox_pedal_pressed.SetToolTip(tooltip) + self.current_position = [0, 0, 0] + self.current_orientation = [None, None, None] + self.current_seed = 0, 0, 0 - if pedal_connection is not None: - pedal_connection.add_callback(name='gui', callback=checkbox_pedal_pressed.SetValue) + self.markers = [] + self.nav_status = False + self.target_mode = False - if neuronavigation_api is not None: - neuronavigation_api.add_pedal_callback(name='gui', callback=checkbox_pedal_pressed.SetValue) + self.marker_colour = const.MARKER_COLOUR + self.marker_size = const.MARKER_SIZE + self.arrow_marker_size = const.ARROW_MARKER_SIZE + self.current_session = 1 - self.checkbox_pedal_pressed = checkbox_pedal_pressed - else: - txt_pedal_pressed = None - self.checkbox_pedal_pressed = None + self.brain_actor = None + # Change session + spin_session = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) + spin_session.SetRange(1, 99) + spin_session.SetValue(self.current_session) + spin_session.SetToolTip("Set session") + spin_session.Bind(wx.EVT_TEXT, partial(self.OnSessionChanged, ctrl=spin_session)) + spin_session.Bind(wx.EVT_SPINCTRL, partial(self.OnSessionChanged, ctrl=spin_session)) - # "Lock to target" text and checkbox - tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) - lock_to_target_text = wx.StaticText(self, -1, _('Lock to target:')) - lock_to_target_checkbox = wx.CheckBox(self, -1, _(' ')) - lock_to_target_checkbox.SetValue(False) - lock_to_target_checkbox.Enable(False) - lock_to_target_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnLockToTargetCheckbox, ctrl=lock_to_target_checkbox)) - lock_to_target_checkbox.SetToolTip(tooltip) + # Marker colour select + select_colour = csel.ColourSelect(self, -1, colour=[255*s for s in self.marker_colour], size=wx.Size(20, 23)) + select_colour.SetToolTip("Set colour") + select_colour.Bind(csel.EVT_COLOURSELECT, partial(self.OnSelectColour, ctrl=select_colour)) - self.lock_to_target_checkbox = lock_to_target_checkbox + btn_create = wx.Button(self, -1, label=_('Create marker'), size=wx.Size(135, 23)) + btn_create.Bind(wx.EVT_BUTTON, self.OnCreateMarker) - # Image and tracker coordinates number controls - for m in range(len(self.btns_set_fiducial)): - for n in range(3): - if m <= 2: - value = self.image.GetImageFiducialForUI(m, n) - else: - value = self.tracker.GetTrackerFiducialForUI(m - 3, n) + sizer_create = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) + sizer_create.AddMany([(spin_session, 1), + (select_colour, 0), + (btn_create, 0)]) - self.numctrls_fiducial[m].append( - wx.lib.masked.numctrl.NumCtrl(parent=self, integerWidth=4, fractionWidth=1, value=value)) + # Buttons to save and load markers and to change its visibility as well + btn_save = wx.Button(self, -1, label=_('Save'), size=wx.Size(65, 23)) + btn_save.Bind(wx.EVT_BUTTON, self.OnSaveMarkers) - # Sizers to group all GUI objects - choice_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) - choice_sizer.AddMany([(select_tracker_elem, wx.LEFT), - (choice_ref, wx.RIGHT)]) + btn_load = wx.Button(self, -1, label=_('Load'), size=wx.Size(65, 23)) + btn_load.Bind(wx.EVT_BUTTON, self.OnLoadMarkers) - coord_sizer = wx.GridBagSizer(hgap=5, vgap=5) + btn_visibility = wx.ToggleButton(self, -1, _("Hide"), size=wx.Size(65, 23)) + btn_visibility.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnMarkersVisibility, ctrl=btn_visibility)) - for m in range(len(self.btns_set_fiducial)): - coord_sizer.Add(self.btns_set_fiducial[m], pos=wx.GBPosition(m, 0)) - for n in range(3): - coord_sizer.Add(self.numctrls_fiducial[m][n], pos=wx.GBPosition(m, n+1)) - if m in range(1, 6): - self.numctrls_fiducial[m][n].SetEditable(False) + sizer_btns = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) + sizer_btns.AddMany([(btn_save, 1, wx.RIGHT), + (btn_load, 0, wx.LEFT | wx.RIGHT), + (btn_visibility, 0, wx.LEFT)]) - nav_sizer = wx.FlexGridSizer(rows=1, cols=5, hgap=5, vgap=5) - nav_sizer.AddMany([(txt_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), - (txtctrl_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), - (btn_nav, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), - (txt_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), - (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) + # Buttons to delete or remove markers + btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23)) + btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteMultipleMarkers) - checkboxes_sizer = wx.FlexGridSizer(rows=1, cols=4, hgap=5, vgap=5) - checkboxes_sizer.AddMany([(lock_to_target_text, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), - (lock_to_target_checkbox, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) + btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) + btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) - if (pedal_connection is not None and pedal_connection.in_use) or neuronavigation_api is not None: - checkboxes_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), - (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) + sizer_delete = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) + sizer_delete.AddMany([(btn_delete_single, 1, wx.RIGHT), + (btn_delete_all, 0, wx.LEFT)]) - group_sizer = wx.FlexGridSizer(rows=10, cols=1, hgap=5, vgap=5) - group_sizer.AddGrowableCol(0, 1) - group_sizer.AddGrowableRow(0, 1) - group_sizer.AddGrowableRow(1, 1) - group_sizer.AddGrowableRow(2, 1) - group_sizer.SetFlexibleDirection(wx.BOTH) - group_sizer.AddMany([(choice_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), - (coord_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), - (nav_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), - (checkboxes_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL)]) + # List of markers + marker_list_ctrl = wx.ListCtrl(self, -1, style=wx.LC_REPORT, size=wx.Size(0,120)) + marker_list_ctrl.InsertColumn(const.ID_COLUMN, '#') + marker_list_ctrl.SetColumnWidth(const.ID_COLUMN, 28) - main_sizer = wx.BoxSizer(wx.HORIZONTAL) - main_sizer.Add(group_sizer, 1)# wx.ALIGN_CENTER_HORIZONTAL, 10) - self.sizer = main_sizer - self.SetSizer(main_sizer) - self.Fit() + marker_list_ctrl.InsertColumn(const.SESSION_COLUMN, 'Session') + marker_list_ctrl.SetColumnWidth(const.SESSION_COLUMN, 52) - def __bind_events(self): - Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials') - Publisher.subscribe(self.SetImageFiducial, 'Set image fiducial') - Publisher.subscribe(self.SetTrackerFiducial, 'Set tracker fiducial') - Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point') - Publisher.subscribe(self.DisconnectTracker, 'Disconnect tracker') - Publisher.subscribe(self.OnCloseProject, 'Close project data') - Publisher.subscribe(self.UpdateTrekkerObject, 'Update Trekker object') - Publisher.subscribe(self.UpdateNumTracts, 'Update number of tracts') - Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset') - Publisher.subscribe(self.UpdateSeedRadius, 'Update seed radius') - Publisher.subscribe(self.UpdateSleep, 'Update sleep') - Publisher.subscribe(self.UpdateNumberThreads, 'Update number of threads') - Publisher.subscribe(self.UpdateTractsVisualization, 'Update tracts visualization') - Publisher.subscribe(self.UpdatePeelVisualization, 'Update peel visualization') - Publisher.subscribe(self.UpdateEfieldVisualization, 'Update e-field visualization') - Publisher.subscribe(self.EnableACT, 'Enable ACT') - Publisher.subscribe(self.UpdateACTData, 'Update ACT data') - Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') - Publisher.subscribe(self.UpdateTarget, 'Update target') - #Publisher.subscribe(self.OnStartNavigation, 'Start navigation') - Publisher.subscribe(self.OnStopNavigation, 'Stop navigation') + marker_list_ctrl.InsertColumn(const.LABEL_COLUMN, 'Label') + marker_list_ctrl.SetColumnWidth(const.LABEL_COLUMN, 118) - def LoadImageFiducials(self, label, position): - fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', label) + marker_list_ctrl.InsertColumn(const.TARGET_COLUMN, 'Target') + marker_list_ctrl.SetColumnWidth(const.TARGET_COLUMN, 45) - fiducial_index = fiducial['fiducial_index'] - fiducial_name = fiducial['fiducial_name'] + if self.session.GetConfig('debug'): + marker_list_ctrl.InsertColumn(const.X_COLUMN, 'X') + marker_list_ctrl.SetColumnWidth(const.X_COLUMN, 45) - if self.btns_set_fiducial[fiducial_index].GetValue(): - print("Fiducial {} already set, not resetting".format(label)) - return + marker_list_ctrl.InsertColumn(const.Y_COLUMN, 'Y') + marker_list_ctrl.SetColumnWidth(const.Y_COLUMN, 45) - Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=position) + marker_list_ctrl.InsertColumn(const.Z_COLUMN, 'Z') + marker_list_ctrl.SetColumnWidth(const.Z_COLUMN, 45) - self.btns_set_fiducial[fiducial_index].SetValue(True) - for m in [0, 1, 2]: - self.numctrls_fiducial[fiducial_index][m].SetValue(position[m]) + marker_list_ctrl.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown) + marker_list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink) + marker_list_ctrl.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnStopItemBlink) - def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): - found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] + self.marker_list_ctrl = marker_list_ctrl - assert len(found) != 0, "No fiducial found for which {} = {}".format(attribute_name, attribute_value) - return found[0] + # Add all lines into main sizer + group_sizer = wx.BoxSizer(wx.VERTICAL) + group_sizer.Add(sizer_create, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) + group_sizer.Add(sizer_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) + group_sizer.Add(sizer_delete, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) + group_sizer.Add(marker_list_ctrl, 0, wx.EXPAND | wx.ALL, 5) + group_sizer.Fit(self) - def SetImageFiducial(self, fiducial_name, position): - fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'fiducial_name', fiducial_name) - fiducial_index = fiducial['fiducial_index'] + self.SetSizer(group_sizer) + self.Update() - self.image.SetImageFiducial(fiducial_index, position) + self.LoadState() - def SetTrackerFiducial(self, fiducial_name): - if not self.tracker.IsTrackerInitialized(): - dlg.ShowNavigationTrackerWarning(0, 'choose') - return + def __bind_events(self): + Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point') + Publisher.subscribe(self.OnDeleteMultipleMarkers, 'Delete fiducial marker') + Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') + Publisher.subscribe(self.CreateMarker, 'Create marker') + Publisher.subscribe(self.SetMarkers, 'Set markers') + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts') + Publisher.subscribe(self.OnChangeCurrentSession, 'Current session changed') + Publisher.subscribe(self.UpdateMarkerOrientation, 'Open marker orientation dialog') + Publisher.subscribe(self.OnActivateTargetMode, 'Target navigation mode') + Publisher.subscribe(self.AddPeeledSurface, 'Update peel') - fiducial = self.GetFiducialByAttribute(const.TRACKER_FIDUCIALS, 'fiducial_name', fiducial_name) - fiducial_index = fiducial['fiducial_index'] + def SaveState(self): + state = [marker.to_dict() for marker in self.markers] - # XXX: The reference mode is fetched from navigation object, however it seems like not quite - # navigation-related attribute here, as the reference mode used during the fiducial registration - # is more concerned with the calibration than the navigation. - # - ref_mode_id = self.navigation.GetReferenceMode() - self.tracker.SetTrackerFiducial(ref_mode_id, fiducial_index) + session = ses.Session() + session.SetState('markers', state) - self.ResetICP() - self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) + def LoadState(self): + session = ses.Session() + state = session.GetState('markers') - def UpdatePeelVisualization(self, data): - self.navigation.peel_loaded = data + if state is None: + return - def UpdateEfieldVisualization(self, data): - self.navigation.e_field_loaded = data + for d in state: + self.CreateMarker( + position=d['position'], + orientation=d['orientation'], + colour=d['colour'], + size=d['size'], + label=d['label'], + # XXX: See comment below. Should be improved so that is_target wouldn't need to be set as False here. + is_target=False, + seed=d['seed'], + session_id=d['session_id'] + ) + # XXX: Do the same thing as in OnLoadMarkers function: first create marker that is never set as a target, + # then set as target if needed. This could be refactored so that a CreateMarker call would + # suffice to set it as target. + if d['is_target']: + self.__set_marker_as_target(len(self.markers) - 1, display_messagebox=False) - def UpdateNavigationStatus(self, nav_status, vis_status): - self.nav_status = nav_status - if nav_status and self.icp.m_icp is not None: - self.checkbox_icp.Enable(True) - else: - self.checkbox_icp.Enable(False) + def __find_target_marker(self): + """ + Return the index of the marker currently selected as target (there + should be at most one). If there is no such marker, return None. + """ + for i in range(len(self.markers)): + if self.markers[i].is_target: + return i + + return None - def UpdateTrekkerObject(self, data): - # self.trk_inp = data - self.navigation.trekker = data + def __get_brain_target_markers(self): + """ + Return the index of the marker currently selected as target (there + should be at most one). If there is no such marker, return None. + """ + brain_target_list = [] + for i in range(len(self.markers)): + if self.markers[i].is_brain_target: + brain_target_list.append(self.markers[i].coordinate) + if brain_target_list: + return brain_target_list - def UpdateNumTracts(self, data): - self.navigation.n_tracts = data + return None - def UpdateSeedOffset(self, data): - self.navigation.seed_offset = data + def __get_selected_items(self): + """ + Returns a (possibly empty) list of the selected items in the list control. + """ + selection = [] - def UpdateSeedRadius(self, data): - self.navigation.seed_radius = data + next = self.marker_list_ctrl.GetFirstSelected() - def UpdateSleep(self, data): - self.navigation.UpdateSleep(data) + while next != -1: + selection.append(next) + next = self.marker_list_ctrl.GetNextSelected(next) - def UpdateNumberThreads(self, data): - self.navigation.n_threads = data + return selection - def UpdateTractsVisualization(self, data): - self.navigation.view_tracts = data + def __delete_all_markers(self): + """ + Delete all markers + """ + for i in reversed(range(len(self.markers))): + del self.markers[i] + self.marker_list_ctrl.DeleteItem(i) - def UpdateACTData(self, data): - self.navigation.act_data = data + def __delete_multiple_markers(self, indexes): + """ + Delete multiple markers indexed by 'indexes'. Indexes must be sorted in + the ascending order. + """ + for i in reversed(indexes): + del self.markers[i] + self.marker_list_ctrl.DeleteItem(i) + for n in range(0, self.marker_list_ctrl.GetItemCount()): + self.marker_list_ctrl.SetItem(n, 0, str(n + 1)) - def UpdateTarget(self, coord): - self.navigation.target = coord + Publisher.sendMessage('Remove multiple markers', indexes=indexes) - if coord is not None: - self.lock_to_target_checkbox.Enable(True) - self.lock_to_target_checkbox.SetValue(True) - self.navigation.SetLockToTarget(True) + def __delete_all_brain_targets(self): + """ + Delete all brain targets markers + """ + brain_target_index = [] + for index in range(len(self.markers)): + if self.markers[index].is_brain_target: + brain_target_index.append(index) + for index in reversed(brain_target_index): + self.marker_list_ctrl.SetItemBackgroundColour(index, 'white') + del self.markers[index] + self.marker_list_ctrl.DeleteItem(index) + for n in range(0, self.marker_list_ctrl.GetItemCount()): + self.marker_list_ctrl.SetItem(n, 0, str(n + 1)) + Publisher.sendMessage('Remove multiple markers', indexes=brain_target_index) - def EnableACT(self, data): - self.navigation.enable_act = data + def __set_marker_as_target(self, idx, display_messagebox=True): + """ + Set marker indexed by idx as the new target. idx must be a valid index. + """ + # Find the previous target + prev_idx = self.__find_target_marker() - def UpdateImageCoordinates(self, position): - # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. - self.current_coord = position + # If the new target is same as the previous do nothing. + if prev_idx == idx: + return - for m in [0, 1, 2]: - if not self.btns_set_fiducial[m].GetValue(): - for n in [0, 1, 2]: - self.numctrls_fiducial[m][n].SetValue(float(position[n])) + # Unset the previous target + if prev_idx is not None: + self.markers[prev_idx].is_target = False + self.marker_list_ctrl.SetItemBackgroundColour(prev_idx, 'white') + Publisher.sendMessage('Set target transparency', status=False, index=prev_idx) + self.marker_list_ctrl.SetItem(prev_idx, const.TARGET_COLUMN, "") - def ResetICP(self): - self.icp.ResetICP() - self.checkbox_icp.Enable(False) - self.checkbox_icp.SetValue(False) + # Set the new target + self.markers[idx].is_target = True + self.marker_list_ctrl.SetItemBackgroundColour(idx, 'RED') + self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, _("Yes")) - def DisconnectTracker(self): - self.tracker.DisconnectTracker() - self.robot.DisconnectRobot() - self.ResetICP() - self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) + Publisher.sendMessage('Update target', coord=self.markers[idx].position+self.markers[idx].orientation) + Publisher.sendMessage('Set target transparency', status=True, index=idx) + #self.__delete_all_brain_targets() + if display_messagebox: + wx.MessageBox(_("New target selected."), _("InVesalius 3")) - def OnLockToTargetCheckbox(self, evt, ctrl): - value = ctrl.GetValue() - self.navigation.SetLockToTarget(value) + @staticmethod + def __list_fiducial_labels(): + """Return the list of marker labels denoting fiducials.""" + return list(itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS))) - def OnChooseTracker(self, evt, ctrl): - Publisher.sendMessage('Update status text in GUI', - label=_("Configuring tracker ...")) - if hasattr(evt, 'GetSelection'): - choice = evt.GetSelection() + def UpdateCurrentCoord(self, position): + self.current_position = list(position[:3]) + self.current_orientation = list(position[3:]) + if not self.navigation.track_obj: + self.current_orientation = None, None, None + + def UpdateNavigationStatus(self, nav_status, vis_status): + if not nav_status: + self.nav_status = False + self.current_orientation = None, None, None else: - choice = None + self.nav_status = True - self.DisconnectTracker() - self.tracker.ResetTrackerFiducials() - self.tracker.SetTracker(choice) + def UpdateSeedCoordinates(self, root=None, affine_vtk=None, coord_offset=(0, 0, 0), coord_offset_w=(0, 0, 0)): + self.current_seed = coord_offset_w - # If 'robot tracker' was selected, configure and initialize robot. - if self.tracker.tracker_id == const.ROBOT: - success = self.robot.ConfigureRobot() - if success: - self.robot.InitializeRobot() - else: - self.DisconnectTracker() + def OnMouseRightDown(self, evt): + # TODO: Enable the "Set as target" only when target is created with registered object + menu_id = wx.Menu() - # XXX: This could be refactored so that all these attributes from this class wouldn't be passed - # onto tracker object. (If tracker needs them, maybe at least some of them should be attributes of - # Tracker class.) - self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) - - Publisher.sendMessage('Update status text in GUI', label=_("Ready")) - - def OnChooseReferenceMode(self, evt, ctrl): - self.navigation.SetReferenceMode(evt.GetSelection()) + edit_id = menu_id.Append(0, _('Edit label')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id) - # When ref mode is changed the tracker coordinates are set to zero - self.tracker.ResetTrackerFiducials() + color_id = menu_id.Append(1, _('Edit color')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id) - # Some trackers do not accept restarting within this time window - # TODO: Improve the restarting of trackers after changing reference mode + menu_id.AppendSeparator() - self.ResetICP() + if self.__find_target_marker() == self.marker_list_ctrl.GetFocusedItem(): + target_menu = menu_id.Append(2, _('Remove target')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuRemoveTarget, target_menu) + if has_mTMS: + brain_target_menu = menu_id.Append(3, _('Set brain target')) + menu_id.Bind(wx.EVT_MENU, self.OnSetBrainTarget, brain_target_menu) + else: + target_menu = menu_id.Append(2, _('Set as target')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuSetTarget, target_menu) - print("Reference mode changed!") + orientation_menu = menu_id.Append(5, _('Set coil target orientation')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuSetCoilOrientation, orientation_menu) + is_brain_target = self.markers[self.marker_list_ctrl.GetFocusedItem()].is_brain_target + if is_brain_target and has_mTMS: + send_brain_target_menu = menu_id.Append(6, _('Send brain target to mTMS')) + menu_id.Bind(wx.EVT_MENU, self.OnSendBrainTarget, send_brain_target_menu) - def OnImageFiducials(self, n, evt): - fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] + menu_id.AppendSeparator() - # XXX: This is still a bit hard to read, could be cleaned up. - label = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] + # Enable "Send target to robot" button only if tracker is robot, if navigation is on and if target is not none + if self.robot.IsConnected(): + send_target_to_robot = menu_id.Append(7, _('Send InVesalius target to robot')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuSendTargetToRobot, send_target_to_robot) - if self.btns_set_fiducial[n].GetValue(): - position = self.numctrls_fiducial[n][0].GetValue(),\ - self.numctrls_fiducial[n][1].GetValue(),\ - self.numctrls_fiducial[n][2].GetValue() - orientation = None, None, None + send_target_to_robot.Enable(False) - Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=position) + if self.nav_status and self.target_mode and (self.marker_list_ctrl.GetFocusedItem() == self.__find_target_marker()): + send_target_to_robot.Enable(True) - colour = (0., 1., 0.) - size = 2 - seed = 3 * [0.] + is_target_orientation_set = all([elem is not None for elem in self.markers[self.marker_list_ctrl.GetFocusedItem()].orientation]) - Publisher.sendMessage('Create marker', position=position, orientation=orientation, colour=colour, size=size, - label=label, seed=seed) + if is_target_orientation_set and not is_brain_target: + target_menu.Enable(True) else: - for m in [0, 1, 2]: - self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) + target_menu.Enable(False) - Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=np.nan) - Publisher.sendMessage('Delete fiducial marker', label=label) + self.PopupMenu(menu_id) + menu_id.Destroy() - def OnTrackerFiducials(self, n, evt, ctrl): + def OnItemBlink(self, evt): + Publisher.sendMessage('Blink Marker', index=self.marker_list_ctrl.GetFocusedItem()) - # Do not allow several tracker fiducials to be set at the same time. - if self.tracker_fiducial_being_set is not None and self.tracker_fiducial_being_set != n: - ctrl.SetValue(False) + def OnStopItemBlink(self, evt): + Publisher.sendMessage('Stop Blink Marker') + + def OnMenuEditMarkerLabel(self, evt): + list_index = self.marker_list_ctrl.GetFocusedItem() + if list_index == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) return - # Called when the button for setting the tracker fiducial is enabled and either pedal is pressed - # or the button is pressed again. - # - def set_fiducial_callback(state): - if state: - fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name'] - Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) + new_label = dlg.ShowEnterMarkerID(self.marker_list_ctrl.GetItemText(list_index, const.LABEL_COLUMN)) + self.markers[list_index].label = str(new_label) + self.marker_list_ctrl.SetItem(list_index, const.LABEL_COLUMN, new_label) - ctrl.SetValue(False) - self.tracker_fiducial_being_set = None + self.SaveState() - if ctrl.GetValue(): - self.tracker_fiducial_being_set = n + def OnMenuSetTarget(self, evt): + idx = self.marker_list_ctrl.GetFocusedItem() + if idx == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return - if self.pedal_connection is not None: - self.pedal_connection.add_callback( - name='fiducial', - callback=set_fiducial_callback, - remove_when_released=True, - ) + if self.robot.IsConnected(): + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) + self.__set_marker_as_target(idx) - if self.neuronavigation_api is not None: - self.neuronavigation_api.add_pedal_callback( - name='fiducial', - callback=set_fiducial_callback, - remove_when_released=True, - ) - else: - set_fiducial_callback(True) + self.SaveState() - if self.pedal_connection is not None: - self.pedal_connection.remove_callback(name='fiducial') + def OnMenuSetCoilOrientation(self, evt): + list_index = self.marker_list_ctrl.GetFocusedItem() + position = self.markers[list_index].position + orientation = self.markers[list_index].orientation - if self.neuronavigation_api is not None: - self.neuronavigation_api.remove_pedal_callback(name='fiducial') + dialog = dlg.SetCoilOrientationDialog(marker=position+orientation, brain_actor=self.brain_actor) + if dialog.ShowModal() == wx.ID_OK: + coil_position_list, coil_orientation_list, brain_position_list, brain_orientation_list = dialog.GetValue() + self.CreateMarker(list(coil_position_list[0]), list(coil_orientation_list[0]), is_brain_target=False) + for (position, orientation) in zip(brain_position_list, brain_orientation_list): + self.CreateMarker(list(position), list(orientation), is_brain_target=True) + dialog.Destroy() - def OnStopNavigation(self): - select_tracker_elem = self.select_tracker_elem - choice_ref = self.choice_ref + self.SaveState() - self.navigation.StopNavigation() - if self.tracker.tracker_id == const.ROBOT: + def OnMenuRemoveTarget(self, evt): + idx = self.marker_list_ctrl.GetFocusedItem() + self.markers[idx].is_target = False + self.marker_list_ctrl.SetItemBackgroundColour(idx, 'white') + Publisher.sendMessage('Set target transparency', status=False, index=idx) + self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, "") + Publisher.sendMessage('Disable or enable coil tracker', status=False) + Publisher.sendMessage('Update target', coord=None) + if self.robot.IsConnected(): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) + #self.__delete_all_brain_targets() + wx.MessageBox(_("Target removed."), _("InVesalius 3")) - # Enable all navigation buttons - choice_ref.Enable(True) - select_tracker_elem.Enable(True) + self.SaveState() - for btn_c in self.btns_set_fiducial: - btn_c.Enable(True) + def OnMenuSetColor(self, evt): + index = self.marker_list_ctrl.GetFocusedItem() + if index == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return - def CheckFiducialRegistrationError(self): - self.navigation.UpdateFiducialRegistrationError(self.tracker, self.image) - fre, fre_ok = self.navigation.GetFiducialRegistrationError(self.icp) + color_current = [ch * 255 for ch in self.markers[index].colour] - self.txtctrl_fre.SetValue(str(round(fre, 2))) - if fre_ok: - self.txtctrl_fre.SetBackgroundColour('GREEN') - else: - self.txtctrl_fre.SetBackgroundColour('RED') + color_new = dlg.ShowColorDialog(color_current=color_current) - return fre_ok + if not color_new: + return - def OnStartNavigation(self): - select_tracker_elem = self.select_tracker_elem - choice_ref = self.choice_ref + assert len(color_new) == 3 - if not self.tracker.AreTrackerFiducialsSet() or not self.image.AreImageFiducialsSet(): - wx.MessageBox(_("Invalid fiducials, select all coordinates."), _("InVesalius 3")) + # XXX: Seems like a slightly too early point for rounding; better to round only when the value + # is printed to the screen or file. + # + self.markers[index].colour = [round(s / 255.0, 3) for s in color_new] - elif not self.tracker.IsTrackerInitialized(): - dlg.ShowNavigationTrackerWarning(0, 'choose') - errors = True + Publisher.sendMessage('Set new color', index=index, color=color_new) - else: - # Prepare GUI for navigation. - Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS) - Publisher.sendMessage("Hide current mask") + self.SaveState() - # Disable all navigation buttons. - choice_ref.Enable(False) - select_tracker_elem.Enable(False) - for btn_c in self.btns_set_fiducial: - btn_c.Enable(False) + def OnMenuSendTargetToRobot(self, evt): + if isinstance(evt, int): + self.marker_list_ctrl.Focus(evt) - self.navigation.EstimateTrackerToInVTransformationMatrix(self.tracker, self.image) + index = self.marker_list_ctrl.GetFocusedItem() + if index == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return - if not self.CheckFiducialRegistrationError(): - # TODO: Exhibit FRE in a warning dialog and only starts navigation after user clicks ok - print("WARNING: Fiducial registration error too large.") + Publisher.sendMessage('Reset robot process', data=None) + matrix_tracker_fiducials = self.tracker.GetMatrixTrackerFiducials() + Publisher.sendMessage('Update tracker fiducials matrix', + matrix_tracker_fiducials=matrix_tracker_fiducials) - self.icp.RegisterICP(self.navigation, self.tracker) - if self.icp.use_icp: - self.checkbox_icp.Enable(True) - self.checkbox_icp.SetValue(True) - # Update FRE once more after starting the navigation, due to the optional use of ICP, - # which improves FRE. - self.CheckFiducialRegistrationError() + nav_target = self.markers[index].position + self.markers[index].orientation + coord_raw, markers_flag = self.tracker.TrackerCoordinates.GetCoordinates() + m_target = dcr.image_to_tracker(self.navigation.m_change, coord_raw, nav_target, self.icp, self.navigation.obj_data) - self.navigation.StartNavigation(self.tracker, self.icp) + Publisher.sendMessage('Update robot target', robot_tracker_flag=True, target_index=self.marker_list_ctrl.GetFocusedItem(), target=m_target.tolist()) - def OnNavigate(self, evt, btn_nav): - select_tracker_elem = self.select_tracker_elem - choice_ref = self.choice_ref + def OnSetBrainTarget(self, evt): + if isinstance(evt, int): + self.marker_list_ctrl.Focus(evt) + index = self.marker_list_ctrl.GetFocusedItem() + if index == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return - nav_id = btn_nav.GetValue() - if not nav_id: - wx.CallAfter(Publisher.sendMessage, 'Stop navigation') + position = self.markers[index].position + orientation = self.markers[index].orientation + dialog = dlg.SetCoilOrientationDialog(mTMS=self.mTMS, marker=position+orientation, brain_target=True, brain_actor=self.brain_actor) - tooltip = wx.ToolTip(_("Start neuronavigation")) - btn_nav.SetToolTip(tooltip) - else: - Publisher.sendMessage("Start navigation") + if dialog.ShowModal() == wx.ID_OK: + position_list, orientation_list = dialog.GetValueBrainTarget() + for (position, orientation) in zip(position_list, orientation_list): + self.CreateMarker(list(position), list(orientation), size=0.05, is_brain_target=True) + dialog.Destroy() - if self.nav_status: - tooltip = wx.ToolTip(_("Stop neuronavigation")) - btn_nav.SetToolTip(tooltip) - else: - btn_nav.SetValue(False) + self.SaveState() - def ResetUI(self): - for m in range(0, 3): - self.btns_set_fiducial[m].SetValue(False) - for n in range(0, 3): - self.numctrls_fiducial[m][n].SetValue(0.0) - - def OnCheckboxICP(self, evt, ctrl): - self.icp.SetICP(self.navigation, ctrl.GetValue()) - self.CheckFiducialRegistrationError() - - def OnCloseProject(self): - self.ResetUI() - Publisher.sendMessage('Disconnect tracker') - Publisher.sendMessage('Update object registration') - Publisher.sendMessage('Show and track coil', enabled=False) - Publisher.sendMessage('Delete all markers') - Publisher.sendMessage("Update marker offset state", create=False) - Publisher.sendMessage("Remove tracts") - Publisher.sendMessage("Set cross visibility", visibility=0) - # TODO: Reset camera initial focus - Publisher.sendMessage('Reset cam clipping range') - self.navigation.StopNavigation() - self.navigation.__init__( - pedal_connection=self.pedal_connection, - neuronavigation_api=self.neuronavigation_api - ) - self.tracker.__init__() - self.icp.__init__() - -class ObjectRegistrationPanel(wx.Panel): - def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): - wx.Panel.__init__(self, parent) - try: - default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) - except AttributeError: - default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) - self.SetBackgroundColour(default_colour) - - self.coil_list = const.COIL - - self.tracker = tracker - self.pedal_connection = pedal_connection - self.neuronavigation_api = neuronavigation_api - - self.nav_prop = None - self.obj_fiducials = None - self.obj_orients = None - self.obj_ref_mode = None - self.obj_name = None - self.timestamp = const.TIMESTAMP + def OnSendBrainTarget(self, evt): + if isinstance(evt, int): + self.marker_list_ctrl.Focus(evt) + index = self.marker_list_ctrl.GetFocusedItem() + if index == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return + brain_target = self.markers[index].position + self.markers[index].orientation + if self.__find_target_marker(): + coil_pose = self.markers[self.__find_target_marker()].position+self.markers[self.__find_target_marker()].orientation + if self.navigation.coil_at_target: + self.mTMS.UpdateTarget(coil_pose, brain_target) + #wx.CallAfter(Publisher.sendMessage, 'Send brain target to mTMS API', coil_pose=coil_pose, brain_target=brain_target) + print("Send brain target to mTMS API") + else: + print("The coil is not at the target") + else: + print("Target not set") + + def OnSessionChanged(self, evt, ctrl): + value = ctrl.GetValue() + Publisher.sendMessage('Current session changed', new_session_id=value) + + def OnDeleteAllMarkers(self, evt=None): + if evt is not None: + result = dlg.ShowConfirmationDialog(msg=_("Remove all markers? Cannot be undone.")) + if result != wx.ID_OK: + return - self.SetAutoLayout(1) - self.__bind_events() + if self.__find_target_marker() is not None: + Publisher.sendMessage('Disable or enable coil tracker', status=False) + if evt is not None: + wx.MessageBox(_("Target deleted."), _("InVesalius 3")) + if self.robot.IsConnected(): + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) - # Button for creating new coil - tooltip = wx.ToolTip(_("Create new coil")) - btn_new = wx.Button(self, -1, _("New"), size=wx.Size(65, 23)) - btn_new.SetToolTip(tooltip) - btn_new.Enable(1) - btn_new.Bind(wx.EVT_BUTTON, self.OnCreateNewCoil) - self.btn_new = btn_new + self.markers = [] + Publisher.sendMessage('Remove all markers', indexes=self.marker_list_ctrl.GetItemCount()) + self.marker_list_ctrl.DeleteAllItems() + Publisher.sendMessage('Stop Blink Marker', index='DeleteAll') - # Button for loading coil config file - tooltip = wx.ToolTip(_("Load coil configuration file")) - btn_load = wx.Button(self, -1, _("Load"), size=wx.Size(65, 23)) - btn_load.SetToolTip(tooltip) - btn_load.Enable(1) - btn_load.Bind(wx.EVT_BUTTON, self.OnLoadCoil) - self.btn_load = btn_load + self.SaveState() - # Save button for saving coil config file - tooltip = wx.ToolTip(_(u"Save coil configuration file")) - btn_save = wx.Button(self, -1, _(u"Save"), size=wx.Size(65, 23)) - btn_save.SetToolTip(tooltip) - btn_save.Enable(1) - btn_save.Bind(wx.EVT_BUTTON, self.OnSaveCoil) - self.btn_save = btn_save + def OnDeleteMultipleMarkers(self, evt=None, label=None): + # OnDeleteMultipleMarkers is used for both pubsub and button click events + # Pubsub is used for fiducial handle and button click for all others - # Create a horizontal sizer to represent button save - line_save = wx.BoxSizer(wx.HORIZONTAL) - line_save.Add(btn_new, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) - line_save.Add(btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) - line_save.Add(btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) + if not evt: + # Called through pubsub. - # Change angles threshold - text_angles = wx.StaticText(self, -1, _("Angle threshold [degrees]:")) - spin_size_angles = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) - spin_size_angles.SetRange(0.1, 99) - spin_size_angles.SetValue(const.COIL_ANGLES_THRESHOLD) - spin_size_angles.Bind(wx.EVT_TEXT, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles)) - spin_size_angles.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles)) + indexes = [] + if label and (label in self.__list_fiducial_labels()): + for id_n in range(self.marker_list_ctrl.GetItemCount()): + item = self.marker_list_ctrl.GetItem(id_n, const.LABEL_COLUMN) + if item.GetText() == label: + self.marker_list_ctrl.Focus(item.GetId()) + indexes = [self.marker_list_ctrl.GetFocusedItem()] + else: + # Called using a button click. + indexes = self.__get_selected_items() - # Change dist threshold - text_dist = wx.StaticText(self, -1, _("Distance threshold [mm]:")) - spin_size_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) - spin_size_dist.SetRange(0.1, 99) - spin_size_dist.SetValue(const.COIL_ANGLES_THRESHOLD) - spin_size_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist)) - spin_size_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist)) + if not indexes: + # Don't show the warning if called through pubsub + if evt: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return - # Change timestamp interval - text_timestamp = wx.StaticText(self, -1, _("Timestamp interval [s]:")) - spin_timestamp_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc = 0.1) - spin_timestamp_dist.SetRange(0.5, 60.0) - spin_timestamp_dist.SetValue(self.timestamp) - spin_timestamp_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist)) - spin_timestamp_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist)) - self.spin_timestamp_dist = spin_timestamp_dist + # If current target is removed, handle it as a special case. + if self.__find_target_marker() in indexes: + Publisher.sendMessage('Disable or enable coil tracker', status=False) + Publisher.sendMessage('Update target', coord=None) + if self.robot.IsConnected(): + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) + wx.MessageBox(_("Target deleted."), _("InVesalius 3")) - # Create a horizontal sizer to threshold configs - line_angle_threshold = wx.BoxSizer(wx.HORIZONTAL) - line_angle_threshold.AddMany([(text_angles, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), - (spin_size_angles, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)]) + self.__delete_multiple_markers(indexes) + self.SaveState() - line_dist_threshold = wx.BoxSizer(wx.HORIZONTAL) - line_dist_threshold.AddMany([(text_dist, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), - (spin_size_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)]) + def OnCreateMarker(self, evt): + self.CreateMarker() - line_timestamp = wx.BoxSizer(wx.HORIZONTAL) - line_timestamp.AddMany([(text_timestamp, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), - (spin_timestamp_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)]) + self.SaveState() - # Check box for trigger monitoring to create markers from serial port - checkrecordcoords = wx.CheckBox(self, -1, _('Record coordinates')) - checkrecordcoords.SetValue(False) - checkrecordcoords.Enable(0) - checkrecordcoords.Bind(wx.EVT_CHECKBOX, partial(self.OnRecordCoords, ctrl=checkrecordcoords)) - self.checkrecordcoords = checkrecordcoords + def OnLoadMarkers(self, evt): + """Loads markers from file and appends them to the current marker list. + The file should contain no more than a single target marker. Also the + file should not contain any fiducials already in the list.""" + filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"), + wildcard=const.WILDCARD_MARKER_FILES) + + if not filename: + return + + try: + with open(filename, 'r') as file: + magick_line = file.readline() + assert magick_line.startswith(const.MARKER_FILE_MAGICK_STRING) + ver = int(magick_line.split('_')[-1]) + if ver != 0: + wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3")) + return + + file.readline() # skip the header line - # Check box to track object or simply the stylus - checkbox_track_object = wx.CheckBox(self, -1, _('Track object')) - checkbox_track_object.SetValue(False) - checkbox_track_object.Enable(0) - checkbox_track_object.Bind(wx.EVT_CHECKBOX, partial(self.OnTrackObjectCheckbox, ctrl=checkbox_track_object)) - self.checkbox_track_object = checkbox_track_object + # Read the data lines and create markers + for line in file.readlines(): + marker = self.Marker() + marker.from_string(line) + self.CreateMarker(position=marker.position, orientation=marker.orientation, colour=marker.colour, size=marker.size, + label=marker.label, is_target=False, seed=marker.seed, session_id=marker.session_id, is_brain_target=marker.is_brain_target) - line_checks = wx.BoxSizer(wx.HORIZONTAL) - line_checks.Add(checkrecordcoords, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) - line_checks.Add(checkbox_track_object, 0, wx.RIGHT | wx.LEFT, 5) + if marker.label in self.__list_fiducial_labels(): + Publisher.sendMessage('Load image fiducials', label=marker.label, position=marker.position) - # Add line sizers into main sizer - main_sizer = wx.BoxSizer(wx.VERTICAL) - main_sizer.Add(line_save, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.ALIGN_CENTER_HORIZONTAL, 5) - main_sizer.Add(line_angle_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) - main_sizer.Add(line_dist_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) - main_sizer.Add(line_timestamp, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) - main_sizer.Add(line_checks, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 10) - main_sizer.Fit(self) + # If the new marker has is_target=True, we first create + # a marker with is_target=False, and then call __set_marker_as_target + if marker.is_target: + self.__set_marker_as_target(len(self.markers) - 1) - self.SetSizer(main_sizer) - self.Update() + except Exception as e: + wx.MessageBox(_("Invalid markers file."), _("InVesalius 3")) - self.LoadState() + self.SaveState() - def __bind_events(self): - Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') - Publisher.subscribe(self.OnCloseProject, 'Close project data') - Publisher.subscribe(self.OnRemoveObject, 'Remove object data') + def OnMarkersVisibility(self, evt, ctrl): + if ctrl.GetValue(): + Publisher.sendMessage('Hide all markers', indexes=self.marker_list_ctrl.GetItemCount()) + ctrl.SetLabel('Show') + else: + Publisher.sendMessage('Show all markers', indexes=self.marker_list_ctrl.GetItemCount()) + ctrl.SetLabel('Hide') - # Externally check/uncheck and enable/disable checkboxes. - Publisher.subscribe(self.CheckTrackObjectCheckbox, 'Check track-object checkbox') - Publisher.subscribe(self.EnableTrackObjectCheckbox, 'Enable track-object checkbox') + def OnSaveMarkers(self, evt): + prj_data = prj.Project() + timestamp = time.localtime(time.time()) + stamp_date = '{:0>4d}{:0>2d}{:0>2d}'.format(timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday) + stamp_time = '{:0>2d}{:0>2d}{:0>2d}'.format(timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec) + sep = '-' + parts = [stamp_date, stamp_time, prj_data.name, 'markers'] + default_filename = sep.join(parts) + '.mkss' - def SaveState(self): - track_object = self.checkbox_track_object - state = { - 'track_object': { - 'checked': track_object.IsChecked(), - 'enabled': track_object.IsEnabled(), - } - } + filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."), + wildcard=const.WILDCARD_MARKER_FILES, + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + default_filename=default_filename) - session = ses.Session() - session.SetState('object_registration_panel', state) + if not filename: + return - def LoadState(self): - session = ses.Session() - state = session.GetState('object_registration_panel') + try: + with open(filename, 'w', newline='') as file: + file.writelines(['%s%i\n' % (const.MARKER_FILE_MAGICK_STRING, const.CURRENT_MARKER_FILE_VERSION)]) + file.writelines(['%s\n' % self.Marker.to_string_headers()]) + file.writelines('%s\n' % marker.to_string() for marker in self.markers) + file.close() + except: + wx.MessageBox(_("Error writing markers file."), _("InVesalius 3")) - if state is None: - return + def OnSelectColour(self, evt, ctrl): + # TODO: Make sure GetValue returns 3 numbers (without alpha) + self.marker_colour = [colour / 255.0 for colour in ctrl.GetValue()][:3] - track_object = state['track_object'] + def OnSelectSize(self, evt, ctrl): + self.marker_size = ctrl.GetValue() - self.EnableTrackObjectCheckbox(track_object['enabled']) - self.CheckTrackObjectCheckbox(track_object['checked']) + def OnChangeCurrentSession(self, new_session_id): + self.current_session = new_session_id - def UpdateNavigationStatus(self, nav_status, vis_status): - if nav_status: - self.checkrecordcoords.Enable(1) - self.checkbox_track_object.Enable(0) - self.btn_save.Enable(0) - self.btn_new.Enable(0) - self.btn_load.Enable(0) - else: - self.OnRecordCoords(nav_status, self.checkrecordcoords) - self.checkrecordcoords.SetValue(False) - self.checkrecordcoords.Enable(0) - self.btn_save.Enable(1) - self.btn_new.Enable(1) - self.btn_load.Enable(1) - if self.obj_fiducials is not None: - self.checkbox_track_object.Enable(1) - #Publisher.sendMessage('Enable target button', True) + def UpdateMarkerOrientation(self, marker_id=None): + list_index = marker_id if marker_id else 0 + position = self.markers[list_index].position + orientation = self.markers[list_index].orientation + dialog = dlg.SetCoilOrientationDialog(mTMS=self.mTMS, marker=position+orientation) - def OnSelectAngleThreshold(self, evt, ctrl): - Publisher.sendMessage('Update angle threshold', angle=ctrl.GetValue()) + if dialog.ShowModal() == wx.ID_OK: + orientation = dialog.GetValue() + Publisher.sendMessage('Update target orientation', + target_id=marker_id, orientation=list(orientation)) + dialog.Destroy() - def OnSelectDistThreshold(self, evt, ctrl): - Publisher.sendMessage('Update dist threshold', dist_threshold=ctrl.GetValue()) + def OnActivateTargetMode(self, evt=None, target_mode=None): + self.target_mode = target_mode - def OnSelectTimestamp(self, evt, ctrl): - self.timestamp = ctrl.GetValue() + def AddPeeledSurface(self, flag, actor): + self.brain_actor = actor - def OnRecordCoords(self, evt, ctrl): - if ctrl.GetValue() and evt: - self.spin_timestamp_dist.Enable(0) - self.thr_record = rec.Record(ctrl.GetValue(), self.timestamp) - elif (not ctrl.GetValue() and evt) or (ctrl.GetValue() and not evt) : - self.spin_timestamp_dist.Enable(1) - self.thr_record.stop() - elif not ctrl.GetValue() and not evt: - None + def SetMarkers(self, markers): + """ + Set all markers, overwriting the previous markers. + """ - # 'Track object' checkbox + self.__delete_all_markers() - def EnableTrackObjectCheckbox(self, enabled): - self.checkbox_track_object.Enable(enabled) + for marker in markers: + size = marker["size"] + colour = marker["colour"] + position = marker["position"] + orientation = marker["orientation"] - def CheckTrackObjectCheckbox(self, checked): - self.checkbox_track_object.SetValue(checked) - self.OnTrackObjectCheckbox() + self.CreateMarker( + size=size, + colour=colour, + position=position, + orientation=orientation, + ) - def OnTrackObjectCheckbox(self, evt=None, ctrl=None): - checked = self.checkbox_track_object.IsChecked() - Publisher.sendMessage('Track object', enabled=checked) + self.SaveState() - # Disable or enable 'Show coil' checkbox, based on if 'Track object' checkbox is checked. - Publisher.sendMessage('Enable show-coil checkbox', enabled=checked) - # Also, automatically check or uncheck 'Show coil' checkbox. - Publisher.sendMessage('Check show-coil checkbox', checked=checked) + def CreateMarker(self, position=None, orientation=None, colour=None, size=None, label='*', is_target=False, seed=None, session_id=None, is_brain_target=False): + new_marker = self.Marker() + new_marker.position = position or self.current_position + new_marker.orientation = orientation or self.current_orientation + new_marker.colour = colour or self.marker_colour + new_marker.size = size or self.marker_size + new_marker.label = label + new_marker.is_target = is_target + new_marker.seed = seed or self.current_seed + new_marker.session_id = session_id or self.current_session + new_marker.is_brain_target = is_brain_target - self.SaveState() + if self.robot.IsConnected() and self.nav_status: + current_head_robot_target_status = True + else: + current_head_robot_target_status = False - def OnComboCoil(self, evt): - # coil_name = evt.GetString() - coil_index = evt.GetSelection() - Publisher.sendMessage('Change selected coil', self.coil_list[coil_index][1]) + if all([elem is not None for elem in new_marker.orientation]): + arrow_flag = True + else: + arrow_flag = False - def OnCreateNewCoil(self, event=None): - if self.tracker.IsTrackerInitialized(): - dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection, self.neuronavigation_api) - try: - if dialog.ShowModal() == wx.ID_OK: - self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() + if is_brain_target: + new_marker.colour = [0, 0, 1] - self.neuronavigation_api.update_coil_mesh(polydata) + # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added + marker_id = len(self.markers) - if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all(): - Publisher.sendMessage('Update object registration', - data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) - Publisher.sendMessage('Update status text in GUI', - label=_("Ready")) - Publisher.sendMessage( - 'Configure object', - obj_name=self.obj_name, - polydata=polydata, - use_default_object=use_default_object, - ) + Publisher.sendMessage('Add marker', + marker_id=marker_id, + size=new_marker.size, + colour=new_marker.colour, + position=new_marker.position, + orientation=new_marker.orientation, + arrow_flag=arrow_flag) - # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. - Publisher.sendMessage('Enable track-object checkbox', enabled=True) - Publisher.sendMessage('Check track-object checkbox', checked=True) - Publisher.sendMessage('Check volume camera checkbox', checked=False) + self.markers.append(new_marker) - Publisher.sendMessage('Disable target mode') + # Add item to list control in panel + num_items = self.marker_list_ctrl.GetItemCount() + self.marker_list_ctrl.InsertItem(num_items, str(num_items + 1)) + if is_brain_target: + self.marker_list_ctrl.SetItemBackgroundColour(num_items, wx.Colour(102, 178, 255)) + self.marker_list_ctrl.SetItem(num_items, const.SESSION_COLUMN, str(new_marker.session_id)) + self.marker_list_ctrl.SetItem(num_items, const.LABEL_COLUMN, new_marker.label) - except wx._core.PyAssertionError: # TODO FIX: win64 - pass - dialog.Destroy() - else: - dlg.ShowNavigationTrackerWarning(0, 'choose') + if self.session.GetConfig('debug'): + self.marker_list_ctrl.SetItem(num_items, const.X_COLUMN, str(round(new_marker.x, 1))) + self.marker_list_ctrl.SetItem(num_items, const.Y_COLUMN, str(round(new_marker.y, 1))) + self.marker_list_ctrl.SetItem(num_items, const.Z_COLUMN, str(round(new_marker.z, 1))) - def OnLoadCoil(self, event=None): - filename = dlg.ShowLoadSaveDialog(message=_(u"Load object registration"), - wildcard=_("Registration files (*.obr)|*.obr")) - # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609' - # coil_path = 'magstim_coil_dell_laptop.obr' - # filename = os.path.join(data_dir, coil_path) + self.marker_list_ctrl.EnsureVisible(num_items) + +''' +Deprecated Code +Tractography moved to task_tractorgraphy.py +E-Field moved to task_efield.py +Other functionalities are fragmented and/or removed. + +class NeuronavigationPanel(wx.Panel): + def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): + wx.Panel.__init__(self, parent) try: - if filename: - with open(filename, 'r') as text_file: - data = [s.split('\t') for s in text_file.readlines()] + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.SetBackgroundColour(default_colour) - registration_coordinates = np.array(data[1:]).astype(np.float32) - self.obj_fiducials = registration_coordinates[:, :3] - self.obj_orients = registration_coordinates[:, 3:] + self.SetAutoLayout(1) - self.obj_name = data[0][1].encode(const.FS_ENCODE) - self.obj_ref_mode = int(data[0][-1]) + self.__bind_events() - if not os.path.exists(self.obj_name): - self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + # Initialize global variables + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api - polydata = vtk_utils.CreateObjectPolyData(self.obj_name) - if polydata: - self.neuronavigation_api.update_coil_mesh(polydata) - else: - self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + self.navigation = navigation + self.icp = icp + self.tracker = tracker + self.robot = robot + self.image = image - if os.path.basename(self.obj_name) == "magstim_fig8_coil.stl": - use_default_object = True - else: - use_default_object = False + self.nav_status = False + self.tracker_fiducial_being_set = None + self.current_coord = 0, 0, 0, None, None, None - Publisher.sendMessage('Update object registration', - data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) - Publisher.sendMessage('Update status text in GUI', - label=_("Object file successfully loaded")) - Publisher.sendMessage( - 'Configure object', - obj_name=self.obj_name, - polydata=polydata, - use_default_object=use_default_object - ) + # Initialize list of buttons and numctrls for wx objects + self.btns_set_fiducial = [None, None, None, None, None, None] + self.numctrls_fiducial = [[], [], [], [], [], []] - # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. - Publisher.sendMessage('Enable track-object checkbox', enabled=True) - Publisher.sendMessage('Check track-object checkbox', checked=True) - Publisher.sendMessage('Check volume camera checkbox', checked=False) + # ComboBox for spatial tracker device selection + tracker_options = [_("Select tracker:")] + self.tracker.get_trackers() + select_tracker_elem = wx.ComboBox(self, -1, "", size=(145, -1), + choices=tracker_options, style=wx.CB_DROPDOWN|wx.CB_READONLY) - Publisher.sendMessage('Disable target mode') - if use_default_object: - msg = _("Default object file successfully loaded") - else: - msg = _("Object file successfully loaded") - wx.MessageBox(msg, _("InVesalius 3")) - except: - wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3")) - Publisher.sendMessage('Update status text in GUI', label="") + tooltip = wx.ToolTip(_("Choose the tracking device")) + select_tracker_elem.SetToolTip(tooltip) - def OnSaveCoil(self, evt): - if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any(): - wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error")) - else: - filename = dlg.ShowLoadSaveDialog(message=_(u"Save object registration as..."), - wildcard=_("Registration files (*.obr)|*.obr"), - style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, - default_filename="object_registration.obr", save_ext="obr") - if filename: - hdr = 'Object' + "\t" + utils.decode(self.obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode) - data = np.hstack([self.obj_fiducials, self.obj_orients]) - np.savetxt(filename, data, fmt='%.4f', delimiter='\t', newline='\n', header=hdr) - wx.MessageBox(_("Object file successfully saved"), _("Save")) + select_tracker_elem.SetSelection(self.tracker.tracker_id) + select_tracker_elem.Bind(wx.EVT_COMBOBOX, partial(self.OnChooseTracker, ctrl=select_tracker_elem)) + self.select_tracker_elem = select_tracker_elem - def OnCloseProject(self): - self.OnRemoveObject() + # ComboBox for tracker reference mode + tooltip = wx.ToolTip(_("Choose the navigation reference mode")) + choice_ref = wx.ComboBox(self, -1, "", + choices=const.REF_MODE, style=wx.CB_DROPDOWN|wx.CB_READONLY) + choice_ref.SetSelection(const.DEFAULT_REF_MODE) + choice_ref.SetToolTip(tooltip) + choice_ref.Bind(wx.EVT_COMBOBOX, partial(self.OnChooseReferenceMode, ctrl=select_tracker_elem)) + self.choice_ref = choice_ref - def OnRemoveObject(self): - self.checkrecordcoords.SetValue(False) - self.checkrecordcoords.Enable(0) - self.checkbox_track_object.SetValue(False) - self.checkbox_track_object.Enable(0) + # Toggle buttons for image fiducials + for n, fiducial in enumerate(const.IMAGE_FIDUCIALS): + button_id = fiducial['button_id'] + label = fiducial['label'] + tip = fiducial['tip'] - self.nav_prop = None - self.obj_fiducials = None - self.obj_orients = None - self.obj_ref_mode = None - self.obj_name = None - self.timestamp = const.TIMESTAMP + ctrl = wx.ToggleButton(self, button_id, label=label) + ctrl.SetMinSize((gui_utils.calc_width_needed(ctrl, 3), -1)) + ctrl.SetToolTip(wx.ToolTip(tip)) + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnImageFiducials, n)) + ctrl.SetValue(self.image.IsImageFiducialSet(n)) -class MarkersPanel(wx.Panel): - @dataclasses.dataclass - class Marker: - """Class for storing markers. @dataclass decorator simplifies - setting default values, serialization, etc.""" - x : float = 0 - y : float = 0 - z : float = 0 - alpha : float = dataclasses.field(default = None) - beta : float = dataclasses.field(default = None) - gamma : float = dataclasses.field(default = None) - r : float = 0 - g : float = 1 - b : float = 0 - size : float = 2 - label : str = '*' - x_seed : float = 0 - y_seed : float = 0 - z_seed : float = 0 - is_target : bool = False - session_id : int = 1 - is_brain_target : bool = False + self.btns_set_fiducial[n] = ctrl - # x, y, z can be jointly accessed as position - @property - def position(self): - return list((self.x, self.y, self.z)) + # Push buttons for tracker fiducials + for n, fiducial in enumerate(const.TRACKER_FIDUCIALS): + button_id = fiducial['button_id'] + label = fiducial['label'] + tip = fiducial['tip'] - @position.setter - def position(self, new_position): - self.x, self.y, self.z = new_position + ctrl = wx.ToggleButton(self, button_id, label=label) + ctrl.SetMinSize((gui_utils.calc_width_needed(ctrl, 3), -1)) + ctrl.SetToolTip(wx.ToolTip(tip)) + ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, n, ctrl=ctrl)) - # alpha, beta, gamma can be jointly accessed as orientation - @property - def orientation(self): - return list((self.alpha, self.beta, self.gamma)) + self.btns_set_fiducial[n + 3] = ctrl - @orientation.setter - def orientation(self, new_orientation): - self.alpha, self.beta, self.gamma = new_orientation + # TODO: Find a better alignment between FRE, text and navigate button - # alpha, beta, gamma can be jointly accessed as orientation - @property - def coordinate(self): - return list((self.x, self.y, self.z, self.alpha, self.beta, self.gamma)) + # Fiducial registration error text and checkbox + txt_fre = wx.StaticText(self, -1, _('FRE:')) + tooltip = wx.ToolTip(_("Fiducial registration error")) - # r, g, b can be jointly accessed as colour - @property - def colour(self): - return list((self.r, self.g, self.b),) + # XXX: Currently always shows ICP corrected FRE (fiducial registration error) initially + # in the FRE textbox. This is a compromise, done due to corrected and non-corrected FRE values + # being split between Navigation and IterativeClosestPoint classes, and hence it being + # difficult to access both at this stage. This could be improved, e.g., by creating + # a separate class, which would hold both FRE values and would also know whether ICP + # corrected or non-corrected value is being used. + # + value = self.icp.GetFreForUI() - @colour.setter - def colour(self, new_colour): - self.r, self.g, self.b = new_colour + txtctrl_fre = wx.TextCtrl(self, value=value, size=wx.Size(60, -1), style=wx.TE_CENTRE) + txtctrl_fre.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + txtctrl_fre.SetBackgroundColour('WHITE') + txtctrl_fre.SetEditable(0) + txtctrl_fre.SetToolTip(tooltip) + self.txtctrl_fre = txtctrl_fre - # x_seed, y_seed, z_seed can be jointly accessed as seed - @property - def seed(self): - return list((self.x_seed, self.y_seed, self.z_seed),) + # Toggle button for neuronavigation + tooltip = wx.ToolTip(_("Start navigation")) + btn_nav = wx.ToggleButton(self, -1, _("Navigate"), size=wx.Size(80, -1)) + btn_nav.SetToolTip(tooltip) + btn_nav.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnNavigate, btn_nav=btn_nav)) - @seed.setter - def seed(self, new_seed): - self.x_seed, self.y_seed, self.z_seed = new_seed + # "Refine" text and checkbox + txt_icp = wx.StaticText(self, -1, _('Refine:')) + tooltip = wx.ToolTip(_(u"Refine the coregistration")) + checkbox_icp = wx.CheckBox(self, -1, _(' ')) + checkbox_icp.SetValue(self.icp.use_icp) + checkbox_icp.Enable(False) + checkbox_icp.Bind(wx.EVT_CHECKBOX, partial(self.OnCheckboxICP, ctrl=checkbox_icp)) + checkbox_icp.SetToolTip(tooltip) + self.checkbox_icp = checkbox_icp + + # "Pedal pressed" text and an indicator (checkbox) for pedal press + if (pedal_connection is not None and pedal_connection.in_use) or neuronavigation_api is not None: + txt_pedal_pressed = wx.StaticText(self, -1, _('Pedal pressed:')) + tooltip = wx.ToolTip(_(u"Is the pedal pressed")) + checkbox_pedal_pressed = wx.CheckBox(self, -1, _(' ')) + checkbox_pedal_pressed.SetValue(False) + checkbox_pedal_pressed.Enable(False) + checkbox_pedal_pressed.SetToolTip(tooltip) - @classmethod - def to_string_headers(cls): - """Return the string containing tab-separated list of field names (headers).""" - res = [field.name for field in dataclasses.fields(cls)] - res.extend(['x_world', 'y_world', 'z_world', 'alpha_world', 'beta_world', 'gamma_world']) - return '\t'.join(map(lambda x: '\"%s\"' % x, res)) + if pedal_connection is not None: + pedal_connection.add_callback(name='gui', callback=checkbox_pedal_pressed.SetValue) - def to_string(self): - """Serialize to excel-friendly tab-separated string""" - res = '' - for field in dataclasses.fields(self.__class__): - if field.type is str: - res += ('\"%s\"\t' % getattr(self, field.name)) - else: - res += ('%s\t' % str(getattr(self, field.name))) + if neuronavigation_api is not None: + neuronavigation_api.add_pedal_callback(name='gui', callback=checkbox_pedal_pressed.SetValue) - if self.alpha is not None and self.beta is not None and self.gamma is not None: - # Add world coordinates (in addition to the internal ones). - position_world, orientation_world = imagedata_utils.convert_invesalius_to_world( - position=[self.x, self.y, self.z], - orientation=[self.alpha, self.beta, self.gamma], - ) + self.checkbox_pedal_pressed = checkbox_pedal_pressed + else: + txt_pedal_pressed = None + self.checkbox_pedal_pressed = None - else: - position_world, orientation_world = imagedata_utils.convert_invesalius_to_world( - position=[self.x, self.y, self.z], - orientation=[0,0,0], - ) + # "Lock to target" text and checkbox + tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) + lock_to_target_text = wx.StaticText(self, -1, _('Lock to target:')) + lock_to_target_checkbox = wx.CheckBox(self, -1, _(' ')) + lock_to_target_checkbox.SetValue(False) + lock_to_target_checkbox.Enable(False) + lock_to_target_checkbox.Bind(wx.EVT_CHECKBOX, partial(self.OnLockToTargetCheckbox, ctrl=lock_to_target_checkbox)) + lock_to_target_checkbox.SetToolTip(tooltip) - res += '\t'.join(map(lambda x: 'N/A' if x is None else str(x), (*position_world, *orientation_world))) - return res + self.lock_to_target_checkbox = lock_to_target_checkbox - def from_string(self, inp_str): - """Deserialize from a tab-separated string. If the string is not - properly formatted, might throw an exception and leave the object - in an inconsistent state.""" - for field, str_val in zip(dataclasses.fields(self.__class__), inp_str.split('\t')): - if field.type is float and str_val != 'None': - setattr(self, field.name, float(str_val)) - if field.type is float and str_val == 'None': - setattr(self, field.name, None) - if field.type is float and str_val != 'None': - setattr(self, field.name, float(str_val)) - if field.type is str: - setattr(self, field.name, str_val[1:-1]) # remove the quotation marks - if field.type is bool: - setattr(self, field.name, str_val=='True') + # Image and tracker coordinates number controls + for m in range(len(self.btns_set_fiducial)): + for n in range(3): + if m <= 2: + value = self.image.GetImageFiducialForUI(m, n) + else: + value = self.tracker.GetTrackerFiducialForUI(m - 3, n) - def to_dict(self): - return { - 'position': self.position, - 'orientation': self.orientation, - 'colour': self.colour, - 'size': self.size, - 'label': self.label, - 'is_target': self.is_target, - 'seed': self.seed, - 'session_id': self.session_id, - } + self.numctrls_fiducial[m].append( + wx.lib.masked.numctrl.NumCtrl(parent=self, integerWidth=4, fractionWidth=1, value=value)) + # Sizers to group all GUI objects + choice_sizer = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) + choice_sizer.AddMany([(select_tracker_elem, wx.LEFT), + (choice_ref, wx.RIGHT)]) - def __init__(self, parent, navigation, tracker, robot, icp): - wx.Panel.__init__(self, parent) - try: - default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) - except AttributeError: - default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) - self.SetBackgroundColour(default_colour) + coord_sizer = wx.GridBagSizer(hgap=5, vgap=5) - self.SetAutoLayout(1) + for m in range(len(self.btns_set_fiducial)): + coord_sizer.Add(self.btns_set_fiducial[m], pos=wx.GBPosition(m, 0)) + for n in range(3): + coord_sizer.Add(self.numctrls_fiducial[m][n], pos=wx.GBPosition(m, n+1)) + if m in range(1, 6): + self.numctrls_fiducial[m][n].SetEditable(False) - self.navigation = navigation - self.tracker = tracker - self.robot = robot - self.icp = icp - if has_mTMS: - self.mTMS = mTMS() - else: - self.mTMS = None + nav_sizer = wx.FlexGridSizer(rows=1, cols=5, hgap=5, vgap=5) + nav_sizer.AddMany([(txt_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (txtctrl_fre, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (btn_nav, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (txt_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (checkbox_icp, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) - self.__bind_events() + checkboxes_sizer = wx.FlexGridSizer(rows=1, cols=4, hgap=5, vgap=5) + checkboxes_sizer.AddMany([(lock_to_target_text, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (lock_to_target_checkbox, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) - self.session = ses.Session() + if (pedal_connection is not None and pedal_connection.in_use) or neuronavigation_api is not None: + checkboxes_sizer.AddMany([(txt_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL), + (checkbox_pedal_pressed, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL)]) - self.current_position = [0, 0, 0] - self.current_orientation = [None, None, None] - self.current_seed = 0, 0, 0 + group_sizer = wx.FlexGridSizer(rows=10, cols=1, hgap=5, vgap=5) + group_sizer.AddGrowableCol(0, 1) + group_sizer.AddGrowableRow(0, 1) + group_sizer.AddGrowableRow(1, 1) + group_sizer.AddGrowableRow(2, 1) + group_sizer.SetFlexibleDirection(wx.BOTH) + group_sizer.AddMany([(choice_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + (coord_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + (nav_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL), + (checkboxes_sizer, 0, wx.ALIGN_CENTER_HORIZONTAL)]) - self.markers = [] - self.nav_status = False - self.target_mode = False + main_sizer = wx.BoxSizer(wx.HORIZONTAL) + main_sizer.Add(group_sizer, 1)# wx.ALIGN_CENTER_HORIZONTAL, 10) + self.sizer = main_sizer + self.SetSizer(main_sizer) + self.Fit() - self.marker_colour = const.MARKER_COLOUR - self.marker_size = const.MARKER_SIZE - self.arrow_marker_size = const.ARROW_MARKER_SIZE - self.current_session = 1 + def __bind_events(self): + Publisher.subscribe(self.LoadImageFiducials, 'Load image fiducials') + Publisher.subscribe(self.SetImageFiducial, 'Set image fiducial') + Publisher.subscribe(self.SetTrackerFiducial, 'Set tracker fiducial') + Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point') + Publisher.subscribe(self.DisconnectTracker, 'Disconnect tracker') + Publisher.subscribe(self.OnCloseProject, 'Close project data') + Publisher.subscribe(self.UpdateTrekkerObject, 'Update Trekker object') + Publisher.subscribe(self.UpdateNumTracts, 'Update number of tracts') + Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset') + Publisher.subscribe(self.UpdateSeedRadius, 'Update seed radius') + Publisher.subscribe(self.UpdateSleep, 'Update sleep') + Publisher.subscribe(self.UpdateNumberThreads, 'Update number of threads') + Publisher.subscribe(self.UpdateTractsVisualization, 'Update tracts visualization') + Publisher.subscribe(self.UpdatePeelVisualization, 'Update peel visualization') + Publisher.subscribe(self.UpdateEfieldVisualization, 'Update e-field visualization') + Publisher.subscribe(self.EnableACT, 'Enable ACT') + Publisher.subscribe(self.UpdateACTData, 'Update ACT data') + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + Publisher.subscribe(self.UpdateTarget, 'Update target') + #Publisher.subscribe(self.OnStartNavigation, 'Start navigation') + Publisher.subscribe(self.OnStopNavigation, 'Stop navigation') - self.brain_actor = None - # Change session - spin_session = wx.SpinCtrl(self, -1, "", size=wx.Size(40, 23)) - spin_session.SetRange(1, 99) - spin_session.SetValue(self.current_session) - spin_session.SetToolTip("Set session") - spin_session.Bind(wx.EVT_TEXT, partial(self.OnSessionChanged, ctrl=spin_session)) - spin_session.Bind(wx.EVT_SPINCTRL, partial(self.OnSessionChanged, ctrl=spin_session)) + def LoadImageFiducials(self, label, position): + fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', label) - # Marker colour select - select_colour = csel.ColourSelect(self, -1, colour=[255*s for s in self.marker_colour], size=wx.Size(20, 23)) - select_colour.SetToolTip("Set colour") - select_colour.Bind(csel.EVT_COLOURSELECT, partial(self.OnSelectColour, ctrl=select_colour)) + fiducial_index = fiducial['fiducial_index'] + fiducial_name = fiducial['fiducial_name'] - btn_create = wx.Button(self, -1, label=_('Create marker'), size=wx.Size(135, 23)) - btn_create.Bind(wx.EVT_BUTTON, self.OnCreateMarker) + if self.btns_set_fiducial[fiducial_index].GetValue(): + print("Fiducial {} already set, not resetting".format(label)) + return - sizer_create = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) - sizer_create.AddMany([(spin_session, 1), - (select_colour, 0), - (btn_create, 0)]) + Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=position) - # Buttons to save and load markers and to change its visibility as well - btn_save = wx.Button(self, -1, label=_('Save'), size=wx.Size(65, 23)) - btn_save.Bind(wx.EVT_BUTTON, self.OnSaveMarkers) + self.btns_set_fiducial[fiducial_index].SetValue(True) + for m in [0, 1, 2]: + self.numctrls_fiducial[fiducial_index][m].SetValue(position[m]) - btn_load = wx.Button(self, -1, label=_('Load'), size=wx.Size(65, 23)) - btn_load.Bind(wx.EVT_BUTTON, self.OnLoadMarkers) + def GetFiducialByAttribute(self, fiducials, attribute_name, attribute_value): + found = [fiducial for fiducial in fiducials if fiducial[attribute_name] == attribute_value] - btn_visibility = wx.ToggleButton(self, -1, _("Hide"), size=wx.Size(65, 23)) - btn_visibility.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnMarkersVisibility, ctrl=btn_visibility)) + assert len(found) != 0, "No fiducial found for which {} = {}".format(attribute_name, attribute_value) + return found[0] - sizer_btns = wx.FlexGridSizer(rows=1, cols=3, hgap=5, vgap=5) - sizer_btns.AddMany([(btn_save, 1, wx.RIGHT), - (btn_load, 0, wx.LEFT | wx.RIGHT), - (btn_visibility, 0, wx.LEFT)]) + def SetImageFiducial(self, fiducial_name, position): + fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'fiducial_name', fiducial_name) + fiducial_index = fiducial['fiducial_index'] - # Buttons to delete or remove markers - btn_delete_single = wx.Button(self, -1, label=_('Remove'), size=wx.Size(65, 23)) - btn_delete_single.Bind(wx.EVT_BUTTON, self.OnDeleteMultipleMarkers) + self.image.SetImageFiducial(fiducial_index, position) - btn_delete_all = wx.Button(self, -1, label=_('Delete all'), size=wx.Size(135, 23)) - btn_delete_all.Bind(wx.EVT_BUTTON, self.OnDeleteAllMarkers) + def SetTrackerFiducial(self, fiducial_name): + if not self.tracker.IsTrackerInitialized(): + dlg.ShowNavigationTrackerWarning(0, 'choose') + return - sizer_delete = wx.FlexGridSizer(rows=1, cols=2, hgap=5, vgap=5) - sizer_delete.AddMany([(btn_delete_single, 1, wx.RIGHT), - (btn_delete_all, 0, wx.LEFT)]) + fiducial = self.GetFiducialByAttribute(const.TRACKER_FIDUCIALS, 'fiducial_name', fiducial_name) + fiducial_index = fiducial['fiducial_index'] - # List of markers - marker_list_ctrl = wx.ListCtrl(self, -1, style=wx.LC_REPORT, size=wx.Size(0,120)) - marker_list_ctrl.InsertColumn(const.ID_COLUMN, '#') - marker_list_ctrl.SetColumnWidth(const.ID_COLUMN, 28) + # XXX: The reference mode is fetched from navigation object, however it seems like not quite + # navigation-related attribute here, as the reference mode used during the fiducial registration + # is more concerned with the calibration than the navigation. + # + ref_mode_id = self.navigation.GetReferenceMode() + self.tracker.SetTrackerFiducial(ref_mode_id, fiducial_index) - marker_list_ctrl.InsertColumn(const.SESSION_COLUMN, 'Session') - marker_list_ctrl.SetColumnWidth(const.SESSION_COLUMN, 52) + self.ResetICP() + self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) - marker_list_ctrl.InsertColumn(const.LABEL_COLUMN, 'Label') - marker_list_ctrl.SetColumnWidth(const.LABEL_COLUMN, 118) + def UpdatePeelVisualization(self, data): + self.navigation.peel_loaded = data - marker_list_ctrl.InsertColumn(const.TARGET_COLUMN, 'Target') - marker_list_ctrl.SetColumnWidth(const.TARGET_COLUMN, 45) + def UpdateEfieldVisualization(self, data): + self.navigation.e_field_loaded = data - if self.session.GetConfig('debug'): - marker_list_ctrl.InsertColumn(const.X_COLUMN, 'X') - marker_list_ctrl.SetColumnWidth(const.X_COLUMN, 45) + def UpdateNavigationStatus(self, nav_status, vis_status): + self.nav_status = nav_status + if nav_status and self.icp.m_icp is not None: + self.checkbox_icp.Enable(True) + else: + self.checkbox_icp.Enable(False) - marker_list_ctrl.InsertColumn(const.Y_COLUMN, 'Y') - marker_list_ctrl.SetColumnWidth(const.Y_COLUMN, 45) + def UpdateTrekkerObject(self, data): + # self.trk_inp = data + self.navigation.trekker = data - marker_list_ctrl.InsertColumn(const.Z_COLUMN, 'Z') - marker_list_ctrl.SetColumnWidth(const.Z_COLUMN, 45) + def UpdateNumTracts(self, data): + self.navigation.n_tracts = data - marker_list_ctrl.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnMouseRightDown) - marker_list_ctrl.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemBlink) - marker_list_ctrl.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnStopItemBlink) + def UpdateSeedOffset(self, data): + self.navigation.seed_offset = data - self.marker_list_ctrl = marker_list_ctrl + def UpdateSeedRadius(self, data): + self.navigation.seed_radius = data - # Add all lines into main sizer - group_sizer = wx.BoxSizer(wx.VERTICAL) - group_sizer.Add(sizer_create, 0, wx.TOP | wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) - group_sizer.Add(sizer_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) - group_sizer.Add(sizer_delete, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, 5) - group_sizer.Add(marker_list_ctrl, 0, wx.EXPAND | wx.ALL, 5) - group_sizer.Fit(self) + def UpdateSleep(self, data): + self.navigation.UpdateSleep(data) - self.SetSizer(group_sizer) - self.Update() + def UpdateNumberThreads(self, data): + self.navigation.n_threads = data - self.LoadState() + def UpdateTractsVisualization(self, data): + self.navigation.view_tracts = data - def __bind_events(self): - Publisher.subscribe(self.UpdateCurrentCoord, 'Set cross focal point') - Publisher.subscribe(self.OnDeleteMultipleMarkers, 'Delete fiducial marker') - Publisher.subscribe(self.OnDeleteAllMarkers, 'Delete all markers') - Publisher.subscribe(self.CreateMarker, 'Create marker') - Publisher.subscribe(self.SetMarkers, 'Set markers') - Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') - Publisher.subscribe(self.UpdateSeedCoordinates, 'Update tracts') - Publisher.subscribe(self.OnChangeCurrentSession, 'Current session changed') - Publisher.subscribe(self.UpdateMarkerOrientation, 'Open marker orientation dialog') - Publisher.subscribe(self.OnActivateTargetMode, 'Target navigation mode') - Publisher.subscribe(self.AddPeeledSurface, 'Update peel') + def UpdateACTData(self, data): + self.navigation.act_data = data - def SaveState(self): - state = [marker.to_dict() for marker in self.markers] + def UpdateTarget(self, coord): + self.navigation.target = coord - session = ses.Session() - session.SetState('markers', state) + if coord is not None: + self.lock_to_target_checkbox.Enable(True) + self.lock_to_target_checkbox.SetValue(True) + self.navigation.SetLockToTarget(True) - def LoadState(self): - session = ses.Session() - state = session.GetState('markers') + def EnableACT(self, data): + self.navigation.enable_act = data - if state is None: - return + def UpdateImageCoordinates(self, position): + # TODO: Change from world coordinates to matrix coordinates. They are better for multi software communication. + self.current_coord = position - for d in state: - self.CreateMarker( - position=d['position'], - orientation=d['orientation'], - colour=d['colour'], - size=d['size'], - label=d['label'], - # XXX: See comment below. Should be improved so that is_target wouldn't need to be set as False here. - is_target=False, - seed=d['seed'], - session_id=d['session_id'] - ) - # XXX: Do the same thing as in OnLoadMarkers function: first create marker that is never set as a target, - # then set as target if needed. This could be refactored so that a CreateMarker call would - # suffice to set it as target. - if d['is_target']: - self.__set_marker_as_target(len(self.markers) - 1, display_messagebox=False) + for m in [0, 1, 2]: + if not self.btns_set_fiducial[m].GetValue(): + for n in [0, 1, 2]: + self.numctrls_fiducial[m][n].SetValue(float(position[n])) - def __find_target_marker(self): - """ - Return the index of the marker currently selected as target (there - should be at most one). If there is no such marker, return None. - """ - for i in range(len(self.markers)): - if self.markers[i].is_target: - return i - - return None + def ResetICP(self): + self.icp.ResetICP() + self.checkbox_icp.Enable(False) + self.checkbox_icp.SetValue(False) - def __get_brain_target_markers(self): - """ - Return the index of the marker currently selected as target (there - should be at most one). If there is no such marker, return None. - """ - brain_target_list = [] - for i in range(len(self.markers)): - if self.markers[i].is_brain_target: - brain_target_list.append(self.markers[i].coordinate) - if brain_target_list: - return brain_target_list + def DisconnectTracker(self): + self.tracker.DisconnectTracker() + self.robot.DisconnectRobot() + self.ResetICP() + self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) - return None + def OnLockToTargetCheckbox(self, evt, ctrl): + value = ctrl.GetValue() + self.navigation.SetLockToTarget(value) - def __get_selected_items(self): - """ - Returns a (possibly empty) list of the selected items in the list control. - """ - selection = [] + def OnChooseTracker(self, evt, ctrl): + Publisher.sendMessage('Update status text in GUI', + label=_("Configuring tracker ...")) + if hasattr(evt, 'GetSelection'): + choice = evt.GetSelection() + else: + choice = None - next = self.marker_list_ctrl.GetFirstSelected() + self.DisconnectTracker() + self.tracker.ResetTrackerFiducials() + self.tracker.SetTracker(choice) - while next != -1: - selection.append(next) - next = self.marker_list_ctrl.GetNextSelected(next) + # If 'robot tracker' was selected, configure and initialize robot. + if self.tracker.tracker_id == const.ROBOT: + success = self.robot.ConfigureRobot() + if success: + self.robot.InitializeRobot() + else: + self.DisconnectTracker() - return selection + # XXX: This could be refactored so that all these attributes from this class wouldn't be passed + # onto tracker object. (If tracker needs them, maybe at least some of them should be attributes of + # Tracker class.) + self.tracker.UpdateUI(self.select_tracker_elem, self.numctrls_fiducial[3:6], self.txtctrl_fre) - def __delete_all_markers(self): - """ - Delete all markers - """ - for i in reversed(range(len(self.markers))): - del self.markers[i] - self.marker_list_ctrl.DeleteItem(i) + Publisher.sendMessage('Update status text in GUI', label=_("Ready")) - def __delete_multiple_markers(self, indexes): - """ - Delete multiple markers indexed by 'indexes'. Indexes must be sorted in - the ascending order. - """ - for i in reversed(indexes): - del self.markers[i] - self.marker_list_ctrl.DeleteItem(i) - for n in range(0, self.marker_list_ctrl.GetItemCount()): - self.marker_list_ctrl.SetItem(n, 0, str(n + 1)) + def OnChooseReferenceMode(self, evt, ctrl): + self.navigation.SetReferenceMode(evt.GetSelection()) - Publisher.sendMessage('Remove multiple markers', indexes=indexes) + # When ref mode is changed the tracker coordinates are set to zero + self.tracker.ResetTrackerFiducials() - def __delete_all_brain_targets(self): - """ - Delete all brain targets markers - """ - brain_target_index = [] - for index in range(len(self.markers)): - if self.markers[index].is_brain_target: - brain_target_index.append(index) - for index in reversed(brain_target_index): - self.marker_list_ctrl.SetItemBackgroundColour(index, 'white') - del self.markers[index] - self.marker_list_ctrl.DeleteItem(index) - for n in range(0, self.marker_list_ctrl.GetItemCount()): - self.marker_list_ctrl.SetItem(n, 0, str(n + 1)) - Publisher.sendMessage('Remove multiple markers', indexes=brain_target_index) + # Some trackers do not accept restarting within this time window + # TODO: Improve the restarting of trackers after changing reference mode - def __set_marker_as_target(self, idx, display_messagebox=True): - """ - Set marker indexed by idx as the new target. idx must be a valid index. - """ - # Find the previous target - prev_idx = self.__find_target_marker() + self.ResetICP() - # If the new target is same as the previous do nothing. - if prev_idx == idx: - return + print("Reference mode changed!") - # Unset the previous target - if prev_idx is not None: - self.markers[prev_idx].is_target = False - self.marker_list_ctrl.SetItemBackgroundColour(prev_idx, 'white') - Publisher.sendMessage('Set target transparency', status=False, index=prev_idx) - self.marker_list_ctrl.SetItem(prev_idx, const.TARGET_COLUMN, "") + def OnImageFiducials(self, n, evt): + fiducial_name = const.IMAGE_FIDUCIALS[n]['fiducial_name'] - # Set the new target - self.markers[idx].is_target = True - self.marker_list_ctrl.SetItemBackgroundColour(idx, 'RED') - self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, _("Yes")) + # XXX: This is still a bit hard to read, could be cleaned up. + label = list(const.BTNS_IMG_MARKERS[evt.GetId()].values())[0] - Publisher.sendMessage('Update target', coord=self.markers[idx].position+self.markers[idx].orientation) - Publisher.sendMessage('Set target transparency', status=True, index=idx) - #self.__delete_all_brain_targets() - if display_messagebox: - wx.MessageBox(_("New target selected."), _("InVesalius 3")) + if self.btns_set_fiducial[n].GetValue(): + position = self.numctrls_fiducial[n][0].GetValue(),\ + self.numctrls_fiducial[n][1].GetValue(),\ + self.numctrls_fiducial[n][2].GetValue() + orientation = None, None, None - @staticmethod - def __list_fiducial_labels(): - """Return the list of marker labels denoting fiducials.""" - return list(itertools.chain(*(const.BTNS_IMG_MARKERS[i].values() for i in const.BTNS_IMG_MARKERS))) + Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=position) - def UpdateCurrentCoord(self, position): - self.current_position = list(position[:3]) - self.current_orientation = list(position[3:]) - if not self.navigation.track_obj: - self.current_orientation = None, None, None + colour = (0., 1., 0.) + size = 2 + seed = 3 * [0.] - def UpdateNavigationStatus(self, nav_status, vis_status): - if not nav_status: - self.nav_status = False - self.current_orientation = None, None, None + Publisher.sendMessage('Create marker', position=position, orientation=orientation, colour=colour, size=size, + label=label, seed=seed) else: - self.nav_status = True + for m in [0, 1, 2]: + self.numctrls_fiducial[n][m].SetValue(float(self.current_coord[m])) - def UpdateSeedCoordinates(self, root=None, affine_vtk=None, coord_offset=(0, 0, 0), coord_offset_w=(0, 0, 0)): - self.current_seed = coord_offset_w + Publisher.sendMessage('Set image fiducial', fiducial_name=fiducial_name, position=np.nan) + Publisher.sendMessage('Delete fiducial marker', label=label) - def OnMouseRightDown(self, evt): - # TODO: Enable the "Set as target" only when target is created with registered object - menu_id = wx.Menu() + def OnTrackerFiducials(self, n, evt, ctrl): - edit_id = menu_id.Append(0, _('Edit label')) - menu_id.Bind(wx.EVT_MENU, self.OnMenuEditMarkerLabel, edit_id) + # Do not allow several tracker fiducials to be set at the same time. + if self.tracker_fiducial_being_set is not None and self.tracker_fiducial_being_set != n: + ctrl.SetValue(False) + return - color_id = menu_id.Append(1, _('Edit color')) - menu_id.Bind(wx.EVT_MENU, self.OnMenuSetColor, color_id) + # Called when the button for setting the tracker fiducial is enabled and either pedal is pressed + # or the button is pressed again. + # + def set_fiducial_callback(state): + if state: + fiducial_name = const.TRACKER_FIDUCIALS[n]['fiducial_name'] + Publisher.sendMessage('Set tracker fiducial', fiducial_name=fiducial_name) - menu_id.AppendSeparator() + ctrl.SetValue(False) + self.tracker_fiducial_being_set = None - if self.__find_target_marker() == self.marker_list_ctrl.GetFocusedItem(): - target_menu = menu_id.Append(2, _('Remove target')) - menu_id.Bind(wx.EVT_MENU, self.OnMenuRemoveTarget, target_menu) - if has_mTMS: - brain_target_menu = menu_id.Append(3, _('Set brain target')) - menu_id.Bind(wx.EVT_MENU, self.OnSetBrainTarget, brain_target_menu) + if ctrl.GetValue(): + self.tracker_fiducial_being_set = n + + if self.pedal_connection is not None: + self.pedal_connection.add_callback( + name='fiducial', + callback=set_fiducial_callback, + remove_when_released=True, + ) + + if self.neuronavigation_api is not None: + self.neuronavigation_api.add_pedal_callback( + name='fiducial', + callback=set_fiducial_callback, + remove_when_released=True, + ) else: - target_menu = menu_id.Append(2, _('Set as target')) - menu_id.Bind(wx.EVT_MENU, self.OnMenuSetTarget, target_menu) + set_fiducial_callback(True) - orientation_menu = menu_id.Append(5, _('Set coil target orientation')) - menu_id.Bind(wx.EVT_MENU, self.OnMenuSetCoilOrientation, orientation_menu) - is_brain_target = self.markers[self.marker_list_ctrl.GetFocusedItem()].is_brain_target - if is_brain_target and has_mTMS: - send_brain_target_menu = menu_id.Append(6, _('Send brain target to mTMS')) - menu_id.Bind(wx.EVT_MENU, self.OnSendBrainTarget, send_brain_target_menu) + if self.pedal_connection is not None: + self.pedal_connection.remove_callback(name='fiducial') - menu_id.AppendSeparator() + if self.neuronavigation_api is not None: + self.neuronavigation_api.remove_pedal_callback(name='fiducial') - # Enable "Send target to robot" button only if tracker is robot, if navigation is on and if target is not none - if self.robot.IsConnected(): - send_target_to_robot = menu_id.Append(7, _('Send InVesalius target to robot')) - menu_id.Bind(wx.EVT_MENU, self.OnMenuSendTargetToRobot, send_target_to_robot) + def OnStopNavigation(self): + select_tracker_elem = self.select_tracker_elem + choice_ref = self.choice_ref - send_target_to_robot.Enable(False) + self.navigation.StopNavigation() + if self.tracker.tracker_id == const.ROBOT: + Publisher.sendMessage('Update robot target', robot_tracker_flag=False, + target_index=None, target=None) - if self.nav_status and self.target_mode and (self.marker_list_ctrl.GetFocusedItem() == self.__find_target_marker()): - send_target_to_robot.Enable(True) + # Enable all navigation buttons + choice_ref.Enable(True) + select_tracker_elem.Enable(True) - is_target_orientation_set = all([elem is not None for elem in self.markers[self.marker_list_ctrl.GetFocusedItem()].orientation]) + for btn_c in self.btns_set_fiducial: + btn_c.Enable(True) - if is_target_orientation_set and not is_brain_target: - target_menu.Enable(True) + def CheckFiducialRegistrationError(self): + self.navigation.UpdateFiducialRegistrationError(self.tracker, self.image) + fre, fre_ok = self.navigation.GetFiducialRegistrationError(self.icp) + + self.txtctrl_fre.SetValue(str(round(fre, 2))) + if fre_ok: + self.txtctrl_fre.SetBackgroundColour('GREEN') else: - target_menu.Enable(False) + self.txtctrl_fre.SetBackgroundColour('RED') - self.PopupMenu(menu_id) - menu_id.Destroy() + return fre_ok - def OnItemBlink(self, evt): - Publisher.sendMessage('Blink Marker', index=self.marker_list_ctrl.GetFocusedItem()) + def OnStartNavigation(self): + select_tracker_elem = self.select_tracker_elem + choice_ref = self.choice_ref - def OnStopItemBlink(self, evt): - Publisher.sendMessage('Stop Blink Marker') + if not self.tracker.AreTrackerFiducialsSet() or not self.image.AreImageFiducialsSet(): + wx.MessageBox(_("Invalid fiducials, select all coordinates."), _("InVesalius 3")) - def OnMenuEditMarkerLabel(self, evt): - list_index = self.marker_list_ctrl.GetFocusedItem() - if list_index == -1: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return + elif not self.tracker.IsTrackerInitialized(): + dlg.ShowNavigationTrackerWarning(0, 'choose') + errors = True - new_label = dlg.ShowEnterMarkerID(self.marker_list_ctrl.GetItemText(list_index, const.LABEL_COLUMN)) - self.markers[list_index].label = str(new_label) - self.marker_list_ctrl.SetItem(list_index, const.LABEL_COLUMN, new_label) + else: + # Prepare GUI for navigation. + Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS) + Publisher.sendMessage("Hide current mask") - self.SaveState() + # Disable all navigation buttons. + choice_ref.Enable(False) + select_tracker_elem.Enable(False) + for btn_c in self.btns_set_fiducial: + btn_c.Enable(False) - def OnMenuSetTarget(self, evt): - idx = self.marker_list_ctrl.GetFocusedItem() - if idx == -1: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return + self.navigation.EstimateTrackerToInVTransformationMatrix(self.tracker, self.image) - if self.robot.IsConnected(): - Publisher.sendMessage('Update robot target', robot_tracker_flag=False, - target_index=None, target=None) - self.__set_marker_as_target(idx) + if not self.CheckFiducialRegistrationError(): + # TODO: Exhibit FRE in a warning dialog and only starts navigation after user clicks ok + print("WARNING: Fiducial registration error too large.") - self.SaveState() + self.icp.RegisterICP(self.navigation, self.tracker) + if self.icp.use_icp: + self.checkbox_icp.Enable(True) + self.checkbox_icp.SetValue(True) + # Update FRE once more after starting the navigation, due to the optional use of ICP, + # which improves FRE. + self.CheckFiducialRegistrationError() - def OnMenuSetCoilOrientation(self, evt): - list_index = self.marker_list_ctrl.GetFocusedItem() - position = self.markers[list_index].position - orientation = self.markers[list_index].orientation + self.navigation.StartNavigation(self.tracker, self.icp) - dialog = dlg.SetCoilOrientationDialog(marker=position+orientation, brain_actor=self.brain_actor) - if dialog.ShowModal() == wx.ID_OK: - coil_position_list, coil_orientation_list, brain_position_list, brain_orientation_list = dialog.GetValue() - self.CreateMarker(list(coil_position_list[0]), list(coil_orientation_list[0]), is_brain_target=False) - for (position, orientation) in zip(brain_position_list, brain_orientation_list): - self.CreateMarker(list(position), list(orientation), is_brain_target=True) - dialog.Destroy() + def OnNavigate(self, evt, btn_nav): + select_tracker_elem = self.select_tracker_elem + choice_ref = self.choice_ref - self.SaveState() + nav_id = btn_nav.GetValue() + if not nav_id: + wx.CallAfter(Publisher.sendMessage, 'Stop navigation') - def OnMenuRemoveTarget(self, evt): - idx = self.marker_list_ctrl.GetFocusedItem() - self.markers[idx].is_target = False - self.marker_list_ctrl.SetItemBackgroundColour(idx, 'white') - Publisher.sendMessage('Set target transparency', status=False, index=idx) - self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, "") - Publisher.sendMessage('Disable or enable coil tracker', status=False) - Publisher.sendMessage('Update target', coord=None) - if self.robot.IsConnected(): - Publisher.sendMessage('Update robot target', robot_tracker_flag=False, - target_index=None, target=None) - #self.__delete_all_brain_targets() - wx.MessageBox(_("Target removed."), _("InVesalius 3")) + tooltip = wx.ToolTip(_("Start neuronavigation")) + btn_nav.SetToolTip(tooltip) + else: + Publisher.sendMessage("Start navigation") - self.SaveState() + if self.nav_status: + tooltip = wx.ToolTip(_("Stop neuronavigation")) + btn_nav.SetToolTip(tooltip) + else: + btn_nav.SetValue(False) - def OnMenuSetColor(self, evt): - index = self.marker_list_ctrl.GetFocusedItem() - if index == -1: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return + def ResetUI(self): + for m in range(0, 3): + self.btns_set_fiducial[m].SetValue(False) + for n in range(0, 3): + self.numctrls_fiducial[m][n].SetValue(0.0) - color_current = [ch * 255 for ch in self.markers[index].colour] + def OnCheckboxICP(self, evt, ctrl): + self.icp.SetICP(self.navigation, ctrl.GetValue()) + self.CheckFiducialRegistrationError() - color_new = dlg.ShowColorDialog(color_current=color_current) + def OnCloseProject(self): + self.ResetUI() + Publisher.sendMessage('Disconnect tracker') + Publisher.sendMessage('Update object registration') + Publisher.sendMessage('Show and track coil', enabled=False) + Publisher.sendMessage('Delete all markers') + Publisher.sendMessage("Update marker offset state", create=False) + Publisher.sendMessage("Remove tracts") + Publisher.sendMessage("Set cross visibility", visibility=0) + # TODO: Reset camera initial focus + Publisher.sendMessage('Reset cam clipping range') + self.navigation.StopNavigation() + self.navigation.__init__( + pedal_connection=self.pedal_connection, + neuronavigation_api=self.neuronavigation_api + ) + self.tracker.__init__() + self.icp.__init__() - if not color_new: - return +class ObjectRegistrationPanel(wx.Panel): + def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): + wx.Panel.__init__(self, parent) + try: + default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) + except AttributeError: + default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) + self.SetBackgroundColour(default_colour) - assert len(color_new) == 3 + self.coil_list = const.COIL - # XXX: Seems like a slightly too early point for rounding; better to round only when the value - # is printed to the screen or file. - # - self.markers[index].colour = [round(s / 255.0, 3) for s in color_new] + self.tracker = tracker + self.pedal_connection = pedal_connection + self.neuronavigation_api = neuronavigation_api - Publisher.sendMessage('Set new color', index=index, color=color_new) + self.nav_prop = None + self.obj_fiducials = None + self.obj_orients = None + self.obj_ref_mode = None + self.obj_name = None + self.timestamp = const.TIMESTAMP - self.SaveState() + self.SetAutoLayout(1) + self.__bind_events() - def OnMenuSendTargetToRobot(self, evt): - if isinstance(evt, int): - self.marker_list_ctrl.Focus(evt) + # Button for creating new coil + tooltip = wx.ToolTip(_("Create new coil")) + btn_new = wx.Button(self, -1, _("New"), size=wx.Size(65, 23)) + btn_new.SetToolTip(tooltip) + btn_new.Enable(1) + btn_new.Bind(wx.EVT_BUTTON, self.OnCreateNewCoil) + self.btn_new = btn_new - index = self.marker_list_ctrl.GetFocusedItem() - if index == -1: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return + # Button for loading coil config file + tooltip = wx.ToolTip(_("Load coil configuration file")) + btn_load = wx.Button(self, -1, _("Load"), size=wx.Size(65, 23)) + btn_load.SetToolTip(tooltip) + btn_load.Enable(1) + btn_load.Bind(wx.EVT_BUTTON, self.OnLoadCoil) + self.btn_load = btn_load - Publisher.sendMessage('Reset robot process', data=None) - matrix_tracker_fiducials = self.tracker.GetMatrixTrackerFiducials() - Publisher.sendMessage('Update tracker fiducials matrix', - matrix_tracker_fiducials=matrix_tracker_fiducials) + # Save button for saving coil config file + tooltip = wx.ToolTip(_(u"Save coil configuration file")) + btn_save = wx.Button(self, -1, _(u"Save"), size=wx.Size(65, 23)) + btn_save.SetToolTip(tooltip) + btn_save.Enable(1) + btn_save.Bind(wx.EVT_BUTTON, self.OnSaveCoil) + self.btn_save = btn_save - nav_target = self.markers[index].position + self.markers[index].orientation - coord_raw, markers_flag = self.tracker.TrackerCoordinates.GetCoordinates() - m_target = dcr.image_to_tracker(self.navigation.m_change, coord_raw, nav_target, self.icp, self.navigation.obj_data) + # Create a horizontal sizer to represent button save + line_save = wx.BoxSizer(wx.HORIZONTAL) + line_save.Add(btn_new, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) + line_save.Add(btn_load, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) + line_save.Add(btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 4) - Publisher.sendMessage('Update robot target', robot_tracker_flag=True, target_index=self.marker_list_ctrl.GetFocusedItem(), target=m_target.tolist()) + # Change angles threshold + text_angles = wx.StaticText(self, -1, _("Angle threshold [degrees]:")) + spin_size_angles = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) + spin_size_angles.SetRange(0.1, 99) + spin_size_angles.SetValue(const.COIL_ANGLES_THRESHOLD) + spin_size_angles.Bind(wx.EVT_TEXT, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles)) + spin_size_angles.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectAngleThreshold, ctrl=spin_size_angles)) - def OnSetBrainTarget(self, evt): - if isinstance(evt, int): - self.marker_list_ctrl.Focus(evt) - index = self.marker_list_ctrl.GetFocusedItem() - if index == -1: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return + # Change dist threshold + text_dist = wx.StaticText(self, -1, _("Distance threshold [mm]:")) + spin_size_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) + spin_size_dist.SetRange(0.1, 99) + spin_size_dist.SetValue(const.COIL_ANGLES_THRESHOLD) + spin_size_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist)) + spin_size_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectDistThreshold, ctrl=spin_size_dist)) - position = self.markers[index].position - orientation = self.markers[index].orientation - dialog = dlg.SetCoilOrientationDialog(mTMS=self.mTMS, marker=position+orientation, brain_target=True, brain_actor=self.brain_actor) + # Change timestamp interval + text_timestamp = wx.StaticText(self, -1, _("Timestamp interval [s]:")) + spin_timestamp_dist = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc = 0.1) + spin_timestamp_dist.SetRange(0.5, 60.0) + spin_timestamp_dist.SetValue(self.timestamp) + spin_timestamp_dist.Bind(wx.EVT_TEXT, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist)) + spin_timestamp_dist.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectTimestamp, ctrl=spin_timestamp_dist)) + self.spin_timestamp_dist = spin_timestamp_dist - if dialog.ShowModal() == wx.ID_OK: - position_list, orientation_list = dialog.GetValueBrainTarget() - for (position, orientation) in zip(position_list, orientation_list): - self.CreateMarker(list(position), list(orientation), size=0.05, is_brain_target=True) - dialog.Destroy() + # Create a horizontal sizer to threshold configs + line_angle_threshold = wx.BoxSizer(wx.HORIZONTAL) + line_angle_threshold.AddMany([(text_angles, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_size_angles, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)]) - self.SaveState() + line_dist_threshold = wx.BoxSizer(wx.HORIZONTAL) + line_dist_threshold.AddMany([(text_dist, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_size_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)]) - def OnSendBrainTarget(self, evt): - if isinstance(evt, int): - self.marker_list_ctrl.Focus(evt) - index = self.marker_list_ctrl.GetFocusedItem() - if index == -1: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return - brain_target = self.markers[index].position + self.markers[index].orientation - if self.__find_target_marker(): - coil_pose = self.markers[self.__find_target_marker()].position+self.markers[self.__find_target_marker()].orientation - if self.navigation.coil_at_target: - self.mTMS.UpdateTarget(coil_pose, brain_target) - #wx.CallAfter(Publisher.sendMessage, 'Send brain target to mTMS API', coil_pose=coil_pose, brain_target=brain_target) - print("Send brain target to mTMS API") - else: - print("The coil is not at the target") - else: - print("Target not set") - - def OnSessionChanged(self, evt, ctrl): - value = ctrl.GetValue() - Publisher.sendMessage('Current session changed', new_session_id=value) - - def OnDeleteAllMarkers(self, evt=None): - if evt is not None: - result = dlg.ShowConfirmationDialog(msg=_("Remove all markers? Cannot be undone.")) - if result != wx.ID_OK: - return + line_timestamp = wx.BoxSizer(wx.HORIZONTAL) + line_timestamp.AddMany([(text_timestamp, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_timestamp_dist, 0, wx.ALL | wx.EXPAND | wx.GROW, 5)]) - if self.__find_target_marker() is not None: - Publisher.sendMessage('Disable or enable coil tracker', status=False) - if evt is not None: - wx.MessageBox(_("Target deleted."), _("InVesalius 3")) - if self.robot.IsConnected(): - Publisher.sendMessage('Update robot target', robot_tracker_flag=False, - target_index=None, target=None) + # Check box for trigger monitoring to create markers from serial port + checkrecordcoords = wx.CheckBox(self, -1, _('Record coordinates')) + checkrecordcoords.SetValue(False) + checkrecordcoords.Enable(0) + checkrecordcoords.Bind(wx.EVT_CHECKBOX, partial(self.OnRecordCoords, ctrl=checkrecordcoords)) + self.checkrecordcoords = checkrecordcoords - self.markers = [] - Publisher.sendMessage('Remove all markers', indexes=self.marker_list_ctrl.GetItemCount()) - self.marker_list_ctrl.DeleteAllItems() - Publisher.sendMessage('Stop Blink Marker', index='DeleteAll') + # Check box to track object or simply the stylus + checkbox_track_object = wx.CheckBox(self, -1, _('Track object')) + checkbox_track_object.SetValue(False) + checkbox_track_object.Enable(0) + checkbox_track_object.Bind(wx.EVT_CHECKBOX, partial(self.OnTrackObjectCheckbox, ctrl=checkbox_track_object)) + self.checkbox_track_object = checkbox_track_object - self.SaveState() + line_checks = wx.BoxSizer(wx.HORIZONTAL) + line_checks.Add(checkrecordcoords, 0, wx.ALIGN_LEFT | wx.RIGHT | wx.LEFT, 5) + line_checks.Add(checkbox_track_object, 0, wx.RIGHT | wx.LEFT, 5) - def OnDeleteMultipleMarkers(self, evt=None, label=None): - # OnDeleteMultipleMarkers is used for both pubsub and button click events - # Pubsub is used for fiducial handle and button click for all others + # Add line sizers into main sizer + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(line_save, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.ALIGN_CENTER_HORIZONTAL, 5) + main_sizer.Add(line_angle_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) + main_sizer.Add(line_dist_threshold, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) + main_sizer.Add(line_timestamp, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) + main_sizer.Add(line_checks, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, 10) + main_sizer.Fit(self) - if not evt: - # Called through pubsub. + self.SetSizer(main_sizer) + self.Update() - indexes = [] - if label and (label in self.__list_fiducial_labels()): - for id_n in range(self.marker_list_ctrl.GetItemCount()): - item = self.marker_list_ctrl.GetItem(id_n, const.LABEL_COLUMN) - if item.GetText() == label: - self.marker_list_ctrl.Focus(item.GetId()) - indexes = [self.marker_list_ctrl.GetFocusedItem()] - else: - # Called using a button click. - indexes = self.__get_selected_items() + self.LoadState() - if not indexes: - # Don't show the warning if called through pubsub - if evt: - wx.MessageBox(_("No data selected."), _("InVesalius 3")) - return + def __bind_events(self): + Publisher.subscribe(self.UpdateNavigationStatus, 'Navigation status') + Publisher.subscribe(self.OnCloseProject, 'Close project data') + Publisher.subscribe(self.OnRemoveObject, 'Remove object data') - # If current target is removed, handle it as a special case. - if self.__find_target_marker() in indexes: - Publisher.sendMessage('Disable or enable coil tracker', status=False) - Publisher.sendMessage('Update target', coord=None) - if self.robot.IsConnected(): - Publisher.sendMessage('Update robot target', robot_tracker_flag=False, - target_index=None, target=None) - wx.MessageBox(_("Target deleted."), _("InVesalius 3")) + # Externally check/uncheck and enable/disable checkboxes. + Publisher.subscribe(self.CheckTrackObjectCheckbox, 'Check track-object checkbox') + Publisher.subscribe(self.EnableTrackObjectCheckbox, 'Enable track-object checkbox') - self.__delete_multiple_markers(indexes) - self.SaveState() + def SaveState(self): + track_object = self.checkbox_track_object + state = { + 'track_object': { + 'checked': track_object.IsChecked(), + 'enabled': track_object.IsEnabled(), + } + } - def OnCreateMarker(self, evt): - self.CreateMarker() + session = ses.Session() + session.SetState('object_registration_panel', state) - self.SaveState() + def LoadState(self): + session = ses.Session() + state = session.GetState('object_registration_panel') - def OnLoadMarkers(self, evt): - """Loads markers from file and appends them to the current marker list. - The file should contain no more than a single target marker. Also the - file should not contain any fiducials already in the list.""" - filename = dlg.ShowLoadSaveDialog(message=_(u"Load markers"), - wildcard=const.WILDCARD_MARKER_FILES) - - if not filename: + if state is None: return - - try: - with open(filename, 'r') as file: - magick_line = file.readline() - assert magick_line.startswith(const.MARKER_FILE_MAGICK_STRING) - ver = int(magick_line.split('_')[-1]) - if ver != 0: - wx.MessageBox(_("Unknown version of the markers file."), _("InVesalius 3")) - return - - file.readline() # skip the header line - # Read the data lines and create markers - for line in file.readlines(): - marker = self.Marker() - marker.from_string(line) - self.CreateMarker(position=marker.position, orientation=marker.orientation, colour=marker.colour, size=marker.size, - label=marker.label, is_target=False, seed=marker.seed, session_id=marker.session_id, is_brain_target=marker.is_brain_target) + track_object = state['track_object'] + + self.EnableTrackObjectCheckbox(track_object['enabled']) + self.CheckTrackObjectCheckbox(track_object['checked']) + + def UpdateNavigationStatus(self, nav_status, vis_status): + if nav_status: + self.checkrecordcoords.Enable(1) + self.checkbox_track_object.Enable(0) + self.btn_save.Enable(0) + self.btn_new.Enable(0) + self.btn_load.Enable(0) + else: + self.OnRecordCoords(nav_status, self.checkrecordcoords) + self.checkrecordcoords.SetValue(False) + self.checkrecordcoords.Enable(0) + self.btn_save.Enable(1) + self.btn_new.Enable(1) + self.btn_load.Enable(1) + if self.obj_fiducials is not None: + self.checkbox_track_object.Enable(1) + #Publisher.sendMessage('Enable target button', True) - if marker.label in self.__list_fiducial_labels(): - Publisher.sendMessage('Load image fiducials', label=marker.label, position=marker.position) + def OnSelectAngleThreshold(self, evt, ctrl): + Publisher.sendMessage('Update angle threshold', angle=ctrl.GetValue()) - # If the new marker has is_target=True, we first create - # a marker with is_target=False, and then call __set_marker_as_target - if marker.is_target: - self.__set_marker_as_target(len(self.markers) - 1) + def OnSelectDistThreshold(self, evt, ctrl): + Publisher.sendMessage('Update dist threshold', dist_threshold=ctrl.GetValue()) - except Exception as e: - wx.MessageBox(_("Invalid markers file."), _("InVesalius 3")) + def OnSelectTimestamp(self, evt, ctrl): + self.timestamp = ctrl.GetValue() - self.SaveState() + def OnRecordCoords(self, evt, ctrl): + if ctrl.GetValue() and evt: + self.spin_timestamp_dist.Enable(0) + self.thr_record = rec.Record(ctrl.GetValue(), self.timestamp) + elif (not ctrl.GetValue() and evt) or (ctrl.GetValue() and not evt) : + self.spin_timestamp_dist.Enable(1) + self.thr_record.stop() + elif not ctrl.GetValue() and not evt: + None - def OnMarkersVisibility(self, evt, ctrl): - if ctrl.GetValue(): - Publisher.sendMessage('Hide all markers', indexes=self.marker_list_ctrl.GetItemCount()) - ctrl.SetLabel('Show') - else: - Publisher.sendMessage('Show all markers', indexes=self.marker_list_ctrl.GetItemCount()) - ctrl.SetLabel('Hide') + # 'Track object' checkbox - def OnSaveMarkers(self, evt): - prj_data = prj.Project() - timestamp = time.localtime(time.time()) - stamp_date = '{:0>4d}{:0>2d}{:0>2d}'.format(timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday) - stamp_time = '{:0>2d}{:0>2d}{:0>2d}'.format(timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec) - sep = '-' - parts = [stamp_date, stamp_time, prj_data.name, 'markers'] - default_filename = sep.join(parts) + '.mkss' + def EnableTrackObjectCheckbox(self, enabled): + self.checkbox_track_object.Enable(enabled) - filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."), - wildcard=const.WILDCARD_MARKER_FILES, - style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, - default_filename=default_filename) + def CheckTrackObjectCheckbox(self, checked): + self.checkbox_track_object.SetValue(checked) + self.OnTrackObjectCheckbox() - if not filename: - return + def OnTrackObjectCheckbox(self, evt=None, ctrl=None): + checked = self.checkbox_track_object.IsChecked() + Publisher.sendMessage('Track object', enabled=checked) - try: - with open(filename, 'w', newline='') as file: - file.writelines(['%s%i\n' % (const.MARKER_FILE_MAGICK_STRING, const.CURRENT_MARKER_FILE_VERSION)]) - file.writelines(['%s\n' % self.Marker.to_string_headers()]) - file.writelines('%s\n' % marker.to_string() for marker in self.markers) - file.close() - except: - wx.MessageBox(_("Error writing markers file."), _("InVesalius 3")) + # Disable or enable 'Show coil' checkbox, based on if 'Track object' checkbox is checked. + Publisher.sendMessage('Enable show-coil checkbox', enabled=checked) - def OnSelectColour(self, evt, ctrl): - # TODO: Make sure GetValue returns 3 numbers (without alpha) - self.marker_colour = [colour / 255.0 for colour in ctrl.GetValue()][:3] + # Also, automatically check or uncheck 'Show coil' checkbox. + Publisher.sendMessage('Check show-coil checkbox', checked=checked) - def OnSelectSize(self, evt, ctrl): - self.marker_size = ctrl.GetValue() + self.SaveState() - def OnChangeCurrentSession(self, new_session_id): - self.current_session = new_session_id + def OnComboCoil(self, evt): + # coil_name = evt.GetString() + coil_index = evt.GetSelection() + Publisher.sendMessage('Change selected coil', self.coil_list[coil_index][1]) - def UpdateMarkerOrientation(self, marker_id=None): - list_index = marker_id if marker_id else 0 - position = self.markers[list_index].position - orientation = self.markers[list_index].orientation - dialog = dlg.SetCoilOrientationDialog(mTMS=self.mTMS, marker=position+orientation) + def OnCreateNewCoil(self, event=None): + if self.tracker.IsTrackerInitialized(): + dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection, self.neuronavigation_api) + try: + if dialog.ShowModal() == wx.ID_OK: + self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() - if dialog.ShowModal() == wx.ID_OK: - orientation = dialog.GetValue() - Publisher.sendMessage('Update target orientation', - target_id=marker_id, orientation=list(orientation)) - dialog.Destroy() + self.neuronavigation_api.update_coil_mesh(polydata) - def OnActivateTargetMode(self, evt=None, target_mode=None): - self.target_mode = target_mode + if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all(): + Publisher.sendMessage('Update object registration', + data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) + Publisher.sendMessage('Update status text in GUI', + label=_("Ready")) + Publisher.sendMessage( + 'Configure object', + obj_name=self.obj_name, + polydata=polydata, + use_default_object=use_default_object, + ) - def AddPeeledSurface(self, flag, actor): - self.brain_actor = actor + # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. + Publisher.sendMessage('Enable track-object checkbox', enabled=True) + Publisher.sendMessage('Check track-object checkbox', checked=True) + Publisher.sendMessage('Check volume camera checkbox', checked=False) - def SetMarkers(self, markers): - """ - Set all markers, overwriting the previous markers. - """ + Publisher.sendMessage('Disable target mode') - self.__delete_all_markers() + except wx._core.PyAssertionError: # TODO FIX: win64 + pass + dialog.Destroy() + else: + dlg.ShowNavigationTrackerWarning(0, 'choose') - for marker in markers: - size = marker["size"] - colour = marker["colour"] - position = marker["position"] - orientation = marker["orientation"] + def OnLoadCoil(self, event=None): + filename = dlg.ShowLoadSaveDialog(message=_(u"Load object registration"), + wildcard=_("Registration files (*.obr)|*.obr")) + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609' + # coil_path = 'magstim_coil_dell_laptop.obr' + # filename = os.path.join(data_dir, coil_path) - self.CreateMarker( - size=size, - colour=colour, - position=position, - orientation=orientation, - ) + try: + if filename: + with open(filename, 'r') as text_file: + data = [s.split('\t') for s in text_file.readlines()] - self.SaveState() + registration_coordinates = np.array(data[1:]).astype(np.float32) + self.obj_fiducials = registration_coordinates[:, :3] + self.obj_orients = registration_coordinates[:, 3:] + self.obj_name = data[0][1].encode(const.FS_ENCODE) + self.obj_ref_mode = int(data[0][-1]) - def CreateMarker(self, position=None, orientation=None, colour=None, size=None, label='*', is_target=False, seed=None, session_id=None, is_brain_target=False): - new_marker = self.Marker() - new_marker.position = position or self.current_position - new_marker.orientation = orientation or self.current_orientation - new_marker.colour = colour or self.marker_colour - new_marker.size = size or self.marker_size - new_marker.label = label - new_marker.is_target = is_target - new_marker.seed = seed or self.current_seed - new_marker.session_id = session_id or self.current_session - new_marker.is_brain_target = is_brain_target + if not os.path.exists(self.obj_name): + self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") - if self.robot.IsConnected() and self.nav_status: - current_head_robot_target_status = True - else: - current_head_robot_target_status = False + polydata = vtk_utils.CreateObjectPolyData(self.obj_name) + if polydata: + self.neuronavigation_api.update_coil_mesh(polydata) + else: + self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") - if all([elem is not None for elem in new_marker.orientation]): - arrow_flag = True - else: - arrow_flag = False + if os.path.basename(self.obj_name) == "magstim_fig8_coil.stl": + use_default_object = True + else: + use_default_object = False - if is_brain_target: - new_marker.colour = [0, 0, 1] + Publisher.sendMessage('Update object registration', + data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) + Publisher.sendMessage('Update status text in GUI', + label=_("Object file successfully loaded")) + Publisher.sendMessage( + 'Configure object', + obj_name=self.obj_name, + polydata=polydata, + use_default_object=use_default_object + ) - # Note that ball_id is zero-based, so we assign it len(self.markers) before the new marker is added - marker_id = len(self.markers) + # Automatically enable and check 'Track object' checkbox and uncheck 'Disable Volume Camera' checkbox. + Publisher.sendMessage('Enable track-object checkbox', enabled=True) + Publisher.sendMessage('Check track-object checkbox', checked=True) + Publisher.sendMessage('Check volume camera checkbox', checked=False) - Publisher.sendMessage('Add marker', - marker_id=marker_id, - size=new_marker.size, - colour=new_marker.colour, - position=new_marker.position, - orientation=new_marker.orientation, - arrow_flag=arrow_flag) + Publisher.sendMessage('Disable target mode') + if use_default_object: + msg = _("Default object file successfully loaded") + else: + msg = _("Object file successfully loaded") + wx.MessageBox(msg, _("InVesalius 3")) + except: + wx.MessageBox(_("Object registration file incompatible."), _("InVesalius 3")) + Publisher.sendMessage('Update status text in GUI', label="") - self.markers.append(new_marker) + def OnSaveCoil(self, evt): + if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any(): + wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error")) + else: + filename = dlg.ShowLoadSaveDialog(message=_(u"Save object registration as..."), + wildcard=_("Registration files (*.obr)|*.obr"), + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + default_filename="object_registration.obr", save_ext="obr") + if filename: + hdr = 'Object' + "\t" + utils.decode(self.obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode) + data = np.hstack([self.obj_fiducials, self.obj_orients]) + np.savetxt(filename, data, fmt='%.4f', delimiter='\t', newline='\n', header=hdr) + wx.MessageBox(_("Object file successfully saved"), _("Save")) - # Add item to list control in panel - num_items = self.marker_list_ctrl.GetItemCount() - self.marker_list_ctrl.InsertItem(num_items, str(num_items + 1)) - if is_brain_target: - self.marker_list_ctrl.SetItemBackgroundColour(num_items, wx.Colour(102, 178, 255)) - self.marker_list_ctrl.SetItem(num_items, const.SESSION_COLUMN, str(new_marker.session_id)) - self.marker_list_ctrl.SetItem(num_items, const.LABEL_COLUMN, new_marker.label) + def OnCloseProject(self): + self.OnRemoveObject() - if self.session.GetConfig('debug'): - self.marker_list_ctrl.SetItem(num_items, const.X_COLUMN, str(round(new_marker.x, 1))) - self.marker_list_ctrl.SetItem(num_items, const.Y_COLUMN, str(round(new_marker.y, 1))) - self.marker_list_ctrl.SetItem(num_items, const.Z_COLUMN, str(round(new_marker.z, 1))) + def OnRemoveObject(self): + self.checkrecordcoords.SetValue(False) + self.checkrecordcoords.Enable(0) + self.checkbox_track_object.SetValue(False) + self.checkbox_track_object.Enable(0) - self.marker_list_ctrl.EnsureVisible(num_items) + self.nav_prop = None + self.obj_fiducials = None + self.obj_orients = None + self.obj_ref_mode = None + self.obj_name = None + self.timestamp = const.TIMESTAMP class DbsPanel(wx.Panel): def __init__(self, parent): @@ -4338,3 +4144,5 @@ def __init__(self, *initial_data, **kwargs): setattr(self, key, dictionary[key]) for key in kwargs: setattr(self, key, kwargs[key]) + +''' \ No newline at end of file From 4938f4ddd604ee032b5ba8aab40239c76e6debfb Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:49:28 +0300 Subject: [PATCH 56/99] CLP: clean preferences --- invesalius/gui/preferences.py | 45 ++++++++++------------------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index ba32ebe75..29d89eba7 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -35,20 +35,16 @@ def __init__( ): super().__init__(parent, id_, title, style=style) tracker = Tracker() - robot = Robot( - tracker=tracker - ) + robot = Robot() pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None neuronavigation_api = NeuronavigationApi() self.book = wx.Notebook(self, -1) - #self.pnl_viewer2d = Viewer2D(self.book) + self.pnl_viewer3d = Viewer3D(self.book) - # self.pnl_surface = SurfaceCreation(self) self.pnl_language = Language(self.book) - #self.book.AddPage(self.pnl_viewer2d, _("2D Visualization")) self.book.AddPage(self.pnl_viewer3d, _("Visualization")) session = ses.Session() mode = session.GetConfig('mode') @@ -80,7 +76,6 @@ def GetPreferences(self): values = {} lang = self.pnl_language.GetSelection() viewer = self.pnl_viewer3d.GetSelection() - #viewer2d = self.pnl_viewer2d.GetSelection() values.update(lang) values.update(viewer) @@ -96,7 +91,7 @@ def LoadPreferences(self): session = ses.Session() mode = session.GetConfig('mode') if mode == const.MODE_NAVIGATOR: - self.pnl_object.LoadState() + self.pnl_object.LoadConfig() values = { const.RENDERING: rendering, @@ -105,11 +100,9 @@ def LoadPreferences(self): const.SLICE_INTERPOLATION: slice_interpolation, } - #self.pnl_viewer2d.LoadSelection(values) self.pnl_viewer3d.LoadSelection(values) self.pnl_language.LoadSelection(values) - class Viewer3D(wx.Panel): def __init__(self, parent): @@ -195,7 +188,7 @@ def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): self.obj_ref_mode = None self.obj_name = None self.timestamp = const.TIMESTAMP - self.state = self.LoadState() + self.state = self.LoadConfig() # Button for creating new stimulator tooltip = wx.ToolTip(_("Create new stimulator")) @@ -324,7 +317,7 @@ def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): def __bind_events(self): pass - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('navigation') @@ -462,7 +455,7 @@ def __init__(self, parent, tracker, robot): self.robot = robot self.robot_ip = None self.matrix_tracker_to_robot = None - self.state = self.LoadState() + self.state = self.LoadConfig() # ComboBox for spatial tracker device selection tracker_options = [_("Select")] + self.tracker.get_trackers() @@ -476,7 +469,6 @@ def __init__(self, parent, tracker, robot): select_tracker_label = wx.StaticText(self, -1, _('Choose the tracking device: ')) - # ComboBox for tracker reference mode tooltip = wx.ToolTip(_("Choose the navigation reference mode")) choice_ref = wx.ComboBox(self, -1, "", size=(145, -1), @@ -534,7 +526,7 @@ def __init__(self, parent, tracker, robot): btn_rob_con = wx.Button(self, -1, _("Register")) btn_rob_con.SetToolTip("Register robot tracking") btn_rob_con.Enable(1) - btn_rob_con.Bind(wx.EVT_BUTTON, self.OnRobotCon) + btn_rob_con.Bind(wx.EVT_BUTTON, self.OnRobotRegister) if self.robot.IsConnected(): if self.matrix_tracker_to_robot is None: btn_rob_con.Show() @@ -545,8 +537,6 @@ def __init__(self, parent, tracker, robot): btn_rob_con.Hide() self.btn_rob_con = btn_rob_con - - rob_sizer = wx.FlexGridSizer(rows=2, cols=3, hgap=5, vgap=5) rob_sizer.AddMany([ (lbl_rob, 0, wx.LEFT), @@ -573,7 +563,7 @@ def __bind_events(self): Publisher.subscribe(self.OnRobotStatus, "Robot connection status") Publisher.subscribe(self.OnTransformationMatrix, "Load robot transformation matrix") - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('robot') @@ -624,23 +614,10 @@ def OnChoiceIP(self, evt, ctrl): def OnRobotConnect(self, evt): if self.robot_ip is not None: - # self.configuration = { - # 'tracker_id': self.tracker.GetTrackerId(), - # 'robot_ip': self.robot_ip, - # 'tracker_configuration': self.tracker.tracker_connection.GetConfiguration(), - # } - # self.connection = self.tracker.tracker_connection self.robot.SetRobotIP(self.robot_ip) Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) - def OnRobot(self, evt): - self.HideParent() - if self.robot.ConfigureRobot(): - self.ShowParent() - else: - self.ShowParent() - - def OnRobotCon(self, evt): + def OnRobotRegister(self, evt): self.HideParent() self.robot.RegisterRobot() self.ShowParent() @@ -691,8 +668,9 @@ def LoadSelection(self, values): self.cmb_lang.SetSelection(int(selection)) +''' +Deprecated code -# Deprecated code class SurfaceCreation(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -748,3 +726,4 @@ def LoadSelection(self, values): value = values[const.SLICE_INTERPOLATION] self.rb_inter.SetSelection(int(value)) +''' \ No newline at end of file From fddd7c677728e043ddb02ab1c8522cbdaf315f1e Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:49:49 +0300 Subject: [PATCH 57/99] FIX: convert state to config --- invesalius/data/viewer_volume.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 229dc0502..bd5f0d485 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -277,7 +277,7 @@ def __init__(self, parent): self.set_camera_position = True self.old_coord = np.zeros((6,),dtype=float) - self.LoadState() + self.LoadConfig() def __bind_events(self): Publisher.subscribe(self.LoadActor, @@ -405,7 +405,7 @@ def __bind_events(self): Publisher.subscribe(self.ActivateRobotMode, 'Robot navigation mode') Publisher.subscribe(self.OnUpdateRobotStatus, 'Update robot status') - def SaveState(self): + def SaveConfig(self): object_path = self.obj_name.decode(const.FS_ENCODE) if self.obj_name is not None else None use_default_object = self.use_default_object @@ -417,7 +417,7 @@ def SaveState(self): session = ses.Session() session.SetConfig('viewer', state) - def LoadState(self): + def LoadConfig(self): session = ses.Session() state = session.GetConfig('viewer') @@ -1963,7 +1963,7 @@ def ConfigureObject(self, obj_name=None, polydata=None, use_default_object=True) self.polydata = polydata self.use_default_object = use_default_object - self.SaveState() + self.SaveConfig() def TrackObject(self, enabled): if enabled: From 95acf739b13ac1ca8fb6491ef2541124e0fc6d86 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 9 Aug 2023 11:50:19 +0300 Subject: [PATCH 58/99] CLP: clean tracker connection --- invesalius/data/tracker_connection.py | 160 +++++++++++++------------- 1 file changed, 83 insertions(+), 77 deletions(-) diff --git a/invesalius/data/tracker_connection.py b/invesalius/data/tracker_connection.py index 1152f23fb..d7b031268 100644 --- a/invesalius/data/tracker_connection.py +++ b/invesalius/data/tracker_connection.py @@ -502,6 +502,88 @@ def Disconnect(self): super().Disconnect() +class DebugTrackerRandomConnection(TrackerConnection): + def __init__(self, model=None): + super().__init__(model) + + def Configure(self): + return True + + def Connect(self): + self.connection = True + self.lib_mode = 'debug' + print('Debug device (random) started.') + + def Disconnect(self): + self.connection = False + self.lib_mode = 'debug' + print('Debug tracker (random) disconnected.') + + +class DebugTrackerApproachConnection(TrackerConnection): + def __init__(self, model=None): + super().__init__(model) + + def Configure(self): + return True + + def Connect(self): + self.connection = True + self.lib_mode = 'debug' + print('Debug device (approach) started.') + + def Disconnect(self): + self.connection = False + self.lib_mode = 'debug' + print('Debug tracker (approach) disconnected.') + + +TRACKER_CONNECTION_CLASSES = { + const.MTC: ClaronTrackerConnection, + const.FASTRAK: PolhemusTrackerConnection, + const.ISOTRAKII: PolhemusTrackerConnection, + const.PATRIOT: PolhemusTrackerConnection, + const.CAMERA: CameraTrackerConnection, + const.POLARIS: PolarisTrackerConnection, + const.POLARISP4: PolarisP4TrackerConnection, + const.OPTITRACK: OptitrackTrackerConnection, + const.DEBUGTRACKRANDOM: DebugTrackerRandomConnection, + const.DEBUGTRACKAPPROACH: DebugTrackerApproachConnection, +} + + +def CreateTrackerConnection(tracker_id): + """ + Initialize spatial tracker connection for coordinate detection during navigation. + + :param tracker_id: ID of tracking device. + :return spatial tracker connection instance or None if could not open device. + """ + tracker_connection_class = TRACKER_CONNECTION_CLASSES[tracker_id] + + # XXX: A better solution than to pass a 'model' parameter to the constructor of tracker + # connection would be to have separate class for each model, possibly inheriting + # the same base class, e.g., in this case, PolhemusTrackerConnection base class, which + # would be inherited by FastrakTrackerConnection class, etc. + if tracker_id == const.FASTRAK: + model = 'fastrak' + elif tracker_id == const.ISOTRAKII: + model = 'isotrak' + elif tracker_id == const.PATRIOT: + model = 'patriot' + else: + model = None + + tracker_connection = tracker_connection_class( + model=model + ) + return tracker_connection + + + +''' +Deprecated Code + class RobotTrackerConnection(TrackerConnection): def __init__(self, model=None): super().__init__(model) @@ -590,80 +672,4 @@ def SetConfiguration(self, configuration): self.configuration = configuration return True - -class DebugTrackerRandomConnection(TrackerConnection): - def __init__(self, model=None): - super().__init__(model) - - def Configure(self): - return True - - def Connect(self): - self.connection = True - self.lib_mode = 'debug' - print('Debug device (random) started.') - - def Disconnect(self): - self.connection = False - self.lib_mode = 'debug' - print('Debug tracker (random) disconnected.') - - -class DebugTrackerApproachConnection(TrackerConnection): - def __init__(self, model=None): - super().__init__(model) - - def Configure(self): - return True - - def Connect(self): - self.connection = True - self.lib_mode = 'debug' - print('Debug device (approach) started.') - - def Disconnect(self): - self.connection = False - self.lib_mode = 'debug' - print('Debug tracker (approach) disconnected.') - - -TRACKER_CONNECTION_CLASSES = { - const.MTC: ClaronTrackerConnection, - const.FASTRAK: PolhemusTrackerConnection, - const.ISOTRAKII: PolhemusTrackerConnection, - const.PATRIOT: PolhemusTrackerConnection, - const.CAMERA: CameraTrackerConnection, - const.POLARIS: PolarisTrackerConnection, - const.POLARISP4: PolarisP4TrackerConnection, - const.OPTITRACK: OptitrackTrackerConnection, - const.DEBUGTRACKRANDOM: DebugTrackerRandomConnection, - const.DEBUGTRACKAPPROACH: DebugTrackerApproachConnection, -} - - -def CreateTrackerConnection(tracker_id): - """ - Initialize spatial tracker connection for coordinate detection during navigation. - - :param tracker_id: ID of tracking device. - :return spatial tracker connection instance or None if could not open device. - """ - tracker_connection_class = TRACKER_CONNECTION_CLASSES[tracker_id] - - # XXX: A better solution than to pass a 'model' parameter to the constructor of tracker - # connection would be to have separate class for each model, possibly inheriting - # the same base class, e.g., in this case, PolhemusTrackerConnection base class, which - # would be inherited by FastrakTrackerConnection class, etc. - if tracker_id == const.FASTRAK: - model = 'fastrak' - elif tracker_id == const.ISOTRAKII: - model = 'isotrak' - elif tracker_id == const.PATRIOT: - model = 'patriot' - else: - model = None - - tracker_connection = tracker_connection_class( - model=model - ) - return tracker_connection +''' \ No newline at end of file From 5ea45e8193d1bb4d175f45666cc6f14a5d0b654f Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 10 Aug 2023 11:20:23 +0300 Subject: [PATCH 59/99] FIX: bug fix --- invesalius/gui/task_navigator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 7d3c9573d..2d6a96e4f 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1240,7 +1240,8 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect checkbox_track_object.SetBackgroundColour(GREY_COLOR) checkbox_track_object.SetBitmap(BMP_TRACK) checkbox_track_object.SetValue(False) - checkbox_track_object.Enable(False) + if not self.track_obj: + checkbox_track_object.Enable(False) checkbox_track_object.SetToolTip(tooltip) checkbox_track_object.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackObjectCheckbox, ctrl=checkbox_track_object)) self.checkbox_track_object = checkbox_track_object From e3014d206f57ce99e33ab7aa6ead2fd5b518b2fa Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 10 Aug 2023 16:17:27 +0300 Subject: [PATCH 60/99] FIX: bug fix --- invesalius/gui/dialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 2a0c57ede..b3cfae521 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -5773,7 +5773,7 @@ def _init_gui(self): if not self.robot.robot_status: btn_load.Enable(False) else: - self.btn_load.Enable(True) + btn_load.Enable(True) if self.GetAcquiredPoints() >= 3: self.btn_apply_reg.Enable(True) self.btn_load = btn_load @@ -5948,7 +5948,7 @@ def LoadRegistration(self, evt): Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) # Enable 'Ok' button if connection to robot is ok. - if self.robot_status: + if self.robot.robot_status: self.btn_ok.Enable(True) def GetValue(self): From 979dbec2d2a977f46b69593a352ab7771e4fc844 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 10 Aug 2023 16:17:43 +0300 Subject: [PATCH 61/99] FIX: bug fix --- invesalius/gui/default_viewers.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/invesalius/gui/default_viewers.py b/invesalius/gui/default_viewers.py index 4acf6c57b..f2cb5f581 100644 --- a/invesalius/gui/default_viewers.py +++ b/invesalius/gui/default_viewers.py @@ -360,8 +360,8 @@ def __init__(self, parent): self.button_raycasting = pbtn.PlateButton(self, -1,"", BMP_RAYCASTING, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) self.button_stereo = pbtn.PlateButton(self, -1,"", BMP_3D_STEREO, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) self.button_slice_plane = pbtn.PlateButton(self, -1, "", BMP_SLICE_PLANE, style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) - self.button_target = pbtn.PlateButton(self, -1,"", BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE) - self.button_target.Enable(0) + #self.button_target = pbtn.PlateButton(self, -1,"", BMP_TARGET, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE) + #self.button_target.Enable(0) # self.button_3d_mask = pbtn.PlateButton(self, -1, "", BMP_3D_MASK, style=pbtn.PB_STYLE_SQUARE|pbtn.PB_STYLE_TOGGLE, size=ICON_SIZE) # VOLUME VIEW ANGLE BUTTON @@ -385,7 +385,7 @@ def __init__(self, parent): sizer.Add(self.button_view, 0, wx.TOP|wx.BOTTOM, 1) sizer.Add(self.button_slice_plane, 0, wx.TOP|wx.BOTTOM, 1) sizer.Add(self.button_stereo, 0, wx.TOP|wx.BOTTOM, 1) - sizer.Add(self.button_target, 0, wx.TOP | wx.BOTTOM, 1) + #sizer.Add(self.button_target, 0, wx.TOP | wx.BOTTOM, 1) # sizer.Add(self.button_3d_mask, 0, wx.TOP | wx.BOTTOM, 1) self.navigation_status = False @@ -409,7 +409,7 @@ def __bind_events(self): 'Change volume viewer gui colour') Publisher.subscribe(self.DisablePreset, 'Close project data') Publisher.subscribe(self.Uncheck, 'Uncheck image plane menu') - Publisher.subscribe(self.DisableVolumeCutMenu, 'Disable volume cut menu') + ''' Publisher.subscribe(self.ShowTargetButton, 'Show target button') Publisher.subscribe(self.HideTargetButton, 'Hide target button') Publisher.subscribe(self.DisableTargetMode, 'Disable target mode') @@ -417,6 +417,7 @@ def __bind_events(self): # Conditions for enabling target button: Publisher.subscribe(self.ShowCoilChecked, 'Show-coil checked') Publisher.subscribe(self.TargetSelected, 'Target selected') + ''' def DisablePreset(self): self.off_item.Check(1) @@ -428,7 +429,7 @@ def __bind_events_wx(self): self.button_view.Bind(wx.EVT_LEFT_DOWN, self.OnButtonView) self.button_colour.Bind(csel.EVT_COLOURSELECT, self.OnSelectColour) self.button_stereo.Bind(wx.EVT_LEFT_DOWN, self.OnButtonStereo) - self.button_target.Bind(wx.EVT_LEFT_DOWN, self.OnButtonTarget) + #self.button_target.Bind(wx.EVT_LEFT_DOWN, self.OnButtonTarget) def OnButtonRaycasting(self, evt): # MENU RELATED TO RAYCASTING TYPES @@ -443,6 +444,7 @@ def OnButtonView(self, evt): def OnButtonSlicePlane(self, evt): self.button_slice_plane.PopupMenu(self.slice_plane_menu) + ''' def ShowCoilChecked(self, checked): self.show_coil_checked = checked self.UpdateTargetButton() @@ -481,7 +483,7 @@ def OnButtonTarget(self, evt): Publisher.sendMessage('Enable volume camera checkbox', enabled=True) Publisher.sendMessage('Update robot target', robot_tracker_flag=False, target_index=None, target=None) - + ''' def OnSavePreset(self, evt): d = wx.TextEntryDialog(self, _("Preset name")) if d.ShowModal() == wx.ID_OK: From 77582028cd0bba76f0f2dad986d8af3eb59f32ac Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 10 Aug 2023 16:17:54 +0300 Subject: [PATCH 62/99] FIX: bug fix --- invesalius/gui/preferences.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 29d89eba7..a58723d77 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -544,7 +544,7 @@ def __init__(self, parent, tracker, robot): (btn_rob, 0, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15), (status_text, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15), (0, 0), - (btn_rob_con, 0, wx.RIGHT) + (btn_rob_con, 0, wx.LEFT | wx.ALIGN_CENTER_HORIZONTAL, 15) ]) rob_static_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Setup robot")) @@ -577,7 +577,7 @@ def LoadConfig(self): def OnChooseTracker(self, evt, ctrl): self.HideParent() - + Publisher.sendMessage('Begin busy cursor') Publisher.sendMessage('Update status text in GUI', label=_("Configuring tracker ...")) if hasattr(evt, 'GetSelection'): @@ -593,7 +593,8 @@ def OnChooseTracker(self, evt, ctrl): Publisher.sendMessage("Tracker changed") ctrl.SetSelection(self.tracker.tracker_id) self.ShowParent() - + Publisher.sendMessage('End busy cursor') + def OnChooseReferenceMode(self, evt, ctrl): # Probably need to refactor object registration as a whole to use the # OnChooseReferenceMode function which was used earlier. It can be found in @@ -614,6 +615,9 @@ def OnChoiceIP(self, evt, ctrl): def OnRobotConnect(self, evt): if self.robot_ip is not None: + self.robot.DisconnectRobot() + self.status_text.SetLabelText("Trying to connect to robot...") + self.btn_rob_con.Hide() self.robot.SetRobotIP(self.robot_ip) Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) @@ -628,11 +632,12 @@ def OnRobotStatus(self, data): self.btn_rob_con.Show() def OnTransformationMatrix(self, data): - if data: + if self.robot.matrix_tracker_to_robot is not None: self.status_text.SetLabelText("Robot is fully setup!") self.btn_rob_con.SetLabel("Register Again") self.btn_rob_con.Show() self.btn_rob_con.Layout() + self.Parent.Update() class Language(wx.Panel): def __init__(self, parent): From 045f366f90b74fa528c19ed2e8de626715672a2a Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 10 Aug 2023 16:18:04 +0300 Subject: [PATCH 63/99] FIX: bug fix --- invesalius/gui/task_navigator.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 2d6a96e4f..aad1d9413 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1155,11 +1155,14 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.pedal_connection = pedal_connection self.neuronavigation_api = neuronavigation_api + self.control_panel = ControlPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.image, self.pedal_connection, self.neuronavigation_api) + self.marker_panel = MarkersPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.control_panel) + top_sizer = wx.BoxSizer(wx.HORIZONTAL) - top_sizer.Add(MarkersPanel(self, self.navigation, self.tracker, self.robot, self.icp), 1, wx.GROW | wx.EXPAND ) + top_sizer.Add(self.marker_panel, 1, wx.GROW | wx.EXPAND ) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) - bottom_sizer.Add(ControlPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.image, self.pedal_connection, self.neuronavigation_api), 0, wx.EXPAND | wx.TOP, 20) + bottom_sizer.Add(self.control_panel, 0, wx.EXPAND | wx.TOP, 20) main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.AddMany([(top_sizer, 1, wx.EXPAND | wx.GROW), @@ -1318,6 +1321,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect target_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTargetCheckbox, ctrl=target_checkbox)) target_checkbox.SetToolTip(tooltip) self.target_checkbox = target_checkbox + self.UpdateTargetButton() #Sizers button_sizer = wx.BoxSizer(wx.VERTICAL) @@ -1446,7 +1450,6 @@ def OnNavigate(self, evt, btn_nav): nav_id = btn_nav.GetValue() if not nav_id: wx.CallAfter(Publisher.sendMessage, 'Stop navigation') - Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) tooltip = wx.ToolTip(_("Start neuronavigation")) btn_nav.SetToolTip(tooltip) btn_nav.SetLabelText(_("Start neuronavigation")) @@ -1472,6 +1475,8 @@ def UpdateTarget(self, coord): self.EnableToggleButton(self.lock_to_target_checkbox, 1) self.UpdateToggleButton(self.lock_to_target_checkbox, True) self.navigation.SetLockToTarget(True) + self.target_selected = True + self.UpdateTargetButton() def UpdateNavigationStatus(self, nav_status, vis_status): if not nav_status: @@ -1501,6 +1506,7 @@ def OnCheckStatus(self, nav_status, vis_status): def OnRobotStatus(self, data): if data: self.btn_robot.Show() + self.Layout() def OnStopRobot(self, evt, ctrl): Publisher.sendMessage('Update robot target', robot_tracker_flag=False, @@ -1564,6 +1570,7 @@ def OnShowCoil(self, evt=None): self.UpdateToggleButton(self.checkobj) checked = self.checkobj.GetValue() Publisher.sendMessage('Show-coil checked', checked=checked) + self.show_coil_checked = checked # 'Volume camera' checkbox @@ -1610,8 +1617,9 @@ def ShowCoilChecked(self, checked): self.UpdateTargetButton() def TargetSelected(self, status): - self.target_selected = status - self.UpdateTargetButton() + if status is not None: + self.target_selected = status + self.UpdateTargetButton() def ShowTargetButton(self): self.target_checkbox.Show() @@ -1620,24 +1628,24 @@ def HideTargetButton(self): self.target_checkbox.Hide() def DisableTargetMode(self): - self.OnTargetCheckbox(False) self.UpdateToggleButton(self.target_checkbox, False) + self.OnTargetCheckbox(False) def UpdateTargetButton(self): if self.target_selected and self.show_coil_checked: self.EnableToggleButton(self.target_checkbox, True) + self.UpdateToggleButton(self.target_checkbox, False) else: self.DisableTargetMode() self.EnableToggleButton(self.target_checkbox, False) def OnTargetCheckbox(self, evt): - if not self.target_checkbox.GetValue() and evt is not False: + if self.target_checkbox.GetValue(): self.UpdateToggleButton(self.target_checkbox, True) Publisher.sendMessage('Target navigation mode', target_mode=self.target_checkbox.GetValue()) Publisher.sendMessage('Check volume camera checkbox', checked=False) Publisher.sendMessage('Enable volume camera checkbox', enabled=False) - - elif self.target_checkbox.GetValue() or evt is False: + else: self.UpdateToggleButton(self.target_checkbox, False) Publisher.sendMessage('Target navigation mode', target_mode=self.target_checkbox.GetValue()) Publisher.sendMessage('Enable volume camera checkbox', enabled=True) @@ -1769,7 +1777,7 @@ def to_dict(self): } - def __init__(self, parent, navigation, tracker, robot, icp): + def __init__(self, parent, navigation, tracker, robot, icp, control): wx.Panel.__init__(self, parent) try: default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) @@ -1783,6 +1791,7 @@ def __init__(self, parent, navigation, tracker, robot, icp): self.tracker = tracker self.robot = robot self.icp = icp + self.control = control if has_mTMS: self.mTMS = mTMS() else: @@ -2040,6 +2049,7 @@ def __set_marker_as_target(self, idx, display_messagebox=True): self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, _("Yes")) Publisher.sendMessage('Update target', coord=self.markers[idx].position+self.markers[idx].orientation) + self.control.target_selected = True Publisher.sendMessage('Set target transparency', status=True, index=idx) #self.__delete_all_brain_targets() if display_messagebox: From 9eb391f43dddcc6a64644971464c06e89ea122fe Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 10 Aug 2023 16:18:54 +0300 Subject: [PATCH 64/99] FIX: bug fix These message boxes stop the building of the frame when connecting to robot from config. --- invesalius/navigation/robot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invesalius/navigation/robot.py b/invesalius/navigation/robot.py index 72866913a..7b62ed30b 100644 --- a/invesalius/navigation/robot.py +++ b/invesalius/navigation/robot.py @@ -113,12 +113,12 @@ def SetRobotIP(self, data): def ConnectToRobot(self): Publisher.sendMessage('Connect to robot', robot_IP=self.robot_ip) - wx.MessageBox(_("Connected to Robot!"), _("InVesalius 3")) + print("Connected to Robot!") def InitializeRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=True) Publisher.sendMessage('Load robot transformation matrix', data=self.matrix_tracker_to_robot.tolist()) - wx.MessageBox(_("Robot Initialized!"), _("InVesalius 3")) + print("Robot Initialized!") def DisconnectRobot(self): Publisher.sendMessage('Robot navigation mode', robot_mode=False) From cdbc01b12f9f6a799a70d113871f8801f0f3e18b Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 11 Aug 2023 14:40:43 +0300 Subject: [PATCH 65/99] FIX: merging bugs --- invesalius/gui/default_viewers.py | 3 +- invesalius/gui/preferences.py | 56 +++++++------ invesalius/gui/task_efield.py | 86 ++++++++++++++++--- invesalius/gui/task_navigator.py | 132 +++++++++++++++++++++++++++++- 4 files changed, 239 insertions(+), 38 deletions(-) diff --git a/invesalius/gui/default_viewers.py b/invesalius/gui/default_viewers.py index 3c15d4996..e38cf154e 100644 --- a/invesalius/gui/default_viewers.py +++ b/invesalius/gui/default_viewers.py @@ -417,8 +417,9 @@ def __bind_events(self): # Conditions for enabling target button: Publisher.subscribe(self.TargetSelected, 'Target selected') - ''' Publisher.subscribe(self.TrackObject, 'Track object') + ''' + def DisablePreset(self): self.off_item.Check(1) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index a58723d77..3916a8c9d 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -18,6 +18,7 @@ from invesalius.navigation.tracker import Tracker from invesalius.navigation.robot import Robot from invesalius.net.neuronavigation_api import NeuronavigationApi +from invesalius.navigation.navigation import Navigation HAS_PEDAL_CONNECTION = True try: @@ -36,9 +37,12 @@ def __init__( super().__init__(parent, id_, title, style=style) tracker = Tracker() robot = Robot() - pedal_connection = PedalConnection() if HAS_PEDAL_CONNECTION else None neuronavigation_api = NeuronavigationApi() + navigation = Navigation( + pedal_connection=pedal_connection, + neuronavigation_api=neuronavigation_api, + ) self.book = wx.Notebook(self, -1) @@ -50,7 +54,7 @@ def __init__( mode = session.GetConfig('mode') if mode == const.MODE_NAVIGATOR: self.pnl_tracker = TrackerPage(self.book, tracker, robot) - self.pnl_object = ObjectPage(self.book, tracker, pedal_connection, neuronavigation_api) + self.pnl_object = ObjectPage(self.book, navigation, tracker, pedal_connection, neuronavigation_api) self.book.AddPage(self.pnl_tracker, _("Tracker")) self.book.AddPage(self.pnl_object, _("Stimulator")) self.book.AddPage(self.pnl_language, _("Language")) @@ -173,7 +177,7 @@ def LoadSelection(self, values): self.rb_inter_sl.SetSelection(int(slice_interpolation)) class ObjectPage(wx.Panel): - def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): + def __init__(self, parent, navigation, tracker, pedal_connection, neuronavigation_api): wx.Panel.__init__(self, parent) self.coil_list = const.COIL @@ -181,12 +185,12 @@ def __init__(self, parent, tracker, pedal_connection, neuronavigation_api): self.tracker = tracker self.pedal_connection = pedal_connection self.neuronavigation_api = neuronavigation_api - - self.__bind_events() + self.navigation = navigation self.obj_fiducials = None self.obj_orients = None self.obj_ref_mode = None self.obj_name = None + self.__bind_events() self.timestamp = const.TIMESTAMP self.state = self.LoadConfig() @@ -331,24 +335,24 @@ def LoadConfig(self): self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name = object_fiducials, object_orientations, object_reference_mode, object_name return True - + def OnCreateNewCoil(self, event=None): if self.tracker.IsTrackerInitialized(): dialog = dlg.ObjectCalibrationDialog(self.tracker, self.pedal_connection, self.neuronavigation_api) try: if dialog.ShowModal() == wx.ID_OK: - self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name, polydata, use_default_object = dialog.GetValue() + obj_fiducials, obj_orients, obj_ref_mode, obj_name, polydata, use_default_object = dialog.GetValue() self.neuronavigation_api.update_coil_mesh(polydata) - if np.isfinite(self.obj_fiducials).all() and np.isfinite(self.obj_orients).all(): + if np.isfinite(obj_fiducials).all() and np.isfinite(obj_orients).all(): Publisher.sendMessage('Update object registration', - data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) + data=(obj_fiducials, obj_orients, obj_ref_mode, obj_name)) Publisher.sendMessage('Update status text in GUI', label=_("Ready")) Publisher.sendMessage( 'Configure object', - obj_name=self.obj_name, + obj_name=obj_name, polydata=polydata, use_default_object=use_default_object, ) @@ -369,6 +373,9 @@ def OnCreateNewCoil(self, event=None): def OnLoadCoil(self, event=None): filename = dlg.ShowLoadSaveDialog(message=_(u"Load object registration"), wildcard=_("Registration files (*.obr)|*.obr")) + # data_dir = os.environ.get('OneDrive') + r'\data\dti_navigation\baran\anat_reg_improve_20200609' + # coil_path = 'magstim_coil_dell_laptop.obr' + # filename = os.path.join(data_dir, coil_path) try: if filename: @@ -376,33 +383,33 @@ def OnLoadCoil(self, event=None): data = [s.split('\t') for s in text_file.readlines()] registration_coordinates = np.array(data[1:]).astype(np.float32) - self.obj_fiducials = registration_coordinates[:, :3] - self.obj_orients = registration_coordinates[:, 3:] + obj_fiducials = registration_coordinates[:, :3] + obj_orients = registration_coordinates[:, 3:] - self.obj_name = data[0][1].encode(const.FS_ENCODE) - self.obj_ref_mode = int(data[0][-1]) + obj_name = data[0][1].encode(const.FS_ENCODE) + obj_ref_mode = int(data[0][-1]) - if not os.path.exists(self.obj_name): - self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + if not os.path.exists(obj_name): + obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") - polydata = vtk_utils.CreateObjectPolyData(self.obj_name) + polydata = vtk_utils.CreateObjectPolyData(obj_name) if polydata: self.neuronavigation_api.update_coil_mesh(polydata) else: - self.obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") + obj_name = os.path.join(inv_paths.OBJ_DIR, "magstim_fig8_coil.stl") - if os.path.basename(self.obj_name) == "magstim_fig8_coil.stl": + if os.path.basename(obj_name) == "magstim_fig8_coil.stl": use_default_object = True else: use_default_object = False Publisher.sendMessage('Update object registration', - data=(self.obj_fiducials, self.obj_orients, self.obj_ref_mode, self.obj_name)) + data=(obj_fiducials, obj_orients, obj_ref_mode, obj_name)) Publisher.sendMessage('Update status text in GUI', label=_("Object file successfully loaded")) Publisher.sendMessage( 'Configure object', - obj_name=self.obj_name, + obj_name=obj_name, polydata=polydata, use_default_object=use_default_object ) @@ -423,7 +430,8 @@ def OnLoadCoil(self, event=None): Publisher.sendMessage('Update status text in GUI', label="") def OnSaveCoil(self, evt): - if np.isnan(self.obj_fiducials).any() or np.isnan(self.obj_orients).any(): + obj_fiducials, obj_orients, obj_ref_mode, obj_name = self.navigation.GetObjectRegistration() + if np.isnan(obj_fiducials).any() or np.isnan(obj_orients).any(): wx.MessageBox(_("Digitize all object fiducials before saving"), _("Save error")) else: filename = dlg.ShowLoadSaveDialog(message=_(u"Save object registration as..."), @@ -431,8 +439,8 @@ def OnSaveCoil(self, evt): style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, default_filename="object_registration.obr", save_ext="obr") if filename: - hdr = 'Object' + "\t" + utils.decode(self.obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % self.obj_ref_mode) - data = np.hstack([self.obj_fiducials, self.obj_orients]) + hdr = 'Object' + "\t" + utils.decode(obj_name, const.FS_ENCODE) + "\t" + 'Reference' + "\t" + str('%d' % obj_ref_mode) + data = np.hstack([obj_fiducials, obj_orients]) np.savetxt(filename, data, fmt='%.4f', delimiter='\t', newline='\n', header=hdr) wx.MessageBox(_("Object file successfully saved"), _("Save")) diff --git a/invesalius/gui/task_efield.py b/invesalius/gui/task_efield.py index 5dc6a4a2f..e91d4eb18 100644 --- a/invesalius/gui/task_efield.py +++ b/invesalius/gui/task_efield.py @@ -136,6 +136,11 @@ def __init__(self, parent, navigation): enable_efield.Bind(wx.EVT_CHECKBOX, partial(self.OnEnableEfield, ctrl=enable_efield)) self.enable_efield = enable_efield + plot_vectors = wx.CheckBox(self, -1, _('Plot Efield vectors')) + plot_vectors.SetValue(False) + plot_vectors.Enable(1) + plot_vectors.Bind(wx.EVT_CHECKBOX, partial(self.OnEnablePlotVectors, ctrl=plot_vectors)) + tooltip2 = wx.ToolTip(_("Load Brain Json config")) btn_act2 = wx.Button(self, -1, _("Load Config"), size=wx.Size(100, 23)) btn_act2.SetToolTip(tooltip2) @@ -148,6 +153,12 @@ def __init__(self, parent, navigation): self.btn_save.Bind(wx.EVT_BUTTON, self.OnSaveEfield) self.btn_save.Enable(False) + tooltip3 = wx.ToolTip(_("Save All Efield")) + self.btn_all_save = wx.Button(self, -1, _("Save All Efield"), size=wx.Size(80, -1)) + self.btn_all_save.SetToolTip(tooltip3) + self.btn_all_save.Bind(wx.EVT_BUTTON, self.OnSaveAllDataEfield) + self.btn_all_save.Enable(False) + text_sleep = wx.StaticText(self, -1, _("Sleep (s):")) spin_sleep = wx.SpinCtrlDouble(self, -1, "", size = wx.Size(50,23), inc = 0.01) spin_sleep.Enable(1) @@ -165,11 +176,12 @@ def __init__(self, parent, navigation): line_btns_save = wx.BoxSizer(wx.HORIZONTAL) line_btns_save.Add(self.btn_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) + line_btns_save.Add(self.btn_all_save, 1, wx.LEFT | wx.TOP | wx.RIGHT, 2) # Add line sizers into main sizer border_last = 5 - txt_surface = wx.StaticText(self, -1, _('Change coil:'), pos=(0,100)) - self.combo_surface_name = wx.ComboBox(self, -1, size=(210, 23), pos=(25, 50), + txt_surface = wx.StaticText(self, -1, _('Change coil:'), pos=(20,100)) + self.combo_surface_name = wx.ComboBox(self, -1, size=(100, 23), pos=(25, 20), style=wx.CB_DROPDOWN | wx.CB_READONLY) # combo_surface_name.SetSelection(0) self.combo_surface_name.Bind(wx.EVT_COMBOBOX_DROPDOWN, self.OnComboCoilNameClic) @@ -180,8 +192,9 @@ def __init__(self, parent, navigation): main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(line_btns, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border_last) main_sizer.Add(enable_efield, 1, wx.LEFT | wx.RIGHT, 2) - main_sizer.Add(line_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) - main_sizer.Add(self.combo_surface_name, 1, wx.BOTTOM | wx.ALIGN_RIGHT) + main_sizer.Add(plot_vectors, 1, wx.LEFT | wx.RIGHT, 2) + main_sizer.Add(self.combo_surface_name, 1, wx.ALIGN_CENTER_HORIZONTAL,2) + main_sizer.Add(line_sleep, 0, wx.LEFT | wx.RIGHT | wx.TOP, border) main_sizer.Add(line_btns_save, 0, wx.BOTTOM | wx.ALIGN_CENTER_HORIZONTAL, border_last) main_sizer.SetSizeHints(self) @@ -192,6 +205,8 @@ def __bind_events(self): Publisher.subscribe(self.OnGetEfieldActor, 'Get Efield actor from json') Publisher.subscribe(self.OnGetEfieldPaths, 'Get Efield paths') Publisher.subscribe(self.OnGetMultilocusCoils,'Get multilocus paths from json') + Publisher.subscribe(self.SendNeuronavigationApi, 'Send Neuronavigation Api') + Publisher.subscribe(self.GetEfieldDataStatus, 'Get status of Efield saved data') def OnAddConfig(self, evt): filename = dlg.LoadConfigEfield() @@ -200,13 +215,16 @@ def OnAddConfig(self, evt): Publisher.sendMessage('Update status in GUI', value=50, label="Loading E-field...") Publisher.sendMessage('Update convert_to_inv flag', convert_to_inv=convert_to_inv) Publisher.sendMessage('Read json config file for efield', filename=filename, convert_to_inv=convert_to_inv) + self.e_field_brain = brain.E_field_brain(self.e_field_mesh) self.Init_efield() + def Init_efield(self): self.navigation.neuronavigation_api.initialize_efield( cortex_model_path=self.cortex_file, mesh_models_paths=self.meshes_file, coil_model_path=self.coil, + coil_set = False, conductivities_inside=self.ci, conductivities_outside=self.co, ) @@ -232,19 +250,23 @@ def OnEnableEfield(self, evt, ctrl): self.enable_efield.Enable(False) self.e_field_loaded = False return - self.e_field_brain = brain.E_field_brain(self.e_field_mesh) Publisher.sendMessage('Initialize E-field brain', e_field_brain=self.e_field_brain) Publisher.sendMessage('Initialize color array') self.e_field_loaded = True self.combo_surface_name.Enable(True) - self.btn_save.Enable(True) + self.btn_all_save.Enable(True) + else: Publisher.sendMessage('Recolor again') self.e_field_loaded = False #self.combo_surface_name.Enable(True) self.navigation.e_field_loaded = self.e_field_loaded + def OnEnablePlotVectors(self, evt, ctrl): + self.plot_efield_vectors = ctrl.GetValue() + self.navigation.plot_efield_vectors = self.plot_efield_vectors + def OnComboNameClic(self, evt): import invesalius.project as prj proj = prj.Project() @@ -261,20 +283,27 @@ def OnComboCoilNameClic(self, evt): def OnComboCoil(self, evt): coil_name = evt.GetString() coil_index = evt.GetSelection() - self.OnChangeCoil(self.multilocus_coil[coil_index]) + if coil_index==6: + coil_set = True + else: + coil_set = False + self.OnChangeCoil(self.multilocus_coil[coil_index], coil_set) #self.e_field_mesh = self.proj.surface_dict[self.surface_index].polydata #Publisher.sendMessage('Get Actor', surface_index = self.surface_index) - def OnChangeCoil(self, coil_model_path): + def OnChangeCoil(self, coil_model_path, coil_set): self.navigation.neuronavigation_api.efield_coil( coil_model_path=coil_model_path, + coil_set= coil_set ) def UpdateNavigationStatus(self, nav_status, vis_status): if nav_status: self.enable_efield.Enable(False) + self.btn_save.Enable(True) else: self.enable_efield.Enable(True) + self.btn_save.Enable(False) def OnSelectSleep(self, evt, ctrl): self.sleep_nav = ctrl.GetValue() @@ -311,14 +340,49 @@ def OnSaveEfield(self, evt): else: current_folder_path = self.path_meshes parts = [current_folder_path,'/',stamp_date, stamp_time, proj.name, 'Efield'] - default_filename = sep.join(parts) + '.txt' + default_filename = sep.join(parts) + '.csv' filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."), - wildcard='(*.txt)|*.txt', + wildcard='(*.csv)|*.csv', style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, default_filename=default_filename) if not filename: return + plot_efield_vectors = self.navigation.plot_efield_vectors + Publisher.sendMessage('Save Efield data', filename = filename, plot_efield_vectors= plot_efield_vectors) + + def OnSaveAllDataEfield(self, evt): + Publisher.sendMessage('Check efield data') + if self.efield_data_saved: + import invesalius.project as prj + proj = prj.Project() + timestamp = time.localtime(time.time()) + stamp_date = '{:0>4d}{:0>2d}{:0>2d}'.format(timestamp.tm_year, timestamp.tm_mon, timestamp.tm_mday) + stamp_time = '{:0>2d}{:0>2d}{:0>2d}'.format(timestamp.tm_hour, timestamp.tm_min, timestamp.tm_sec) + sep = '-' + if self.path_meshes is None: + import os + current_folder_path = os.getcwd() + else: + current_folder_path = self.path_meshes + parts = [current_folder_path,'/',stamp_date, stamp_time, proj.name, 'Efield'] + default_filename = sep.join(parts) + '.csv' + + filename = dlg.ShowLoadSaveDialog(message=_(u"Save markers as..."), + wildcard='(*.csv)|*.csv', + style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT, + default_filename=default_filename) + + if not filename: + return + + Publisher.sendMessage('Save all Efield data', filename = filename) + else: + dlg.Efield_no_data_to_save_warning() + + def SendNeuronavigationApi(self): + Publisher.sendMessage('Get Neuronavigation Api', neuronavigation_api = self.navigation.neuronavigation_api) - Publisher.sendMessage('Save Efield data', filename = filename) + def GetEfieldDataStatus(self, efield_data_loaded, indexes_saved_list): + self.efield_data_saved = efield_data_loaded \ No newline at end of file diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 87ad17a2e..2df0d791a 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1153,6 +1153,8 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.pedal_connection = pedal_connection self.neuronavigation_api = neuronavigation_api + self.__bind_events() + self.control_panel = ControlPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.image, self.pedal_connection, self.neuronavigation_api) self.marker_panel = MarkersPanel(self, self.navigation, self.tracker, self.robot, self.icp, self.control_panel) @@ -1170,6 +1172,28 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect self.SetSizerAndFit(main_sizer) self.Update() + def __bind_events(self): + Publisher.subscribe(self.OnCloseProject, 'Close project data') + + def OnCloseProject(self): + Publisher.sendMessage('Disconnect tracker') + Publisher.sendMessage('Update object registration') + Publisher.sendMessage('Show and track coil', enabled=False) + Publisher.sendMessage('Delete all markers') + Publisher.sendMessage("Update marker offset state", create=False) + Publisher.sendMessage("Remove tracts") + Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) + # TODO: Reset camera initial focus + Publisher.sendMessage('Reset cam clipping range') + self.navigation.StopNavigation() + self.navigation.__init__( + pedal_connection=self.pedal_connection, + neuronavigation_api=self.neuronavigation_api + ) + self.tracker.__init__() + self.icp.__init__() + + class ControlPanel(wx.Panel): def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connection, neuronavigation_api): @@ -1381,8 +1405,8 @@ def __bind_events(self): Publisher.subscribe(self.DisableTargetMode, 'Disable target mode') # Conditions for enabling target button: - Publisher.subscribe(self.ShowCoilChecked, 'Show-coil checked') Publisher.subscribe(self.TargetSelected, 'Target selected') + Publisher.subscribe(self.TrackObject, 'Track object') # Config def SaveConfig(self): @@ -1499,6 +1523,11 @@ def OnCheckStatus(self, nav_status, vis_status): if self.track_obj: self.EnableToggleButton(self.checkobj, 1) self.UpdateToggleButton(self.checkobj) + # Enable/Disable track-object checkbox if navigation is off/on and object registration is valid. + obj_registration = self.navigation.GetObjectRegistration() + enable_track_object = obj_registration is not None and obj_registration[0] is not None and not nav_status + # Publisher.sendMessage('Enable track-object checkbox', enabled=enable_track_object) + self.EnableTrackObjectCheckbox(enable_track_object) # 'Robot' def OnRobotStatus(self, data): @@ -1528,6 +1557,9 @@ def UpdateTractsVisualization(self, data): def EnableTrackObjectCheckbox(self, enabled): self.EnableToggleButton(self.checkbox_track_object, enabled) self.UpdateToggleButton(self.checkbox_track_object) + if enabled: + checked = self.checkbox_track_object.GetValue() + self.EnableShowCoil(enabled=checked) def CheckTrackObjectCheckbox(self, checked): self.UpdateToggleButton(self.checkbox_track_object, checked) @@ -1618,6 +1650,10 @@ def TargetSelected(self, status): if status is not None: self.target_selected = status self.UpdateTargetButton() + + def TrackObject(self, enabled): + self.track_obj = enabled + self.UpdateTargetButton() def ShowTargetButton(self): self.target_checkbox.Show() @@ -1630,7 +1666,7 @@ def DisableTargetMode(self): self.OnTargetCheckbox(False) def UpdateTargetButton(self): - if self.target_selected and self.show_coil_checked: + if self.target_selected and self.track_obj: self.EnableToggleButton(self.target_checkbox, True) self.UpdateToggleButton(self.target_checkbox, False) else: @@ -1805,6 +1841,9 @@ def __init__(self, parent, navigation, tracker, robot, icp, control): self.markers = [] self.nav_status = False + self.efield_loaded = False + self.efield_data_saved = False + self.efield_target_idx = None self.target_mode = False self.marker_colour = const.MARKER_COLOUR @@ -1915,6 +1954,9 @@ def __bind_events(self): Publisher.subscribe(self.UpdateMarkerOrientation, 'Open marker orientation dialog') Publisher.subscribe(self.OnActivateTargetMode, 'Target navigation mode') Publisher.subscribe(self.AddPeeledSurface, 'Update peel') + Publisher.subscribe(self.GetEfieldDataStatus, 'Get status of Efield saved data') + Publisher.subscribe(self.GetIdList, 'Get ID list') + Publisher.subscribe(self.GetRotationPosition, 'Send coil position and rotation') def SaveState(self): state = [marker.to_dict() for marker in self.markers] @@ -2103,6 +2145,29 @@ def OnMouseRightDown(self, evt): send_brain_target_menu = menu_id.Append(6, _('Send brain target to mTMS')) menu_id.Bind(wx.EVT_MENU, self.OnSendBrainTarget, send_brain_target_menu) + if self.nav_status and self.navigation.e_field_loaded: + #Publisher.sendMessage('Check efield data') + #if not tuple(np.argwhere(self.indexes_saved_lists == self.marker_list_ctrl.GetFocusedItem())): + if self.__find_target_marker() == self.marker_list_ctrl.GetFocusedItem(): + efield_menu = menu_id.Append(8, _('Save Efield target Data')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuSaveEfieldTargetData, efield_menu) + + if self.navigation.e_field_loaded: + Publisher.sendMessage('Check efield data') + if self.efield_data_saved: + if tuple(np.argwhere(self.indexes_saved_lists==self.marker_list_ctrl.GetFocusedItem())): + if self.efield_target_idx == self.marker_list_ctrl.GetFocusedItem(): + efield_target_menu = menu_id.Append(9, _('Remove Efield target')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuRemoveEfieldTarget, efield_target_menu ) + else: + efield_target_menu = menu_id.Append(9, _('Set as Efield target')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuSetEfieldTarget, efield_target_menu) + + if self.navigation.e_field_loaded and not self.nav_status: + if self.__find_target_marker() == self.marker_list_ctrl.GetFocusedItem(): + efield_vector_plot_menu = menu_id.Append(10,_('Show vector field')) + menu_id.Bind(wx.EVT_MENU, self.OnMenuShowVectorField, efield_vector_plot_menu) + menu_id.AppendSeparator() # Enable "Send target to robot" button only if tracker is robot, if navigation is on and if target is not none @@ -2156,6 +2221,57 @@ def OnMenuSetTarget(self, evt): self.SaveState() + def GetEfieldDataStatus(self, efield_data_loaded, indexes_saved_list): + self.indexes_saved_lists= [] + self.efield_data_saved = efield_data_loaded + self.indexes_saved_lists = indexes_saved_list + + def OnMenuShowVectorField(self, evt): + session = ses.Session() + list_index = self.marker_list_ctrl.GetFocusedItem() + position = self.markers[list_index].position + orientation = np.radians(self.markers[list_index].orientation) + Publisher.sendMessage('Calculate position and rotation', position=position, orientation=orientation) + coord = [position, orientation] + coord = np.array(coord).flatten() + + #Check here, it resets the radious list + Publisher.sendMessage('Update interseccion offline', m_img =self.m_img_offline, coord = coord) + + if session.GetConfig('debug_efield'): + enorm = self.navigation.debug_efield_enorm + else: + enorm = self.navigation.neuronavigation_api.update_efield_vectorROI(position=self.cp, + orientation=orientation, + T_rot=self.T_rot, + id_list=self.ID_list) + enorm_data = [self.T_rot, self.cp, coord, enorm, self.ID_list] + Publisher.sendMessage('Get enorm', enorm_data = enorm_data , plot_vector = True) + + def GetRotationPosition(self, T_rot, cp, m_img): + self.T_rot = T_rot + self.cp = cp + self.m_img_offline = m_img + + def GetIdList(self, ID_list): + self.ID_list = ID_list + + def OnMenuSetEfieldTarget(self,evt): + idx = self.marker_list_ctrl.GetFocusedItem() + if idx == -1: + wx.MessageBox(_("No data selected."), _("InVesalius 3")) + return + self.__set_marker_as_target(idx) + self.efield_target_idx = idx + Publisher.sendMessage('Get target index efield', target_index_list = idx ) + + def OnMenuSaveEfieldTargetData(self,evt): + list_index = self.marker_list_ctrl.GetFocusedItem() + position = self.markers[list_index].position + orientation = self.markers[list_index].orientation + plot_efield_vectors = self.navigation.plot_efield_vectors + Publisher.sendMessage('Save target data', target_list_index = list_index, position = position, orientation = orientation, plot_efield_vectors= plot_efield_vectors) + def OnMenuSetCoilOrientation(self, evt): list_index = self.marker_list_ctrl.GetFocusedItem() position = self.markers[list_index].position @@ -2171,6 +2287,18 @@ def OnMenuSetCoilOrientation(self, evt): self.SaveState() + def OnMenuRemoveEfieldTarget(self,evt): + idx = self.marker_list_ctrl.GetFocusedItem() + self.markers[idx].is_target = False + self.marker_list_ctrl.SetItemBackgroundColour(idx, 'white') + Publisher.sendMessage('Set target transparency', status=False, index=idx) + self.marker_list_ctrl.SetItem(idx, const.TARGET_COLUMN, "") + Publisher.sendMessage('Disable or enable coil tracker', status=False) + Publisher.sendMessage('Update target', coord=None) + self.efield_target_idx = None + #self.__delete_all_brain_targets() + wx.MessageBox(_("Efield target removed."), _("InVesalius 3")) + def OnMenuRemoveTarget(self, evt): idx = self.marker_list_ctrl.GetFocusedItem() self.markers[idx].is_target = False From 3f2b941c27910328fb33ad720bb5aa41280482ab Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 14 Aug 2023 09:50:05 +0300 Subject: [PATCH 66/99] FIX: Bug Fix --- invesalius/gui/task_navigator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 2df0d791a..075dce7ba 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1128,9 +1128,11 @@ def __bind_events(self): Publisher.subscribe(self.OnObjectUpdate, 'Update object registration') def OnObjectUpdate(self, data=None): - self.object_reg = self.navigation.GetObjectRegistration() self.lbl.SetLabel("Current Configuration:") - self.config_txt.SetLabelText(os.path.basename(self.object_reg[-1])) + if self.object_reg is not None: + self.config_txt.SetLabelText(os.path.basename(self.object_reg[-1])) + else: + self.config_txt.SetLabelText("None") self.lbl.Show() self.config_txt.Show() self.next_button.Enable() From fb87e6cd127dbce273ac54aea60fcbd0cf913da8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 14 Aug 2023 10:11:18 +0300 Subject: [PATCH 67/99] FIX: Target mode --- invesalius/gui/task_navigator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 075dce7ba..b8d148ff2 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1342,7 +1342,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect target_checkbox.SetBitmap(BMP_TARGET) target_checkbox.SetValue(False) target_checkbox.Enable(False) - target_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTargetCheckbox, ctrl=target_checkbox)) + target_checkbox.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTargetCheckbox)) target_checkbox.SetToolTip(tooltip) self.target_checkbox = target_checkbox self.UpdateTargetButton() From f437870e8596ba9e935ec2bc9b65f8173c8afe83 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Mon, 14 Aug 2023 10:15:40 +0300 Subject: [PATCH 68/99] FIX: Object registration --- invesalius/gui/task_navigator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index b8d148ff2..5590732b0 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1127,8 +1127,12 @@ def __init__(self, parent, navigation): def __bind_events(self): Publisher.subscribe(self.OnObjectUpdate, 'Update object registration') + def UpdateObjectRegistration(self): + self.object_reg = self.navigation.GetObjectRegistration() + def OnObjectUpdate(self, data=None): self.lbl.SetLabel("Current Configuration:") + self.UpdateObjectRegistration() if self.object_reg is not None: self.config_txt.SetLabelText(os.path.basename(self.object_reg[-1])) else: From 23bc0394a529517379eba6f27a998399fa9e725b Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 10:43:52 +0300 Subject: [PATCH 69/99] UPD: Minor update to object in preferences --- invesalius/gui/preferences.py | 63 +++++++++++++++-------------------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 3916a8c9d..7b0ecc773 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -219,44 +219,30 @@ def __init__(self, parent, navigation, tracker, pedal_connection, neuronavigatio self.btn_save = btn_save if self.state: - lbl = wx.StaticText(self, -1, _("Current Configuration:")) - lbl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) config_txt = wx.StaticText(self, -1, os.path.basename(self.obj_name)) - - lbl_new = wx.StaticText(self, -1, _("Create a new stimulator registration: ")) - lbl_load = wx.StaticText(self, -1, _("Load a stimulator registration: ")) - lbl_save = wx.StaticText(self, -1, _("Save current stimulator registration: ")) - - load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Stimulator registration")) - inner_load_sizer = wx.FlexGridSizer(2, 4, 5) - inner_load_sizer.AddMany([ - (lbl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (config_txt, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (lbl_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (btn_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (lbl_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (btn_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (lbl_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (btn_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - ]) - load_sizer.Add(inner_load_sizer, 0, wx.ALL | wx.EXPAND, 10) else: - lbl_new = wx.StaticText(self, -1, _("Create a new stimulator registration: ")) - lbl_load = wx.StaticText(self, -1, _("Load a stimulator registration: ")) - lbl_save = wx.StaticText(self, -1, _("Save current stimulator registration: ")) - - load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Stimulator registration")) - inner_load_sizer = wx.FlexGridSizer(2, 3, 5) - inner_load_sizer.AddMany([ - (lbl_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (btn_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (lbl_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (btn_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (lbl_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - (btn_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), - ]) - load_sizer.Add(inner_load_sizer, 0, wx.ALL | wx.EXPAND, 10) - + config_txt = wx.StaticText(self, -1, "None") + + self.config_txt = config_txt + lbl = wx.StaticText(self, -1, _("Current Configuration:")) + lbl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) + lbl_new = wx.StaticText(self, -1, _("Create a new stimulator registration: ")) + lbl_load = wx.StaticText(self, -1, _("Load a stimulator registration: ")) + lbl_save = wx.StaticText(self, -1, _("Save current stimulator registration: ")) + + load_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Stimulator registration")) + inner_load_sizer = wx.FlexGridSizer(2, 4, 5) + inner_load_sizer.AddMany([ + (lbl, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (config_txt, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_new, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_load, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (lbl_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + (btn_save, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL, 5), + ]) + load_sizer.Add(inner_load_sizer, 0, wx.ALL | wx.EXPAND, 10) # Change angles threshold text_angles = wx.StaticText(self, -1, _("Angle threshold [degrees]:")) spin_size_angles = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23)) @@ -319,7 +305,7 @@ def __init__(self, parent, navigation, tracker, pedal_connection, neuronavigatio self.Layout() def __bind_events(self): - pass + Publisher.subscribe(self.OnObjectUpdate, 'Update object registration') def LoadConfig(self): session = ses.Session() @@ -453,6 +439,9 @@ def OnSelectDistThreshold(self, evt, ctrl): def OnSelectTimestamp(self, evt, ctrl): self.timestamp = ctrl.GetValue() + def OnObjectUpdate(self, data=None): + self.config_txt.SetLabel(os.path.basename(data[-1])) + class TrackerPage(wx.Panel): def __init__(self, parent, tracker, robot): wx.Panel.__init__(self, parent) From 5ffe2200d1456ddab7251f2ddf1416d8a35ed97b Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:15:21 +0300 Subject: [PATCH 70/99] ADD: Sleep Time configuration --- invesalius/data/coordinates.py | 11 +++++++++-- invesalius/gui/preferences.py | 1 + invesalius/navigation/navigation.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/invesalius/data/coordinates.py b/invesalius/data/coordinates.py index 43eacc8c3..d5826fce2 100644 --- a/invesalius/data/coordinates.py +++ b/invesalius/data/coordinates.py @@ -576,14 +576,21 @@ def offset_coordinate(p_old, norm_vec, offset): class ReceiveCoordinates(threading.Thread): def __init__(self, tracker_connection, tracker_id, TrackerCoordinates, event): threading.Thread.__init__(self, name='ReceiveCoordinates') - + self.__bind_events() + self.sleep_coord = const.SLEEP_COORDINATES self.tracker_connection = tracker_connection self.tracker_id = tracker_id self.event = event self.TrackerCoordinates = TrackerCoordinates + def __bind_events(self): + Publisher.subscribe(self.UpdateCoordSleep, 'Update coord sleep') + + def UpdateCoordSleep(self, data): + self.sleep_coord = data + def run(self): while not self.event.is_set(): coord_raw, markers_flag = GetCoordinatesForThread(self.tracker_connection, self.tracker_id, const.DEFAULT_REF_MODE) self.TrackerCoordinates.SetCoordinates(coord_raw, markers_flag) - sleep(const.SLEEP_COORDINATES) + sleep(self.sleep_coord) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 7b0ecc773..8e4793484 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -53,6 +53,7 @@ def __init__( session = ses.Session() mode = session.GetConfig('mode') if mode == const.MODE_NAVIGATOR: + self.pnl_navigation = NavigationPage(self.book, navigation) self.pnl_tracker = TrackerPage(self.book, tracker, robot) self.pnl_object = ObjectPage(self.book, navigation, tracker, pedal_connection, neuronavigation_api) self.book.AddPage(self.pnl_tracker, _("Tracker")) diff --git a/invesalius/navigation/navigation.py b/invesalius/navigation/navigation.py index b40511f5c..2eeee1bfa 100644 --- a/invesalius/navigation/navigation.py +++ b/invesalius/navigation/navigation.py @@ -246,7 +246,7 @@ def LoadConfig(self): def CoilAtTarget(self, state): self.coil_at_target = state - def UpdateSleep(self, sleep): + def UpdateNavSleep(self, sleep): self.sleep_nav = sleep # self.serial_port_connection.sleep_nav = sleep From 9d3106c201a2acd8db28787eb22f3ff6a67891c9 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:15:58 +0300 Subject: [PATCH 71/99] ADD: Sleep time configuration --- invesalius/gui/preferences.py | 62 ++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index 8e4793484..a20bf4695 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -56,6 +56,7 @@ def __init__( self.pnl_navigation = NavigationPage(self.book, navigation) self.pnl_tracker = TrackerPage(self.book, tracker, robot) self.pnl_object = ObjectPage(self.book, navigation, tracker, pedal_connection, neuronavigation_api) + self.book.AddPage(self.pnl_navigation, _("Navigation")) self.book.AddPage(self.pnl_tracker, _("Tracker")) self.book.AddPage(self.pnl_object, _("Stimulator")) self.book.AddPage(self.pnl_language, _("Language")) @@ -177,6 +178,65 @@ def LoadSelection(self, values): self.rb_inter.SetSelection(int(surface_interpolation)) self.rb_inter_sl.SetSelection(int(slice_interpolation)) +class NavigationPage(wx.Panel): + def __init__(self, parent, navigation): + wx.Panel.__init__(self, parent) + self.navigation = navigation + self.sleep_nav = const.SLEEP_NAVIGATION + self.sleep_coord = const.SLEEP_COORDINATES + + text_note = wx.StaticText(self, -1, _("Note: Using too low sleep times can result in Invesalius crashing!")) + # Change sleep pause between navigation loops + nav_sleep = wx.StaticText(self, -1, _("Navigation Sleep (s):")) + spin_nav_sleep = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.01) + spin_nav_sleep.Enable(1) + spin_nav_sleep.SetRange(0.01, 10.0) + spin_nav_sleep.SetValue(self.sleep_nav) + spin_nav_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectNavSleep, ctrl=spin_nav_sleep)) + spin_nav_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectNavSleep, ctrl=spin_nav_sleep)) + + # Change sleep pause between coordinate update + coord_sleep = wx.StaticText(self, -1, _("Coordinate Sleep (s):")) + spin_coord_sleep = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.01) + spin_coord_sleep.Enable(1) + spin_coord_sleep.SetRange(0.01, 10.0) + spin_coord_sleep.SetValue(self.sleep_coord) + spin_coord_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectCoordSleep, ctrl=spin_coord_sleep)) + spin_coord_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectCoordSleep, ctrl=spin_coord_sleep)) + + line_nav_sleep = wx.BoxSizer(wx.HORIZONTAL) + line_nav_sleep.AddMany([ + (nav_sleep, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_nav_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, 5) + ]) + + line_coord_sleep = wx.BoxSizer(wx.HORIZONTAL) + line_coord_sleep.AddMany([ + (coord_sleep, 1, wx.EXPAND | wx.GROW | wx.TOP| wx.RIGHT | wx.LEFT, 5), + (spin_coord_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, 5) + ]) + + # Add line sizers into main sizer + conf_sizer = wx.StaticBoxSizer(wx.VERTICAL, self, _("Sleep time configuration")) + conf_sizer.AddMany([ + (text_note, 0, wx.ALL, 10), + (line_nav_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5), + (line_coord_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 5) + ]) + + main_sizer = wx.BoxSizer(wx.VERTICAL) + main_sizer.Add(conf_sizer, 0, wx.ALL | wx.EXPAND, 10) + self.SetSizerAndFit(main_sizer) + self.Layout() + + def OnSelectNavSleep(self, evt, ctrl): + self.sleep_nav = ctrl.GetValue() + self.navigation.UpdateNavSleep(self.sleep_nav) + + def OnSelectCoordSleep(self, evt, ctrl): + self.sleep_coord = ctrl.GetValue() + Publisher.sendMessage('Update coord sleep', data=self.sleep_coord) + class ObjectPage(wx.Panel): def __init__(self, parent, navigation, tracker, pedal_connection, neuronavigation_api): wx.Panel.__init__(self, parent) @@ -223,7 +283,7 @@ def __init__(self, parent, navigation, tracker, pedal_connection, neuronavigatio config_txt = wx.StaticText(self, -1, os.path.basename(self.obj_name)) else: config_txt = wx.StaticText(self, -1, "None") - + self.config_txt = config_txt lbl = wx.StaticText(self, -1, _("Current Configuration:")) lbl.SetFont(wx.Font(9, wx.DEFAULT, wx.NORMAL, wx.BOLD)) From 436efc7e9282b1561a684bdb11d04a7934b36809 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:16:23 +0300 Subject: [PATCH 72/99] FIX: Live Tractography --- invesalius/gui/task_navigator.py | 41 ++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 5590732b0..ee8973fba 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1414,6 +1414,18 @@ def __bind_events(self): Publisher.subscribe(self.TargetSelected, 'Target selected') Publisher.subscribe(self.TrackObject, 'Track object') + #Tractography + Publisher.subscribe(self.UpdateTrekkerObject, 'Update Trekker object') + Publisher.subscribe(self.UpdateNumTracts, 'Update number of tracts') + Publisher.subscribe(self.UpdateSeedOffset, 'Update seed offset') + Publisher.subscribe(self.UpdateSeedRadius, 'Update seed radius') + Publisher.subscribe(self.UpdateNumberThreads, 'Update number of threads') + Publisher.subscribe(self.UpdateTractsVisualization, 'Update tracts visualization') + Publisher.subscribe(self.UpdatePeelVisualization, 'Update peel visualization') + Publisher.subscribe(self.UpdateEfieldVisualization, 'Update e-field visualization') + Publisher.subscribe(self.EnableACT, 'Enable ACT') + Publisher.subscribe(self.UpdateACTData, 'Update ACT data') + # Config def SaveConfig(self): track_object = self.checkbox_track_object @@ -1556,9 +1568,38 @@ def OnTractographyCheckbox(self, evt, ctrl): Publisher.sendMessage("Update marker offset state", create=False) def UpdateTractsVisualization(self, data): + self.navigation.view_tracts = data self.EnableToggleButton(self.tractography_checkbox, 1) self.UpdateToggleButton(self.tractography_checkbox, data) + def UpdatePeelVisualization(self, data): + self.navigation.peel_loaded = data + + def UpdateEfieldVisualization(self, data): + self.navigation.e_field_loaded = data + + def UpdateTrekkerObject(self, data): + # self.trk_inp = data + self.navigation.trekker = data + + def UpdateNumTracts(self, data): + self.navigation.n_tracts = data + + def UpdateSeedOffset(self, data): + self.navigation.seed_offset = data + + def UpdateSeedRadius(self, data): + self.navigation.seed_radius = data + + def UpdateNumberThreads(self, data): + self.navigation.n_threads = data + + def UpdateACTData(self, data): + self.navigation.act_data = data + + def EnableACT(self, data): + self.navigation.enable_act = data + # 'Track object' checkbox def EnableTrackObjectCheckbox(self, enabled): self.EnableToggleButton(self.checkbox_track_object, enabled) From b900c848579895ac1a5ce392526b185ef9cd4ee5 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:25:59 +0300 Subject: [PATCH 73/99] FIX: Minor bug fix --- invesalius/gui/task_navigator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index ee8973fba..d41b85496 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1607,10 +1607,12 @@ def EnableTrackObjectCheckbox(self, enabled): if enabled: checked = self.checkbox_track_object.GetValue() self.EnableShowCoil(enabled=checked) + self.SaveConfig() def CheckTrackObjectCheckbox(self, checked): self.UpdateToggleButton(self.checkbox_track_object, checked) self.OnTrackObjectCheckbox() + self.SaveConfig() def OnTrackObjectCheckbox(self, evt=None, ctrl=None): if ctrl is not None: From 05800d6fa1c229307c7368d52c3ffd5ca76bfff8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:31:13 +0300 Subject: [PATCH 74/99] FIX: Tracker fiducials saved in config --- invesalius/navigation/tracker.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index 62e42d5d8..667665c63 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -48,9 +48,9 @@ def __init__(self): self.TrackerCoordinates = dco.TrackerCoordinates() - self.LoadConfig() + self.LoadState() - def SaveConfig(self): + def SaveState(self): tracker_id = self.tracker_id tracker_fiducials = self.tracker_fiducials.tolist() tracker_fiducials_raw = self.tracker_fiducials_raw.tolist() @@ -65,11 +65,11 @@ def SaveConfig(self): 'configuration': configuration, } session = ses.Session() - session.SetConfig('tracker', state) + session.SetState('tracker', state) - def LoadConfig(self): + def LoadState(self): session = ses.Session() - state = session.GetConfig('tracker') + state = session.GetState('tracker') if state is None: return @@ -129,8 +129,8 @@ def SetTracker(self, tracker_id, configuration=None): self.event_coord) self.thread_coord.start() - self.SaveConfig() Publisher.sendMessage('End busy cursor') + self.SaveState() def DisconnectTracker(self): @@ -209,14 +209,14 @@ def SetTrackerFiducial(self, ref_mode_id, fiducial_index): print("Set tracker fiducial {} to coordinates {}.".format(fiducial_index, coord[0:3])) - self.SaveConfig() + self.SaveState() def ResetTrackerFiducials(self): Publisher.sendMessage("Reset tracker fiducials") for m in range(3): self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan] - self.SaveConfig() + self.SaveState() def GetTrackerFiducials(self): return self.tracker_fiducials, self.tracker_fiducials_raw From 5623ea77484703ab76fbf993ccc4016b64ea30e7 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:31:24 +0300 Subject: [PATCH 75/99] FIX: Bug fix for busy cursor --- invesalius/navigation/tracker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index 667665c63..277c2b83c 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -91,7 +91,6 @@ def LoadState(self): ) def SetTracker(self, tracker_id, configuration=None): - Publisher.sendMessage('Begin busy cursor') if tracker_id: self.tracker_connection = tc.CreateTrackerConnection(tracker_id) @@ -129,7 +128,6 @@ def SetTracker(self, tracker_id, configuration=None): self.event_coord) self.thread_coord.start() - Publisher.sendMessage('End busy cursor') self.SaveState() From d014502192ec54008e1635a22cb5713e9cb2b761 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:36:17 +0300 Subject: [PATCH 76/99] REM: Sleep time from tractography --- invesalius/gui/task_tractography.py | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index 671eb1e80..b91e8da80 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -207,13 +207,13 @@ def __init__(self, parent): # self.spin_radius = spin_radius # Change sleep pause between navigation loops - text_sleep = wx.StaticText(self, -1, _("Sleep (s):")) - spin_sleep = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.01) - spin_sleep.Enable(1) - spin_sleep.SetRange(0.01, 10.0) - spin_sleep.SetValue(self.sleep_nav) - spin_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectSleep, ctrl=spin_sleep)) - spin_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectSleep, ctrl=spin_sleep)) + # text_sleep = wx.StaticText(self, -1, _("Sleep (s):")) + # spin_sleep = wx.SpinCtrlDouble(self, -1, "", size=wx.Size(50, 23), inc=0.01) + # spin_sleep.Enable(1) + # spin_sleep.SetRange(0.01, 10.0) + # spin_sleep.SetValue(self.sleep_nav) + # spin_sleep.Bind(wx.EVT_TEXT, partial(self.OnSelectSleep, ctrl=spin_sleep)) + # spin_sleep.Bind(wx.EVT_SPINCTRL, partial(self.OnSelectSleep, ctrl=spin_sleep)) # Change opacity of brain mask visualization text_opacity = wx.StaticText(self, -1, _("Brain opacity:")) @@ -243,9 +243,9 @@ def __init__(self, parent): line_radius.AddMany([(text_radius, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), (spin_radius, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) - line_sleep = wx.BoxSizer(wx.HORIZONTAL) - line_sleep.AddMany([(text_sleep, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), - (spin_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) + # line_sleep = wx.BoxSizer(wx.HORIZONTAL) + # line_sleep.AddMany([(text_sleep, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), + # (spin_sleep, 0, wx.ALL | wx.EXPAND | wx.GROW, border)]) line_opacity = wx.BoxSizer(wx.HORIZONTAL) line_opacity.AddMany([(text_opacity, 1, wx.EXPAND | wx.GROW | wx.TOP | wx.RIGHT | wx.LEFT, border), @@ -287,7 +287,7 @@ def __init__(self, parent): main_sizer.Add(line_ntracts, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) main_sizer.Add(line_offset, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) main_sizer.Add(line_radius, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) - main_sizer.Add(line_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) + #main_sizer.Add(line_sleep, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) main_sizer.Add(line_opacity, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, border) main_sizer.Add(line_checks, 0, wx.GROW | wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP | wx.BOTTOM, border_last) main_sizer.Fit(self) @@ -324,10 +324,10 @@ def OnSelectRadius(self, evt, ctrl): # self.tract.seed_offset = ctrl.GetValue() Publisher.sendMessage('Update seed radius', data=self.seed_radius) - def OnSelectSleep(self, evt, ctrl): - self.sleep_nav = ctrl.GetValue() - # self.tract.seed_offset = ctrl.GetValue() - Publisher.sendMessage('Update sleep', data=self.sleep_nav) + # def OnSelectSleep(self, evt, ctrl): + # self.sleep_nav = ctrl.GetValue() + # # self.tract.seed_offset = ctrl.GetValue() + # Publisher.sendMessage('Update sleep', data=self.sleep_nav) def OnSelectOpacity(self, evt, ctrl): self.brain_actor.GetProperty().SetOpacity(ctrl.GetValue()) From 5dd0f6eefee7923029fd9c588cf92245f4e79b6f Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 11:47:27 +0300 Subject: [PATCH 77/99] ADD: Custom page fold in preferences --- invesalius/gui/frame.py | 4 ++-- invesalius/gui/preferences.py | 6 +++--- invesalius/gui/task_navigator.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/invesalius/gui/frame.py b/invesalius/gui/frame.py index 912ef2bac..360f4c57d 100644 --- a/invesalius/gui/frame.py +++ b/invesalius/gui/frame.py @@ -644,8 +644,8 @@ def OnMove(self, evt): pos = aui_manager.GetPane("Data").window.GetScreenPosition() self.mw.SetPosition(pos) - def ShowPreferences(self): - preferences_dialog = preferences.Preferences(self) + def ShowPreferences(self, page=0): + preferences_dialog = preferences.Preferences(self, page) preferences_dialog.LoadPreferences() preferences_dialog.Center() diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index a20bf4695..c0c3da8e8 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -30,9 +30,10 @@ class Preferences(wx.Dialog): def __init__( self, parent, + page, id_=-1, title=_("Preferences"), - style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER ): super().__init__(parent, id_, title, style=style) tracker = Tracker() @@ -62,12 +63,11 @@ def __init__( self.book.AddPage(self.pnl_language, _("Language")) btnsizer = self.CreateStdDialogButtonSizer(wx.OK | wx.CANCEL) - min_width = max([i.GetMinWidth() for i in (self.book.GetChildren())]) min_height = max([i.GetMinHeight() for i in (self.book.GetChildren())]) if sys.platform.startswith("linux"): self.book.SetMinClientSize((min_width * 2, min_height * 2)) - + self.book.SetSelection(page) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.book, 1, wx.EXPAND | wx.ALL) sizer.Add(btnsizer, 0, wx.GROW | wx.RIGHT | wx.TOP | wx.BOTTOM, 5) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index d41b85496..e507cc75a 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -873,7 +873,7 @@ def OnBack(self, evt): Publisher.sendMessage('Back to image fiducials') def OnPreferences(self, evt): - Publisher.sendMessage("Open preferences menu") + Publisher.sendMessage("Open preferences menu", page=2) def OnRegisterEnable(self): self.register_button.Enable() @@ -1142,7 +1142,7 @@ def OnObjectUpdate(self, data=None): self.next_button.Enable() def OnEditPreferences(self, evt): - Publisher.sendMessage('Open preferences menu') + Publisher.sendMessage('Open preferences menu', page=3) def OnNext(self, evt): Publisher.sendMessage('Open navigation menu') From e57d9d4ffb19052bee316ca0983f2b6618d5b78c Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 15:47:39 +0300 Subject: [PATCH 78/99] ADD: Progress bars for ACT, Brain --- invesalius/gui/dialogs.py | 7 +- invesalius/gui/task_tractography.py | 107 +++++++++++++++++++--------- 2 files changed, 76 insertions(+), 38 deletions(-) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 2680c2d68..93009c89c 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -5336,10 +5336,10 @@ def GetValueBrainTarget(self): brain_target_orientation.append(orientation) return brain_target_position, brain_target_orientation -class FODProgressWindow(object): - def __init__(self): +class TractographyProgressWindow(object): + def __init__(self, msg): self.title = "InVesalius 3" - self.msg = _("Setting up FOD ...") + self.msg = msg self.style = wx.PD_APP_MODAL | wx.PD_APP_MODAL | wx.PD_CAN_ABORT self.dlg = wx.ProgressDialog(self.title, self.msg, @@ -5350,7 +5350,6 @@ def __init__(self): self.dlg.Show() def WasCancelled(self): - # print("Cancelled?", self.dlg.WasCancelled()) return self.dlg.WasCancelled() def Update(self, msg=None, value=None): diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index b91e8da80..91713a2e5 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -84,6 +84,10 @@ from invesalius import inv_paths +from concurrent.futures import wait, FIRST_COMPLETED, ThreadPoolExecutor +import multiprocessing +import wx.lib.agw.genericmessagedialog as GMD + class TaskPanel(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) @@ -364,6 +368,13 @@ def OnEnableACT(self, evt, ctrl): def UpdateNavigationStatus(self, nav_status, vis_status): self.nav_status = nav_status + def UpdateDialog(self, msg): + while self.tp.running: + self.tp.dlg.Pulse(msg) + if not self.tp.running: + break + wx.Yield() + def OnLinkBrain(self, event=None): Publisher.sendMessage('Begin busy cursor') inv_proj = prj.Project() @@ -371,6 +382,10 @@ def OnLinkBrain(self, event=None): ret = peels_dlg.ShowModal() method = peels_dlg.method if ret == wx.ID_OK: + t_init = time.time() + msg = "Setting up Brain..." + self.tp = dlg.TractographyProgressWindow(msg) + slic = sl.Slice() ww = slic.window_width wl = slic.window_level @@ -386,10 +401,24 @@ def OnLinkBrain(self, event=None): choices = [i for i in inv_proj.mask_dict.values()] mask_index = peels_dlg.cb_masks.GetSelection() mask = choices[mask_index] - self.brain_peel.from_mask(mask) + option = 1 else: - mask_path = peels_dlg.mask_path - self.brain_peel.from_mask_file(mask_path) + mask = peels_dlg.mask_path + option = 2 + with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as exec: + futures = [exec.submit(self.UpdateDialog, msg), exec.submit(self.BrainLoading, option, mask)] + done, not_done = wait(futures, return_when=FIRST_COMPLETED) + self.tp.running = False + + t_end = time.time() + print("Elapsed time - {}".format(t_end-t_init)) + self.tp.Close() + if self.tp.error: + dlgg = GMD.GenericMessageDialog(None, self.tp.error, + "Exception!", + wx.OK|wx.ICON_ERROR) + dlgg.ShowModal() + self.brain_actor = self.brain_peel.get_actor(self.peel_depth) self.brain_actor.GetProperty().SetOpacity(self.brain_opacity) Publisher.sendMessage('Update peel', flag=True, actor=self.brain_actor) @@ -402,10 +431,17 @@ def OnLinkBrain(self, event=None): Publisher.sendMessage('Update status text in GUI', label=_("Brain model loaded")) self.peel_loaded = True Publisher.sendMessage('Update peel visualization', data= self.peel_loaded) - + del self.tp + wx.MessageBox(_("Brain Import successful"), _("InVesalius 3")) peels_dlg.Destroy() Publisher.sendMessage('End busy cursor') + def BrainLoading(self, option, mask): + if option == 1: + self.brain_peel.from_mask(mask) + else: + self.brain_peel.from_mask_file(mask) + def OnLinkFOD(self, event=None): Publisher.sendMessage('Begin busy cursor') filename = dlg.ShowImportOtherFilesDialog(const.ID_NIFTI_IMPORT, msg=_("Import Trekker FOD")) @@ -431,27 +467,19 @@ def OnLinkFOD(self, event=None): Publisher.sendMessage('Update status text in GUI', label=_("Busy")) t_init = time.time() try: - import concurrent.futures as mp - from concurrent.futures import wait - from concurrent.futures import FIRST_COMPLETED - from concurrent.futures import ThreadPoolExecutor - import multiprocessing - import functools - import wx.lib.agw.genericmessagedialog as GMD - self.tp = dlg.FODProgressWindow() + msg = "Setting up FOD ... " + self.tp = dlg.TractographyProgressWindow(msg) self.trekker = None file = filename.encode('utf-8') - with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as exec: - futures = [exec.submit(self.UpdateDialog), exec.submit(Trekker.initialize, file)] + futures = [exec.submit(self.UpdateDialog, msg), exec.submit(Trekker.initialize, file)] done, not_done = wait(futures, return_when=FIRST_COMPLETED) completed_future = done.pop() self.TrekkerCallback(completed_future) t_end = time.time() print("Elapsed time - {}".format(t_end-t_init)) - self.tp.running = False self.tp.Close() if self.tp.error: dlgg = GMD.GenericMessageDialog(None, self.tp.error, @@ -466,13 +494,6 @@ def OnLinkFOD(self, event=None): Publisher.sendMessage('End busy cursor') - def UpdateDialog(self): - while self.tp.running: - self.tp.dlg.Pulse("Setting up FOD ... ") - if not self.tp.running: - break - wx.Yield() - def TrekkerCallback(self, trekker): self.tp.running = False print("Import Complete") @@ -510,24 +531,32 @@ def OnLoadACT(self, event=None): self.affine_vtk = vtk_utils.numpy_to_vtkMatrix4x4(self.affine) try: + t_init = time.time() + msg = "Setting up ACT..." + self.tp = dlg.TractographyProgressWindow(msg) + Publisher.sendMessage('Update status text in GUI', label=_("Busy")) if filename: - act_data = nb.squeeze_image(nb.load(filename)) - act_data = nb.as_closest_canonical(act_data) - act_data.update_header() - act_data_arr = act_data.get_fdata() - + with ThreadPoolExecutor(max_workers=multiprocessing.cpu_count()) as exec: + futures = [exec.submit(self.UpdateDialog, msg), exec.submit(self.ACTLoading, filename)] + done, not_done = wait(futures, return_when=FIRST_COMPLETED) + self.tp.running = False + + t_end = time.time() + print("Elapsed time - {}".format(t_end-t_init)) + self.tp.Close() + if self.tp.error: + dlgg = GMD.GenericMessageDialog(None, self.tp.error, + "Exception!", + wx.OK|wx.ICON_ERROR) + dlgg.ShowModal() + del self.tp self.checkACT.Enable(1) self.checkACT.SetValue(True) - - # ACT rules should be as follows: - self.trekker.pathway_stop_at_entry(filename.encode('utf-8'), -1) # outside - self.trekker.pathway_discard_if_ends_inside(filename.encode('utf-8'), 1) # wm - self.trekker.pathway_discard_if_enters(filename.encode('utf-8'), 0) # csf - - Publisher.sendMessage('Update ACT data', data=act_data_arr) + Publisher.sendMessage('Update ACT data', data=self.act_data_arr) Publisher.sendMessage('Enable ACT', data=True) Publisher.sendMessage('Update status text in GUI', label=_("Trekker ACT loaded")) + wx.MessageBox(_("ACT Import successful"), _("InVesalius 3")) except: Publisher.sendMessage('Update status text in GUI', label=_("ACT initialization failed.")) wx.MessageBox(_("Unable to load ACT."), _("InVesalius 3")) @@ -536,6 +565,16 @@ def OnLoadACT(self, event=None): else: wx.MessageBox(_("Load FOD image before the ACT."), _("InVesalius 3")) + def ACTLoading(self, filename): + act_data = nb.squeeze_image(nb.load(filename)) + act_data = nb.as_closest_canonical(act_data) + act_data.update_header() + self.act_data_arr = act_data.get_fdata() + # ACT rules should be as follows: + self.trekker.pathway_stop_at_entry(filename.encode('utf-8'), -1) # outside + self.trekker.pathway_discard_if_ends_inside(filename.encode('utf-8'), 1) # wm + self.trekker.pathway_discard_if_enters(filename.encode('utf-8'), 0) # csf + def OnLoadParameters(self, event=None): import json filename = dlg.ShowLoadSaveDialog(message=_(u"Load Trekker configuration"), From e25d54526e844f2463c6bd2ce6e678a446f03332 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Tue, 15 Aug 2023 18:20:59 +0300 Subject: [PATCH 79/99] ADD: New icons --- icons/coil_eye.png | Bin 0 -> 502 bytes icons/field.png | Bin 0 -> 774 bytes icons/lock_to_target.png | Bin 0 -> 651 bytes icons/orbit.png | Bin 0 -> 722 bytes icons/target.png | Bin 564 -> 510 bytes icons/target_black.png | Bin 0 -> 185 bytes icons/tract.png | Bin 534 -> 971 bytes icons/wave.png | Bin 0 -> 187 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 icons/coil_eye.png create mode 100644 icons/field.png create mode 100644 icons/lock_to_target.png create mode 100644 icons/orbit.png create mode 100644 icons/target_black.png create mode 100644 icons/wave.png diff --git a/icons/coil_eye.png b/icons/coil_eye.png new file mode 100644 index 0000000000000000000000000000000000000000..ce25947e56ee478d7adef710833e7ef759268045 GIT binary patch literal 502 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J2BoosZ$T+a29w(76a+I zAk5fb^gIv9m=@p@;tHe(A+X{;s|9qHQAv{RPy$+tbA{q+-t0fa`os4gxVQ zR!v`)l~`2${6GIQgXe`OO6N|WJ9lRr^AFZnPVq0fzv>$69OvG0pjhYg!tO~m!B=*# zx|8B*BQ+~;#Dljx6dndo>9;u yxv=!wvo%&nIL@Xt-{`WKvqSHv!=vecy2WQjs&9L?eCu~m+<3bBxvX#g6H ze%;g(kDRktc6rLn&)a@peewPAmnG+{HmL4eZJ)a^Xv(LDKknRp{m5_U?CwKbA3VG8 z=h)l!HL)dmXSuvTy!*)6l+J(J*1&)NNg)ayxVkFpPK(Gl)b?9M%LT!WoJ`k^jlW( z?A%o<<$Fvl^Gioa$!!VQI|_F~qBrzfAK4XQdQR`1;J2EFzs#DQv5#e{7C1lqCV6|- z?9Y8E`x+&jS+^!PR5d>j^xUmus$%98B`95>H1RR#ft5PX&usrYk9``e_@#Q2#rBQf zis5=OgTe~ HDWM4fdok!p literal 0 HcmV?d00001 diff --git a/icons/lock_to_target.png b/icons/lock_to_target.png new file mode 100644 index 0000000000000000000000000000000000000000..a8daa6aba4bb1a286ed31092a2c0fc31f78a7e77 GIT binary patch literal 651 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J2BoosZ$T+a29w(76a+I zAk5fb^gIv9I3M5>;tHgP6PTI2j0NZ)*ODN=Uc3luTSFgPK`dgES{}$%-`}@B5nEJT7x3brUkb8lFo37;BCStyr^S)-o-lzm3}^72mgTQTFoGVUmml8L(# zw65l+>^zjwJNs=}^J|Xq(^|QnnIG36=tk_wDqa@ q`MrM}3-)}TI3FM|CtE#W6 z>rDx>aSoI56F)G5tN5rfl(7-3(Sq;seE^0qFSmILc7>A@Xs<&&6{lnGHcZT|b>Rlw ztV6IK4^at6bK`1951vQlr5g4_&5=qi3`^M!1lfB@j_nx5Mcl>j8uq0LlI9Z&7FSBy zy+S88Np+cvCGqSfZlk-vz8u_kM?`rX!iB2rHU!!9s=Y0^6gGM6#XeF|M~vy%jq`E8 z8b@&)SJaoYA1mU%gh#;*PDv7UNJVW4@_sDEvpU2XTJbnIdLpR5C`&~=YD~`~K`t_E z1eLIRsGz-j@D5G!`&dLif}IUIm>_jprD(Ao->?X4CC$f!>vGXIf&2Y8D8Qg^+;TfB)( zMsYTrd?~R1C?u+NWR|4XeIgy5ZI}~R8`edJMTbp>q3olpDz&{*k@}>68?`V6CvhKJ zr7q0Sg~1YL;fz#=Uvb^THGHZ=o<~{Ac9kS~6F#H&4+8pmFb1fxegFUf07*qoM6N<$ Ef=9D9z5oCK literal 0 HcmV?d00001 diff --git a/icons/target.png b/icons/target.png index f8e9dfa1375574540eae4f602d4bd56473ad161e..a4832eabb10de6af744241103bd70172d1cb339a 100644 GIT binary patch literal 510 zcmVhzoxW>e z9XL6QV>;wuuY9gNY$xnPsXIstVI8mw($xX`AqNdm29bBb8;}Uw02_TLTI+(HltA;% z0g$Tfww_|GC=(Bx026PXNT>F&t+-O(L9X0p-htlys*vGT;s;VlcdmIRuDtIO6>!m3 z`#L{L>7cYsMBE?pRoF&4PBy)LC(deAtN7Q5o}#B5a`ZBN)Lpeo|D>tH8w z9-QDz;yjrH&QiWE<3*D9b^e=J-;l*y2?ZsUH4ROz z-IHg}U%vYI>GRj`KYsr9-TM!pzyJQDxTgIPP#b4~Mf@X2XsU{v3_Ri!WZwFq4#B_q*X$)4GkO&IxL<%Cq+77~6XP zFjA}Fyr1z`eXE*()RhV5Pr~kPwP{t%UD)TGal=w3a(Tw(ZKb{3)D9_qB%>*abIG-#BIc!A;yq$ VR;ls40{ah;`JS$RF6*2UngFc&Hnso& literal 0 HcmV?d00001 diff --git a/icons/tract.png b/icons/tract.png index f4b56f6bfacdc414214bfc6658a3f9bd35f42285..ab92c8fa3e165d4207322c510b22539e470837a4 100644 GIT binary patch literal 971 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM3?#3wJbMaAF%}28J2BoosZ$T+a29w(7Bet# z3xhBt!>l<#<%}}|d_r7-lA~Y{hQRY*?Qek2>L>~F3uaJuDV|gmXn6nM|G#%8Y`pk! zwww6V?+c&T%AI(a_1JOex7_V(d9AiY#J>^f()Ih)KehGv^z0A46~-J(PL>9%)_2@1 zIIuLvV@Eop#xL7LSGA4K?o~6Bx+}PE%8wqEuaZpb6}sPU-tB$OV1`&z+s#UI_4n+n zgcFwMw?FW4whOpqef^dR3)hupWo&PHJ^<5=g{O;SNX4Aw1O;vbBg4%bHf>zYU0hI9 z`1!-9PanBCxD>m&yE}>syE?rM3_OgyJx?ompFDL^TSH58HN(tRS$|$}a2oQmW%}yo z*2xOEhx^OxB`O?F$VfSpkd&2nF6Usv$#mAq8@45Gcq$y8x?yv_CG+lr{HGwBN zfup~Jwt~hWm!k$`T!{3=2zidjw~tfpRB< Mr>mdKI;Vst00U{^+W-In delta 520 zcmV+j0{8vP2bKho8Gi%-002sU^>Y9K03c&XQcVB=dL{q>fP?@5`Tzg`fam}Kbua(` z>RI+y?e7jT@qQ9J+u00Lr5M??Sss*NKu00009a7bBm0001B0001B0Pt%<&j0`b z2XskIMF-~x7zhqBxL*%U0004dNkl!o>*DqM&v~iy#PEq##VV)V9$o-MC3@6A5N8Au?oHLAP62t>BVSPds7KU35!h z+6$5~gn|%fw0|MA8Lx2PvJ~hP)MM#@PqJvV=yg3qb5>x~q$P)HOG8!_cDdi3ia_PX zo%c@0jKYpOEAq8Z8FLj`t3FM8d+qgGS5dLkhQL?NdsCiQc>XBmXGNZu)lxk}WTZn9 zY0oS;;kxx=<)9HQPW!c+T91d$RlTGvyHidtxaN4wMt{a+qhOyYft*dFHIS1omP{wi zc@%}i)}(ROmi7wxu!1cUpxIL!jz)!XV?{`=X1`DLm*N$dBRhOHph2MBVYe)m6mA;S zYhDIFychm0H2LC#ae32K-@M<52aZ&al_*kfJ7(6)a+m*hwgFS{d3N9=RO(g8jpu0R?Wm=gF%0Vu~=666=m@ao;SwD%RKUmhy4E&&Rvc)B=-RLn_E zaA524a#-+&;o*iRj%_W1XA{{4b=(#tWCA4^tUqz}T|4)GCddL$S3j3^P6 Date: Tue, 15 Aug 2023 18:21:12 +0300 Subject: [PATCH 80/99] ADD: New icons to GUI --- invesalius/gui/task_navigator.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index e507cc75a..6826c96e3 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1266,7 +1266,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle Button to track object or simply the stylus tooltip = wx.ToolTip(_(u"Track the object")) - BMP_TRACK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("track.png")), wx.BITMAP_TYPE_PNG) + BMP_TRACK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("coil.png")), wx.BITMAP_TYPE_PNG) checkbox_track_object = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) checkbox_track_object.SetBackgroundColour(GREY_COLOR) checkbox_track_object.SetBitmap(BMP_TRACK) @@ -1279,7 +1279,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle Button for Lock to Target tooltip = wx.ToolTip(_(u"Allow triggering stimulation pulse only if the coil is at the target")) - BMP_LOCK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("lock.png")), wx.BITMAP_TYPE_PNG) + BMP_LOCK = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("lock_to_target.png")), wx.BITMAP_TYPE_PNG) lock_to_target_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) lock_to_target_checkbox.SetBackgroundColour(GREY_COLOR) lock_to_target_checkbox.SetBitmap(BMP_LOCK) @@ -1291,7 +1291,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle Button for object position and orientation update in volume rendering during navigation tooltip = wx.ToolTip(_("Show and track TMS coil")) - BMP_SHOW = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("coil.png")), wx.BITMAP_TYPE_PNG) + BMP_SHOW = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("coil_eye.png")), wx.BITMAP_TYPE_PNG) checkobj = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) checkobj.SetBackgroundColour(GREY_COLOR) checkobj.SetBitmap(BMP_SHOW) @@ -1303,7 +1303,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle Button for camera update in volume rendering during navigation tooltip = wx.ToolTip(_("Update camera in volume")) - BMP_UPDATE = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("camera.png")), wx.BITMAP_TYPE_PNG) + BMP_UPDATE = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("orbit.png")), wx.BITMAP_TYPE_PNG) checkcamera = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) checkcamera.SetBitmap(BMP_UPDATE) checkcamera.SetToolTip(tooltip) @@ -1317,7 +1317,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Toggle Button to use serial port to trigger pulse signal and create markers tooltip = wx.ToolTip(_("Enable serial port communication to trigger pulse and create markers")) - BMP_PORT = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("port.png")), wx.BITMAP_TYPE_PNG) + BMP_PORT = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("wave.png")), wx.BITMAP_TYPE_PNG) checkbox_serial_port = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) checkbox_serial_port.SetBackgroundColour(RED_COLOR) checkbox_serial_port.SetBitmap(BMP_PORT) @@ -1328,7 +1328,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect #Toggle Button for Efield tooltip = wx.ToolTip(_(u"Control E-Field")) - BMP_FIELD = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("efield.png")), wx.BITMAP_TYPE_PNG) + BMP_FIELD = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("field.png")), wx.BITMAP_TYPE_PNG) efield_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) efield_checkbox.SetBackgroundColour(GREY_COLOR) efield_checkbox.SetBitmap(BMP_FIELD) @@ -1340,7 +1340,7 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect #Toggle Button for Target Mode tooltip = wx.ToolTip(_(u"Control Target Mode")) - BMP_TARGET = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("track.png")), wx.BITMAP_TYPE_PNG) + BMP_TARGET = wx.Bitmap(str(inv_paths.ICON_DIR.joinpath("target.png")), wx.BITMAP_TYPE_PNG) target_checkbox = wx.ToggleButton(self, -1, "", style=pbtn.PB_STYLE_SQUARE, size=ICON_SIZE) target_checkbox.SetBackgroundColour(GREY_COLOR) target_checkbox.SetBitmap(BMP_TARGET) @@ -1361,13 +1361,14 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect checkbox_sizer = wx.FlexGridSizer(4, 5, 5) checkbox_sizer.AddMany([ (tractography_checkbox), - (checkbox_track_object), - (lock_to_target_checkbox), - (checkobj), (checkcamera), - (checkbox_serial_port), + (target_checkbox), + (checkbox_track_object), (efield_checkbox), - (target_checkbox) + (checkbox_serial_port), + (lock_to_target_checkbox), + (checkobj) + ]) main_sizer = wx.BoxSizer(wx.VERTICAL) From 2054981be711767739f039773ce1accc82a6f692 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 13:34:24 +0300 Subject: [PATCH 81/99] FIX: Bug fix for cross tool --- invesalius/gui/task_navigator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 6826c96e3..81530a8c3 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -610,12 +610,14 @@ def OnNextDisable(self): def OnStartRegistration(self, evt, ctrl): value = ctrl.GetValue() if value: - Publisher.sendMessage("Toggle Cross", id=const.SLICE_STATE_CROSS) + Publisher.sendMessage("Enable style", style=const.SLICE_STATE_CROSS) for button in self.btns_set_fiducial: button.Enable() self.start_button.SetLabel("Stop registration") else: self.start_button.SetLabel("Start registration") + for button in self.btns_set_fiducial: + button.Disable() Publisher.sendMessage("Disable style", style=const.SLICE_STATE_CROSS) class TrackerPage(wx.Panel): From b3b7ab26c352e463f981aa24e8bb20aa87967dc2 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 13:44:14 +0300 Subject: [PATCH 82/99] ADD: Disable tabs when project not loaded --- invesalius/gui/default_tasks.py | 2 ++ invesalius/gui/task_imports.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index fdf303f2d..5968f804c 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -325,6 +325,8 @@ def __init__(self, parent): sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND) self.sizer = sizer + if mode == const.MODE_RP: + self.SetStateProjectClose() self.SetSizerAndFit(sizer) self.__bind_events() diff --git a/invesalius/gui/task_imports.py b/invesalius/gui/task_imports.py index c97849677..ec6392840 100644 --- a/invesalius/gui/task_imports.py +++ b/invesalius/gui/task_imports.py @@ -212,8 +212,8 @@ def __init__(self, parent): # All items, except the first one, should be disabled if # no data has been imported initially. - # if i != 0: - # self.enable_items.append(item) + if i != 0: + self.enable_items.append(item) # If it is related to mask, this value should be kept # It is used as reference to set mouse cursor related to From 67c65de6560540f835276c2f4598151def3e24c3 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 14:07:09 +0300 Subject: [PATCH 83/99] FIX: Minor bug fix --- invesalius/gui/default_tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 5968f804c..174a41c20 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -325,8 +325,7 @@ def __init__(self, parent): sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND) self.sizer = sizer - if mode == const.MODE_RP: - self.SetStateProjectClose() + self.SetStateProjectClose() self.SetSizerAndFit(sizer) self.__bind_events() From d479fdc25ff3ba13f2932e9168e34aaaac6c9bca Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 14:23:18 +0300 Subject: [PATCH 84/99] FIX: SIze fixing --- invesalius/gui/task_imports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/task_imports.py b/invesalius/gui/task_imports.py index ec6392840..e25e4fe87 100644 --- a/invesalius/gui/task_imports.py +++ b/invesalius/gui/task_imports.py @@ -166,7 +166,7 @@ def __init__(self, parent): except AttributeError: default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, - (10, 330), 0, fpb.FPB_SINGLE_FOLD) + (-1, 400), 0, fpb.FPB_SINGLE_FOLD) image_list = wx.ImageList(16,16) image_list.Add(GetExpandedIconBitmap()) @@ -228,7 +228,7 @@ def __init__(self, parent): self.image_list = image_list sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(fold_panel, 1, wx.GROW|wx.EXPAND) + sizer.Add(fold_panel, 1, wx.EXPAND) self.sizer = sizer self.SetSizerAndFit(sizer) self.SetStateProjectClose() From 10a4e18ddec1e11ebf524ebe5e9f38c449884f62 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 15:46:01 +0300 Subject: [PATCH 85/99] FIX: Minor bug fix --- invesalius/gui/default_tasks.py | 2 +- invesalius/gui/task_navigator.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 174a41c20..0cede9903 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -334,10 +334,10 @@ def __bind_events(self): mode = session.GetConfig('mode') if mode == const.MODE_RP: self.fold_panel.Bind(fpb.EVT_CAPTIONBAR, self.OnFoldPressCaption) - Publisher.subscribe(self.OnEnableState, "Enable state project") Publisher.subscribe(self.OnOverwrite, 'Create surface from index') Publisher.subscribe(self.OnFoldSurface, 'Fold surface task') Publisher.subscribe(self.OnFoldExport, 'Fold export task') + Publisher.subscribe(self.OnEnableState, "Enable state project") # Publisher.subscribe(self.SetNavigationMode, "Set navigation mode") def OnOverwrite(self, surface_parameters): diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 81530a8c3..9c2252cd2 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -248,8 +248,8 @@ def __init__(self, parent): self.SetAutoLayout(1) def __bind_events(self): - Publisher.subscribe(self.OnShowDbs, "Show dbs folder") - Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") + #Publisher.subscribe(self.OnShowDbs, "Show dbs folder") + #Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") Publisher.subscribe(self.OpenNavigation, 'Open navigation menu') def __calc_best_size(self, panel): From 8a00127094357903d4a281645d1de51b48525da8 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 16:09:54 +0300 Subject: [PATCH 86/99] FIX: Size bug for imports section Reported by @vhosouza --- invesalius/gui/task_imports.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/invesalius/gui/task_imports.py b/invesalius/gui/task_imports.py index e25e4fe87..583bd199a 100644 --- a/invesalius/gui/task_imports.py +++ b/invesalius/gui/task_imports.py @@ -166,7 +166,7 @@ def __init__(self, parent): except AttributeError: default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) fold_panel = fpb.FoldPanelBar(self, -1, wx.DefaultPosition, - (-1, 400), 0, fpb.FPB_SINGLE_FOLD) + wx.DefaultSize, 0, fpb.FPB_SINGLE_FOLD) image_list = wx.ImageList(16,16) image_list.Add(GetExpandedIconBitmap()) @@ -225,6 +225,7 @@ def __init__(self, parent): fold_panel.Expand(fold_panel.GetFoldPanel(0)) self.fold_panel = fold_panel + self.ResizeFPB() self.image_list = image_list sizer = wx.BoxSizer(wx.VERTICAL) @@ -269,9 +270,14 @@ def OnFoldPressCaption(self, evt): def ResizeFPB(self): sizeNeeded = self.fold_panel.GetPanelsLength(0, 0)[2] - self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded )) - self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) - + offset = 0 + panels = [self.fold_panel.GetFoldPanel(panel) for panel in range(self.fold_panel.GetCount())] + for panel in panels: + if not panel.IsExpanded(): + offset += panel.GetSize()[1] + self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded + offset*2)) + self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded + offset*2)) + def OnOverwrite(self, surface_parameters): self.overwrite = surface_parameters['options']['overwrite'] From b3267680b3bf52bcb6bd4a97ea18a78c16deab28 Mon Sep 17 00:00:00 2001 From: Renan Matsuda Date: Wed, 16 Aug 2023 13:00:48 -0300 Subject: [PATCH 87/99] FIX: robot transformation dialog --- invesalius/gui/dialogs.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/invesalius/gui/dialogs.py b/invesalius/gui/dialogs.py index 93009c89c..a9fc6ecbc 100644 --- a/invesalius/gui/dialogs.py +++ b/invesalius/gui/dialogs.py @@ -5737,7 +5737,6 @@ def __init__(self, robot, tracker, title=_("Create transformation matrix to robo ''' #TODO: make aboutbox self.matrix_tracker_to_robot = [] - self.robot_status = False self.robot = robot self.tracker = tracker @@ -5784,8 +5783,6 @@ def _init_gui(self): btn_load.Enable(False) else: btn_load.Enable(True) - if self.GetAcquiredPoints() >= 3: - self.btn_apply_reg.Enable(True) self.btn_load = btn_load @@ -5846,7 +5843,6 @@ def _init_gui(self): def __bind_events(self): Publisher.subscribe(self.UpdateRobotTransformationMatrix, 'Update robot transformation matrix') - Publisher.subscribe(self.UpdateRobotConnectionStatus, 'Robot connection status') Publisher.subscribe(self.PointRegisteredByRobot, 'Coordinates for the robot transformation matrix collected') def OnContinuousAcquisitionButton(self, evt=None, btn=None): @@ -5880,17 +5876,7 @@ def PointRegisteredByRobot(self): self.SetAcquiredPoints(num_points) # Enable 'Apply registration' button only when the robot connection is ok and there are enough acquired points. - if self.robot_status and num_points >= 3: - self.btn_apply_reg.Enable(True) - - def UpdateRobotConnectionStatus(self, data): - self.robot_status = data - if not self.robot_status: - return - - # Enable 'Load' and 'Apply registration' buttons only when robot connection is ok. - self.btn_load.Enable(True) - if self.GetAcquiredPoints() >= 3: + if self.robot.robot_status and num_points >= 3: self.btn_apply_reg.Enable(True) def ResetPoints(self, evt): From aa0b4ac6c109b75bcf19dbbd3b354a5e38609c6d Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Wed, 16 Aug 2023 22:06:32 +0300 Subject: [PATCH 88/99] FIX: Nav sleep times --- invesalius/gui/preferences.py | 2 +- invesalius/gui/task_tractography.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/invesalius/gui/preferences.py b/invesalius/gui/preferences.py index c0c3da8e8..5f8c29bf0 100644 --- a/invesalius/gui/preferences.py +++ b/invesalius/gui/preferences.py @@ -182,7 +182,7 @@ class NavigationPage(wx.Panel): def __init__(self, parent, navigation): wx.Panel.__init__(self, parent) self.navigation = navigation - self.sleep_nav = const.SLEEP_NAVIGATION + self.sleep_nav = self.navigation.sleep_nav self.sleep_coord = const.SLEEP_COORDINATES text_note = wx.StaticText(self, -1, _("Note: Using too low sleep times can result in Invesalius crashing!")) diff --git a/invesalius/gui/task_tractography.py b/invesalius/gui/task_tractography.py index 91713a2e5..dd4c87ae6 100644 --- a/invesalius/gui/task_tractography.py +++ b/invesalius/gui/task_tractography.py @@ -120,7 +120,6 @@ def __init__(self, parent): self.view_tracts = False self.seed_offset = const.SEED_OFFSET self.seed_radius = const.SEED_RADIUS - self.sleep_nav = const.SLEEP_NAVIGATION self.brain_opacity = const.BRAIN_OPACITY self.brain_peel = None self.brain_actor = None From 04976996f27bb7b708ad6f29fa7b103c098dd3db Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 11:20:08 +0300 Subject: [PATCH 89/99] UPD: Show coil enabled with nav --- invesalius/gui/task_navigator.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 9c2252cd2..f05003830 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1532,22 +1532,13 @@ def OnCheckStatus(self, nav_status, vis_status): if nav_status: self.UpdateToggleButton(self.checkbox_serial_port) self.EnableToggleButton(self.checkbox_serial_port, 0) - self.UpdateToggleButton(self.checkobj) - self.EnableToggleButton(self.checkobj, 0) - self.UpdateToggleButton(self.checkbox_track_object) - self.EnableToggleButton(self.checkbox_track_object, 0) else: self.EnableToggleButton(self.checkbox_serial_port, 1) self.UpdateToggleButton(self.checkbox_serial_port) - self.EnableToggleButton(self.checkbox_track_object, 1) - self.UpdateToggleButton(self.checkbox_track_object) - if self.track_obj: - self.EnableToggleButton(self.checkobj, 1) - self.UpdateToggleButton(self.checkobj) + # Enable/Disable track-object checkbox if navigation is off/on and object registration is valid. obj_registration = self.navigation.GetObjectRegistration() enable_track_object = obj_registration is not None and obj_registration[0] is not None and not nav_status - # Publisher.sendMessage('Enable track-object checkbox', enabled=enable_track_object) self.EnableTrackObjectCheckbox(enable_track_object) # 'Robot' @@ -1607,9 +1598,6 @@ def EnableACT(self, data): def EnableTrackObjectCheckbox(self, enabled): self.EnableToggleButton(self.checkbox_track_object, enabled) self.UpdateToggleButton(self.checkbox_track_object) - if enabled: - checked = self.checkbox_track_object.GetValue() - self.EnableShowCoil(enabled=checked) self.SaveConfig() def CheckTrackObjectCheckbox(self, checked): @@ -1641,7 +1629,6 @@ def OnLockToTargetCheckbox(self, evt, ctrl): # 'Show coil' checkbox def CheckShowCoil(self, checked=False): self.UpdateToggleButton(self.checkobj, checked) - self.track_obj = checked self.OnShowCoil() def EnableShowCoil(self, enabled=False): @@ -1651,8 +1638,7 @@ def EnableShowCoil(self, enabled=False): def OnShowCoil(self, evt=None): self.UpdateToggleButton(self.checkobj) checked = self.checkobj.GetValue() - Publisher.sendMessage('Show-coil checked', checked=checked) - self.show_coil_checked = checked + Publisher.sendMessage('Show-coil checked', checked=checked) # 'Volume camera' checkbox @@ -1693,11 +1679,7 @@ def OnEfieldCheckbox(self, evt, ctrl): self.UpdateToggleButton(ctrl) - # 'Target Button' - def ShowCoilChecked(self, checked): - self.show_coil_checked = checked - self.UpdateTargetButton() - + # 'Target Button' def TargetSelected(self, status): if status is not None: self.target_selected = status From 6e1c4d34f35f3ba4f39b3fd77ef5a25ec4f40ca5 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 15:27:13 +0300 Subject: [PATCH 90/99] Fix: Panel Sizing --- invesalius/gui/task_imports.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/task_imports.py b/invesalius/gui/task_imports.py index 583bd199a..79b652e6d 100644 --- a/invesalius/gui/task_imports.py +++ b/invesalius/gui/task_imports.py @@ -270,13 +270,15 @@ def OnFoldPressCaption(self, evt): def ResizeFPB(self): sizeNeeded = self.fold_panel.GetPanelsLength(0, 0)[2] + offset_constant = 1.8 offset = 0 panels = [self.fold_panel.GetFoldPanel(panel) for panel in range(self.fold_panel.GetCount())] for panel in panels: if not panel.IsExpanded(): offset += panel.GetSize()[1] - self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded + offset*2)) - self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded + offset*2)) + sizeNeeded += int(offset * offset_constant) + self.fold_panel.SetMinSize((self.fold_panel.GetSize()[0], sizeNeeded)) + self.fold_panel.SetSize((self.fold_panel.GetSize()[0], sizeNeeded)) def OnOverwrite(self, surface_parameters): self.overwrite = surface_parameters['options']['overwrite'] From 09c27f27f33c0163288bf8f9cd208912487c6c46 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 15:27:26 +0300 Subject: [PATCH 91/99] FIx: Bug fix tracker disconnection --- invesalius/navigation/tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invesalius/navigation/tracker.py b/invesalius/navigation/tracker.py index 277c2b83c..de0aef7c2 100644 --- a/invesalius/navigation/tracker.py +++ b/invesalius/navigation/tracker.py @@ -210,10 +210,9 @@ def SetTrackerFiducial(self, ref_mode_id, fiducial_index): self.SaveState() def ResetTrackerFiducials(self): - Publisher.sendMessage("Reset tracker fiducials") for m in range(3): self.tracker_fiducials[m, :] = [np.nan, np.nan, np.nan] - + Publisher.sendMessage("Reset tracker fiducials") self.SaveState() def GetTrackerFiducials(self): From a7880fd112d8b94bb89a44041c22b572044f3119 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 15:27:43 +0300 Subject: [PATCH 92/99] FIX: Nav panel undesired behaviour --- invesalius/gui/default_tasks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/default_tasks.py b/invesalius/gui/default_tasks.py index 0cede9903..4d7f1991c 100644 --- a/invesalius/gui/default_tasks.py +++ b/invesalius/gui/default_tasks.py @@ -362,7 +362,10 @@ def SetStateProjectClose(self): item.Disable() def SetStateProjectOpen(self): - self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) + session = ses.Session() + mode = session.GetConfig('mode') + if mode == const.MODE_RP: + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(1)) for item in self.enable_items: item.Enable() From 7def8aaf3e295056b5fbe1ed7db89242e905c9d1 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 16:14:11 +0300 Subject: [PATCH 93/99] ADD: Reset image fiducials function --- invesalius/navigation/image.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/invesalius/navigation/image.py b/invesalius/navigation/image.py index 69182b9e6..d21f33431 100644 --- a/invesalius/navigation/image.py +++ b/invesalius/navigation/image.py @@ -54,6 +54,11 @@ def SetImageFiducial(self, fiducial_index, position): def GetImageFiducials(self): return self.image_fiducials + + def ResetImageFiducials(self): + self.image_fiducials = np.full([3, 3], np.nan) + Publisher.sendMessage("Reset image fiducials") + self.SaveState() def GetImageFiducialForUI(self, fiducial_index, coordinate): value = self.image_fiducials[fiducial_index, coordinate] From dc64b581793b5d5a0aa7781d2ebc150ed919dc15 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 16:16:24 +0300 Subject: [PATCH 94/99] FIX: Close Project fixes --- invesalius/gui/task_navigator.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index f05003830..602e4b870 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -525,6 +525,7 @@ def __bind_events(self): Publisher.subscribe(self.UpdateImageCoordinates, 'Set cross focal point') Publisher.subscribe(self.OnNextEnable, "Next enable for image fiducials") Publisher.subscribe(self.OnNextDisable, "Next disable for image fiducials") + Publisher.subscribe(self.OnResetImageFiducials, "Reset image fiducials") def LoadImageFiducials(self, label, position): fiducial = self.GetFiducialByAttribute(const.IMAGE_FIDUCIALS, 'label', label) @@ -607,6 +608,13 @@ def OnNextEnable(self): def OnNextDisable(self): self.next_button.Disable() + def OnResetImageFiducials(self): + self.OnNextDisable() + for ctrl in self.btns_set_fiducial: + ctrl.SetValue(False) + self.start_button.SetValue(False) + self.OnStartRegistration(self.start_button, self.start_button) + def OnStartRegistration(self, evt, ctrl): value = ctrl.GetValue() if value: @@ -1184,8 +1192,9 @@ def __bind_events(self): Publisher.subscribe(self.OnCloseProject, 'Close project data') def OnCloseProject(self): + self.tracker.ResetTrackerFiducials() + self.image.ResetImageFiducials() Publisher.sendMessage('Disconnect tracker') - Publisher.sendMessage('Update object registration') Publisher.sendMessage('Show and track coil', enabled=False) Publisher.sendMessage('Delete all markers') Publisher.sendMessage("Update marker offset state", create=False) From 522d2bd932c760788d65d4604dfdb2649acd368e Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 16:16:44 +0300 Subject: [PATCH 95/99] MISC: Some fixes --- invesalius/gui/task_navigator.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 602e4b870..f3349999c 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -251,6 +251,7 @@ def __bind_events(self): #Publisher.subscribe(self.OnShowDbs, "Show dbs folder") #Publisher.subscribe(self.OnHideDbs, "Hide dbs folder") Publisher.subscribe(self.OpenNavigation, 'Open navigation menu') + Publisher.subscribe(self.OnEnableState, "Enable state project") def __calc_best_size(self, panel): parent = panel.GetParent() @@ -276,6 +277,11 @@ def __calc_best_size(self, panel): panel.SetInitialSize(size) self.SetInitialSize(self.GetSize()) + def OnEnableState(self, state): + if not state: + self.fold_panel.Expand(self.fold_panel.GetFoldPanel(0)) + Publisher.sendMessage('Back to image fiducials') + def OnShowDbs(self): self.dbs_item.Show() @@ -364,7 +370,9 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect default_colour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_MENUBAR) except AttributeError: default_colour = wx.SystemSettings_GetColour(wx.SYS_COLOUR_MENUBAR) - self.SetBackgroundColour(default_colour) + #Changed from default color for OSX + background_colour = (255, 255, 255) + self.SetBackgroundColour(background_colour) book = wx.Notebook(self, -1,style= wx.BK_DEFAULT) book.Bind(wx.EVT_BOOKCTRL_PAGE_CHANGING, self.OnPageChanging) @@ -492,6 +500,10 @@ def __init__(self, parent, image): start_button.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnStartRegistration, ctrl=start_button)) self.start_button = start_button + reset_button = wx.Button(self, label="Reset", style=wx.BU_EXACTFIT) + reset_button.Bind(wx.EVT_BUTTON, partial(self.OnReset, ctrl=reset_button)) + self.reset_button = reset_button + next_button = wx.Button(self, label="Next") next_button.Bind(wx.EVT_BUTTON, partial(self.OnNext)) if not self.image.AreImageFiducialsSet(): @@ -499,15 +511,18 @@ def __init__(self, parent, image): self.next_button = next_button top_sizer = wx.BoxSizer(wx.HORIZONTAL) - top_sizer.Add(start_button) + top_sizer.AddMany([ + (start_button), + (reset_button) + ]) bottom_sizer = wx.BoxSizer(wx.HORIZONTAL) bottom_sizer.Add(next_button) sizer = wx.GridBagSizer(5, 5) - sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) sizer.Add(self.btns_set_fiducial[2], wx.GBPosition(0, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_HORIZONTAL) - sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) sizer.Add(background, wx.GBPosition(1, 2)) main_sizer = wx.BoxSizer(wx.VERTICAL) @@ -608,6 +623,10 @@ def OnNextEnable(self): def OnNextDisable(self): self.next_button.Disable() + def OnReset(self, evt, ctrl): + self.image.ResetImageFiducials() + self.OnResetImageFiducials() + def OnResetImageFiducials(self): self.OnNextDisable() for ctrl in self.btns_set_fiducial: @@ -749,9 +768,9 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi ]) sizer = wx.GridBagSizer(5, 5) - sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 0), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) sizer.Add(self.btns_set_fiducial[2], wx.GBPosition(0, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_HORIZONTAL) - sizer.Add(self.btns_set_fiducial[1], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) + sizer.Add(self.btns_set_fiducial[0], wx.GBPosition(1, 3), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL) sizer.Add(background, wx.GBPosition(1, 2)) sizer.Add(register_button, wx.GBPosition(2, 2), span=wx.GBSpan(1, 2), flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) From e868c2f52e594bff11adcebd93a6d16e6257dd20 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Thu, 17 Aug 2023 16:59:09 +0300 Subject: [PATCH 96/99] FIX: Minor Bug fix --- invesalius/gui/task_navigator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index f3349999c..28a967c19 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -1155,7 +1155,13 @@ def __init__(self, parent, navigation): def __bind_events(self): Publisher.subscribe(self.OnObjectUpdate, 'Update object registration') + Publisher.subscribe(self.OnCloseProject, 'Close project data') + Publisher.subscribe(self.OnCloseProject, 'Remove object data') + def OnCloseProject(self): + Publisher.sendMessage('Check track-object checkbox', checked=False) + Publisher.sendMessage('Enable track-object checkbox', enabled=False) + def UpdateObjectRegistration(self): self.object_reg = self.navigation.GetObjectRegistration() From f223f7451d96112eb50c6079447b37270e413bf0 Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 18 Aug 2023 11:44:51 +0300 Subject: [PATCH 97/99] MOD: Make status colors color-blind friendly --- invesalius/data/viewer_volume.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 2c3520cd5..5ed54bb86 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -529,18 +529,21 @@ def OnSensors(self, markers_flag): self.probe = True self.CreateSensorID() + green_color = (0.40, 0.76, 0.65) + red_color = (0.99, 0.55, 0.38) + if probe_id: - colour1 = (0, 1, 0) + colour1 = green_color else: - colour1 = (1, 0, 0) + colour1 = red_color if ref_id: - colour2 = (0, 1, 0) + colour2 = green_color else: - colour2 = (1, 0, 0) + colour2 = red_color if obj_id: - colour3 = (0, 1, 0) + colour3 = green_color else: - colour3 = (1, 0, 0) + colour3 = red_color self.dummy_probe_actor.GetProperty().SetColor(colour1) self.dummy_ref_actor.GetProperty().SetColor(colour2) @@ -558,6 +561,7 @@ def CreateSensorID(self): reader.SetFileName(filename) mapper = vtkPolyDataMapper() mapper.SetInputConnection(reader.GetOutputPort()) + mapper.SetScalarVisibility(0) dummy_probe_actor = vtkActor() dummy_probe_actor.SetMapper(mapper) @@ -579,6 +583,7 @@ def CreateSensorID(self): reader.SetFileName(filename) mapper = vtkPolyDataMapper() mapper.SetInputConnection(reader.GetOutputPort()) + mapper.SetScalarVisibility(0) dummy_ref_actor = vtkActor() dummy_ref_actor.SetMapper(mapper) @@ -600,6 +605,7 @@ def CreateSensorID(self): reader.SetFileName(filename) mapper = vtkPolyDataMapper() mapper.SetInputConnection(reader.GetOutputPort()) + mapper.SetScalarVisibility(0) dummy_obj_actor = vtkActor() dummy_obj_actor.SetMapper(mapper) From 69fb27bcee2c6d869e7190d9b6c051c100c1805d Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 18 Aug 2023 14:48:07 +0300 Subject: [PATCH 98/99] FIX: Editable numctrl bug fix --- invesalius/gui/task_navigator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 28a967c19..52c758c11 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -986,7 +986,7 @@ def __init__(self, parent, icp, tracker, image, navigation): coord_sizer.Add(self.labels[m], pos=wx.GBPosition(m, 0)) for n in range(3): coord_sizer.Add(self.numctrls_fiducial[m][n], pos=wx.GBPosition(m, n+1)) - if m in range(1, 6): + if m in range(6): self.numctrls_fiducial[m][n].SetEditable(False) txt_label_track = wx.StaticText(self, -1, _("Tracker Fiducials:")) @@ -1161,7 +1161,7 @@ def __bind_events(self): def OnCloseProject(self): Publisher.sendMessage('Check track-object checkbox', checked=False) Publisher.sendMessage('Enable track-object checkbox', enabled=False) - + def UpdateObjectRegistration(self): self.object_reg = self.navigation.GetObjectRegistration() From f6bd270c93ea4edb2baed1dd2e749f203924104d Mon Sep 17 00:00:00 2001 From: Mahajan Anshul Date: Fri, 18 Aug 2023 15:26:23 +0300 Subject: [PATCH 99/99] FIX: Minor edits --- invesalius/constants.py | 6 ++++++ invesalius/data/viewer_volume.py | 4 ++-- invesalius/gui/task_navigator.py | 28 +++++++++++++--------------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/invesalius/constants.py b/invesalius/constants.py index 5af36811e..6a460bec8 100644 --- a/invesalius/constants.py +++ b/invesalius/constants.py @@ -182,6 +182,12 @@ _("Inverse Gray"):(256, (0, 0), (0, 0), (1,0)), } +#Colors for errors and positives +RED_COLOR_FLOAT = (0.99, 0.55, 0.38) +GREEN_COLOR_FLOAT = (0.40, 0.76, 0.65) +RED_COLOR_RGB = (252, 141, 98) +GREEN_COLOR_RGB = (102, 194, 165) + # Volume view angle VOL_FRONT = wx.NewId() VOL_BACK = wx.NewId() diff --git a/invesalius/data/viewer_volume.py b/invesalius/data/viewer_volume.py index 5ed54bb86..c6b42dff5 100644 --- a/invesalius/data/viewer_volume.py +++ b/invesalius/data/viewer_volume.py @@ -529,8 +529,8 @@ def OnSensors(self, markers_flag): self.probe = True self.CreateSensorID() - green_color = (0.40, 0.76, 0.65) - red_color = (0.99, 0.55, 0.38) + green_color = const.GREEN_COLOR_FLOAT + red_color = const.RED_COLOR_FLOAT if probe_id: colour1 = green_color diff --git a/invesalius/gui/task_navigator.py b/invesalius/gui/task_navigator.py index 52c758c11..ecb526cec 100644 --- a/invesalius/gui/task_navigator.py +++ b/invesalius/gui/task_navigator.py @@ -668,16 +668,13 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi self.bg_bmp = GetBitMapForBackground() - RED_COLOR = (255, 82, 82) + RED_COLOR = const.RED_COLOR_RGB self.RED_COLOR = RED_COLOR - GREEN_COLOR = (118, 255, 3) + GREEN_COLOR = const.GREEN_COLOR_RGB self.GREEN_COLOR = GREEN_COLOR YELLOW_COLOR = (255, 196, 0) self.YELLOW_COLOR = YELLOW_COLOR - TEXT_COLOR = (250, 250, 250) - self.TEXT_COLOR = TEXT_COLOR - # Toggle buttons for image fiducials background = wx.StaticBitmap(self, -1, self.bg_bmp, (0, 0)) for n, fiducial in enumerate(const.TRACKER_FIDUCIALS): @@ -692,14 +689,13 @@ def __init__(self, parent, icp, tracker, navigation, pedal_connection, neuronavi # ctrl.SetValue(self.tracker.IsTrackerFiducialSet(n)) # ctrl.Disable() w, h = wx.ScreenDC().GetTextExtent("M"*len(label)) - ctrl = wx.StaticText(self, button_id, label=label, style=wx.TE_READONLY | wx.BORDER_NONE | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER_HORIZONTAL, size=(55, h+5)) + ctrl = wx.StaticText(self, button_id, label='', style=wx.TE_READONLY | wx.ALIGN_CENTER| wx.ST_NO_AUTORESIZE, size=(55, h+5)) + ctrl.SetLabel(label) ctrl.SetToolTip(wx.ToolTip(tip)) if self.tracker.IsTrackerFiducialSet(n): ctrl.SetBackgroundColour(GREEN_COLOR) else: ctrl.SetBackgroundColour(RED_COLOR) - #ctrl.Bind(wx.EVT_TOGGLEBUTTON, partial(self.OnTrackerFiducials, i=n, ctrl=ctrl)) - self.btns_set_fiducial[n] = ctrl @@ -941,10 +937,12 @@ def OnStartRegistration(self, evt, ctrl): callback=self.set_fiducial_callback, remove_when_released=False, ) - # for button in self.btns_set_fiducial: - # button.Enable() - if self.tracker_fiducial_being_set < 3: + + if self.tracker_fiducial_being_set is None: + return + else: self.LabelHandler(self.btns_set_fiducial[self.tracker_fiducial_being_set], self.tracker_fiducial_being_set) + if not self.tracker.AreTrackerFiducialsSet(): self.OnRegisterEnable() @@ -1077,9 +1075,9 @@ def OnUpdateUI(self): self.txtctrl_fre.SetValue(str(round(fre, 2))) if fre_ok: - self.txtctrl_fre.SetBackgroundColour('GREEN') + self.txtctrl_fre.SetBackgroundColour(const.GREEN_COLOR_RGB) else: - self.txtctrl_fre.SetBackgroundColour('RED') + self.txtctrl_fre.SetBackgroundColour(const.RED_COLOR_RGB) def OnResetTrackerFiducials(self): for m in range(3): @@ -1281,9 +1279,9 @@ def __init__(self, parent, navigation, tracker, robot, icp, image, pedal_connect # Constants for bitmap parent toggle button ICON_SIZE = (48, 48) - RED_COLOR = (255, 82, 82) + RED_COLOR = const.RED_COLOR_RGB self.RED_COLOR = RED_COLOR - GREEN_COLOR = (118, 255, 3) + GREEN_COLOR = const.GREEN_COLOR_RGB self.GREEN_COLOR = GREEN_COLOR GREY_COLOR = (217, 217, 217) self.GREY_COLOR = GREY_COLOR