Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ logs/
build/
bin/
lib/
log/
install/
livestream/
snapshot_files/
msg_gen/
srv_gen/
msg/*Action.msg
Expand Down Expand Up @@ -65,3 +69,5 @@ About

/venv/
/.idea/
.vscode/

12 changes: 12 additions & 0 deletions FastAPI/hosting_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

@app.get('/')
async def main():
def iter_file():
with open("livestream/output.mp4", mode="rb") as stream_file:
yield from stream_file
return StreamingResponse(iter_file(), media_type='text/event-stream')
# return StreamingResponse(iter_file(), media_type='video/mp4')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code can be removed

3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ git clone https://github.com/OrdinaNederland/robotics-workshop
#### Build, Source & Launch Package (Gamepad)
```
cd ~/robotics-workshop
chmod +x host_rtsp_server
colcon build --symlink-install && source install/local_setup.bash
colcon build && source install/local_setup.bash
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to keep the "--symlink-install" in the build-command, as it should keep the amount of times to build the code again to a minimum

ROS_DOMAIN_ID=<INSERT ROBOT NUMBER> ros2 launch robot_app gamepad_launch.py gamepad_type:=playstation
```

Expand Down
Binary file removed host_rtsp_server
Binary file not shown.
87 changes: 0 additions & 87 deletions host_rtsp_server.c

This file was deleted.

File renamed without changes.
1 change: 1 addition & 0 deletions src/camera_capture/camera_capture/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PACKAGE_NAME: str = "camera_capture"
206 changes: 206 additions & 0 deletions src/camera_capture/camera_capture/camera_capture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""ROS2 CSI Camera Image Publisher.
This script publishes csi camera image to a ROS2 topic in sensor_msgs.msg/Image format.
Example:
$ colcon build --symlink-install
$ ros2 launch camera_snap_shot camera_snap_shot.launch.py
"""

# ___Import Modules:
import os
import cv2
import json
import numpy as np

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image
from ament_index_python.packages import get_package_share_directory
from std_msgs.msg import Bool
from cv_bridge import CvBridge

from . import PACKAGE_NAME


# ___Global Variables:
SETTINGS = os.path.join(
get_package_share_directory("camera_capture"), "config/camera_capture_settings.json"
)
with open(SETTINGS) as fp:
json_settings = json.load(fp)


# __Functions:
def gstreamer_pipeline(
capture_width: str = str(json_settings["capture_width_livestream"]),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A number of items in the json_settings are retrieved multiple times at multiple places. It may improve readability and clarity for readers to define constants for those settings e.g. CAPTURE_WIDTH_LIVESTREAM = json_settings["capture_width_livestream"], and use CAPTURE_WIDTH_LIVESTREAM in all other parts of the code

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json_settings['image_location'] is another example

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just occured to me: this also improves testability slightly

capture_height: str = str(json_settings["capture_height_livestream"]),
framerate: str = str(json_settings["framerate"]),
):
return (
"nvarguscamerasrc ! "
"video/x-raw(memory:NVMM), "
f"width=(int){capture_width}, height=(int){capture_height}, "
f"format=(string)NV12, framerate=(fraction){framerate}/1 ! "
"nvvidconv ! "
"video/x-raw, format=(string)BGRx ! "
"videoconvert ! "
"video/x-raw, format=(string)BGR ! "
"appsink"
)


# __Classes:
class CameraPublisher(Node):
"""this class captures immages from a CSI camera and publishes it as a livestream or snapshots to a topic"""

def __init__(
self,
publish_livestream_topic: str,
publish_snapshot_topic: str,
snapshot_trigger_topic: str,
livestream_state_topic: str,
):
super().__init__(PACKAGE_NAME)

# initialize publisher & subscirbers
self.pub_cam_snapshot = self.create_publisher(Image, publish_snapshot_topic, 1)
self.pub_cam_livestream = self.create_publisher(
Image, publish_livestream_topic, 1
)
self.sub_cam_livestream_state = self.create_subscription(
Bool, livestream_state_topic, self.start_livestream_callback, 1
)
self.sub_cam_snapshot_trigger = self.create_subscription(
Bool, snapshot_trigger_topic, self.capture_snapshot_callback, 1
)

self.cap = cv2.VideoCapture(gstreamer_pipeline())
self.bridge = CvBridge()
self.image_location = f"{json_settings['image_location']}"
self.image_counter = 0

def start_livestream_callback(self, topic_msg: Bool):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name does not really reflect its function, as it starts and stops the livestream. Would livestream_state_callback be an option for you?

"""This method starts a livestream when a message is received on the topic livestream/state

Args:
topic_msg (Bool): True for starting livestream, False for stopping livestream
"""
self.cap.set(
cv2.CAP_PROP_FRAME_HEIGHT, json_settings["capture_height_livestream"]
)
self.cap.set(
cv2.CAP_PROP_FRAME_WIDTH, json_settings["capture_width_livestream"]
)
self.get_logger().info(f"message received on topic livestream")
self.get_logger().debug(
f"Incomming message on topic livestream \nwith message: {topic_msg}"
)
timer_period = 0.03 # seconds TODO: make into settings 30hz
if self.cap.isOpened():
self.get_logger().info("camera is available")
if topic_msg.data:
self.get_logger().info("starting livestream")
timer = self.create_timer(timer_period, self.timer_callback)
elif not topic_msg.data:
self.get_logger().info("stopping livestream")
try:
timer.cancel()
timer.destroy()
except UnboundLocalError:
self.get_logger().error("timer was is not defined)")
else:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this else-statement be reached? It seems topic_msg.data is always True or False, so this is never reached right?

self.get_logger().debug(
"can't stop livestream because livestream is already stopped"
)
elif not self.cap.isOpened():
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An 'else`-statement would suffice

self.get_logger().info("camera not available")

def timer_callback(self):
"""callback function to read image data from camera and publish it to the topic of the livestream"""
ret, frame = self.cap.read()
msg_image = self.bridge.cv2_to_imgmsg(np.array(frame), "bgr8")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the np.array() needed? I think it is superfluous, if I recall correctly we implemented it to cast the frame to a certain dtype which turned out to be unnecessary.

self.pub_cam_livestream.publish(msg_image)

def capture_snapshot_callback(self, topic_msg: Bool):
"""Captures images when a True is received on the snapshot topic and publishes the immage on a topic.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in "image"


Args:
topic_msg (Bool): True for capturing snapshot, False is not used.
"""
self.get_logger().info(
f"message received on topic snapshot \nwith message: {topic_msg}\nwith data :{topic_msg.data}"
)
self.cap.set(
cv2.CAP_PROP_FRAME_HEIGHT, json_settings["capture_height_snapshot"]
)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, json_settings["capture_width_snapshot"])
if self.cap.isOpened():
ret, frame = self.cap.read()
print(type(frame))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the print-statement

msg_image = self.bridge.cv2_to_imgmsg(np.array(frame), "bgr8")
msg_image.header.frame_id = str(self.image_counter)
cv2.imwrite(f"{self.image_location}/image{self.image_counter}.jpg", frame)
self.get_logger().info(f"image saved at image{self.image_counter}.jpg")
self.pub_cam_snapshot.publish(msg_image)
self.image_counter += 1
prevent_overflood_image(self.image_counter)
else:
self.get_logger().info("camera not available")
self.cap.set(
cv2.CAP_PROP_FRAME_HEIGHT, json_settings["capture_height_livestream"]
)
self.cap.set(
cv2.CAP_PROP_FRAME_WIDTH, json_settings["capture_width_livestream"]
)


def prevent_overflood_image(img_number: int):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image directory is now fixed, if this is passed as an argument the function is more flexible and may be re-used more often (also easier to test)

"""makes sure that there are only 5 images in the image folder
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment, please do not take the time to change this for this PR. But it is good practice to write the first line of your doc string in a uniform manner. A common pattern is an imperative phrase (e.g. "Make sure that there are only 5 images in the image folder").

Right now, there are multiple styles in this document, making reading them ever so slightly more difficult


Args:
img_number (int): current number of latest image published
"""
if img_number > 4:
item_to_delete = f"{json_settings['image_location']}/image{img_number-5}.jpg"
if os.path.exists(item_to_delete):
os.remove(item_to_delete)


# ___Main Method:
def main(args=None):
"""
This is the Main Method that spins the node and starts the camera publisher.
"""
# initializes node and start publishing
cam_0_base = json_settings["publish_topic_camera"] + json_settings["camera_id"]
livestream_state_topic = (
json_settings["publish_topic_livestream"]
+ json_settings["trigger_topic_livestream"]
)
snapshot_trigger_topic = (
json_settings["publish_topic_snapshot"]
+ json_settings["trigger_topic_snapshot"]
)
publish_livestream_topic = json_settings["publish_topic_livestream"]
publish_snapshot_topic = json_settings["publish_topic_snapshot"]
rclpy.init(args=args)
camera_0 = CameraPublisher(
livestream_state_topic=cam_0_base + livestream_state_topic,
snapshot_trigger_topic=cam_0_base + snapshot_trigger_topic,
publish_livestream_topic=cam_0_base + publish_livestream_topic,
publish_snapshot_topic=cam_0_base + publish_snapshot_topic,
)
rclpy.spin(camera_0)

# shuts down nose and releases everything
camera_0.destroy_node()
rclpy.shutdown()

return None


# ___Driver Program:
if __name__ == "__main__":
main()
15 changes: 15 additions & 0 deletions src/camera_capture/config/camera_capture_settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"publish_topic_camera": "/camera",
"camera_id": "/cam_0",
"publish_topic_livestream": "/livestream",
"publish_topic_snapshot": "/snapshot",
"trigger_topic_livestream": "/state",
"trigger_topic_snapshot": "/trigger",
"capture_width_livestream": 720,
"capture_height_livestream": 480,
"capture_width_snapshot": 1920,
"capture_height_snapshot": 1080,
"framerate": 30,
"image_location": "snapshot_files"

}
38 changes: 38 additions & 0 deletions src/camera_capture/launch/camera_capture.launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pathlib

from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.conditions import IfCondition
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitution
from launch_ros.actions import Node

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.actions import IncludeLaunchDescription
from launch.actions import GroupAction
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch.substitutions import LaunchConfiguration
from launch.substitutions import TextSubstitution
from launch_ros.actions import Node
from launch_ros.actions import PushRosNamespace

DEFAULT_CONFIG_PATH = str(pathlib.Path(f"{get_package_share_directory('camera_capture')}/config/camera_capture.json"))


def generate_launch_description() -> LaunchDescription:
# args that can be set from the command line or a default will be used
config_file_arg = DeclareLaunchArgument(
"config_file", default_value=TextSubstitution(text=DEFAULT_CONFIG_PATH)
)
# start a turtlesim_node in the turtlesim1 namespace
camera_capture = Node(
package='camera_capture',
executable='camera_capture',
name='sim',
)

return LaunchDescription([
# launch_include,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented code

camera_capture,
])
Loading