From b0c9aa281ab9b0083093c0d0cf2a12e32889434c Mon Sep 17 00:00:00 2001 From: Diego Freitas Date: Thu, 14 Aug 2025 12:51:45 -0300 Subject: [PATCH] ajustes no IMU e deteccao de obstaculos --- .../workers/health_worker/modulos/imu.py | 19 ++- .../__pycache__/direcional.cpython-311.pyc | Bin 20685 -> 20939 bytes .../manager_worker/modulos/direcional.py | 32 ++-- .../shared/__pycache__/utils.cpython-311.pyc | Bin 12617 -> 13922 bytes .../Python/Scripts/workers/shared/utils.py | 18 +- .../camera_manager.cpython-311.pyc | Bin 118882 -> 118584 bytes .../__pycache__/config.cpython-311.pyc | Bin 3459 -> 3459 bytes .../workers/visual_worker/camera_manager.py | 11 +- .../Scripts/workers/visual_worker/config.py | 1 - .../processamento/costmap_fuser.py | 154 ++++++++++++++---- 10 files changed, 166 insertions(+), 69 deletions(-) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py index 552996709..00c551ee8 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/health_worker/modulos/imu.py @@ -53,6 +53,16 @@ class IMUCamera(ModuloDiagnosticoBase): self.imu_em_falha = False if imuData is not None: + # dt estimado por pacote + now = time.time() + if self.last_imu_ts is None: + dt = 1.0 / max(1e-3, f_tick) # fallback + else: + dt = max(0.0, now - self.last_imu_ts) + self.last_imu_ts = now + dt = min(dt, 0.05) # clamp anti-bursts (<=50 ms) + self.filtro_imu.Dt = float(dt) + for packet in imuData.packets: accel = packet.acceleroMeter gyro = packet.gyroscope @@ -78,15 +88,6 @@ class IMUCamera(ModuloDiagnosticoBase): yaw -= self.yaw_inicial Rwb = r.as_matrix() - # dt estimado por pacote - now = time.time() - if self.last_imu_ts is None: - dt = 1.0 / max(1e-3, f_tick) # fallback - else: - dt = max(0.0, now - self.last_imu_ts) - self.last_imu_ts = now - dt = min(dt, 0.05) # clamp anti-bursts (<=50 ms) - self.filtro_imu.Dt = float(dt) # 1) aceleração no mundo a_body = np.array([ax, ay, az], dtype=np.float64) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/__pycache__/direcional.cpython-311.pyc b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/__pycache__/direcional.cpython-311.pyc index 1e3631b3b39e90a40a6aa6b50e1ce01a3b6e563f..f818f29e2f53971594d3ffeb704ecfe7152a02fb 100644 GIT binary patch delta 1205 zcmZWnZA=?=7{BWav}Mpzt}mm*-gv3fmi8#4g?4na44i;r$w-(&t=Ha7&lXCq?Iz_2 z_|Xq0g#QO?j9Ce8BU!T4G%@|)%p!hRe2I76B{Vg}=~uth7){I)^?$T;;-1_+zbC){ z@A*B?y?nQWezS|V-EM8wATUn-^6^_go&U7$ZM$qrQ_R{|m2w$-8f@B60xfYC{R8BQ z7mM!`9BS$1w3IzAlbYD)(k&9!gK_|u$E|H+H=x4VP^^MQPgnZ@r>0yVDJVDBdV~aU zdWPtWoSJKiE2M5VMAirKL={)ER=vRJpI}*-1bak+j&j}Hk~H@@Nu6NlmYMB%_c9U zKEB9$`;!sDXGW~+xpu0j@pwwAiPKX)X((hz;z#?jb#@f8KyX{TdyurfzPOZo5!SKZ zaqI>A!K0yBG*BeYXk;5zRWw1y;>k=pV}tVAjT<-dP=moK8egH=7`?K8dw)0S!Y`35 zxlAQpRaG*YN|Uh!ozXvOnuW~%PArp1;v~*qgLI}DCVUD1`Mpo@OlQ4*VVRE6snq@s z%aHxh@a2Xh8-(Xo?FELVV@z_HOk8A^)2XWdT4afQCz8s%NwW+^MwV7m)uv^VWs}#d z>J&rKktjo^*eq-YKX{|45d?j!P0Fi@B$)=E`942U)ifN)t2DSiGNwL&AAJ+4$!3c8 zMo!4(ed-|iHqd*7zXg8W$a(Xxl`U@G;y%<__)`;tZc@}u^18`G(_o=jF!@B2kJtGg zX^k~R-MWN6)mN}C9_uftOOY}$!xJ-yn3cz~g;!y!Uk}dxTf#!X){kRVuIEtYHc+{ z(Hh+I7Os>XW4vRmVzhj~EN(OT7QyHgjZWU^DaZ~CzOunrYQEng7$!u+#FngL#-Fj@ zQdT+)ImPzq)@TXKW%3chIxI2DZU2^EV)(vc!RiH%$Ks*O1IU-gPzQvs>=SMXUw2xi z?7u)j_}<}#neQIgKzKNynd_E6^x<<|a?ymsTI^EJbr<)7cNOTL#rg5a&1el=o}NXG n#og(&94!O$tPB0S_|oioRR0Q!5;f#M`E~H&B|`BR{8jz|eI9Q_ delta 953 zcmZ8fZ%7ki9KQE%vrb*Rxt(h|^K5Fme{O0`V`YsgT7sJNK?!8;7G2Y=ZnvOy^Ut>k ziqZQ)OTtNHVi4B81VL05h*90*_C>V(ARmwmxd1pUsuESXS!FYeE#hd3oQXbkoOLxc}#6 zBE=f~vYeYQVvEEww$+LHKncYJg*-@I!rcI%M6&%0Eb(v2#2VK(=H9X(b^8$ z5^Jr+_+(gzRS_`HIL4KfW8MWdBrM}niFk#R0?BupUl=P4K| zWyY;czlZk(*q}A6{u|LZ8WL6)yy3ARvl?gkOGpn_B4EZDC&O|f!4vRu9wy-P`a+@A z1)lTlXEwAn&Hq&G9}j7YumDy19T5g0~%`fQG*vmtnN+shod}Owiqa zgj|$k*c6tbJD^mD;4%k=8mS%JGTPNmSiZ&y(tuxmz-Kqqj)<=t?7k?Q_PT zZ_aMtlhj0-nvkf8e{-@$n6Bz-&z4d*TX#>BRMw}J^;ry8G-v3FnW39QvsaR|DNUOs z+P=ic@Qi-neAn%+xMRtk)Z5c~dyLE|_e#oI`0VIjvwl`csST3akg?cfowH3zjZxAV zGxWYVouca{x;|rVj&({E#-zpse;)O_PSs#9%Uxa3}`VEzQuspHY@lXc7Cr-P6?E+umKyM$=Bo=xF0=$Fe{Z{ZZ Z(b<0tl$`{?mc@49Fq|2%$$p@3_y@yOA$tG- diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py index 2ccc21e9b..f150180b5 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/manager_worker/modulos/direcional.py @@ -217,25 +217,27 @@ def _regras_taticas(contexto): blocked = bool(block.get("blocked")) if isinstance(block, dict) else False reason = (block.get("reason") if isinstance(block, dict) else "none") or "none" - if blocked: - mostrar_log(f"🟥 Bloqueio detectado ({reason}). d_obs_min={d_obs_min if d_obs_min is not None else '–'} m. Parando.") + if blocked and reason == "blackout": + mostrar_log(f"🟥 Blackout de percepção. Parando.") return _comando_direcional_parado(True) - if (d_obs_min is not None) and (d_obs_min <= dist_necessaria): - mostrar_log(f"🟥 Obstáculo à {d_obs_min:.2f} m < distancia necessária {dist_necessaria:.2f} m. Parando.") - return _comando_direcional_parado(True) + if blocked and reason == "obstacle": + if (d_obs_min is not None) and (d_obs_min <= dist_necessaria): + mostrar_log(f"🟥 Obstáculo a {d_obs_min:.2f} m ≤ {dist_necessaria:.2f} m (necessária). Parando.") + return _comando_direcional_parado(True) + else: + # opcional: limitar velocidade para manter margem de frenagem + # v_max_safe = sqrt(2*a*(dmin - margem)), se dmin existir + if d_obs_min is not None and d_obs_min > margem_parada: + v_max_safe = (2.0 * a_max_freio * max(0.0, d_obs_min - margem_parada)) ** 0.5 + contexto.setdefault("DirecionalHints", {})["v_max_sugerida_mps"] = float(v_max_safe) - # opcional: dica de veto lateral pro MPC (se você consumir) - side_bias = None - if isinstance(block, dict): - sb = block.get("side_bias") or {} - side_bias = sb.get("value", None) - if side_bias is not None: + # motivo narrow: não para, mas dá dica lateral + if isinstance(block, dict) and reason == "narrow": + sb = (block.get("side_bias") or {}).get("value", 0.0) hints = contexto.setdefault("DirecionalHints", {}) - if side_bias > 0.25: # mais fechado à direita -> evite virar p/ direita - hints["vetar_direita"] = True - elif side_bias < -0.25: # mais fechado à esquerda -> evite virar p/ esquerda - hints["vetar_esquerda"] = True + if sb > 0.25: hints["vetar_direita"] = True + if sb < -0.25: hints["vetar_esquerda"] = True # se chegou até aqui, pode seguir return _comando_direcional_parado(False) diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/__pycache__/utils.cpython-311.pyc b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/__pycache__/utils.cpython-311.pyc index e5b2bd2535f621014f243bbab1888756f43f7abf..37c34f295e4eced614aba7b0821ae4123b908669 100644 GIT binary patch delta 2961 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 diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/utils.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/utils.py index 5123a8076..0977e797a 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/utils.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/shared/utils.py @@ -2,9 +2,10 @@ import base64 import math import cv2 import numpy as np -import cupy as cp from manager_worker.config import mostrar_log +from shared.contexto_global_redis import ContextoGlobalRedis +from shared.enums import StatusModulo, T_Code @@ -206,3 +207,18 @@ def resize_keep_width(img: np.ndarray, new_w: int, min_h: int) -> np.ndarray: new_h = min_h return cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_AREA) +# ---------------------------- +# Helpers equipamento +# ---------------------------- +def get_velocidade_atual_ms(): + velocidade = 0.0 + try: + velocidade = ContextoGlobalRedis.get_contexto().get("Gerais", {}).get("velocidade_ms", 0.0) + if velocidade == 0: + imu = ContextoGlobalRedis.get_modulo(T_Code.Imu) + if imu is not None and imu.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value) == StatusModulo.OPERANTE.value: + velocidade = imu.get("vel_mps", 0.0) + except Exception as e: + mostrar_log(f"Erro ao consultar velocidade atual: {e}") + finally: + return velocidade diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/__pycache__/camera_manager.cpython-311.pyc b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/__pycache__/camera_manager.cpython-311.pyc index 9cac9da3d37ab8e561fb5c6f1105e5acb20a6b7f..b8d2cac2750b043ce61ec20e09d3e2f3136de095 100644 GIT binary patch delta 14123 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 b?c>+PtlBf3V(>B3&*N8||IH~%ne+bzFpKB( delta 14203 zcmb7L34D~*wa=X`%Oqrh>?BMQ0t{dX`%X59pa@6;D5%4iS&|{Mac9C3!JyWqQf2wX z5A~^7t$?^-BgWcVYt@4LQY`vW646c! zkt;`XIU_JC=v?Uxj$S!hEfvLLO!o{?E`&wB+_b9uP16s}qO^OJt=yblPK2X^z#%IL zWtnO-QkduH0bHc_I!mvyjY4C)IAbbG5e=5i}RuT-MCnI7aY^}+0-qG{-n4=6>q z6o1I?_A8#SbgJ)X=ZnJbud*vd$!N6YxMKdGHyTrd5tkBD0{*C1j->{5{NU2==D`z0 zDL9hD&^J3im&+XvdEGI^qsVFM{X<5Hitbm3EU;$OQzxfcDj3kBRunA|6>6~PX;G<; zDRzm?>ek|_ydR)(6G7BQ;BaiAkE~E%FTSO?k}CEK-Rg;LjCgy$rH|7pm!rR;ZWvlJ zjT!kNo^K@3ZLQMpajlovq9#E$nI0XhS34ZFwT1*&sILzF4N36Ul6%C^Dd_CP@JuDv zLCft_q${d)c)TRK#lwp0RCE}G4(W5}H8m_=G_Rq#Zth~a6!nlMEsz=EjvZ7HH4``- z@%>ucePGy!CgD`Kmluf%>Va~{lrjwMAFrFEF}dHuM0Bf9((dVI>5WRJWieYvH6v^lXHO#U!H_)!(E$|OMtm;n zK+$QD`_Tbgz|xRzqn1~8FO~JwezlN4rsn}<)Lo;tk~qtp*NJo38%eCaJ9_b~@8E@I z1Z~|OkzSAA9rK4nQ6~JqmhBlcR*3t%cZ^+Twt*A*kox(!i@WYaaWF_E&-W^^pc0V} z<0*Y9)UY`mnHydol2z!n81?ohZ)?ySiYaa-EXSe7I@jCM(JCjP%Mbw6K;BK@Og6Uk zaVqP=r;C^_TzV?+jVtPHrK0+a>ReLux2lf_TKk<7zOg1$N$$cpYc$~XMx=wva;^H< zw0s-*kh|5_rsaz=_2X#{F0QfgMB!XWpZEjg z=)QIO5_1V}&F?|-Jpxub(sDz^BdVBvBbnJvv)^rdg<4PDeEQIy|NPCtIr3!;cn9z< z0OOq|`CF91Uf;@(QCT;|#_P#_K7hyYbQl9&|Z))5HHk1A&9LPPpBjdcpfq!+5ys|qa;u=?PF2GOAYX+fE-0yN%Mb1&$og}r#e>%!hY zmCRJ{Tj(4<89n-`oHMCxLu*()vv5dJ770Rz@SKg3AEj!*8nyJIlH7JG>KO?h!zA9I zy5yn~5<}>se0wei@#J~x_KR}uId~on7@{7$=u(nN+M@HJ>q{4xnZ?U$Z0RLkVUUNi z%Wl9VzS?N|{0I%OrKp|2&{al5VIi;1a@FB0=zhM* zX);AD>YtY#F|Dzx_b$#iSvRSxmlQa~Cd;OzHBff`P&s8lIkm66CQTi;B0tj>!%Xpy zLCBg+W9;hM6=lWN2S{HJKxg^0$>y=nqSB_MJ?ZKf%X8H%Po6z#O-8&qNxgb%ex+?w z@$8Ap8WZRVF>Yph#O#9VbF~k%r*S1uPA%o80;n|XWk)p1>BtLU-A7xL<=G53Q zb^+7n^GT2f51$FqCewv z8Ijs--<%#ASCJH((N~+4G(1U`Z_XeprRuX~x#o;b8CteU6xuVc$_S-+k~Y(~`t6#F zn+GjwX4`L+SI}VgfaGqfX#2_YIq6Fu98_Y`-|3CI5Xt!bO31AkmQDl6k#jik63WTz zya87*5>=nRw6SCuo11Ql(on-NajqCDzDuoKIjrjvJafdt7R>XrO|k`eK+)C_@hCAb zn}Zw+dxH$+c__h#^)$qE!LSGZFcTH#1;GxENaSwxd>wEU4Y?j1Xs(t(*xjzQdR_jm zV6-i~-Z@>O*;t7rmJ?>uAob8ynuq%(<~^&&0h`e>`>H)PwbJR!luwG4)u*%(p6LJ~{t| zz|lt6(MIo^6~3btzPHz`JG!RpZ%Ik*qS5phQl>`J8OC?=3bodqt-JmE2EUni-qF1C zwwq5B4?9*o`K{u~2R%opEIiWohF?8;**(+FACYu$D8aKOjVbhXxKhloOgcOwZGMjV zaCO1_4D*o`E7csyuu{#D95c$5S@S1%AGo~DRPrQA%6|g#ei^b%^>|}mw>#{OLMhei z)%Er@RBw3KRqE!|C0&TZ&+(tym8K3@sROIxmGW`M>YR=VMp7VAi4OQ16q(3uQ_ zDVMt=8Vi5h{bT8}Q8;YcID*i*vAXLQ;DTM-eN@sd5hd29Th@gAAxTC{j@TbuFa6{< z3o+l*=#B7S{t}J3l)(kok=Tuhy)T62WZ)U}8V5+&NsggCE|6MHpj!(f3Z=mE3OuA4 zR6@JCQ4YZ>l`QYyAO5VY;NJ^=crGowQ5bo+e$GXi#G?u zd_+>V0|EdnMh230N)85P zp<9#<{;0$eNbGO}2kn!vSfQIkN=R-+_e`u9S&IBn-L^fiyq=1>-S3II+yNyT^+xBI z(TIQ|PKM(0A?oq%4P}^BUQa+X+(2Kta7WRv@6@?Tt=Tb4>{B=Gm_pJyv9nNpWk*xt zO_*?NoQ-HlkaAHBmOIsHJ4@+wTCsD!cu?K5vxdj)C>=0?#I~L@#WwYP@vwSKEvu@e zMhY#q;md5$M1Q)OSnu-sd~u0xG5n{&+rz5+)~PhlowxpAL42NXFvRK$gq0ZGcNm-z zvw9{nLRTa#<8qOk(NV04TIriFP7K1DP-}NgNJA{`4hJK1eyZNTtB$7lcvl4(SK4j& z*dC$*@>2ED+p5KN>WSO(?Ei@hR>dz>p$+chkOfJho=i6pZA!$eCsP=r#9~r{J&BZ5 zH!odQ$2O{4f?nE91(nwIZC>e>*FwhI(e-BVo=UqNq(q;3eT_Iljv z^s-5BgmQVgex>vJ)Okg(y_5{5n2 zFzjXOAGKzi4J;+Avv&`#`XSQ~g<~#%Ylx=pP+WeGZd%YJ4{(de8;P}%_xK5hJg9El zoiFZJ_wF7udpnvfm{?Ee?TD)=djokly59-F*2rBb-2=FdAUYPl!kJRbAMzNrx7Ub^ zySLo_2b;}=PBqb`wf+JA=b?4>Iq z`BCuFm+W6GUg_Sqe_c`WpQ*W7euuQ>`|9-PN7>jTZB<*JcXoXXMj%e7xfemZA<1mo zAF}o=`jXqIN1udw+W_PP(8mCldt&>rbMgZK?;5wBz$5lbo*Av9G>+71cR(D!``h_uc``r7&va0XX5_t;zvb8Iqb|ryc zs-8Gr+GY3w|n&zpFa>- zLIc>n=++MEJdK@!`~;IN#yq_!^{W~kb?`jocM>{RkWjkCXDOXJ=CGqlreM!f3F0@( z8{*%c@;FGdR(^`oXMoQETQKo=sHZ%Q)<3DY9=<*s>PU`KiMIDC>Et8jwlC4)EA{dt zBT^)*S&~V;`pB&|*4xF<+ojOk@Qd?AdiS9hGqULHeeT^$rrwWnd|<>6FFp+H?r^~6 zrdv*_1Y`|$iC@#=)(|Y-q%PIxi4WA>`fNK6E8{%AW>?CTkqo>qo-XB4y@5k>i68}&qe^5b3)}J5f_h*lO zkXx}F88@6s7hN}?)sHZ+?693gb^TdUzT`aE}kb?JYC4T5*sg-Pp zF!i{iC)LV_upV}!^;DmTTpB4v+k>`y33Mw*t`f{krpuK2$LeX`AbuV3huXq)$*o#H zy=>kABIm!6u3C*D^=Iu} zyHHF>dxBvf?P(FxpnwwduZ!>R1lYvnbVHw4 zQI}4qdZIX2eed)TVebCR=`Sr}X7?w5QB0#*a(s~T{;;R#?W)Hbu7ETP2%4o!jea@4 zxnKV!OEMvYTtf!D!nF97x@hkpkH67wCIxxJaP3Rt2tBtKUKApdQvLeN@+^)8ux#0Z z${Ka_SJMpp&c#{^l8v>jRd4vpDI(pEeRWXSFqzw;?$|S=`^vAcO0&I(;U>hr+F-Mo zF`-VFsT9d<@|zF|OF!Fv)v8d=}-)*#_avl*tS%_$V+SvR2XOBl>4 z;LBR1P>d2=wc86taRn!Yi6EEhLeE-_F!&^q)Q%O3MeQsZN-frTTj?+09A?C*36(*# zYNR3xnBETHd?bZDRWzbdJ`BCltsG>~Z*CodfP4gl9tAuJVC|ZwZ7&i97j>iJb&Rky z)GeyhQy14aHP4(T1T;+Ly&*ru}7baSW4uq|K!3*sQm}aSCqn=(=jXJgS9;ib5NEhPSnw zhl)Dm)Sj*#9V*6(PkTg($h2m$H^IIbj^t#Iqg-qeHHnCN5b^1>85smjkzY?}_HKwZ z;li8^md*5AYI7Q~g|upP@Rttx7Gx1w>&fAc7~(_u4=sDR$g?3@m1){}!$kpUYU6M* zU98r&3>WlPLC>5@@j$wfKr!>bg|v4U#px88)9^hVkf6q0&E74^zx&U-@n9|fVlthEf*0xO$ zdGk)vnwseq05f{^FI`&j_zvcU)9uACPp6hXS$f@P7r#>@-84oQD`gS2eQd2547P#bm6?S!`Go zXY&fkunDn;Wrc<=4@eOYYR4yvy4CD*e}^XCWOlyTWW$#3!Vbby>sA@=2zg`*re^!; z_u9^?gmcx6-y-<8!MULFGi}o(F|+D9(0LwU7z~=u-gaCvt&4BF_R%CUtNv3k^%=m> zBaG&wx!;7O3EZ$-PPkx!x}E5prMV}I<)nE}O%}6-zvt`8BAPNWmD-z0)eZK*$V7~o z1egq%0>GI{o#vtZH|_2@!Z9=nGzCH5^&h9d6VPL>rq2=Oh4tuh32_`vrQvk28y{Jr z<<^ky|5jUEBW|*>E#~%oP$L?|Dt4*J0DE0v$#?hg`9`hD!@2EH2}s+mjdB%K%Q-~ zT{2;_99WG<_k^)>6vldzFx#oDCw;HVw6scj4CK?4K%`BfA6KI>I+^H8)=ewuZ@!PdeR4U*H3OERkEvrC{YB(# z)ZoTg)}Y$lqIh&0fs5KE(H7#}Dv$gwKX9(Haez7vl%@k_0RCNH!AF%RGBp_Y8J(D% zEdV5ZM~Y#cKGZ_q@&c4DB+v)-$+LYuWhqgaa3?rU;?9I}WvFDn_UF|iU;ALS7%ALZ zvLb?pZQs(33Pa6zf-4(7jMD|#YpRu6WkiiX1jP4T_0;#dPrch=O-TF6ID2~ z>DPdNLhVv07v+0&`i-8Z(5)aDP0S(x*)!cI%1yZgRt63YFQrA=L~++Apf`wyb_63E zjVACL@hNqR?H>lZ_MuJH?xC~5_@Ho)1YCjC#ei;lc3cFcTw z(n1h(UU#*aUv(jso4?mp%S(x#o{U!mTpiP7y7u?0NfrL7m0lxeb+OlGmke(_z!}4i zqj%JI5Qx9WgbOq6<8%9A@q!bl|xRP)^XBgTce`$lf}Q zB4T>Y2roUbA2$4T!c{Mz?iB6u>BPW-;qxFHCf!=A--f4$r`#jOcx@=jUw1 zaQuldZ7Zm4!wm5IUH`YwXM1+e8sLIhxxZiaK>SApdOE#7;_@khK#SsTmqFBr00BTd z;1)pFR3Ma6Mo>E2!^^d(lK{AP<39Gg-1`o6VIRK}B~A;zH>w!u=V>dxKPZHwXBRI! zKy@8pJ%OImr)YHGLEUib$&F|+x#e+C; zcw~^6(=&3HxKz}#hhkmi6G8Cl77yXbL4^9Q#OX$S{1m=QH!@C6HZ5|J_N&_{Ypc-S zzl}2d3$1R<^I;fRAMioZpO0N%TCpB@Q&G1~> zgx#Xj_9)izh<5pIkx!F!>=qM6y|#b1cw`zTId!sg$Rp>U{Iup2XD?6wvc`zmklBA@ zugCjJ4t^Q;RNAroBD2%}TqED#yX|%{c-ozyi?!<+kZ07b1iY>dWV_qZ0An?N$zVr@ zH5j?*h1%hV#G*pnA<*T0Al#w^=ygQD$Lngrt1Yil1fU0yh)wH32(f8g zFaz8Hf@vdf`kYGi_Ua~wXSNYJenAx-e5#`E4_nl@M3;RPrnKKS~+t(`^8-C z)FYIvJfqEdRM2ntJ-0n7I>b_z)k%zp*Q6!Fu?+w|;aQe9W9oPU&k-Q|9}f8xYGMte zmwBw(<&TMxBl|Nd-Q-)ZTU+S;i#FqHeS%%OTsZI;`ML`2rN_iv@q(85Q}G`EaBlc; zcv8QGnCorC40PW#AaynF9rtQQ-C~rJSMWQqj*z?4v~UK!XzW-z!?=Q4f`-c>`n_6v zw8h^#|g4b4MgHn{M{{<8C8T=ZyRy*36;#e}S_^ka)S_p*g{<)&BIjD6tPP z04?v3aOT3v>M85}As4^x!dHz`wdO;@(bNsr9|!!D0HM@6`hlG7^Vg_N7-S-%Nx1X| ztnYdBdjW75a6~(NNQ{{ID$1`B=qdPd%q3e}{2Wc4-Bl^0&N^cZ1mnl|bQ zG3$bB&~FZw{a4H~4J8EK#uX|OdmYz(URNvqAAt`*FFB%w=snK1zQTIr)DYSuPl%mj zvsTw5%8Gsu67K=}ZA~7<Ng(agzjGxg%SMsUm)4R=F52X^wZK@po*_VptE;;#Z>D!CW+J(x5@ z{sQHv0cSTQTzRwnWnX{!3`%TWztUQt7o)p|gI*b69bg4uE#N-De!# zEx>1h3<&RSlt!SG1;_>z1BL=t07?jS3;pM%nDIYfI=fIC0$c}p7odR7H7K!#3P?EN z;m*h7P{1k%^jnM4vw-P%#XEf4!A_ z23LTQVuqe4UZ8uW%n{ZpXOjA1X^-i!7-?C|ozc%ZROm3flp!Cr>-`3c2NcE%Q}TKA`}E3IWX3 z^(ggo`c29#ejV96M~<*0!NDX&@9J^CDlB67J9NwXul{L2e@)D(S>zBy&m?^}e!=yB K`<_XB#79*k)PoZ;XsmlSO#*0sAHkw*UYD diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py index bf7427a54..0b6f2fdf5 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/camera_manager.py @@ -14,7 +14,7 @@ from visual_worker.processamento.radar_top_down import Radar2DManager from visual_worker.processamento.segmentacao_semantica import ClassesSegmentacao, SegmentacaoManager from visual_worker.processamento.costmap_fuser import CostmapFuser, unpack_snapshot from shared.enums import StatusModulo, T_Code, CameraFrameType -from shared.utils import analisar_linhas_por_profundidade, decode_image_base64, encode_image_base64, fazer_overlay +from shared.utils import analisar_linhas_por_profundidade, decode_image_base64, encode_image_base64, fazer_overlay, get_velocidade_atual_ms from shared.gps_handler import GPSHandler from camera_worker.camera_oak import CameraOak from shared.contexto_global_redis import ContextoGlobalRedis, CtxKey @@ -401,6 +401,8 @@ class CameraManager: if depth_frame_np is None or depth_frame_np.size == 0: return segmentacao = self._ultima_analise_segmentacao.get("classes") if segmentacao is None: return + + vel = get_velocidade_atual_ms() t0 = time.time() #grid_conf = self._gerar_grid_confianca(depth_frame_np, segmentacao, dist_max) @@ -409,7 +411,7 @@ class CameraManager: t1 = time.time() grid_conf["ultima_chamada"] = self._ultima_analise_matriz_confianca.get("ultima_chamada", t0) self._calcular_performance(t0, t1, grid_conf) - snapshot = self.data_fuser.update(grid_conf, ts=t1) + snapshot = self.data_fuser.update(grid_conf, ts=t1, velocidade_ms=vel) self._ultima_analise_matriz_confianca = grid_conf #self._ultima_analise_segmentacao["corredor_perfil"] = self.segmentacao_manager.calcular_perfil_corredor(matriz, fov_h) ContextoGlobalRedis.atualizar_ctx_dict( @@ -422,11 +424,6 @@ class CameraManager: #self._mostrar_debug_grid_confianca(self._ultimo_rgb_frame, grid_conf["matriz"], True, self._ultima_analise_segmentacao["mask_color"]) #key, vis = self.debug_show_visualworker(frame_bgr=self._ultimo_rgb_frame, grid=grid_conf, wait=1, text_mode="full", draw_grid=True, draw_cells=True, draw_legend=True) - vel = 0.0 - imu = ContextoGlobalRedis.get_modulo(T_Code.Imu) - if imu is not None and imu.get("saude", {}).get("status", StatusModulo.DESCONECTADO.value) == StatusModulo.OPERANTE.value: - vel = imu.get("vel_mps", 0.0) - vis, metrics = self.debug_blockage_imshow(self._ultimo_rgb_frame, snapshot, velocidade_media=vel) except Exception as e: self.mostrar_log(f"❌ Erro na geracao da matriz de confianca: {e}") diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py index c5d1a587d..993d80c7b 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/config.py @@ -32,7 +32,6 @@ def iniciar_camera_manager(mx_id): if manager.camera is not None and manager.operante: mostrar_log(f"✅ Camera manager iniciado, com MX_ID: {mx_id}") - _CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json") _CONFIG_CACHE = None _CONFIG_MTIME = None diff --git a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/processamento/costmap_fuser.py b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/processamento/costmap_fuser.py index 5b679f2d4..b3f34abd7 100644 --- a/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/processamento/costmap_fuser.py +++ b/AgroBase/AgroBase/bin/x64/Debug/Python/Scripts/workers/visual_worker/processamento/costmap_fuser.py @@ -42,6 +42,12 @@ class CostmapFuser: self.buf_zref = [] # opcional self.buf_ts = [] + self._blk_state = { + "on": 0, "off": 0, "latched": False, + "reason": "none", "dmin": None, + "last_decision": "LIVRE" + } + self.seq = 0 def _stack(self, lst, fallback_val=0.0): @@ -112,7 +118,7 @@ class CostmapFuser: # metros por coluna return (row_width / float(self.grid_w)).astype(np.float32) # (H,) - def update(self, grid_dict, ts=None): + def update(self, grid_dict, ts=None, velocidade_ms=0.0): """ grid_dict deve conter (grid_h,grid_w): "custo","conf","anom","navegavel" opcional: "z_ref" (m) 2D @@ -189,6 +195,15 @@ class CostmapFuser: # escala X por linha (m/col) row_scale_x = self._row_scale_x(row_dist) # (H,) ou None + block = self._compute_blockage_metrics( + custo_f, anom_f, conf_f, nav_f, + row_dist_m, row_scale_x, self.central_cols, + use_persistence=True, + velocidade_mps=velocidade_ms, + a_max_freio=0.8, margem_parada=0.25, + N_on=2, N_off=5, blackout_imediato=True + ) + # incrementa seq self.seq += 1 @@ -210,13 +225,14 @@ class CostmapFuser: "near_is_bottom": bool(self.near_is_bottom), }, "y_range_m": [float(self.y_range_m[0]), float(self.y_range_m[1])], - #"d_obs_min": (None if d_obs_min is None else float(d_obs_min)), "row_dist_m": row_dist_m.tolist(), "row_scale_x_m": row_scale_x.tolist(), "custo_u8": to_u8_list(custo_f), "conf_u8": to_u8_list(conf_f), "anom_u8": to_u8_list(anom_f), - "nav_mask": nav_f.astype(np.uint8).ravel().tolist() + "nav_mask": nav_f.astype(np.uint8).ravel().tolist(), + "d_obs_min": block["d_obs_min"], + "block": block } # (opcional) incluir z_ref_u8 pra debug/visualização @@ -224,16 +240,6 @@ class CostmapFuser: # zref_u8 = np.clip(zref_f / self.y_range_m[1] * 255.0, 0, 255).astype(np.uint8) # snap["zref_u8"] = zref_u8.ravel().tolist() - block = self._compute_blockage_metrics( - custo_f, anom_f, conf_f, nav_f, - row_dist_m, row_scale_x, - central_cols=self.central_cols, - robot_width_m=self.robot_width_m, # defina no __init__ ou config - margin_m=0.12, - ) - snap["block"] = block - snap["d_obs_min"] = block["d_obs_min"] # mantém campo raiz por compatibilidade - return snap def _compute_blockage_metrics( @@ -243,57 +249,69 @@ class CostmapFuser: robot_width_m=0.84, margin_m=0.12, thr_anom_block=0.50, thr_cost_block=0.65, thr_conf_low=0.35, rho_block_central=0.70, rho_block_global=0.60, - near_is_bottom=True + near_is_bottom=True, + # ------ NOVOS (opcionais) ------ + use_persistence=False, + velocidade_mps=None, # m/s; se None, decisão não usa distância de frenagem + a_max_freio=0.8, # m/s² + margem_parada=0.25, # m + N_on=2, # frames p/ entrar + N_off=5, # frames p/ sair + blackout_imediato=True ): """ Retorna dict com: - d_obs_min (m) ou None - - blocked (bool) + - blocked (bool) -> se use_persistence=False: igual ao "raw"; se True: já com persistência + - blocked_raw (bool) - reason ('obstacle','blackout','narrow','none') - coverage: {'central_max':..., 'global':...} - side_bias: {'value': -1..+1, 'left_frac':..., 'right_frac':...} - - j_block (índice da linha que bloqueia) ou None + - j_block (índice) ou None + - decision: { 'parar': bool, 'dist_necessaria': float, 'v_max_sugerida_mps': float|None, + 'frames_on':int, 'frames_off':int, 'N_on':int, 'N_off':int } """ + import numpy as np + H, W = custo_f.shape - c0, c1 = central_cols # intervalo central sugerido pelo seu snap + c0, c1 = central_cols c0 = max(0, min(W-1, int(c0))) c1 = max(0, min(W, int(c1))) if c1 <= c0: c0, c1 = W//3, 2*W//3 # fallback - # 1) Máscaras “inseguras” + # 1) Máscaras inseguras mask_anom = (anom_f >= thr_anom_block) mask_cost = (custo_f >= thr_cost_block) mask_conf = (conf_f < thr_conf_low) unsafe = mask_anom | mask_cost | mask_conf - # 2) Largura em colunas por linha (corredor = robô + margem) + # 2) Largura em colunas por linha width_need_m = robot_width_m + margin_m cols_need = [] for j in range(H): sx = row_scale_x_m[j] if row_scale_x_m is not None else (width_need_m / max(1, (c1 - c0))) if sx is None or sx <= 1e-6: - cols_need.append(c1 - c0) # fallback + cols_need.append(c1 - c0) else: ncols = int(np.ceil(width_need_m / sx)) cols_need.append(max(1, min(W, ncols))) cols_need = np.asarray(cols_need, dtype=int) - # 3) Varredura por linha: janela central com largura cols_need[j] + # 3) Varredura central def central_window(j, ncols): - # centra no meio de [c0,c1) mid = (c0 + c1) // 2 half = ncols // 2 a = max(0, mid - half) b = min(W, a + ncols) - # ajusta se estourou esquerda/direita a = max(0, b - ncols) return a, b coverage_central = np.zeros(H, np.float32) j_block = None - for j in (range(H-1, -1, -1) if near_is_bottom else range(H)): # começa pelo "mais perto" + it = (range(H-1, -1, -1) if near_is_bottom else range(H)) + for j in it: a, b = central_window(j, cols_need[j]) unsafe_row = unsafe[j, a:b] coverage = unsafe_row.mean() if (b > a) else 1.0 @@ -302,45 +320,109 @@ class CostmapFuser: j_block = j break - # 4) Cobertura global (fallback diagnóstico) + # 4) Cobertura global global_cov = unsafe.mean() - # 5) d_obs_min em metros + # 5) Distância do 1º bloqueio d_obs_min = None if j_block is None else float(row_dist_m[j_block]) - # 6) Viés lateral (onde está mais “fechado”) - # mede cobertura do lado esquerdo vs direito dentro do intervalo [c0,c1) + # 6) Viés lateral left = unsafe[:, c0:(c0+c1)//2].mean() if (c1-c0) >= 2 else 0.0 right = unsafe[:, (c0+c1)//2:c1].mean() if (c1-c0) >= 2 else 0.0 side_bias_val = float(np.clip((right - left) / max(1e-6, (right + left)), -1.0, 1.0)) - # 7) Decisão de bloqueio e razão - blocked = False + # 7) Decisão "raw" (sem persistência) + blocked_raw = False reason = "none" if j_block is not None: - blocked = True + blocked_raw = True reason = "obstacle" elif global_cov >= rho_block_global and (conf_f.mean() < 0.45): - blocked = True - reason = "blackout" # visão ruim / depth ruim geral - # opcional: “narrow” se central ok mas laterais muito ruins + blocked_raw = True + reason = "blackout" elif coverage_central.max() > 0.45 and (left > 0.7 or right > 0.7): reason = "narrow" + # 8) Persistência / histerese + decisão de parada (opcional) + decision = { + "parar": False, + "dist_necessaria": None, + "v_max_sugerida_mps": None, + "frames_on": 0, + "frames_off": 0, + "N_on": N_on, + "N_off": N_off + } + + blocked_out = blocked_raw # default: compatível com antes + + if use_persistence: + # calcula distância necessária se tivermos velocidade + v = float(max(0.0, velocidade_mps or 0.0)) + dist_freio = max(0.30, (v*v) / max(1e-9, 2.0 * a_max_freio)) + dist_necessaria = dist_freio + margem_parada + decision["dist_necessaria"] = float(dist_necessaria) + + # regra "stop_now" crua (antes do debounce) + stop_now = False + v_max_sug = None + + if blocked_raw and reason == "blackout": + stop_now = True if blackout_imediato else False + + if blocked_raw and reason == "obstacle": + if (d_obs_min is not None) and np.isfinite(d_obs_min) and (d_obs_min <= dist_necessaria): + stop_now = True + else: + if (d_obs_min is not None) and np.isfinite(d_obs_min) and (d_obs_min > margem_parada): + v_max_sug = float(np.sqrt(max(0.0, 2.0 * a_max_freio * (d_obs_min - margem_parada)))) + + # histerese + st = self._blk_state + if stop_now: + st["on"] += 1 + st["off"] = 0 + else: + st["off"] += 1 + st["on"] = 0 + + if not st["latched"] and st["on"] >= N_on: + st["latched"] = True + elif st["latched"] and st["off"] >= N_off: + st["latched"] = False + + # saída persistente + decision.update({ + "parar": bool(st["latched"]), + "v_max_sugerida_mps": v_max_sug, + "frames_on": int(st["on"]), + "frames_off": int(st["off"]) + }) + + # quando usamos persistência, o 'blocked' exposto passa a refletir a decisão debounced: + blocked_out = bool(st["latched"]) + + # guarda debug + st["reason"] = reason + st["dmin"] = (None if d_obs_min is None else float(d_obs_min)) + st["last_decision"] = "PARAR" if st["latched"] else "LIVRE" + return { "d_obs_min": d_obs_min, - "blocked": bool(blocked), + "blocked": bool(blocked_out), # <- já com persistência se habilitada + "blocked_raw": bool(blocked_raw), # <- útil pra debug/HUD "reason": reason, "coverage": { "central_max": float(coverage_central.max()), "global": float(global_cov), }, "side_bias": { - "value": side_bias_val, # <0 = mais fechado à esquerda; >0 = direita + "value": side_bias_val, "left_frac": float(left), "right_frac": float(right), }, "j_block": (None if j_block is None else int(j_block)), + "decision": decision }