Skip to content

Commit bcffd52

Browse files
merging cv stuff into main (#70)
* merging cv stuff into main * wtf was that random pkg --------- Co-authored-by: Cameron <[email protected]>
1 parent 2eecd1a commit bcffd52

File tree

7 files changed

+381
-22
lines changed

7 files changed

+381
-22
lines changed

bestHammer.pt

5.96 MB
Binary file not shown.

src/aruco_detector/package.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
<!-- Rosdep fails with the following: -->
1818
<!-- <depend>cv2_aruco</depend> -->
1919
<!-- <depend>OpenCV</depend> -->
20+
<!-- ROWTAG MERGE WARNING -->
21+
<depend>OpenCV</depend>
22+
<depend>cv2_aruco</depend>
2023

2124
<test_depend>ament_lint_auto</test_depend>
2225
<test_depend>ament_lint_common</test_depend>

src/aruco_detector/src/DetectMarker.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,4 @@ int main(int argc, char** argv) {
8686
rclcpp::spin(node);
8787
rclcpp::shutdown();
8888
return 0;
89-
9089
}

src/object_detection/object_detection/yolo_detector_node.py

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,55 @@
1+
#!/usr/bin/env python3
2+
# This line should be added at the beginning of any ROS node. It specifies the location of the Python interpreter
3+
# The 'env' command helps ensure that the correct interpreter is used based on your system’s environment
4+
# This concept is useful beyond ROS, as it deals with specifying the interpreter path in scripts
5+
# For more details, you can check this link: https://stackoverflow.com/questions/7670303/purpose-of-usr-bin-python3-shebang
6+
# If you're already familiar with this and just forgot to include it, feel free to ignore this comment up to yo
7+
8+
"""
9+
Run yolo on a detected camera feed
10+
11+
How to use:
12+
first run: 'ros2 run cameras cameras_node'
13+
then run: 'ros2 run object_detection yolo_detector_node'
14+
now to display the results: 'ros2 run rqt_image_view rqt_image_view'
15+
16+
do this seperately in 3 terminals. TODO: setup a launch file so this is easier lol
17+
18+
Make sure to build and source before running
19+
"""
20+
121
import rclpy
222
from rclpy.node import Node
323
from sensor_msgs.msg import Image
424
from cv_bridge import CvBridge
525
import cv2
626
from ultralytics import YOLO
727

8-
928
class YOLODetectorNode(Node):
1029
def __init__(self):
1130
super().__init__('yolo_detector_node')
12-
self.publisher_ = self.create_publisher(Image, 'detected_frames', 10)
31+
self.publisher_ = self.create_publisher(Image, 'camera/yolo/object_detection', 30) # Changed the topic name for some more clarity
32+
self.subscription = self.create_subscription(
33+
Image,
34+
'/camera/color/image_raw', # This can be changed to any camera topic we want to use (realsense raw rn), for comp this will be the ptz topic
35+
self.image_callback,
36+
10)
1337
self.bridge = CvBridge()
1438

1539
# TODO Change this to whatever YOLO model we decide to use
16-
self.yolo = YOLO('yolov8n.pt')
40+
# For this I want to do some unit testing to use it for the KRIA on fpga, not sure if yolov8 will be able to be used for this
41+
self.yolo = YOLO('bestHammer.pt') # right now this is fine
1742

18-
# TODO Not sure if this is the right camera for video capture for the rover
19-
self.videoCap = cv2.VideoCapture(0)
43+
## We do this via ros since we have the topics we can use a subscriber for this
44+
# # TODO Not sure if this is the right camera for video capture for the rover
45+
# self.videoCap = cv2.VideoCapture(0)
2046

2147
# Timer to process frames
22-
self.timer = self.create_timer(0.1, self.process_frame)
48+
# self.timer = self.create_timer(0.1, self.process_frame)
49+
2350

2451
def get_colours(self, cls_num):
52+
# You'll have to explain to me whats going on here lol
2553
base_colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)]
2654
color_index = cls_num % len(base_colors)
2755
increments = [(1, -2, 1), (-2, 1, -1), (1, -1, 2)]
@@ -31,14 +59,23 @@ def get_colours(self, cls_num):
3159
]
3260
return tuple(color)
3361

34-
def process_frame(self):
35-
ret, frame = self.videoCap.read()
36-
if not ret:
37-
self.get_logger().warning("Failed to capture frame.")
62+
# For now lets try it this way
63+
# Instead of using a timer to process frames periodically, the node now processes frames as they arrive via the callback
64+
# process frame here the same way, but more reactively
65+
# Callback meth is called when each msg is received, so each video frame
66+
def image_callback(self, msg):
67+
# Convert ROS Image message to OpenCV image
68+
try:
69+
frame = self.bridge.imgmsg_to_cv2(msg, desired_encoding='bgr8')
70+
except Exception as e:
71+
self.get_logger().error(f'Error converting image: {e}')
3872
return
3973

74+
# Process frame with YOLO
4075
results = self.yolo.track(frame, stream=True)
41-
76+
77+
# Draw detections on frame
78+
annotated_frame = frame.copy()
4279
for result in results:
4380
classes_names = result.names
4481
for box in result.boxes:
@@ -48,14 +85,17 @@ def process_frame(self):
4885
cls = int(box.cls[0])
4986
class_name = classes_names[cls]
5087
colour = self.get_colours(cls)
51-
cv2.rectangle(frame, (x1, y1), (x2, y2), colour, 2)
52-
cv2.putText(frame, f'{class_name} {box.conf[0]:.2f}',
53-
(x1, y1), cv2.FONT_HERSHEY_SIMPLEX, 1, colour, 2)
5488

89+
if class_name == "hammer":
90+
cv2.rectangle(annotated_frame, (x1, y1), (x2, y2), colour, 2)
91+
cv2.putText(annotated_frame, f'{class_name} {box.conf[0]:.2f}',
92+
(x1, y1), cv2.FONT_HERSHEY_SIMPLEX, 1, colour, 2)
93+
5594
# Convert frame to ROS2 Image message and publish
56-
msg = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
57-
self.publisher_.publish(msg)
58-
self.get_logger().info("Published frame with detections.")
95+
detected_msg = self.bridge.cv2_to_imgmsg(annotated_frame, encoding='bgr8')
96+
detected_msg.header = msg.header # Preserve original timestamp and frame_id, just allows us to communicate timestamped data if needed
97+
self.publisher_.publish(detected_msg)
98+
self.get_logger().debug("Published frame with detections")
5999

60100
def destroy_node(self):
61101
# Release video capture when node is destroyed

src/object_detection/package.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
<maintainer email="[email protected]">ubuntu</maintainer>
88
<license>TODO: License declaration</license>
99

10+
<!-- ros uses opencv on local so need to install it seperately to work
11+
with cv bridge -->
1012
<depend>rclpy</depend>
1113
<depend>sensor_msgs</depend>
1214
<depend>cv_bridge</depend>
1315

14-
<test_depend>ament_copyright</test_depend>
15-
<test_depend>ament_flake8</test_depend>
16-
<test_depend>ament_pep257</test_depend>
17-
<test_depend>python3-pytest</test_depend>
16+
<!-- Added ultralytics exec dependancy, need to pip install ultralytics on local -->
17+
<!-- <exec_depend>python3-ultralytics</exec_depend>
18+
<exec_depend>python3-opencv</exec_depend> -->
1819

1920
<export>
2021
<build_type>ament_python</build_type>
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
#!/usr/bin/env python3
2+
import rclpy
3+
from rclpy.node import Node
4+
from sensor_msgs.msg import Image, Imu
5+
import cv2
6+
from cv_bridge import CvBridge
7+
import pyrealsense2 as rs
8+
import numpy as np
9+
from typing import List, Dict, Tuple, Optional
10+
11+
class CameraPublisherFull(Node):
12+
def __init__(self):
13+
super().__init__('camera_publisher_full')
14+
self.bridge = CvBridge()
15+
16+
# Initialize RealSense if present
17+
self.rs_pipeline = None
18+
self.rs_config = None
19+
self.rs_publishers = {}
20+
self.imu_publisher = self.create_publisher(Imu, '/camera/imu/data', 30)
21+
self.has_realsense = self.setup_realsense()
22+
23+
# Initialize standard cameras (regardless of RealSense detection)
24+
self.std_publishers = []
25+
self.std_caps = []
26+
self.std_dev_paths = []
27+
28+
# Auto-detect available standard camera devices
29+
device_paths = self.detect_available_cameras()
30+
for i, dev_path in enumerate(device_paths):
31+
pub = self.create_publisher(Image, f'cam_{i}', 30)
32+
cap = cv2.VideoCapture(dev_path)
33+
if not cap.isOpened():
34+
self.get_logger().error(f'Could not open video device at: {dev_path}')
35+
continue
36+
self.std_publishers.append(pub)
37+
self.std_caps.append(cap)
38+
self.std_dev_paths.append(dev_path)
39+
40+
# Check if we have any camera at all
41+
if not self.has_realsense and len(self.std_caps) == 0:
42+
self.get_logger().error('No active video devices found.')
43+
exit(1)
44+
45+
# Setup timer callback
46+
self.timer_period = 0.033 # ~30 FPS
47+
self.timer = self.create_timer(self.timer_period, self.timer_callback)
48+
49+
def setup_realsense(self) -> bool:
50+
"""Initialize RealSense camera if available and IMU stream"""
51+
try:
52+
# Create pipeline and config
53+
self.rs_pipeline = rs.pipeline()
54+
self.rs_config = rs.config()
55+
56+
# Try to find RealSense devices
57+
ctx = rs.context()
58+
devices = ctx.query_devices()
59+
if len(devices) == 0:
60+
self.get_logger().info('No RealSense devices detected')
61+
return False
62+
63+
# Setup streams for the first RealSense device
64+
self.get_logger().info(f'Found RealSense device: {devices[0].get_info(rs.camera_info.name)}')
65+
66+
# Enable video streams
67+
self.rs_config.enable_stream(rs.stream.color, 640, 480, rs.format.bgr8, 30)
68+
self.rs_config.enable_stream(rs.stream.infrared, 1, 640, 480, rs.format.y8, 30)
69+
self.rs_config.enable_stream(rs.stream.infrared, 2, 640, 480, rs.format.y8, 30)
70+
self.rs_config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 30)
71+
72+
# Enable IMU stream
73+
self.rs_config.enable_stream(rs.stream.accel, rs.format.motion_xyz32f, 30)
74+
self.rs_config.enable_stream(rs.stream.gyro, rs.format.motion_xyz32f, 30)
75+
76+
# Start streaming
77+
self.rs_pipeline.start(self.rs_config)
78+
79+
# Create publishers for each stream
80+
self.rs_publishers['color'] = self.create_publisher(Image, '/camera/color/image_raw', 30)
81+
self.rs_publishers['infra1'] = self.create_publisher(Image, '/camera/infra1/image_rect_raw', 30)
82+
self.rs_publishers['infra2'] = self.create_publisher(Image, '/camera/infra2/image_rect_raw', 30)
83+
self.rs_publishers['depth'] = self.create_publisher(Image, '/camera/depth/image_rect_raw', 30)
84+
85+
self.get_logger().info('RealSense camera initialized successfully')
86+
return True
87+
88+
except Exception as e:
89+
self.get_logger().warn(f'Failed to initialize RealSense: {e}')
90+
return False
91+
92+
def detect_available_cameras(self) -> List[str]:
93+
"""Detect standard USB cameras, excluding RealSense devices"""
94+
available_cameras = []
95+
max_tested = 10
96+
97+
for i in range(max_tested):
98+
cap = cv2.VideoCapture(f'/dev/video{i}')
99+
if cap.isOpened():
100+
# Check if this is not a RealSense camera by looking at properties
101+
name = cap.getBackendName()
102+
if 'realsense' not in name.lower():
103+
available_cameras.append(f'/dev/video{i}')
104+
else:
105+
self.get_logger().info(f'Skipping RealSense camera at /dev/video{i}')
106+
cap.release()
107+
108+
self.get_logger().info(f'Detected standard camera devices: {available_cameras}')
109+
return available_cameras
110+
111+
def publish_realsense_frame(self, frame, frame_type):
112+
if frame_type in self.rs_publishers:
113+
if frame_type in ['color', 'infra1', 'infra2', 'depth']:
114+
# Process image based on type
115+
if frame_type == 'color':
116+
# Convert BGR to RGB
117+
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
118+
msg = self.bridge.cv2_to_imgmsg(img, encoding="rgb8")
119+
elif frame_type in ['infra1', 'infra2']:
120+
# Infrared is already grayscale (y8)
121+
msg = self.bridge.cv2_to_imgmsg(frame, encoding="mono8")
122+
else: # depth
123+
# Depth is 16-bit
124+
msg = self.bridge.cv2_to_imgmsg(frame, encoding="16UC1")
125+
126+
self.rs_publishers[frame_type].publish(msg)
127+
128+
def publish_imu_data(self, accel_data, gyro_data):
129+
imu_msg = Imu()
130+
imu_msg.header.stamp = self.get_clock().now().to_msg()
131+
132+
# Fill in IMU data for linear acceleration
133+
imu_msg.linear_acceleration.x = accel_data[0]
134+
imu_msg.linear_acceleration.y = accel_data[1]
135+
imu_msg.linear_acceleration.z = accel_data[2]
136+
137+
# Fill in IMU data for angular velocity
138+
imu_msg.angular_velocity.x = gyro_data[0]
139+
imu_msg.angular_velocity.y = gyro_data[1]
140+
imu_msg.angular_velocity.z = gyro_data[2]
141+
142+
self.imu_publisher.publish(imu_msg)
143+
144+
def timer_callback(self):
145+
# Process RealSense frames if available
146+
if self.has_realsense:
147+
try:
148+
frames = self.rs_pipeline.wait_for_frames()
149+
150+
# Process camera frames
151+
color_frame = frames.get_color_frame()
152+
if color_frame:
153+
color_image = np.asanyarray(color_frame.get_data())
154+
self.publish_realsense_frame(color_image, 'color')
155+
156+
depth_frame = frames.get_depth_frame()
157+
if depth_frame:
158+
depth_image = np.asanyarray(depth_frame.get_data())
159+
self.publish_realsense_frame(depth_image, 'depth')
160+
161+
# Process infrared frames
162+
for i in [1, 2]:
163+
ir_frame = frames.get_infrared_frame(i)
164+
if ir_frame:
165+
ir_image = np.asanyarray(ir_frame.get_data())
166+
self.publish_realsense_frame(ir_image, f'infra{i}')
167+
168+
# Process IMU frames
169+
accel_frame = frames.first(rs.stream.accel)
170+
gyro_frame = frames.first(rs.stream.gyro)
171+
if accel_frame and gyro_frame:
172+
accel_data = accel_frame.as_motion_frame().get_motion_data()
173+
gyro_data = gyro_frame.as_motion_frame().get_motion_data()
174+
self.publish_imu_data(accel_data, gyro_data)
175+
176+
except Exception as e:
177+
self.get_logger().error(f'Error processing RealSense frames: {e}')
178+
179+
# Process standard camera frames
180+
for i in range(len(self.std_caps)):
181+
cap = self.std_caps[i]
182+
dev_path = self.std_dev_paths[i]
183+
ret, frame = cap.read()
184+
if ret:
185+
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
186+
msg = self.bridge.cv2_to_imgmsg(frame, encoding="rgb8")
187+
self.std_publishers[i].publish(msg)
188+
else:
189+
pass
190+
191+
def main(args=None):
192+
# Initialize node
193+
rclpy.init(args=args)
194+
node = CameraPublisherFull()
195+
try:
196+
# Spin
197+
rclpy.spin(node)
198+
except KeyboardInterrupt:
199+
pass
200+
finally:
201+
# Shutdown
202+
if node.rs_pipeline:
203+
node.rs_pipeline.stop()
204+
for cap in node.std_caps:
205+
cap.release()
206+
node.destroy_node()
207+
rclpy.shutdown()
208+
209+
if __name__ == '__main__':
210+
main()

0 commit comments

Comments
 (0)