Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34aa2655e7 | |||
| 4f7227c0e9 | |||
| fed1844282 |
52
.devcontainer/devcontainer.json
Normal file
52
.devcontainer/devcontainer.json
Normal 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
187
.devcontainer/postStartCommand.sh
Executable 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
2
.gitignore
vendored
@@ -208,3 +208,5 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
.json
|
||||
.task/
|
||||
3
.taskrc.yml
Normal file
3
.taskrc.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Enable experimental features
|
||||
experiments:
|
||||
REMOTE_TASKFILES: 1
|
||||
30
CMakeLists.txt
Normal file
30
CMakeLists.txt
Normal 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
9
CMakeUserPresets.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": 4,
|
||||
"vendor": {
|
||||
"conan": {}
|
||||
},
|
||||
"include": [
|
||||
"build/Release/generators/CMakePresets.json"
|
||||
]
|
||||
}
|
||||
14
LICENSE
Normal file
14
LICENSE
Normal 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
136
Taskfile.ros2.yml
Normal 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
47
Taskfile.yml
Normal 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
152
conanfile.py
Normal 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
4
debug/conanfile.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
[requires]
|
||||
video-to-rosbag/latest
|
||||
[generators]
|
||||
ROSEnv
|
||||
12
debug/run_bash.sh
Executable file
12
debug/run_bash.sh
Executable 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
12
debug/run_zsh.sh
Executable 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
28
package.xml
Normal 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
144
src/video_to_rosbag.cc
Normal 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 auto‑create 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;
|
||||
}
|
||||
Reference in New Issue
Block a user