Módulo de Detecção de Obstáculos¶
Detecção de obstáculos baseada em eventos, com um strategy pattern para comportamentos de
desvio de obstáculos. Um detector reporta obstáculos, uma strategy decide como reagir, e o
ObstacleManager os executa como parte da navegação — associe um a qualquer drone com
add_obstacle_detector.
Resumo rápido¶
from nectar.control import DepthObstacleDetector, strategies
drone.add_obstacle_detector("depth", DepthObstacleDetector(), strategies.PauseStrategy())
drone.enable_all_obstacle_detectors()
drone.move_to(x=10.0, y=0.0, z=0.0) # pauses while an obstacle is detected ahead
Arquitetura¶
classDiagram
class ObstacleDetector {
<<protocol>>
+is_enabled bool
+enable()
+disable()
+update() ObstacleInfo
+reset()
}
class ObstacleInfo {
<<dataclass>>
+detected bool
+direction Optional~ObstacleDirection~
+distance Optional~float~
}
class ObstacleDirection {
<<enum>>
FRONT
BACK
LEFT
RIGHT
UP
DOWN
}
class AvoidanceStrategy {
<<abstract>>
+execute(drone, info) bool
+reset()*
}
class ObstacleHandler {
-_detector ObstacleDetector
-_strategy AvoidanceStrategy
-_node Node
-_config ObstacleHandlerConfig
-_timer Optional~Timer~
-_last_info ObstacleInfo
-_lock Lock
+detector ObstacleDetector
+strategy AvoidanceStrategy
+is_enabled bool
+last_info ObstacleInfo
+enable()
+disable()
+update() ObstacleInfo
+should_continue(drone) bool
+get_axis_modifiers() tuple~bool,bool,bool~
+reset()
+cleanup()
-_update_callback()
}
class ObstacleHandlerConfig {
<<dataclass>>
+enabled bool
+strategy Optional~AvoidanceStrategy~
+update_rate float
}
class ObstacleManager {
-_handlers dict~str,ObstacleHandler~
+handlers dict~str,ObstacleHandler~
+add(name, handler)
+remove(name) Optional~ObstacleHandler~
+get(name) Optional~ObstacleHandler~
+enable(name)
+disable(name)
+enable_all()
+disable_all()
+should_continue_navigation(drone) bool
+get_axis_control() tuple~bool,bool,bool~
+reset_all()
+cleanup()
}
class BaseObstacleDetector {
<<abstract>>
-_enabled bool
-_lock Lock
-_last_info ObstacleInfo
+is_enabled bool
+enable()
+disable()
+update() ObstacleInfo
+get_last_info() ObstacleInfo
+reset()
#_detect()* ObstacleInfo
#_on_enable()
#_on_disable()
#_on_reset()
}
class DepthObstacleDetector {
-_camera Optional~RealsenseCam~
-_image_handler Optional~ImageHandler~
-_detection_event Event
-_color_topic str
-_depth_topic str
-_min_distance_mm float
-_max_distance_mm float
-_cluster_eps float
-_cluster_min_samples int
-_min_cluster_pixels int
-_depth_threshold_mm float
+update() ObstacleInfo
-_detect() ObstacleInfo
-_on_enable()
-_on_disable()
-_on_reset()
}
class PauseStrategy {
-_paused bool
+execute(drone, info) bool
+reset()
}
class DisableAxisStrategy {
+disable_x bool
+disable_y bool
+disable_z bool
+execute(drone, info) bool
+reset()
}
class SequenceStrategy {
-_sequence_func Callable
-_executed bool
-_executing Event
+execute(drone, info) bool
+reset()
}
ObstacleDetector <|.. BaseObstacleDetector : implements
BaseObstacleDetector <|-- DepthObstacleDetector
AvoidanceStrategy <|-- PauseStrategy
AvoidanceStrategy <|-- DisableAxisStrategy
AvoidanceStrategy <|-- SequenceStrategy
ObstacleHandler *-- ObstacleDetector
ObstacleHandler *-- AvoidanceStrategy
ObstacleHandler o-- ObstacleHandlerConfig
ObstacleHandler ..> ObstacleInfo : uses
ObstacleManager o-- ObstacleHandler
ObstacleDetector ..> ObstacleInfo : returns
ObstacleInfo o-- ObstacleDirection
Fluxo de Detecção em Tempo Real¶
sequenceDiagram
participant Nav as Navigation Loop<br/>(Main Thread)
participant Mgr as ObstacleManager
participant H as ObstacleHandler
participant T as ROS2 Timer<br/>(Background)
participant D as DepthDetector
participant Cam as RealSense Camera
participant S as Strategy
Note over T,Cam: Background Detection (10Hz)
loop Every 0.1s (Timer)
T->>H: _update_callback()
H->>D: update()
D->>Cam: get_depth_frame()
Cam-->>D: depth image
D->>D: DBSCAN clustering
D->>D: filter & classify
D-->>H: ObstacleInfo
H->>H: Lock & store
end
Note over Nav,S: Navigation Loop (100Hz)
loop Every 0.01s
Nav->>Mgr: should_continue_navigation(drone)
Mgr->>H: should_continue(drone)
H->>H: Lock & read last_info
H->>S: execute(drone, info)
alt Obstacle Detected
S->>S: PauseStrategy
S->>Nav: move_velocity(0,0,0,0)
S-->>H: False (pause)
H-->>Mgr: False
Mgr-->>Nav: False (skip iteration)
else Path Clear
S-->>H: True (continue)
H-->>Mgr: True
Mgr-->>Nav: True
Nav->>Nav: Compute PID
Nav->>Nav: move_velocity(vx,vy,vz,vyaw)
end
end
Conceitos Centrais¶
Separação de Responsabilidades¶
- Detecção (O quê): identifica obstáculos e retorna
ObstacleInfo - Strategy (Como): define o comportamento de resposta
- Handler (Quando): gerencia o timing e a integração
- Manager (Onde): coordena múltiplos detectores em um drone
ObstacleInfo¶
Estrutura de dados do resultado da detecção.
@dataclass
class ObstacleInfo:
detected: bool
direction: Optional[ObstacleDirection] = None # FRONT, BACK, LEFT, RIGHT, UP, DOWN
distance: Optional[float] = None # meters
Protocolo ObstacleDetector¶
Interface para detectores de obstáculos.
class ObstacleDetector(Protocol):
@property
def is_enabled(self) -> bool: ...
def enable(self) -> None: ...
def disable(self) -> None: ...
def update(self) -> ObstacleInfo: ...
def reset(self) -> None: ...
Detectores¶
Zonas de Detecção¶
A direção é derivada da posição do cluster em relação ao centro da imagem (veja o
flowchart abaixo e
depth_camera.py):
- Todos os clusters à esquerda do centro →
ObstacleDirection.RIGHT - Todos os clusters à direita do centro →
ObstacleDirection.LEFT - Clusters nos dois lados →
ObstacleDirection.FRONT
Limiares de distância (mm, salvo indicação contrária):
| Parâmetro | Valor | Papel |
|---|---|---|
| Distância mínima | 100 | Ignora retornos mais próximos do que isso |
| Distância máxima | 1500 | Limite do alcance de detecção |
| Filtro de cluster | 1300 | Profundidade média máxima do cluster para contar como obstáculo |
DepthObstacleDetector¶
Detecção baseada na câmera de profundidade RealSense D435i usando clustering DBSCAN.
Configuração:
from nectar.control.obstacles import DepthObstacleDetector
detector = DepthObstacleDetector(
min_distance_mm=100, # ignore returns closer than this
max_distance_mm=1500, # maximum detection range
cluster_eps=20, # DBSCAN epsilon
cluster_min_samples=20, # DBSCAN minimum samples
min_cluster_pixels=50, # minimum valid pixels to attempt clustering
depth_threshold_mm=1300, # max cluster mean depth to count as obstacle
color_topic="/camera/color/image_raw",
depth_topic="/camera/depth/image_rect_raw",
)
Nota: o detector possui seu próprio
ImageHandlerda RealSense (e node ROS) internamente — nenhum argumentonodeé passado.
Processamento da câmera de profundidade¶
flowchart TD
Start([Depth Frame<br/>640x480 @ 30fps]) --> Convert[Convert to mm<br/>depth_mm = depth * 1000]
Convert --> Downsample[Downsample 10%<br/>64x48 for performance]
Downsample --> Filter{Filter Valid Depth}
Filter -->|100mm < depth < 1500mm| ValidMask[Create Valid Mask]
Filter -->|Outside range| Discard[Set to 0]
ValidMask --> Count{Count Valid Pixels}
Count -->|< 50 pixels| NoObstacle[Return: No Obstacle]
Count -->|≥ 50 pixels| ExtractPoints[Extract Point Cloud<br/>xs, ys, depths]
ExtractPoints --> DBSCAN[DBSCAN Clustering<br/>eps=20, min_samples=20]
DBSCAN --> Clusters{Found Clusters?}
Clusters -->|No clusters| NoObstacle
Clusters -->|Yes| FilterClusters
FilterClusters[Filter by Threshold<br/>mean_depth < 1300mm] --> ValidClusters{Valid Clusters?}
ValidClusters -->|None| NoObstacle
ValidClusters -->|Yes| AnalyzePosition
AnalyzePosition[Analyze Cluster Position<br/>vs Image Center] --> Direction{Determine Direction}
Direction -->|All clusters<br/>x_max < center| DirLeft[RIGHT]
Direction -->|All clusters<br/>x_min > center| DirRight[LEFT]
Direction -->|Clusters both sides| DirFront[FRONT]
DirLeft --> CalcDist[Calculate Distance<br/>avg depth of clusters]
DirRight --> CalcDist
DirFront --> CalcDist
CalcDist --> Return([Return ObstacleInfo<br/>detected=True, direction, distance])
NoObstacle --> ReturnFalse([Return ObstacleInfo<br/>detected=False])
BaseObstacleDetector¶
Classe base abstrata que fornece gerenciamento de estado thread-safe.
from nectar.control.obstacles import BaseObstacleDetector
class CustomDetector(BaseObstacleDetector):
def _detect(self) -> ObstacleInfo:
# Custom detection logic
return ObstacleInfo(detected=True, direction=ObstacleDirection.FRONT)
def _on_reset(self) -> None:
# Reset internal state (optional hook)
pass
Strategies¶
Fluxo de Decisão da Strategy¶
flowchart TD
Start([drone.move_to<br/>x=10, y=0, z=0]) --> Nav{Navigation Loop}
Nav --> CheckObs{ObstacleManager:<br/>Check all detectors}
CheckObs -->|No obstacles| ComputePID[Compute PID<br/>vx, vy, vz, vyaw]
ComputePID --> SendVel[drone.move_velocity<br/>vx, vy, vz, vyaw]
SendVel --> CheckDist{Distance to<br/>target < precision?}
CheckObs -->|Obstacle detected| StrategyType{Which Strategy?}
StrategyType -->|PAUSE| Pause[Stop drone<br/>move_velocity<br/>0, 0, 0, 0]
Pause --> WaitClear{Path cleared?}
WaitClear -->|No| Pause
WaitClear -->|Yes| Nav
StrategyType -->|DISABLE_AXIS| DisableZ[Disable Z axis<br/>Continue XY only]
DisableZ --> ComputePID2[Compute PID<br/>vx, vy ONLY<br/>vz=0]
ComputePID2 --> SendVel
StrategyType -->|SEQUENCE| ExecuteSeq[Execute Sequence:<br/>Lateral → Forward → Return]
ExecuteSeq --> SeqComplete{Sequence<br/>complete?}
SeqComplete -->|No| ExecuteSeq
SeqComplete -->|Yes| Nav
CheckDist -->|No| Nav
CheckDist -->|Yes| Done([Target Reached])
Resumo das Strategies¶
| Strategy | Comportamento | Retorna | Caso de uso |
|---|---|---|---|
| PauseStrategy | move_velocity(0,0,0,0) até liberar |
False enquanto detectado |
Obstáculos dinâmicos (pessoas, drones) |
| DisableAxisStrategy | Desabilita o controle do eixo especificado | True (continua) |
Terrain following (lidar) |
| SequenceStrategy | Executa uma manobra de evasão | False durante a execução |
Obstáculos estáticos (paredes, árvores) |
Interface AvoidanceStrategy¶
class AvoidanceStrategy(ABC):
@abstractmethod
def execute(self, drone: BaseDrone, info: ObstacleInfo) -> bool:
"""
Execute avoidance behavior.
Returns:
True if navigation should continue, False to pause
"""
pass
@abstractmethod
def reset(self) -> None:
pass
PauseStrategy¶
Para o drone quando um obstáculo é detectado, retoma quando liberado.
Comportamento:
def execute(self, drone, info):
if info.detected:
drone.move_velocity(0, 0, 0, 0) # Stop
return False # Don't continue navigation
else:
return True # Resume navigation
Caso de uso: Obstáculos dinâmicos (pessoas, outros drones) que podem se afastar.
DisableAxisStrategy¶
Desabilita o controle de eixos específicos durante a detecção de obstáculo.
strategy = strategies.DisableAxisStrategy(
disable_x=False,
disable_y=False,
disable_z=True # Disable altitude control
)
Comportamento: Retorna True (continua a navegação) mas sinaliza quais eixos
desabilitar via get_axis_modifiers().
Caso de uso: Terrain following com lidar (desabilita o PID de Z, deixa o ArduPilot controlar a altitude).
SequenceStrategy¶
Executa uma sequência callable customizada quando um obstáculo é detectado.
from functools import partial
strategy = strategies.SequenceStrategy(
partial(
strategies.lateral_pass_return_sequence,
lateral_distance=1.0,
forward_distance=2.5,
precision=0.2
)
)
Comportamento: Chama sequence_func(drone, info), que executa drone.move_to()
diretamente.
Sequências prontas:
| Sequência | Passos | Resultado |
|---|---|---|
lateral_pass_return_sequence |
① y=lateral → ② x=forward → ③ y=-lateral |
Retorna à trajetória original |
lateral_pass_sequence |
① y=lateral → ② x=forward |
Permanece deslocado |
simple_lateral_sequence |
① y=lateral |
Passo lateral único |
climb_over_sequence |
① z=+height → ② x=forward → ③ z=-height |
Contorno vertical |
Lógica de direção: LEFT → move para a direita (-y), RIGHT/FRONT → move para a
esquerda (+y)
Integração¶
ObstacleHandler¶
Combina detector + strategy com timing.
from nectar.control.obstacles import ObstacleHandler, ObstacleHandlerConfig
handler = ObstacleHandler(
detector=DepthObstacleDetector(),
strategy=strategies.PauseStrategy(),
node=node,
config=ObstacleHandlerConfig(
enabled=True,
update_rate=0.1
)
)
Mecanismos de atualização:
- Baseado em timer (padrão): um timer do ROS2 chama
_update_callback()na taxa especificada - Manual: chame
handler.update()explicitamente (definaupdate_rate=0)
Thread safety: usa locks para o acesso ao estado (_last_info).
ObstacleManager¶
Gerencia múltiplos handlers em um drone.
manager = ObstacleManager()
manager.add("depth", depth_handler)
manager.add("lidar", lidar_handler)
manager.enable("depth")
manager.disable("lidar")
# In navigation loop
if not manager.should_continue_navigation(drone):
continue # Strategy is handling obstacle
disable_x, disable_y, disable_z = manager.get_axis_control()
Integração com a navegação:
# In VehicleNavigator.navigate_pid()
while True:
if not drone.obstacle_manager.should_continue_navigation(drone):
continue # Pause or sequence executing
disable_x, disable_y, disable_z = drone.obstacle_manager.get_axis_control()
control_x = x is not None and not disable_x
control_y = y is not None and not disable_y
control_z = z is not None and not disable_z
# Continue with PID control
API do Drone¶
Adicionando Detectores¶
drone.add_obstacle_detector(
name: str,
detector: ObstacleDetector,
strategy: AvoidanceStrategy,
config: Optional[ObstacleHandlerConfig] = None
)
Exemplo:
detector = DepthObstacleDetector()
strategy = strategies.PauseStrategy()
config = ObstacleHandlerConfig(update_rate=0.15) # 6.7 Hz
drone.add_obstacle_detector("depth", detector, strategy, config)
Controle¶
drone.enable_obstacle_detector("depth")
drone.disable_obstacle_detector("depth")
drone.enable_all_obstacle_detectors()
drone.disable_all_obstacle_detectors()
drone.remove_obstacle_detector("depth")
Exemplos de Uso¶
Evasão Lateral¶
from functools import partial
detector = DepthObstacleDetector()
strategy = strategies.SequenceStrategy(
partial(
strategies.lateral_pass_return_sequence,
lateral_distance=1.5,
forward_distance=3.0
)
)
drone.add_obstacle_detector("depth", detector, strategy)
drone.enable_obstacle_detector("depth")
drone.takeoff(1.5)
drone.move_to(x=10.0, y=0.0, z=0.0) # Executes evasion when obstacle detected
drone.land()
Sequência Customizada¶
def my_evasion(drone, info, climb_height=1.0):
"""Climb over obstacle."""
drone.node.get_logger().info("Climbing over obstacle")
drone.move_to(z=climb_height, precision=0.2)
drone.move_to(x=3.0, precision=0.2)
drone.move_to(z=-climb_height, precision=0.2)
strategy = strategies.SequenceStrategy(partial(my_evasion, climb_height=1.5))
drone.add_obstacle_detector("depth", detector, strategy)
Terrain Following¶
# Custom lidar detector (not provided)
lidar_detector = LidarObstacleDetector(node)
strategy = strategies.DisableAxisStrategy(disable_z=True)
drone.add_obstacle_detector("lidar", lidar_detector, strategy)
drone.enable_obstacle_detector("lidar")
# Z control disabled when lidar detects terrain variation
drone.move_to(x=10.0, y=0.0, z=0.0)
Múltiplos Detectores¶
depth_detector = DepthObstacleDetector()
ultrasonic_detector = UltrasonicDetector(node) # Custom
drone.add_obstacle_detector(
"depth", depth_detector,
strategies.SequenceStrategy(strategies.lateral_pass_return_sequence)
)
drone.add_obstacle_detector(
"ultrasonic", ultrasonic_detector,
strategies.PauseStrategy()
)
drone.enable_all_obstacle_detectors()
Exemplo de Strategy Customizada¶
from nectar.control.obstacles.strategies import AvoidanceStrategy
class BackupStrategy(AvoidanceStrategy):
def __init__(self, backup_distance=1.0):
self.backup_distance = backup_distance
self._backing_up = False
def execute(self, drone, info):
if info.detected and not self._backing_up:
self._backing_up = True
drone.move_to(x=-self.backup_distance, precision=0.2)
self._backing_up = False
return False # Don't continue until backup complete
return not info.detected
def reset(self):
self._backing_up = False
drone.add_obstacle_detector("depth", detector, BackupStrategy(backup_distance=2.0))
Thread Safety¶
- Detectores: rodam em timers independentes do ROS2 (threads de fundo)
- Handlers: usam locks para o acesso ao estado
- Strategies: executam na thread de navegação (thread principal)
- Manager: single-threaded (loop de navegação)
Sincronização: os locks do handler previnem race conditions entre o callback do timer e o loop de navegação.