Skip to content

Commit 1a85bf9

Browse files
authored
Vision refactoring (#720)
Adds refactored vision
2 parents d2ad8d9 + e3b3b36 commit 1a85bf9

File tree

7 files changed

+239
-870
lines changed

7 files changed

+239
-870
lines changed

bitbots_vision/bitbots_vision/vision.py

Lines changed: 34 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#! /usr/bin/env python3
22
from copy import deepcopy
3+
from typing import Optional
34

4-
import numpy as np
55
import rclpy
66
from ament_index_python.packages import get_package_share_directory
77
from cv_bridge import CvBridge
@@ -10,7 +10,7 @@
1010
from rclpy.node import Node
1111
from sensor_msgs.msg import Image
1212

13-
from bitbots_vision.vision_modules import ros_utils, yoeo
13+
from bitbots_vision.vision_modules import debug, ros_utils, yoeo
1414

1515
from .params import gen
1616

@@ -48,7 +48,8 @@ def __init__(self) -> None:
4848

4949
self._sub_image = None
5050

51-
self._vision_components: list[yoeo.IVisionComponent] = []
51+
self._vision_components: list[yoeo.AbstractVisionComponent] = []
52+
self._debug_image: Optional[debug.DebugImage] = None
5253

5354
# Setup reconfiguration
5455
gen.declare_params(self)
@@ -85,37 +86,44 @@ def _get_updated_config_with(self, params) -> dict:
8586
def _configure_vision(self, new_config: dict) -> None:
8687
yoeo.YOEOObjectManager.configure(new_config)
8788

88-
self._set_up_vision_components(new_config)
89-
self._register_subscribers(new_config)
89+
debug_image = debug.DebugImage(new_config["component_debug_image_active"])
90+
self._debug_image = debug_image
9091

91-
def _set_up_vision_components(self, new_config: dict) -> None:
92-
self._vision_components = []
93-
self._vision_components.append(yoeo.YOEOComponent())
92+
def make_vision_component(
93+
component_class: type[yoeo.AbstractVisionComponent], **kwargs
94+
) -> yoeo.AbstractVisionComponent:
95+
return component_class(
96+
node=self,
97+
yoeo_handler=yoeo.YOEOObjectManager.get(),
98+
debug_image=debug_image,
99+
config=new_config,
100+
**kwargs,
101+
)
102+
103+
self._vision_components = [make_vision_component(yoeo.YOEOComponent)]
94104

95105
if new_config["component_ball_detection_active"]:
96-
self._vision_components.append(yoeo.BallDetectionComponent(self))
106+
self._vision_components.append(make_vision_component(yoeo.BallDetectionComponent))
97107
if new_config["component_robot_detection_active"]:
98-
if yoeo.YOEOObjectManager.is_team_color_detection_supported():
99-
self._vision_components.append(yoeo.RobotDetectionComponent(self))
100-
else:
101-
self._vision_components.append(yoeo.NoTeamColorRobotDetectionComponent(self))
108+
self._vision_components.append(
109+
make_vision_component(
110+
yoeo.RobotDetectionComponent,
111+
team_color_detection_supported=yoeo.YOEOObjectManager.is_team_color_detection_supported(),
112+
)
113+
)
102114
if new_config["component_goalpost_detection_active"]:
103-
self._vision_components.append(yoeo.GoalpostDetectionComponent(self))
115+
self._vision_components.append(make_vision_component(yoeo.GoalpostDetectionComponent))
104116
if new_config["component_line_detection_active"]:
105-
self._vision_components.append(yoeo.LineDetectionComponent(self))
117+
self._vision_components.append(make_vision_component(yoeo.LineDetectionComponent))
106118
if new_config["component_field_detection_active"]:
107-
self._vision_components.append(yoeo.FieldDetectionComponent(self))
119+
self._vision_components.append(make_vision_component(yoeo.FieldDetectionComponent))
108120
if new_config["component_debug_image_active"]:
109-
self._vision_components.append(yoeo.DebugImageComponent(self))
110-
111-
for vision_component in self._vision_components:
112-
vision_component.configure(new_config, new_config["component_debug_image_active"])
121+
self._vision_components.append(make_vision_component(yoeo.DebugImageComponent))
113122

114-
def _register_subscribers(self, config: dict) -> None:
115123
self._sub_image = ros_utils.create_or_update_subscriber(
116124
self,
117125
self._config,
118-
config,
126+
new_config,
119127
self._sub_image,
120128
"ROS_img_msg_topic",
121129
Image,
@@ -124,24 +132,13 @@ def _register_subscribers(self, config: dict) -> None:
124132

125133
@profile
126134
def _run_vision_pipeline(self, image_msg: Image) -> None:
127-
image = self._extract_image_from_message(image_msg)
128-
if image is None:
129-
logger.error("Vision pipeline - Image content is None")
130-
return
135+
image = self._cv_bridge.imgmsg_to_cv2(image_msg, "bgr8")
131136

132-
self._forward_image_to_components(image)
133-
self._run_components(image_msg)
134-
135-
def _extract_image_from_message(self, image_msg: Image) -> np.ndarray:
136-
return self._cv_bridge.imgmsg_to_cv2(image_msg, "bgr8")
137-
138-
def _forward_image_to_components(self, image: np.ndarray) -> None:
139-
for vision_component in self._vision_components:
140-
vision_component.set_image(image)
137+
assert self._debug_image is not None, "Debug image not initialized"
138+
self._debug_image.set_image(image)
141139

142-
def _run_components(self, image_msg: Image) -> None:
143140
for vision_component in self._vision_components:
144-
vision_component.run(image_msg)
141+
vision_component.run(image, image_msg.header)
145142

146143

147144
def main(args=None):

bitbots_vision/bitbots_vision/vision_modules/candidate.py

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
from typing import TYPE_CHECKING
44

55
if TYPE_CHECKING:
6-
import numpy as np
6+
pass
77

8-
import abc
98

109
from rclpy import logging
1110

@@ -229,61 +228,3 @@ def from_x1y1x2y2(cls, x1, y1, x2, y2, rating=None):
229228
width = abs(x1 - x2)
230229
height = abs(y1 - y2)
231230
return cls(min(x1, x2), min(y1, y2), width, height, rating)
232-
233-
234-
class CandidateFinder:
235-
"""
236-
The abstract class :class:`.CandidateFinder` requires its subclasses to implement the methods
237-
:meth:`.get_candidates`, :meth:`.compute` and :meth:`set_image`.
238-
239-
CandidateFinder implementations produce a set of so called *Candidates* which are instances of the class :class:`bitbots_vision.vision_modules.candidate.Candidate`.
240-
"""
241-
242-
def __init__(self):
243-
"""
244-
Initialization of :class:`.CandidateFinder`.
245-
"""
246-
super().__init__()
247-
248-
def get_top_candidates(self, count: int = 1) -> list[Candidate]:
249-
"""
250-
Returns the count highest rated candidates.
251-
252-
:param int count: Number of top-candidates to return
253-
:return [Candidate]: The count top-candidates
254-
"""
255-
candidates = self.get_candidates()
256-
candidates = Candidate.sort_candidates(candidates)
257-
return candidates[:count]
258-
259-
def get_top_candidate(self) -> Candidate:
260-
"""
261-
Returns the highest rated candidate.
262-
263-
:return Candidate: Top candidate or None
264-
"""
265-
return Candidate.select_top_candidate(self.get_candidates())
266-
267-
@abc.abstractmethod
268-
def get_candidates(self) -> list[Candidate]:
269-
"""
270-
Returns a list of all candidates.
271-
272-
:return [Candidate]: Candidates
273-
"""
274-
raise NotImplementedError
275-
276-
@abc.abstractmethod
277-
def compute(self) -> None:
278-
"""
279-
Runs the most intense calculation without returning any output and caches the result.
280-
"""
281-
raise NotImplementedError
282-
283-
@abc.abstractmethod
284-
def set_image(self, image: np.ndarray) -> None:
285-
"""
286-
Set the image which is going to be processed by calling :meth:`.compute.
287-
"""
288-
289-
raise NotImplementedError

bitbots_vision/bitbots_vision/vision_modules/debug.py

Lines changed: 8 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from collections.abc import Sequence
2-
from typing import Callable, Optional
2+
from typing import Optional
33

44
import cv2
55
import numpy as np
@@ -129,15 +129,14 @@ def draw_mask(self, mask, color, opacity=0.5):
129129
if not self.active:
130130
return
131131
assert self._debug_image is not None, "No image set"
132-
# Make a colored image
133-
colored_image = np.zeros_like(self._debug_image)
134-
colored_image[:, :] = tuple(np.multiply(color, opacity).astype(np.uint8))
135132

136-
# Compose debug image with lines
137-
self._debug_image = cv2.add(
138-
cv2.bitwise_and(self._debug_image, self._debug_image, mask=255 - mask),
139-
cv2.add(colored_image * opacity, self._debug_image * (1 - opacity), mask=mask).astype(np.uint8),
140-
)
133+
# Bring mask into canonical form
134+
mask_bool = mask.astype(bool).squeeze()
135+
136+
# Blend color directly where mask is true
137+
self._debug_image[mask_bool, :] = (
138+
self._debug_image[mask_bool, :] * (1.0 - opacity) + np.array(color, dtype=np.float32) * opacity
139+
).astype(np.uint8)
141140

142141
def get_image(self):
143142
"""
@@ -146,41 +145,3 @@ def get_image(self):
146145
:return: image with debug stuff
147146
"""
148147
return self._debug_image
149-
150-
def draw(self, debug_image_description, image=None):
151-
"""
152-
Draws a debug image description, that contains the style and the data for each object/class that we debug
153-
E.g.:
154-
{
155-
'type': 'field_boundary',
156-
'thickness': 1,
157-
'color': (255,255,255),
158-
'data': #Some data
159-
}
160-
161-
:param debug_image_description: List of dicts contains the style and the date for each object/class that we debug
162-
In the dict 'type' refers to the type that we want to draw. Some types are ['robot', 'field_boundary', 'ball', 'line_point', 'line_segment'].
163-
The key 'color' defines the color as BRG. For most types this is the border color.
164-
The key 'thickness' refers to the border thickness.
165-
The data, so the candidates we want to draw are defined with the 'data' key.
166-
:return: Image with debug stuff
167-
"""
168-
# Set Image if transmitted (optional). Otherwise take the manual set image.
169-
if image:
170-
self.set_image(image)
171-
# Define the draw functions for each type
172-
draw_functions: dict[str, Callable] = {
173-
"robot": self.draw_robot_candidates,
174-
"field_boundary": self.draw_field_boundary,
175-
"ball": self.draw_ball_candidates,
176-
"line_point": self.draw_points,
177-
"line_segment": self.draw_line_segments,
178-
}
179-
# Draw all entries
180-
for draw_type in debug_image_description:
181-
# Get drawing function from dict
182-
draw_function = draw_functions[draw_type["type"]]
183-
# Call drawing function
184-
draw_function(draw_type["data"], draw_type["color"], draw_type["thickness"])
185-
# Return the image
186-
return self.get_image()

0 commit comments

Comments
 (0)