Skip to content

Commit 2b68b48

Browse files
committed
cam and vid node
1 parent 331647c commit 2b68b48

10 files changed

Lines changed: 334 additions & 0 deletions

File tree

ros2_ws/src/extermination_package/extermination_package/__init__.py

Whitespace-only changes.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import os
2+
import cv2
3+
import rclpy
4+
from rclpy.node import Node
5+
from std_msgs.msg import String
6+
from sensor_msgs.msg import Image
7+
from cv_bridge import CvBridge
8+
9+
class CameraNode(Node):
10+
def __init__(self):
11+
super().__init__('camera_node')
12+
13+
# Parameters
14+
self.declare_parameter('input_type', 'video') # 'video' or 'image'
15+
self.declare_parameter('input_path', '') # no video path yet
16+
self.declare_parameter('frame_rate', 10) # Frames per second for video
17+
18+
# Get parameters
19+
self.input_type = self.get_parameter('input_type').get_parameter_value().string_value
20+
self.input_path = self.get_parameter('input_path').get_parameter_value().string_value
21+
self.frame_rate = self.get_parameter('frame_rate').get_parameter_value().integer_value
22+
23+
# Resolve input path
24+
if not os.path.isabs(self.input_path):
25+
self.input_path = os.path.join(os.getcwd(), self.input_path)
26+
27+
# Initialize input source
28+
self.bridge = CvBridge()
29+
self.robot_state = 'stopped' # Default state
30+
self.video_capture = None
31+
32+
if self.input_type == 'video':
33+
self.init_video()
34+
elif self.input_type == 'image':
35+
self.init_image()
36+
else:
37+
self.get_logger().error(f"Invalid input_type: {self.input_type}. Use 'video' or 'image'.")
38+
self.destroy_node()
39+
40+
# ROS 2 communication
41+
self.state_subscription = self.create_subscription(
42+
String,
43+
'/robot_state',
44+
self.state_callback,
45+
10
46+
)
47+
self.image_publisher = self.create_publisher(Image, '/camera/image_raw', 10)
48+
49+
self.get_logger().info('CameraNode has been started.')
50+
51+
def init_video(self):
52+
"""Initialize video input."""
53+
self.video_capture = cv2.VideoCapture(self.input_path)
54+
if not self.video_capture.isOpened():
55+
self.get_logger().error(f"Failed to open video: {self.input_path}")
56+
self.destroy_node()
57+
58+
def init_image(self):
59+
"""Initialize static image input."""
60+
if not os.path.exists(self.input_path):
61+
self.get_logger().error(f"Image file not found: {self.input_path}")
62+
self.destroy_node()
63+
self.static_image = cv2.imread(self.input_path)
64+
if self.static_image is None:
65+
self.get_logger().error(f"Failed to read image: {self.input_path}")
66+
self.destroy_node()
67+
68+
def state_callback(self, msg):
69+
"""Callback for /robot_state topic."""
70+
self.robot_state = msg.data
71+
self.get_logger().info(f'Received robot state: {self.robot_state}')
72+
73+
if self.robot_state == 'moving' and self.input_type == 'video':
74+
self.start_continuous_capture()
75+
elif self.robot_state == 'stopped':
76+
self.capture_static_image()
77+
78+
def start_continuous_capture(self):
79+
"""Continuously capture and publish frames from the video."""
80+
self.get_logger().info('Robot is moving. Starting continuous video capture...')
81+
while self.robot_state == 'moving' and rclpy.ok():
82+
success, frame = self.video_capture.read()
83+
if success:
84+
self.publish_image(frame)
85+
else:
86+
self.get_logger().info('End of video reached. Restarting...')
87+
self.video_capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
88+
89+
def capture_static_image(self):
90+
"""Publish a single static image."""
91+
self.get_logger().info('Robot is stopped. Publishing static image...')
92+
if self.input_type == 'image':
93+
self.publish_image(self.static_image)
94+
elif self.input_type == 'video':
95+
# Publish the last frame from the video
96+
success, frame = self.video_capture.read()
97+
if success:
98+
self.publish_image(frame)
99+
100+
def publish_image(self, frame):
101+
"""Convert and publish an image to the /camera/image_raw topic."""
102+
try:
103+
ros_image = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
104+
self.image_publisher.publish(ros_image)
105+
self.get_logger().info('Published image to /camera/image_raw.')
106+
except Exception as e:
107+
self.get_logger().error(f'Failed to publish image: {e}')
108+
109+
def destroy_node(self):
110+
"""Clean up resources."""
111+
if self.video_capture is not None:
112+
self.video_capture.release()
113+
super().destroy_node()
114+
115+
def main(args=None):
116+
rclpy.init(args=args)
117+
node = CameraNode()
118+
try:
119+
rclpy.spin(node)
120+
except KeyboardInterrupt:
121+
pass
122+
finally:
123+
node.destroy_node()
124+
rclpy.shutdown()
125+
126+
if __name__ == '__main__':
127+
main()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
import cv2
3+
import rclpy
4+
from rclpy.node import Node
5+
from sensor_msgs.msg import Image
6+
from cv_bridge import CvBridge
7+
8+
class VideoNode(Node):
9+
def __init__(self):
10+
super().__init__('video_node')
11+
12+
# Declare parameters
13+
self.declare_parameter('video_path', '') # no video yet
14+
self.declare_parameter('loop', -1) # -1 = loop forever, 0 = no loop, >0 = loop count
15+
self.declare_parameter('frame_rate', 10)
16+
17+
# Get parameters
18+
self.video_path = self.get_parameter('video_path').get_parameter_value().string_value
19+
self.loop = self.get_parameter('loop').get_parameter_value().integer_value
20+
self.frame_rate = self.get_parameter('frame_rate').get_parameter_value().integer_value
21+
22+
# Resolve video path
23+
if not os.path.isabs(self.video_path):
24+
self.video_path = os.path.join(os.getcwd(), self.video_path)
25+
26+
# Initialize video capture
27+
self.bridge = CvBridge()
28+
self.loop_count = 0
29+
self.vr = None
30+
self.open_video()
31+
32+
# ROS 2 publisher
33+
self.image_publisher = self.create_publisher(Image, '/camera/image_raw', 10)
34+
35+
# Timer for publishing frames
36+
self.timer = self.create_timer(1.0 / self.frame_rate, self.publish_frame)
37+
38+
def open_video(self):
39+
"""Open the video file."""
40+
self.vr = cv2.VideoCapture(self.video_path)
41+
if not self.vr.isOpened():
42+
self.get_logger().error(f"Failed to open video: {self.video_path}")
43+
self.destroy_node()
44+
45+
def publish_frame(self):
46+
"""Publish a single frame from the video."""
47+
success, frame = self.vr.read()
48+
if success:
49+
# Convert frame to ROS 2 Image message
50+
ros_image = self.bridge.cv2_to_imgmsg(frame, encoding='bgr8')
51+
self.image_publisher.publish(ros_image)
52+
self.get_logger().info('Published a frame.')
53+
else:
54+
# Handle end of video
55+
if self.loop == -1 or self.loop_count < self.loop:
56+
self.loop_count += 1
57+
self.open_video()
58+
else:
59+
self.get_logger().info('Finished playing video.')
60+
self.destroy_node()
61+
62+
def destroy_node(self):
63+
"""Clean up resources."""
64+
if self.vr is not None:
65+
self.vr.release()
66+
super().destroy_node()
67+
68+
def main(args=None):
69+
rclpy.init(args=args)
70+
node = VideoNode()
71+
try:
72+
rclpy.spin(node)
73+
except KeyboardInterrupt:
74+
pass
75+
finally:
76+
node.destroy_node()
77+
rclpy.shutdown()
78+
79+
if __name__ == '__main__':
80+
main()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0"?>
2+
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
3+
<package format="3">
4+
<name>extermination_package</name>
5+
<version>0.0.0</version>
6+
<description>TODO: Package description</description>
7+
<maintainer email="theanhhoang0201@gmail.com">vscode</maintainer>
8+
<license>TODO: License declaration</license>
9+
10+
<depend>rclpy</depend>
11+
<depend>sensor_msgs</depend>
12+
<depend>std_msgs</depend>
13+
<depend>std_srvs</depend>
14+
15+
<test_depend>ament_copyright</test_depend>
16+
<test_depend>ament_flake8</test_depend>
17+
<test_depend>ament_pep257</test_depend>
18+
<test_depend>python3-pytest</test_depend>
19+
20+
<export>
21+
<build_type>ament_python</build_type>
22+
</export>
23+
</package>

ros2_ws/src/extermination_package/resource/extermination_package

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[develop]
2+
script_dir=$base/lib/extermination_package
3+
[install]
4+
install_scripts=$base/lib/extermination_package
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from setuptools import find_packages, setup
2+
3+
package_name = 'extermination_package'
4+
5+
setup(
6+
name=package_name,
7+
version='0.0.0',
8+
packages=find_packages(exclude=['test']),
9+
data_files=[
10+
('share/ament_index/resource_index/packages',
11+
['resource/' + package_name]),
12+
('share/' + package_name, ['package.xml']),
13+
],
14+
install_requires=['setuptools'],
15+
zip_safe=True,
16+
maintainer='vscode',
17+
maintainer_email='theanhhoang0201@gmail.com',
18+
description='TODO: Package description',
19+
license='TODO: License declaration',
20+
tests_require=['pytest'],
21+
entry_points={
22+
'console_scripts': [
23+
'camera_node = extermination_package.camera_node:main',
24+
'video_node = extermination_package.video_node:main',
25+
],
26+
},
27+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2015 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from ament_copyright.main import main
16+
import pytest
17+
18+
19+
# Remove the `skip` decorator once the source file(s) have a copyright header
20+
@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.')
21+
@pytest.mark.copyright
22+
@pytest.mark.linter
23+
def test_copyright():
24+
rc = main(argv=['.', 'test'])
25+
assert rc == 0, 'Found errors'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright 2017 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from ament_flake8.main import main_with_errors
16+
import pytest
17+
18+
19+
@pytest.mark.flake8
20+
@pytest.mark.linter
21+
def test_flake8():
22+
rc, errors = main_with_errors(argv=[])
23+
assert rc == 0, \
24+
'Found %d code style errors / warnings:\n' % len(errors) + \
25+
'\n'.join(errors)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2015 Open Source Robotics Foundation, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from ament_pep257.main import main
16+
import pytest
17+
18+
19+
@pytest.mark.linter
20+
@pytest.mark.pep257
21+
def test_pep257():
22+
rc = main(argv=['.', 'test'])
23+
assert rc == 0, 'Found code style errors / warnings'

0 commit comments

Comments
 (0)