initial version
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"image": "ubuntu:24.04", // for cuda support: nvidia/cuda:13.1.0-devel-ubuntu24.04
|
"image": "ubuntu:24.04", // for cuda support: nvidia/cuda:13.1.0-devel-ubuntu24.04
|
||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"CONAN_USR": "t", // need to be filled
|
"CONAN_USR": "", // need to be filled
|
||||||
"CONAN_PSW": "" // need to be filled,
|
"CONAN_PSW": "" // need to be filled,
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
@@ -46,8 +46,7 @@
|
|||||||
"runArgs": [
|
"runArgs": [
|
||||||
"--network=host",
|
"--network=host",
|
||||||
"-e HOST_UID=$(id -u)",
|
"-e HOST_UID=$(id -u)",
|
||||||
"-e HOST_GID=$(id -g)",
|
"-e HOST_GID=$(id -g)"
|
||||||
"--gpus=all"
|
|
||||||
],
|
],
|
||||||
"postStartCommand": ".devcontainer/postStartCommand.sh"
|
"postStartCommand": ".devcontainer/postStartCommand.sh"
|
||||||
}
|
}
|
||||||
@@ -148,6 +148,7 @@ configure_persistence() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Add Local Bin to Path
|
# Add Local Bin to Path
|
||||||
|
local path_export='export PATH="$HOME/.local/bin:$PATH"'
|
||||||
append_if_missing "$HOME/.bashrc" "$path_export"
|
append_if_missing "$HOME/.bashrc" "$path_export"
|
||||||
append_if_missing "$zshrc" "$path_export"
|
append_if_missing "$zshrc" "$path_export"
|
||||||
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -208,3 +208,5 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
.json
|
||||||
|
.task/
|
||||||
27
CMakeLists.txt
Normal file
27
CMakeLists.txt
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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()
|
||||||
|
|
||||||
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
|
# [https://taskfile.dev](https://taskfile.dev)
|
||||||
# https://taskfile.dev
|
|
||||||
|
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
@@ -10,6 +9,7 @@ vars:
|
|||||||
SYSTEM_ARCH:
|
SYSTEM_ARCH:
|
||||||
sh: arch
|
sh: arch
|
||||||
|
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Core Logic (Internal)
|
# Core Logic (Internal)
|
||||||
@@ -50,22 +50,6 @@ tasks:
|
|||||||
--version {{.VERSION}}
|
--version {{.VERSION}}
|
||||||
-s build_type={{.BUILD_TYPE}}
|
-s build_type={{.BUILD_TYPE}}
|
||||||
|
|
||||||
_create_with_cuda:
|
|
||||||
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}}
|
|
||||||
-o with_cuda=True
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# GCC 13 (Modern)
|
# GCC 13 (Modern)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -73,7 +57,6 @@ tasks:
|
|||||||
native-x64:gcc13:
|
native-x64:gcc13:
|
||||||
desc: "Build: Native x64 (GCC 13)"
|
desc: "Build: Native x64 (GCC 13)"
|
||||||
cmds:
|
cmds:
|
||||||
# x86_64 is the standard output of 'arch' for Intel/AMD
|
|
||||||
- task: _validate_arch
|
- task: _validate_arch
|
||||||
vars: { EXPECTED_ARCH: "x86_64" }
|
vars: { EXPECTED_ARCH: "x86_64" }
|
||||||
- task: _create
|
- task: _create
|
||||||
@@ -81,21 +64,9 @@ tasks:
|
|||||||
PROFILE_BUILD: x64_linux_gcc_13
|
PROFILE_BUILD: x64_linux_gcc_13
|
||||||
PROFILE_HOST: x64_linux_gcc_13
|
PROFILE_HOST: x64_linux_gcc_13
|
||||||
|
|
||||||
native-x64:gcc13:cuda:
|
|
||||||
desc: "Build: Native x64 (GCC 13) with cuda support"
|
|
||||||
cmds:
|
|
||||||
# x86_64 is the standard output of 'arch' for Intel/AMD
|
|
||||||
- task: _validate_arch
|
|
||||||
vars: { EXPECTED_ARCH: "x86_64" }
|
|
||||||
- task: _create_with_cuda
|
|
||||||
vars:
|
|
||||||
PROFILE_BUILD: x64_linux_gcc_13
|
|
||||||
PROFILE_HOST: x64_linux_gcc_13
|
|
||||||
|
|
||||||
native-armv8:gcc13:
|
native-armv8:gcc13:
|
||||||
desc: "Build: Native ARMv8 (GCC 13)"
|
desc: "Build: Native ARMv8 (GCC 13)"
|
||||||
cmds:
|
cmds:
|
||||||
# aarch64 is the standard output of 'arch' for ARMv8
|
|
||||||
- task: _validate_arch
|
- task: _validate_arch
|
||||||
vars: { EXPECTED_ARCH: "aarch64" }
|
vars: { EXPECTED_ARCH: "aarch64" }
|
||||||
- task: _create
|
- task: _create
|
||||||
@@ -103,21 +74,9 @@ tasks:
|
|||||||
PROFILE_BUILD: armv8_linux_gcc_13
|
PROFILE_BUILD: armv8_linux_gcc_13
|
||||||
PROFILE_HOST: armv8_linux_gcc_13
|
PROFILE_HOST: armv8_linux_gcc_13
|
||||||
|
|
||||||
native-armv8:gcc13:cuda:
|
|
||||||
desc: "Build: Native ARMv8 (GCC 13) with cuda support"
|
|
||||||
cmds:
|
|
||||||
# aarch64 is the standard output of 'arch' for ARMv8
|
|
||||||
- task: _validate_arch
|
|
||||||
vars: { EXPECTED_ARCH: "aarch64" }
|
|
||||||
- task: _create_with_cuda
|
|
||||||
vars:
|
|
||||||
PROFILE_BUILD: armv8_linux_gcc_13
|
|
||||||
PROFILE_HOST: armv8_linux_gcc_13
|
|
||||||
|
|
||||||
cross-armv8:gcc13:
|
cross-armv8:gcc13:
|
||||||
desc: "Build: Cross-Compile x64 -> ARMv8 (GCC 13)"
|
desc: "Build: Cross-Compile x64 -> ARMv8 (GCC 13)"
|
||||||
cmds:
|
cmds:
|
||||||
# No validation needed for Cross-Compilation, as mismatch is intended
|
|
||||||
- echo "⚠️ Cross-compiling for ARMv8 on {{.SYSTEM_ARCH}} (No arch check enforced)"
|
- echo "⚠️ Cross-compiling for ARMv8 on {{.SYSTEM_ARCH}} (No arch check enforced)"
|
||||||
- task: _create
|
- task: _create
|
||||||
vars:
|
vars:
|
||||||
@@ -138,18 +97,6 @@ tasks:
|
|||||||
PROFILE_BUILD: x64_linux_gcc_9
|
PROFILE_BUILD: x64_linux_gcc_9
|
||||||
PROFILE_HOST: x64_linux_gcc_9
|
PROFILE_HOST: x64_linux_gcc_9
|
||||||
|
|
||||||
native-x64:gcc9:cuda:
|
|
||||||
desc: "Build: Native x64 (GCC 9) with cuda support"
|
|
||||||
cmds:
|
|
||||||
# x86_64 is the standard output of 'arch' for Intel/AMD
|
|
||||||
- task: _validate_arch
|
|
||||||
vars: { EXPECTED_ARCH: "x86_64" }
|
|
||||||
- task: _create_with_cuda
|
|
||||||
vars:
|
|
||||||
PROFILE_BUILD: x64_linux_gcc_9
|
|
||||||
PROFILE_HOST: x64_linux_gcc_9
|
|
||||||
|
|
||||||
|
|
||||||
native-armv8:gcc9:
|
native-armv8:gcc9:
|
||||||
desc: "Build: Native ARMv8 (GCC 9)"
|
desc: "Build: Native ARMv8 (GCC 9)"
|
||||||
cmds:
|
cmds:
|
||||||
@@ -160,17 +107,6 @@ tasks:
|
|||||||
PROFILE_BUILD: armv8_linux_gcc_9
|
PROFILE_BUILD: armv8_linux_gcc_9
|
||||||
PROFILE_HOST: armv8_linux_gcc_9
|
PROFILE_HOST: armv8_linux_gcc_9
|
||||||
|
|
||||||
native-armv8:gcc9:cuda:
|
|
||||||
desc: "Build: Native ARMv8 (GCC 9) with cuda support"
|
|
||||||
cmds:
|
|
||||||
# aarch64 is the standard output of 'arch' for ARMv8
|
|
||||||
- task: _validate_arch
|
|
||||||
vars: { EXPECTED_ARCH: "aarch64" }
|
|
||||||
- task: _create_with_cuda
|
|
||||||
vars:
|
|
||||||
PROFILE_BUILD: armv8_linux_gcc_9
|
|
||||||
PROFILE_HOST: armv8_linux_gcc_9
|
|
||||||
|
|
||||||
cross-armv8:gcc9:
|
cross-armv8:gcc9:
|
||||||
desc: "Build: Cross-Compile x64 -> ARMv8 (GCC 9)"
|
desc: "Build: Cross-Compile x64 -> ARMv8 (GCC 9)"
|
||||||
cmds:
|
cmds:
|
||||||
@@ -179,3 +115,22 @@ tasks:
|
|||||||
vars:
|
vars:
|
||||||
PROFILE_BUILD: x64_linux_gcc_9
|
PROFILE_BUILD: x64_linux_gcc_9
|
||||||
PROFILE_HOST: armv8_linux_gcc_9_croco
|
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
|
||||||
@@ -15,7 +15,7 @@ includes:
|
|||||||
|
|
||||||
# Local modules
|
# Local modules
|
||||||
|
|
||||||
hesai: ./Taskfile.hesai.yml
|
ros2: ./Taskfile.ros2.yml
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
default:
|
default:
|
||||||
@@ -29,8 +29,8 @@ tasks:
|
|||||||
desc: Display component version information
|
desc: Display component version information
|
||||||
silent: true
|
silent: true
|
||||||
vars:
|
vars:
|
||||||
NAME_COMPONENT: conan2-ros2-hesai-lidar
|
NAME_COMPONENT: conan2-ros2-video-to-rosbag
|
||||||
URL_COMPONENT: https://package-cloud.dns.army/ros2/conan2-ros2-hesai-lidar
|
URL_COMPONENT: https://package-cloud.dns.army/ros2/conan2-ros2-video-to-rosbag
|
||||||
GIT_SHA:
|
GIT_SHA:
|
||||||
sh: git rev-parse --short HEAD
|
sh: git rev-parse --short HEAD
|
||||||
GIT_REF:
|
GIT_REF:
|
||||||
|
|||||||
169
conanfile.py
169
conanfile.py
@@ -1,31 +1,28 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
|
||||||
from conan import ConanFile
|
from conan import ConanFile
|
||||||
from conan.tools.cmake import CMakeToolchain, CMakeDeps, cmake_layout
|
from conan.tools.cmake import CMakeToolchain, CMakeDeps, cmake_layout
|
||||||
from conan.tools.files import copy, rmdir, patch
|
from conan.tools.files import copy, rmdir
|
||||||
from conan.tools.scm import Git
|
from conan.errors import ConanException
|
||||||
from conan.errors import ConanInvalidConfiguration, ConanException
|
|
||||||
|
|
||||||
class HesaiLidarDriver(ConanFile):
|
|
||||||
name = "hesai-lidar-ros2-jazzy-driver"
|
class VideoToRosbag(ConanFile):
|
||||||
description = "ROS2 (Jazzy) Driver for Hesai LiDAR sensor"
|
name = "video-to-rosbag"
|
||||||
license = "BSD-3-Clause"
|
description = (
|
||||||
url = "https://github.com/HesaiTechnology/HesaiLidar_ROS_2.0"
|
"ROS2 (Jazzy) package to convert MP4 videos to image rosbags (MCAP format)"
|
||||||
author = "thommyho1988@gmail.com"
|
)
|
||||||
topics = ("ros2", "lidar", "hesai", "driver")
|
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"
|
settings = "os", "compiler", "build_type", "arch"
|
||||||
|
|
||||||
options = {
|
|
||||||
"with_cuda": [True, False],
|
|
||||||
}
|
|
||||||
default_options = {
|
|
||||||
"with_cuda": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
def export_sources(self):
|
def export_sources(self):
|
||||||
copy(self, "patches/**", self.recipe_folder, self.export_sources_folder)
|
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, "LICENSE", self.recipe_folder, self.export_sources_folder)
|
||||||
copy(self, "README.md", self.recipe_folder, self.export_sources_folder)
|
copy(self, "README.md", self.recipe_folder, self.export_sources_folder)
|
||||||
|
|
||||||
@@ -35,103 +32,52 @@ class HesaiLidarDriver(ConanFile):
|
|||||||
def build_requirements(self):
|
def build_requirements(self):
|
||||||
self.tool_requires("make/4.4.1")
|
self.tool_requires("make/4.4.1")
|
||||||
self.tool_requires("cmake/3.31.9")
|
self.tool_requires("cmake/3.31.9")
|
||||||
self.tool_requires("ros2-jazzy-toolchain/latest")
|
self.tool_requires(
|
||||||
|
"ros2-jazzy-toolchain/latest", options={"variant": "ros_base"}
|
||||||
|
)
|
||||||
|
|
||||||
def requirements(self):
|
def requirements(self):
|
||||||
|
# Only ros_base - no cv_bridge, no perception
|
||||||
self.requires("ros2-jazzy-python/latest")
|
self.requires("ros2-jazzy-python/latest")
|
||||||
self.requires("ros2-jazzy-toolchain/latest")
|
self.requires("ros2-jazzy-toolchain/latest", options={"variant": "ros_base"})
|
||||||
self.requires("openssl/1.1.1w")
|
self.requires("openssl/1.1.1w")
|
||||||
self.requires("yaml-cpp/0.8.0")
|
self.requires("console_bridge/1.0.2")
|
||||||
self.requires("tinyxml2/7.1.0")
|
self.requires("tinyxml2/7.1.0")
|
||||||
self.requires("boost/1.74.0", options={
|
self.requires("yaml-cpp/0.8.0", options={"shared": True})
|
||||||
"without_thread": False,
|
|
||||||
"without_system": False,
|
|
||||||
"without_chrono": False,
|
|
||||||
})
|
|
||||||
|
|
||||||
def validate(self):
|
# OpenCV for video reading - with FFmpeg for MP4 support
|
||||||
if self.options.with_cuda and self.settings.arch not in ["x86_64", "armv8"]:
|
self.requires(
|
||||||
raise ConanInvalidConfiguration("CUDA is only supported on x86_64 and armv8")
|
"opencv/4.12.0",
|
||||||
|
options={
|
||||||
def source(self):
|
"with_ffmpeg": True,
|
||||||
if self.version not in self.conan_data.get("sources", {}):
|
# "shared": True,
|
||||||
raise ConanInvalidConfiguration(f"Version '{self.version}' not found in conandata.yml")
|
},
|
||||||
|
)
|
||||||
data = self.conan_data["sources"][self.version]
|
|
||||||
tmp_src = "src_tmp"
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.output.info(f"Cloning {data['url']} into temporary directory...")
|
|
||||||
git = Git(self)
|
|
||||||
git.clone(url=data["url"], target=tmp_src, args=["--recursive"])
|
|
||||||
|
|
||||||
if "revision" in data:
|
|
||||||
self.output.info(f"Checking out revision {data['revision']}")
|
|
||||||
git_tmp = Git(self, folder=tmp_src)
|
|
||||||
git_tmp.checkout(commit=data["revision"])
|
|
||||||
git_tmp.run("submodule update --init --recursive")
|
|
||||||
|
|
||||||
self.output.info("Moving sources to recipe root...")
|
|
||||||
copy(self, "*", src=tmp_src, dst=".")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise ConanException(f"Source retrieval failed: {e}")
|
|
||||||
finally:
|
|
||||||
if os.path.exists(tmp_src):
|
|
||||||
rmdir(self, tmp_src)
|
|
||||||
|
|
||||||
patch_dir = os.path.join(self.source_folder, "patches", self.version)
|
|
||||||
if os.path.exists(patch_dir):
|
|
||||||
patches = sorted([p for p in os.listdir(patch_dir) if p.endswith(".patch")])
|
|
||||||
for p in patches:
|
|
||||||
self.output.info(f"Applying patch: {p}")
|
|
||||||
patch(self, patch_file=os.path.join(patch_dir, p))
|
|
||||||
|
|
||||||
def _get_numpy_include(self, python_exe):
|
|
||||||
try:
|
|
||||||
cmd = [python_exe, "-c", "import numpy; print(numpy.get_include())"]
|
|
||||||
return subprocess.check_output(cmd, text=True).strip()
|
|
||||||
except (subprocess.CalledProcessError, FileNotFoundError, OSError) as e:
|
|
||||||
self.output.warning(f"Failed to determine NumPy include path: {e}")
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def generate(self):
|
def generate(self):
|
||||||
deps = CMakeDeps(self)
|
deps = CMakeDeps(self)
|
||||||
deps.generate()
|
deps.generate()
|
||||||
|
|
||||||
tc = CMakeToolchain(self)
|
tc = CMakeToolchain(self)
|
||||||
|
|
||||||
|
# Get Python from Conan
|
||||||
python_dep = self.dependencies["ros2-jazzy-python"]
|
python_dep = self.dependencies["ros2-jazzy-python"]
|
||||||
python_exe = python_dep.conf_info.get("user.ros2:python_interpreter", check_type=str)
|
python_exe = python_dep.conf_info.get(
|
||||||
|
"user.ros2:python_interpreter", check_type=str
|
||||||
|
)
|
||||||
|
|
||||||
if python_exe:
|
if python_exe:
|
||||||
numpy_include = self._get_numpy_include(python_exe)
|
|
||||||
tc.variables["Python3_EXECUTABLE"] = python_exe
|
tc.variables["Python3_EXECUTABLE"] = python_exe
|
||||||
tc.variables["Python_EXECUTABLE"] = python_exe
|
tc.variables["Python_EXECUTABLE"] = python_exe
|
||||||
if numpy_include:
|
|
||||||
tc.variables["Python3_NumPy_INCLUDE_DIR"] = numpy_include
|
|
||||||
|
|
||||||
self._configure_special_flags(tc, python_dep)
|
# OpenCV configuration
|
||||||
|
opencv_dep = self.dependencies.get("opencv")
|
||||||
|
if opencv_dep:
|
||||||
|
tc.variables["OpenCV_ROOT"] = opencv_dep.package_folder.replace("\\", "/")
|
||||||
|
|
||||||
tc.generate()
|
tc.generate()
|
||||||
|
|
||||||
def _configure_special_flags(self, tc, python_dep):
|
|
||||||
cross_blob = False
|
|
||||||
if "cross_blob" in python_dep.options:
|
|
||||||
cross_blob = bool(python_dep.options.cross_blob)
|
|
||||||
|
|
||||||
if self.options.with_cuda:
|
|
||||||
if cross_blob:
|
|
||||||
raise ConanInvalidConfiguration("CUDA not available as conan package due to NVIDIA license.")
|
|
||||||
self.output.info("Enabling CUDA Support")
|
|
||||||
tc.variables["FIND_CUDA"] = "ON"
|
|
||||||
|
|
||||||
if cross_blob:
|
|
||||||
self.output.info("Enabling Cross-Blob workarounds (No LTO)")
|
|
||||||
tc.variables["CMAKE_INTERPROCEDURAL_OPTIMIZATION"] = "OFF"
|
|
||||||
tc.cache_variables["CMAKE_C_FLAGS"] = "-fno-lto"
|
|
||||||
tc.cache_variables["CMAKE_CXX_FLAGS"] = "-fno-lto"
|
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
# 1. Resolve Setup Script
|
|
||||||
build_dep = self.dependencies.get("ros2-jazzy-toolchain")
|
build_dep = self.dependencies.get("ros2-jazzy-toolchain")
|
||||||
setup_script = None
|
setup_script = None
|
||||||
|
|
||||||
@@ -143,14 +89,13 @@ class HesaiLidarDriver(ConanFile):
|
|||||||
elif raw_cmd.strip().endswith((".sh", ".bash")):
|
elif raw_cmd.strip().endswith((".sh", ".bash")):
|
||||||
setup_script = raw_cmd.strip()
|
setup_script = raw_cmd.strip()
|
||||||
|
|
||||||
# 2. Paths
|
|
||||||
tc_file = os.path.join(self.generators_folder, "conan_toolchain.cmake")
|
tc_file = os.path.join(self.generators_folder, "conan_toolchain.cmake")
|
||||||
abs_build_base = os.path.join(self.build_folder, "colcon_build")
|
abs_build_base = os.path.join(self.build_folder, "colcon_build")
|
||||||
abs_install_base = os.path.join(self.build_folder, "install")
|
abs_install_base = os.path.join(self.build_folder, "install")
|
||||||
|
|
||||||
# 3. Colcon Command
|
|
||||||
colcon_cmd = (
|
colcon_cmd = (
|
||||||
f"colcon build --merge-install "
|
f"colcon build --merge-install "
|
||||||
|
f"--packages-select video_to_rosbag "
|
||||||
f"--build-base '{abs_build_base}' "
|
f"--build-base '{abs_build_base}' "
|
||||||
f"--install-base '{abs_install_base}' "
|
f"--install-base '{abs_install_base}' "
|
||||||
f"--event-handlers console_direct+ "
|
f"--event-handlers console_direct+ "
|
||||||
@@ -158,7 +103,6 @@ class HesaiLidarDriver(ConanFile):
|
|||||||
f"-DCMAKE_BUILD_TYPE={self.settings.build_type}"
|
f"-DCMAKE_BUILD_TYPE={self.settings.build_type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 4. Execute (Bash wrapper for 'source')
|
|
||||||
if setup_script:
|
if setup_script:
|
||||||
full_cmd = f'/bin/bash -c "source {setup_script} && {colcon_cmd}"'
|
full_cmd = f'/bin/bash -c "source {setup_script} && {colcon_cmd}"'
|
||||||
else:
|
else:
|
||||||
@@ -170,44 +114,33 @@ class HesaiLidarDriver(ConanFile):
|
|||||||
def package(self):
|
def package(self):
|
||||||
install_dir = os.path.join(self.build_folder, "install")
|
install_dir = os.path.join(self.build_folder, "install")
|
||||||
if not os.path.exists(install_dir):
|
if not os.path.exists(install_dir):
|
||||||
raise ConanException(f"Build failed to produce install directory: {install_dir}")
|
raise ConanException(
|
||||||
|
f"Build failed to produce install directory: {install_dir}"
|
||||||
|
)
|
||||||
|
|
||||||
# 1. Define Subdirectory
|
ros_package_dir = os.path.join(self.package_folder, "video_to_rosbag")
|
||||||
ros_package_dir = os.path.join(self.package_folder, "hesai_ros_driver")
|
|
||||||
|
|
||||||
# 2. Copy ROS Artifacts to Subdirectory
|
|
||||||
copy(self, "*", src=install_dir, dst=ros_package_dir)
|
copy(self, "*", src=install_dir, dst=ros_package_dir)
|
||||||
|
|
||||||
# 3. Copy Metadata to Root
|
|
||||||
copy(self, "LICENSE", src=self.source_folder, dst=self.package_folder)
|
copy(self, "LICENSE", src=self.source_folder, dst=self.package_folder)
|
||||||
copy(self, "README.md", src=self.source_folder, dst=self.package_folder)
|
copy(self, "README.md", src=self.source_folder, dst=self.package_folder)
|
||||||
|
|
||||||
# 4. Cleanup
|
|
||||||
rmdir(self, os.path.join(ros_package_dir, "share", "doc"))
|
rmdir(self, os.path.join(ros_package_dir, "share", "doc"))
|
||||||
rmdir(self, os.path.join(ros_package_dir, "colcon_build"))
|
rmdir(self, os.path.join(ros_package_dir, "colcon_build"))
|
||||||
|
|
||||||
def package_info(self):
|
def package_info(self):
|
||||||
# 1. Define the ROS Root inside the package
|
ros_root = os.path.join(self.package_folder, "video_to_rosbag")
|
||||||
ros_root = os.path.join(self.package_folder, "hesai_ros_driver")
|
|
||||||
|
|
||||||
# 2. Cpp Info (Relative to package_folder)
|
self.cpp_info.libs = []
|
||||||
self.cpp_info.libs = ["hesai_ros_driver"]
|
self.cpp_info.libdirs = [os.path.join("video_to_rosbag", "lib")]
|
||||||
self.cpp_info.libdirs = [os.path.join("hesai_ros_driver", "lib")]
|
self.cpp_info.includedirs = [os.path.join("video_to_rosbag", "include")]
|
||||||
self.cpp_info.includedirs = [os.path.join("hesai_ros_driver", "include")]
|
self.cpp_info.bindirs = [os.path.join("video_to_rosbag", "bin")]
|
||||||
self.cpp_info.bindirs = [os.path.join("hesai_ros_driver", "bin")]
|
|
||||||
|
|
||||||
# 3. Environment Variables (Absolute Paths)
|
|
||||||
# Critical: These ensure ROS 2 finds the package in the subdirectory
|
|
||||||
self.runenv_info.prepend_path("AMENT_PREFIX_PATH", ros_root)
|
self.runenv_info.prepend_path("AMENT_PREFIX_PATH", ros_root)
|
||||||
self.buildenv_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.runenv_info.prepend_path("CMAKE_PREFIX_PATH", ros_root)
|
||||||
self.buildenv_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("PATH", os.path.join(ros_root, "bin"))
|
||||||
self.runenv_info.prepend_path("LD_LIBRARY_PATH", os.path.join(ros_root, "lib"))
|
self.runenv_info.prepend_path("LD_LIBRARY_PATH", os.path.join(ros_root, "lib"))
|
||||||
|
|
||||||
# 4. Python Path Calculation
|
|
||||||
lib_dir = os.path.join(ros_root, "lib")
|
lib_dir = os.path.join(ros_root, "lib")
|
||||||
if os.path.exists(lib_dir):
|
if os.path.exists(lib_dir):
|
||||||
for item in os.listdir(lib_dir):
|
for item in os.listdir(lib_dir):
|
||||||
|
|||||||
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