Compare commits

3 Commits

Author SHA1 Message Date
34aa2655e7 strip a lot debug stuff 2026-02-17 00:29:07 +01:00
4f7227c0e9 initial version 2026-02-17 00:13:26 +01:00
fed1844282 intial stuff 2026-02-16 19:55:01 +01:00
15 changed files with 832 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
{
"image": "ubuntu:24.04", // for cuda support: nvidia/cuda:13.1.0-devel-ubuntu24.04
"containerEnv": {
"CONAN_USR": "", // need to be filled
"CONAN_PSW": "" // need to be filled,
},
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/git-lfs:1": {}
},
"customizations": {
"vscode": {
"settings": {
"git.path": "/usr/bin/git"
},
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"GitHub.copilot-chat",
"njqdev.vscode-python-typehint",
"ms-python.debugpy",
"mhutchie.git-graph",
"ms-vscode.cpptools-themes",
"tamasfe.even-better-toml",
"moshfeu.compare-folders",
"njpwerner.autodocstring",
"ms-python.black-formatter",
"ms-python.isort",
"ms-vscode.live-server",
"yzhang.markdown-all-in-one",
"bierner.markdown-mermaid",
"charliermarsh.ruff",
"shardulm94.trailing-spaces",
"redhat.vscode-yaml",
"ninoseki.vscode-mogami",
"josetr.cmake-language-support-vscode",
"ms-vscode.cmake-tools",
"twxs.cmake",
"cheshirekow.cmake-format",
"ms-azuretools.vscode-docker",
"foxundermoon.shell-format",
"nvidia.nsight-vscode-edition"
]
}
},
"runArgs": [
"--network=host",
"-e HOST_UID=$(id -u)",
"-e HOST_GID=$(id -g)"
],
"postStartCommand": ".devcontainer/postStartCommand.sh"
}

187
.devcontainer/postStartCommand.sh Executable file
View File

@@ -0,0 +1,187 @@
#!/bin/bash
# ==============================================================================
# Conan2 Development Environment Setup
#
# Description: Provisions system dependencies, shell configuration, and Conan2.
# Designed for Debian/Ubuntu-based DevContainers or CI Runners.
# Usage: CONAN_USR=x CONAN_PSW=y ./postStartCommand.sh
# ==============================================================================
set -euo pipefail
# --- Configuration ------------------------------------------------------------
# Versions & Remote Resources
TASK_VERSION="v3.45.5"
GLOW_VERSION="2.1.1"
CONAN_CONFIG_URL="https://package-cloud.dns.army/ros2/conan2-config.git"
CONAN_REMOTE_NAME="package-cloud-ros2-conan2"
# Localization
TIMEZONE="Europe/Berlin"
# Paths
LOCAL_BIN="$HOME/.local/bin"
ZSH_CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
# --- Helpers ------------------------------------------------------------------
log() {
echo -e "\033[1;34m>>> [SETUP] $1\033[0m"
}
error() {
echo -e "\033[1;31m!!! [ERROR] $1\033[0m" >&2
exit 1
}
cleanup() {
# Remove temp files on exit (successful or not)
rm -f glow.deb install_task.sh
}
trap cleanup EXIT
ensure_env_vars() {
if [[ -z "${CONAN_USR:-}" ]] || [[ -z "${CONAN_PSW:-}" ]]; then
error "CONAN_USR and CONAN_PSW environment variables are required."
fi
}
detect_arch() {
local arch
arch=$(dpkg --print-architecture)
case "$arch" in
amd64) echo "amd64" ;;
arm64) echo "arm64" ;;
*) error "Unsupported architecture: $arch" ;;
esac
}
# --- Installation Functions ---------------------------------------------------
install_system_dependencies() {
log "Installing system dependencies..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -q
apt-get install -y -q --no-install-recommends \
tzdata curl git zsh python3 python3-pip python3-venv \
ca-certificates gnupg pipx
# Ensure pipx path is available immediately for this script
export PATH="$LOCAL_BIN:$PATH"
pipx ensurepath
}
configure_timezone() {
log "Configuring timezone ($TIMEZONE)..."
local tz_path="/usr/share/zoneinfo/$TIMEZONE"
# Validation: Ensure the requested timezone actually exists
if [ ! -f "$tz_path" ]; then
error "Timezone data not found at '$tz_path'. Please check the TIMEZONE variable."
fi
ln -fs "$tz_path" /etc/localtime
echo "$TIMEZONE" >/etc/timezone
dpkg-reconfigure -f noninteractive tzdata
}
install_shell_tools() {
log "Setting up Shell (Oh My Zsh)..."
if [ ! -d "$HOME/.oh-my-zsh" ]; then
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
else
log "Oh My Zsh already installed. Skipping."
fi
}
install_conan() {
log "Installing Conan via pipx (Isolated)..."
# Use pipx to avoid breaking system packages
if ! command -v conan &>/dev/null; then
pipx install conan
else
pipx upgrade conan
fi
# Config
log "Installing Conan Config..."
# Force allows overwriting existing configs which is usually desired in setup scripts
conan config install "$CONAN_CONFIG_URL" --type git --args="--branch main"
log "Authenticating Conan..."
conan remote login "$CONAN_REMOTE_NAME" "$CONAN_USR" -p "$CONAN_PSW"
}
install_utilities() {
local arch
arch=$(detect_arch)
# 1. Taskfile
log "Installing Taskfile ($TASK_VERSION)..."
curl -sL -o install_task.sh https://taskfile.dev/install.sh
sh install_task.sh -d -b "$LOCAL_BIN" "$TASK_VERSION"
# 2. Glow
log "Installing Glow ($GLOW_VERSION for $arch)..."
local glow_url="https://github.com/charmbracelet/glow/releases/download/v${GLOW_VERSION}/glow_${GLOW_VERSION}_${arch}.deb"
curl -L -o glow.deb "$glow_url"
dpkg -i glow.deb
}
configure_persistence() {
log "Persisting configurations..."
local zshrc="$HOME/.zshrc"
touch "$zshrc"
# Helper to append if missing
append_if_missing() {
local file="$1"
local content="$2"
if ! grep -Fxq "$content" "$file"; then
echo "$content" >>"$file"
fi
}
# Add Local Bin to Path
local path_export='export PATH="$HOME/.local/bin:$PATH"'
append_if_missing "$HOME/.bashrc" "$path_export"
append_if_missing "$zshrc" "$path_export"
# Shell Completions
append_if_missing "$zshrc" 'eval "$(task --completion zsh)"'
append_if_missing "$zshrc" 'eval "$(glow completion zsh)"'
# Git Safe Directory
git config --global --add safe.directory /workspaces/conan2-ros2-video-to-rosbag
}
run_tasks() {
if [ -f "Taskfile.yml" ] || [ -f "Taskfile.yaml" ]; then
log "Running default Task..."
"$LOCAL_BIN/task" --yes
else
log "No Taskfile found. Skipping 'task --yes'."
fi
}
# --- Main Execution -----------------------------------------------------------
main() {
ensure_env_vars
install_system_dependencies
configure_timezone
install_shell_tools
install_utilities # Task & Glow
install_conan # Depends on pipx (sys deps)
configure_persistence
run_tasks
log "Setup complete successfully."
}
main

2
.gitignore vendored
View File

@@ -208,3 +208,5 @@ cython_debug/
# PyPI configuration file
.pypirc
.json
.task/

3
.taskrc.yml Normal file
View File

@@ -0,0 +1,3 @@
# Enable experimental features
experiments:
REMOTE_TASKFILES: 1

30
CMakeLists.txt Normal file
View File

@@ -0,0 +1,30 @@
cmake_minimum_required(VERSION 3.8)
project(video_to_rosbag)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_link_options("-s") # or use CMAKE_EXE_LINKER_FLAGS_RELEASE
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(sensor_msgs REQUIRED)
find_package(rosbag2_cpp REQUIRED)
find_package(OpenCV REQUIRED COMPONENTS core imgcodecs videoio)
add_executable(video_to_rosbag src/video_to_rosbag.cc)
target_include_directories(video_to_rosbag PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(video_to_rosbag ${OpenCV_LIBS})
ament_target_dependencies(video_to_rosbag
rclcpp
sensor_msgs
rosbag2_cpp
)
install(TARGETS video_to_rosbag
DESTINATION lib/${PROJECT_NAME})
ament_package()

9
CMakeUserPresets.json Normal file
View File

@@ -0,0 +1,9 @@
{
"version": 4,
"vendor": {
"conan": {}
},
"include": [
"build/Release/generators/CMakePresets.json"
]
}

14
LICENSE Normal file
View File

@@ -0,0 +1,14 @@
Copyright (C) 2023 Hesai Technology Co., Ltd.
Copyright (C) 2023 Hesai Technology, zhangyu
Copyright (C) 2023 Hesai Technology, huangzhongbo
All rights reserved.
All code in this repository is released under the terms of the following Modified BSD License. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

136
Taskfile.ros2.yml Normal file
View File

@@ -0,0 +1,136 @@
# [https://taskfile.dev](https://taskfile.dev)
version: "3"
vars:
BUILD_TYPE: '{{.BUILD_TYPE | default "Release"}}'
VERSION: '{{.CONAN_BUILD_VERSION | default .ENV_CONAN_BUILD_VERSION | default "0.0.0-dev"}}'
# Detect System Architecture (x86_64 or aarch64)
SYSTEM_ARCH:
sh: arch
tasks:
# ============================================================================
# Core Logic (Internal)
# ============================================================================
_validate_arch:
internal: true
silent: true
desc: "Checks if the current system architecture supports the requested build"
requires:
vars: [EXPECTED_ARCH]
cmds:
- |
if [ "{{.SYSTEM_ARCH}}" != "{{.EXPECTED_ARCH}}" ]; then
echo "❌ ARCHITECTURE MISMATCH ERROR:"
echo " - System Arch: {{.SYSTEM_ARCH}}"
echo " - Expected Arch: {{.EXPECTED_ARCH}}"
echo " - Task Type: Native Build"
echo ""
echo " You are trying to run a 'native' build for {{.EXPECTED_ARCH}} on a {{.SYSTEM_ARCH}} machine."
echo " Please use the 'cross-...' task instead."
exit 1
else
echo "✅ Architecture match: System={{.SYSTEM_ARCH}} matches Target={{.EXPECTED_ARCH}}"
fi
_create:
internal: true
desc: "Core wrapper for conan create"
requires:
vars: [PROFILE_HOST, PROFILE_BUILD]
cmds:
- >-
conan create .
--profile:build {{.PROFILE_BUILD}}
--profile:host {{.PROFILE_HOST}}
--build=missing
--version {{.VERSION}}
-s build_type={{.BUILD_TYPE}}
# ============================================================================
# GCC 13 (Modern)
# ============================================================================
native-x64:gcc13:
desc: "Build: Native x64 (GCC 13)"
cmds:
- task: _validate_arch
vars: { EXPECTED_ARCH: "x86_64" }
- task: _create
vars:
PROFILE_BUILD: x64_linux_gcc_13
PROFILE_HOST: x64_linux_gcc_13
native-armv8:gcc13:
desc: "Build: Native ARMv8 (GCC 13)"
cmds:
- task: _validate_arch
vars: { EXPECTED_ARCH: "aarch64" }
- task: _create
vars:
PROFILE_BUILD: armv8_linux_gcc_13
PROFILE_HOST: armv8_linux_gcc_13
cross-armv8:gcc13:
desc: "Build: Cross-Compile x64 -> ARMv8 (GCC 13)"
cmds:
- echo "⚠️ Cross-compiling for ARMv8 on {{.SYSTEM_ARCH}} (No arch check enforced)"
- task: _create
vars:
PROFILE_BUILD: x64_linux_gcc_13
PROFILE_HOST: armv8_linux_gcc_13_croco
# ============================================================================
# GCC 9 (Legacy)
# ============================================================================
native-x64:gcc9:
desc: "Build: Native x64 (GCC 9)"
cmds:
- task: _validate_arch
vars: { EXPECTED_ARCH: "x86_64" }
- task: _create
vars:
PROFILE_BUILD: x64_linux_gcc_9
PROFILE_HOST: x64_linux_gcc_9
native-armv8:gcc9:
desc: "Build: Native ARMv8 (GCC 9)"
cmds:
- task: _validate_arch
vars: { EXPECTED_ARCH: "aarch64" }
- task: _create
vars:
PROFILE_BUILD: armv8_linux_gcc_9
PROFILE_HOST: armv8_linux_gcc_9
cross-armv8:gcc9:
desc: "Build: Cross-Compile x64 -> ARMv8 (GCC 9)"
cmds:
- echo "⚠️ Cross-compiling for ARMv8 on {{.SYSTEM_ARCH}} (No arch check enforced)"
- task: _create
vars:
PROFILE_BUILD: x64_linux_gcc_9
PROFILE_HOST: armv8_linux_gcc_9_croco
# ============================================================================
# Utility Tasks
# ============================================================================
clean:
desc: "Clean build artifacts and conan cache for this package"
cmds:
- rm -rf build install colcon_build
- conan remove video-to-rosbag/* -c
test:
desc: "Run the converter test with sample video"
cmds:
- >-
conan install --requires=video-to-rosbag/{{.VERSION}} -g VirtualRunEnv
--profile:host x64_linux_gcc_13 --profile:build x64_linux_gcc_13
-s build_type={{.BUILD_TYPE}}
- source conanrun.sh && ros2 run video_to_rosbag video_to_rosbag --ros-args -p input_video:=/tmp/test.mp4 -p output_bag:=/tmp/test_bag

47
Taskfile.yml Normal file
View File

@@ -0,0 +1,47 @@
# https://taskfile.dev
version: "3"
vars:
# Global Configuration
CONAN2_CONFIG_URL: https://package-cloud.dns.army/ros2/conan2-config
CONAN2_CONFIG_BRANCH: main
CONAN2_CONFIG_TASKFILE_PATH: Taskfile.yml
CONAN_BUILD_VERSION: latest
includes:
# Remote includes
conan2-config: "{{.CONAN2_CONFIG_URL}}/raw/branch/{{.CONAN2_CONFIG_BRANCH}}/{{.CONAN2_CONFIG_TASKFILE_PATH}}"
# Local modules
ros2: ./Taskfile.ros2.yml
tasks:
default:
desc: Show available tasks
silent: true
cmds:
- task: version
- task --list
version:
desc: Display component version information
silent: true
vars:
NAME_COMPONENT: conan2-ros2-video-to-rosbag
URL_COMPONENT: https://package-cloud.dns.army/ros2/conan2-ros2-video-to-rosbag
GIT_SHA:
sh: git rev-parse --short HEAD
GIT_REF:
sh: git rev-parse --abbrev-ref HEAD
cmds:
- printf "{{.B_MAGENTA}}Component{{.RESET}} - {{.CYAN}}{{.NAME_COMPONENT}}{{.RESET}}\n"
- printf "{{.B_MAGENTA}}URL{{.RESET}} - {{.CYAN}}{{.URL_COMPONENT}}{{.RESET}}\n"
- printf "{{.B_MAGENTA}}Branch{{.RESET}} - {{.CYAN}}{{.GIT_REF}} {{.B_WHITE}}{{.RESET}} - {{.B_MAGENTA}}Hash{{.RESET}} - {{.GREEN}}{{.GIT_SHA}}{{.RESET}}\n"
install:gcc:
desc: Install default gcc g++ build-toolchain for ubuntu
cmds:
- apt-get install build-essential --no-install-recommends --yes

152
conanfile.py Normal file
View File

@@ -0,0 +1,152 @@
import os
import subprocess
from conan import ConanFile
from conan.tools.cmake import CMakeToolchain, CMakeDeps, cmake_layout
from conan.tools.files import copy, rmdir
from conan.errors import ConanException
class VideoToRosbag(ConanFile):
name = "video-to-rosbag"
description = (
"ROS2 (Jazzy) package to convert MP4 videos to image rosbags (MCAP format)"
)
license = "MIT"
url = "https://github.com/yourusername/video_to_rosbag"
author = "your.email@example.com"
topics = ("ros2", "jazzy", "video", "rosbag", "mp4", "mcap", "conversion")
settings = "os", "compiler", "build_type", "arch"
def export_sources(self):
copy(self, "CMakeLists.txt", self.recipe_folder, self.export_sources_folder)
copy(self, "package.xml", self.recipe_folder, self.export_sources_folder)
copy(self, "src/*", self.recipe_folder, self.export_sources_folder)
copy(self, "launch/*", self.recipe_folder, self.export_sources_folder)
copy(self, "LICENSE", self.recipe_folder, self.export_sources_folder)
copy(self, "README.md", self.recipe_folder, self.export_sources_folder)
def layout(self):
cmake_layout(self)
def build_requirements(self):
self.tool_requires("make/4.4.1")
self.tool_requires("cmake/3.31.9")
self.tool_requires(
"ros2-jazzy-toolchain/latest", options={"variant": "ros_base"}
)
def requirements(self):
# Only ros_base - no cv_bridge, no perception
self.requires("ros2-jazzy-python/latest")
self.requires("ros2-jazzy-toolchain/latest", options={"variant": "ros_base"})
self.requires("openssl/1.1.1w")
self.requires("console_bridge/1.0.2")
self.requires("tinyxml2/7.1.0")
self.requires("yaml-cpp/0.8.0", options={"shared": True})
# OpenCV for video reading - with FFmpeg for MP4 support
self.requires(
"opencv/4.12.0",
options={
"with_ffmpeg": True,
# "shared": True,
},
)
def generate(self):
deps = CMakeDeps(self)
deps.generate()
tc = CMakeToolchain(self)
# Get Python from Conan
python_dep = self.dependencies["ros2-jazzy-python"]
python_exe = python_dep.conf_info.get(
"user.ros2:python_interpreter", check_type=str
)
if python_exe:
tc.variables["Python3_EXECUTABLE"] = python_exe
tc.variables["Python_EXECUTABLE"] = python_exe
# OpenCV configuration
opencv_dep = self.dependencies.get("opencv")
if opencv_dep:
tc.variables["OpenCV_ROOT"] = opencv_dep.package_folder.replace("\\", "/")
tc.generate()
def build(self):
build_dep = self.dependencies.get("ros2-jazzy-toolchain")
setup_script = None
if build_dep:
env_vars = build_dep.buildenv_info.vars(self)
raw_cmd = env_vars.get("CONAN_ROS2_SOURCE_CMD", "")
if "source " in raw_cmd:
setup_script = raw_cmd.split("source ")[1].strip().split(" ")[0]
elif raw_cmd.strip().endswith((".sh", ".bash")):
setup_script = raw_cmd.strip()
tc_file = os.path.join(self.generators_folder, "conan_toolchain.cmake")
abs_build_base = os.path.join(self.build_folder, "colcon_build")
abs_install_base = os.path.join(self.build_folder, "install")
colcon_cmd = (
f"colcon build --merge-install "
f"--packages-select video_to_rosbag "
f"--build-base '{abs_build_base}' "
f"--install-base '{abs_install_base}' "
f"--event-handlers console_direct+ "
f"--cmake-args -DCMAKE_TOOLCHAIN_FILE='{tc_file}' "
f"-DCMAKE_BUILD_TYPE={self.settings.build_type}"
)
if setup_script:
full_cmd = f'/bin/bash -c "source {setup_script} && {colcon_cmd}"'
else:
full_cmd = colcon_cmd
self.output.info(f"Building with: {full_cmd}")
self.run(full_cmd, shell=True, cwd=self.source_folder)
def package(self):
install_dir = os.path.join(self.build_folder, "install")
if not os.path.exists(install_dir):
raise ConanException(
f"Build failed to produce install directory: {install_dir}"
)
ros_package_dir = os.path.join(self.package_folder, "video_to_rosbag")
copy(self, "*", src=install_dir, dst=ros_package_dir)
copy(self, "LICENSE", src=self.source_folder, dst=self.package_folder)
copy(self, "README.md", src=self.source_folder, dst=self.package_folder)
rmdir(self, os.path.join(ros_package_dir, "share", "doc"))
rmdir(self, os.path.join(ros_package_dir, "colcon_build"))
def package_info(self):
ros_root = os.path.join(self.package_folder, "video_to_rosbag")
self.cpp_info.libs = []
self.cpp_info.libdirs = [os.path.join("video_to_rosbag", "lib")]
self.cpp_info.includedirs = [os.path.join("video_to_rosbag", "include")]
self.cpp_info.bindirs = [os.path.join("video_to_rosbag", "bin")]
self.runenv_info.prepend_path("AMENT_PREFIX_PATH", ros_root)
self.buildenv_info.prepend_path("AMENT_PREFIX_PATH", ros_root)
self.runenv_info.prepend_path("CMAKE_PREFIX_PATH", ros_root)
self.buildenv_info.prepend_path("CMAKE_PREFIX_PATH", ros_root)
self.runenv_info.prepend_path("PATH", os.path.join(ros_root, "bin"))
self.runenv_info.prepend_path("LD_LIBRARY_PATH", os.path.join(ros_root, "lib"))
lib_dir = os.path.join(ros_root, "lib")
if os.path.exists(lib_dir):
for item in os.listdir(lib_dir):
if item.startswith("python"):
site_packages = os.path.join(lib_dir, item, "site-packages")
if os.path.exists(site_packages):
self.runenv_info.prepend_path("PYTHONPATH", site_packages)
self.buildenv_info.prepend_path("PYTHONPATH", site_packages)
break

4
debug/conanfile.txt Normal file
View File

@@ -0,0 +1,4 @@
[requires]
video-to-rosbag/latest
[generators]
ROSEnv

12
debug/run_bash.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
source "$(dirname "$0")/conanrun.sh"
source "$CONAN_ROS2_SETUP_BASH"
exec ros2 run video_to_rosbag video_to_rosbag --ros-args \
-p input_video:=/workspaces/conan2-ros2-video-to-rosbag/videos/video1.mp4 \
-p output_bag:=/workspaces/conan2-ros2-video-to-rosbag/videos/video1 \
-p topic:=/camera/image_raw \
-p storage_id:=mcap \
-p frame_id:=camera_frame \
-p fps_override:=0.0

12
debug/run_zsh.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/zsh
source "$(dirname "$0")/conanrun.sh"
source "$CONAN_ROS2_SETUP_ZSH"
exec ros2 run video_to_rosbag video_to_rosbag --ros-args \
-p input_video:=/workspaces/conan2-ros2-video-to-rosbag/videos/video1.mp4 \
-p output_bag:=/workspaces/conan2-ros2-video-to-rosbag/videos/video1 \
-p topic:=/camera/image_raw \
-p storage_id:=mcap \
-p frame_id:=camera_frame \
-p fps_override:=0.0

28
package.xml Normal file
View File

@@ -0,0 +1,28 @@
<?xml version="1.0"?>
<package format="3">
<name>video_to_rosbag</name>
<version>0.1.0</version>
<description>Convert MP4 video to ROS 2 image rosbag (MCAP format)</description>
<maintainer email="you@example.com">Your Name</maintainer>
<license>MIT</license>
<!-- Build tools -->
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- Run-time / build dependencies (ROS base only) -->
<depend>rclcpp</depend>
<depend>sensor_msgs</depend>
<depend>rosbag2_cpp</depend>
<!-- OpenCV is brought in via Conan; listing here is optional but harmless -->
<exec_depend>opencv2</exec_depend>
<!-- Tests / linting -->
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>

144
src/video_to_rosbag.cc Normal file
View File

@@ -0,0 +1,144 @@
#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/image.hpp>
#include <rosbag2_cpp/writer.hpp>
#include <rosbag2_storage/storage_options.hpp>
#include <opencv2/opencv.hpp>
#include <opencv2/videoio.hpp>
#include <string>
#include <memory>
#include <chrono>
class VideoToRosbag : public rclcpp::Node
{
public:
VideoToRosbag()
: Node("video_to_rosbag")
{
// Parameters
this->declare_parameter<std::string>("input_video", "input.mp4");
this->declare_parameter<std::string>("output_bag", "output_bag");
this->declare_parameter<std::string>("topic", "/camera/image_raw");
this->declare_parameter<std::string>("storage_id", "mcap"); // mcap is default on Jazzy [web:32]
this->declare_parameter<std::string>("frame_id", "camera_frame");
this->declare_parameter<double>("fps_override", 0.0);
const std::string input_video = this->get_parameter("input_video").as_string();
const std::string output_bag = this->get_parameter("output_bag").as_string();
const std::string topic = this->get_parameter("topic").as_string();
const std::string storage_id = this->get_parameter("storage_id").as_string();
const std::string frame_id = this->get_parameter("frame_id").as_string();
double fps_override = this->get_parameter("fps_override").as_double();
RCLCPP_INFO(get_logger(), "Input video: %s", input_video.c_str());
RCLCPP_INFO(get_logger(), "Output bag: %s (storage_id=%s)", output_bag.c_str(), storage_id.c_str());
RCLCPP_INFO(get_logger(), "Topic: %s", topic.c_str());
// Open video
cv::VideoCapture cap(input_video);
if (!cap.isOpened())
{
RCLCPP_ERROR(get_logger(), "Failed to open video file: %s", input_video.c_str());
rclcpp::shutdown();
return;
}
// Video properties
double video_fps = cap.get(cv::CAP_PROP_FPS);
int total_frames = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_COUNT));
int width = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_WIDTH));
int height = static_cast<int>(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
double fps;
if (fps_override > 0.0)
{
fps = fps_override;
RCLCPP_WARN(get_logger(), "Using FPS override: %.3f (video reports %.3f)", fps, video_fps);
}
else if (video_fps > 0.0)
{
fps = video_fps;
RCLCPP_INFO(get_logger(), "Detected FPS from video: %.3f", fps);
}
else
{
fps = 30.0;
RCLCPP_WARN(get_logger(), "Video FPS unknown, falling back to %.3f", fps);
}
RCLCPP_INFO(get_logger(), "Video size: %dx%d, frames: %d, fps used: %.3f",
width, height, total_frames, fps);
// rosbag2 storage options (FIXED line: assign string, not struct)
rosbag2_storage::StorageOptions storage_options;
storage_options.uri = output_bag;
storage_options.storage_id = storage_id; // <- was 'storage_options' before (compile error)
rosbag2_cpp::ConverterOptions converter_options;
converter_options.input_serialization_format = rmw_get_serialization_format();
converter_options.output_serialization_format = rmw_get_serialization_format();
auto writer = std::make_unique<rosbag2_cpp::Writer>();
writer->open(storage_options, converter_options); // Jazzy API, mirrors tutorial [web:32]
// Time base for frame stamps
rclcpp::Time start_time = this->now();
cv::Mat frame;
int frame_count = 0;
RCLCPP_INFO(get_logger(), "Starting frame loop...");
while (cap.read(frame))
{
if (frame.empty())
{
break;
}
sensor_msgs::msg::Image msg;
// Timestamp based on frame index and FPS
rclcpp::Time stamp = start_time +
rclcpp::Duration::from_seconds(static_cast<double>(frame_count) / fps);
msg.header.stamp = stamp;
msg.header.frame_id = frame_id;
msg.height = static_cast<uint32_t>(frame.rows);
msg.width = static_cast<uint32_t>(frame.cols);
msg.encoding = "bgr8"; // OpenCV default color layout
msg.is_bigendian = false;
msg.step = static_cast<sensor_msgs::msg::Image::_step_type>(frame.step);
const size_t size_in_bytes = static_cast<size_t>(frame.step) * frame.rows;
msg.data.assign(frame.data, frame.data + size_in_bytes);
// Let rosbag2_cpp::Writer serialize and autocreate the topic [web:29][web:32]
writer->write(msg, topic, stamp);
++frame_count;
if (total_frames > 0 && frame_count % 100 == 0)
{
double progress = 100.0 * static_cast<double>(frame_count) /
static_cast<double>(total_frames);
RCLCPP_INFO(get_logger(), "Frames: %d / %d (%.1f%%)",
frame_count, total_frames, progress);
}
}
cap.release();
writer->close();
RCLCPP_INFO(get_logger(), "Done. Wrote %d frames to bag '%s'", frame_count, output_bag.c_str());
}
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<VideoToRosbag>();
// All work is done in the constructor; we don't need to spin.
rclcpp::shutdown();
return 0;
}