Skip to content

feat: add video decoder support - #32

Merged
manato merged 9 commits into
mainfrom
feat/video_decoder
Mar 2, 2026
Merged

feat: add video decoder support#32
manato merged 9 commits into
mainfrom
feat/video_decoder

Conversation

@manato

@manato manato commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

PR Type

  • New Feature

Related Links

Description

This pull request introduces the video decoder support based on the FFMPEG libraries.
At this moment, the implementation assumes H.264, H.265, and AV1 encoding, which were introduced by the former [pull request]((#29).

The key modifications are:

  • add a new package named accelerated_image_processor_decompression [1]
  • add a new ROS node named decompress_node [1]

The following diagram depicts an overall structure:

classDiagram
    direction TD
    %% ---------- Base Processor ----------
    class BaseProcessor {
    }
    %% ---------- Video Decompressor ----------
    class VideoDecompressor
    class FfmpegVideoDecompressor {
        +process(image: Image): optional<Image>
        +process_impl(image: Image): Image
        -process_packet(image: Image): vector<Image>
    }
    BaseProcessor <|-- VideoDecompressor
    VideoDecompressor <|-- FfmpegVideoDecompressor
    %% ---------- ROS Node ----------
    class `rclcpp::Node`
    class DecompressNode {
        +on_ffmpeg_packet(msg: ffmpeg_packet::ConstSharedPtr): void
        +publish_decompressed(image: Image): void
    }
    `rclcpp::Node` <|-- DecompressNode
    %% ---------- Association ----------
    DecompressNode  *--  FfmpegVideoDecompressor : instanticate as `decompressor`

    note for FfmpegVideoDecompressor "process_impl() is overridden as dummy due to the return type incompatibility. <br>The actual packet handling is performed in proces_packet()"
Loading

Review Procedure

  • Build package

    colcon build --symlink-install --cmake-args -DCMAKE_BUILD_TYPE=Release \
      --packages-up-to accelerated_image_processor_ros
    
    source ./install/setup.bash
  • Launch the node

    # Since the node publishes the decompressed result in `/image_raw` by default, 
    # remap the decompressed result under environments where other nodes publish `/image_raw` 
    ros2 launch accelerated_image_processor_ros decompress.launch.xml output/image_raw:=/image_decompressed

I also added test codes for the newly introduced functions. All tests passed on my environment:

colcon test --event-handlers console_cohesion+ --packages-select accelerated_image_processor_decompression
$ colcon test --event-handlers console_cohesion+ --packages-select accelerated_image_processor_decompression
Starting >>> accelerated_image_processor_decompression
--- output: accelerated_image_processor_decompression
UpdateCTestConfiguration  from :/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/CTestConfiguration.ini
Parse Config file:/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/CTestConfiguration.ini
   Site: dpc2104002
   Build name: (empty)
 Add coverage exclude regular expressions.
Create new tag: 20260224-0405 - Experimental
UpdateCTestConfiguration  from :/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/CTestConfiguration.ini
Parse Config file:/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/CTestConfiguration.ini
Test project /hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
test 1
    Start 1: test_builder

1: Test command: /usr/bin/python3 "-u" "/opt/ros/humble/share/ament_cmake_test/cmake/run_test.py" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_builder.gtest.xml" "--package-name" "accelerated_image_processor_decompression" "--output-file" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/ament_cmake_gtest/test_builder.txt" "--command" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_builder" "--gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_builder.gtest.xml"
1: Test timeout computed to be: 60
1: -- run_test.py: invoking following command in '/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression':
1:  - /hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_builder --gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_builder.gtest.xml
1: [==========] Running 6 tests from 1 test suite.
1: [----------] Global test environment set-up.
1: [----------] 6 tests from TestDecompressorBuilder
1: [ RUN      ] TestDecompressorBuilder.CreateFFMPEGDecompressor1
1: [       OK ] TestDecompressorBuilder.CreateFFMPEGDecompressor1 (117 ms)
1: [ RUN      ] TestDecompressorBuilder.CreateFFMPEGDecompressor2
1: [       OK ] TestDecompressorBuilder.CreateFFMPEGDecompressor2 (0 ms)
1: [ RUN      ] TestDecompressorBuilder.CreateFFMPEGDecompressor3
1: [       OK ] TestDecompressorBuilder.CreateFFMPEGDecompressor3 (1 ms)
1: [ RUN      ] TestDecompressorBuilder.CreateFFMPEGDecompressor4
1: [       OK ] TestDecompressorBuilder.CreateFFMPEGDecompressor4 (1 ms)
1: [ RUN      ] TestDecompressorBuilder.CreateFFMPEGDecompressor5
1: [       OK ] TestDecompressorBuilder.CreateFFMPEGDecompressor5 (1 ms)
1: [ RUN      ] TestDecompressorBuilder.CreateFFMPEGDecompressor6
1: [       OK ] TestDecompressorBuilder.CreateFFMPEGDecompressor6 (1 ms)
1: [----------] 6 tests from TestDecompressorBuilder (121 ms total)
1:
1: [----------] Global test environment tear-down
1: [==========] 6 tests from 1 test suite ran. (121 ms total)
1: [  PASSED  ] 6 tests.
1: -- run_test.py: return code 0
1: -- run_test.py: inject classname prefix into gtest result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_builder.gtest.xml'
1: -- run_test.py: verify result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_builder.gtest.xml'
1/2 Test #1: test_builder .....................   Passed    0.22 sec
test 2
    Start 2: test_ffmpeg_video_decompressor

2: Test command: /usr/bin/python3 "-u" "/opt/ros/humble/share/ament_cmake_test/cmake/run_test.py" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor.gtest.xml" "--package-name" "accelerated_image_processor_decompression" "--output-file" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/ament_cmake_gtest/test_ffmpeg_video_decompressor.txt" "--command" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor" "--gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor.gtest.xml"
2: Test timeout computed to be: 60
2: -- run_test.py: invoking following command in '/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression':
2:  - /hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor --gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor.gtest.xml
2: [==========] Running 3 tests from 1 test suite.
2: [----------] Global test environment set-up.
2: [----------] 3 tests from TestFFMPEGVideoDecompressor
2: [ RUN      ] TestFFMPEGVideoDecompressor.DecompressH264
2: [Parsed_drawtext_0 @ 0x5730907a4280] Using "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
2: [libx264 @ 0x5730907f1900] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
2: [libx264 @ 0x5730907f1900] profile Constrained Baseline, level 5.1, 4:2:0, 8-bit
2: Succeed to init_decoder
2: [libx264 @ 0x5730907f1900] frame I:20    Avg QP:51.00  size: 18230
2: [libx264 @ 0x5730907f1900] mb I  I16..4: 100.0%  0.0%  0.0%
2: [libx264 @ 0x5730907f1900] final ratefactor: 89.93
2: [libx264 @ 0x5730907f1900] coded y,uvDC,uvAC intra: 0.1% 2.0% 0.1%
2: [libx264 @ 0x5730907f1900] i16 v,h,dc,p: 82%  1% 17%  0%
2: [libx264 @ 0x5730907f1900] i8c dc,h,v,p: 99%  0%  1%  0%
2: [libx264 @ 0x5730907f1900] kb/s:4375.12
2: [       OK ] TestFFMPEGVideoDecompressor.DecompressH264 (852 ms)
2: [ RUN      ] TestFFMPEGVideoDecompressor.DecompressH265
2: [Parsed_drawtext_0 @ 0x5730907dd380] Using "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
2: x265 [info]: HEVC encoder version 3.5+1-f0c1022b6
2: x265 [info]: build info [Linux][GCC 11.2.0][64 bit] 8bit+10bit+12bit
2: x265 [info]: using cpu capabilities: MMX2 SSE2Fast LZCNT SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
2: x265 [info]: Main profile, Level-5 (Main tier)
2: x265 [info]: Thread pool created using 20 threads
2: x265 [info]: Slices                              : 1
2: x265 [info]: frame threads / pool features       : 1 / wpp(59 rows)
2: x265 [info]: Coding QT: max CU size, min CU size : 32 / 16
2: x265 [info]: Residual QT: max TU size, max depth : 32 / 1 inter / 1 intra
2: x265 [info]: ME / range / subpel / merge         : dia / 57 / 0 / 2
2: x265 [info]: Lookahead / bframes / badapt        : 0 / 0 / 0
2: x265 [info]: b-pyramid / weightp / weightb       : 0 / 0 / 0
2: x265 [info]: References / ref-limit  cu / depth  : 1 / off / off
2: x265 [info]: Rate Control / qCompress            : ABR-400 kbps / 0.60
2: x265 [info]: tools: rd=2 psy-rd=2.00 early-skip rskip mode=1 tmvp fast-intra
2: x265 [info]: tools: strong-intra-smoothing lslices=8 deblock
2: x265 [info]: frame I:     20, Avg QP:59.71  kb/s: 994.64
2: x265 [info]: consecutive B-frames: 100.0%
2:
2: encoded 20 frames in 1.06s (18.80 fps), 994.64 kb/s, Avg QP:59.71
2: [       OK ] TestFFMPEGVideoDecompressor.DecompressH265 (1097 ms)
2: [ RUN      ] TestFFMPEGVideoDecompressor.DecompressAV1
2: [Parsed_drawtext_0 @ 0x573090993a40] Using "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
2: [libaom-av1 @ 0x5730908124c0] v3.3.0
2: [       OK ] TestFFMPEGVideoDecompressor.DecompressAV1 (5615 ms)
2: [----------] 3 tests from TestFFMPEGVideoDecompressor (7564 ms total)
2:
2: [----------] Global test environment tear-down
2: [==========] 3 tests from 1 test suite ran. (7564 ms total)
2: [  PASSED  ] 3 tests.
2: -- run_test.py: return code 0
2: -- run_test.py: inject classname prefix into gtest result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor.gtest.xml'
2: -- run_test.py: verify result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_decompression/test_results/accelerated_image_processor_decompression/test_ffmpeg_video_decompressor.gtest.xml'
2/2 Test #2: test_ffmpeg_video_decompressor ...   Passed    7.71 sec

100% tests passed, 0 tests failed out of 2

Label Time Summary:
gtest    =   7.93 sec*proc (2 tests)

Total Test time (real) =   7.93 sec
---
Finished <<< accelerated_image_processor_decompression [7.95s]

Summary: 1 package finished [8.08s]
colcon test --event-handlers console_cohesion+ --packages-select accelerated_image_processor_ros
$ colcon test --event-handlers console_cohesion+ --packages-select accelerated_image_processor_ros
Starting >>> accelerated_image_processor_ros
--- output: accelerated_image_processor_ros
UpdateCTestConfiguration  from :/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/CTestConfiguration.ini
Parse Config file:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/CTestConfiguration.ini
   Site: dpc2104002
   Build name: (empty)
 Add coverage exclude regular expressions.
Create new tag: 20260224-0504 - Experimental
UpdateCTestConfiguration  from :/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/CTestConfiguration.ini
Parse Config file:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/CTestConfiguration.ini
Test project /hdd/dev/v4l2_camera/build/accelerated_image_processor_ros
Constructing a list of tests
Done constructing a list of tests
Updating test list for fixtures
Added 0 tests to meet fixture requirements
Checking test dependency graph...
Checking test dependency graph end
test 1
    Start 1: conversion.cpp_accelerated_image_processor_ros

1: Test command: /usr/bin/python3 "-u" "/opt/ros/humble/share/ament_cmake_test/cmake/run_test.py" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros.gtest.xml" "--package-name" "accelerated_image_processor_ros" "--output-file" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/ament_cmake_gtest/conversion.cpp_accelerated_image_processor_ros.txt" "--command" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros" "--gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros.gtest.xml"
1: Test timeout computed to be: 60
1: -- run_test.py: invoking following command in '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros':
1:  - /hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros --gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros.gtest.xml
1: [==========] Running 37 tests from 19 test suites.
1: [----------] Global test environment set-up.
1: [----------] 3 tests from TestConversionFromRosTime
1: [ RUN      ] TestConversionFromRosTime.Zero
1: [       OK ] TestConversionFromRosTime.Zero (0 ms)
1: [ RUN      ] TestConversionFromRosTime.NonZero
1: [       OK ] TestConversionFromRosTime.NonZero (0 ms)
1: [ RUN      ] TestConversionFromRosTime.BidirectionalConversionIsConsistent
1: [       OK ] TestConversionFromRosTime.BidirectionalConversionIsConsistent (0 ms)
1: [----------] 3 tests from TestConversionFromRosTime (0 ms total)
1:
1: [----------] 3 tests from TestConversionFromRosEncoding
1: [ RUN      ] TestConversionFromRosEncoding.RGB8toRGB
1: [       OK ] TestConversionFromRosEncoding.RGB8toRGB (0 ms)
1: [ RUN      ] TestConversionFromRosEncoding.BGR8toBGR
1: [       OK ] TestConversionFromRosEncoding.BGR8toBGR (0 ms)
1: [ RUN      ] TestConversionFromRosEncoding.UnsupportedThrowsRuntimeError
1: [       OK ] TestConversionFromRosEncoding.UnsupportedThrowsRuntimeError (0 ms)
1: [----------] 3 tests from TestConversionFromRosEncoding (0 ms total)
1:
1: [----------] 2 tests from TestConversionFromRosImage
1: [ RUN      ] TestConversionFromRosImage.CopyFields
1: [       OK ] TestConversionFromRosImage.CopyFields (0 ms)
1: [ RUN      ] TestConversionFromRosImage.UnsupportedEncodingThrowsRuntimeError
1: [       OK ] TestConversionFromRosImage.UnsupportedEncodingThrowsRuntimeError (0 ms)
1: [----------] 2 tests from TestConversionFromRosImage (0 ms total)
1:
1: [----------] 1 test from TestConversionFromRosCameraInfo
1: [ RUN      ] TestConversionFromRosCameraInfo.CopyFields
1: [       OK ] TestConversionFromRosCameraInfo.CopyFields (0 ms)
1: [----------] 1 test from TestConversionFromRosCameraInfo (0 ms total)
1:
1: [----------] 4 tests from TestConversionFromRosDistortionModel
1: [ RUN      ] TestConversionFromRosDistortionModel.PlumbBob
1: [       OK ] TestConversionFromRosDistortionModel.PlumbBob (0 ms)
1: [ RUN      ] TestConversionFromRosDistortionModel.Equidistant
1: [       OK ] TestConversionFromRosDistortionModel.Equidistant (0 ms)
1: [ RUN      ] TestConversionFromRosDistortionModel.RationalPolynomial
1: [       OK ] TestConversionFromRosDistortionModel.RationalPolynomial (0 ms)
1: [ RUN      ] TestConversionFromRosDistortionModel.UnsupportedThrowsRuntimeError
1: [       OK ] TestConversionFromRosDistortionModel.UnsupportedThrowsRuntimeError (0 ms)
1: [----------] 4 tests from TestConversionFromRosDistortionModel (0 ms total)
1:
1: [----------] 1 test from TestConversionFromRosRoi
1: [ RUN      ] TestConversionFromRosRoi.CopyFields
1: [       OK ] TestConversionFromRosRoi.CopyFields (0 ms)
1: [----------] 1 test from TestConversionFromRosRoi (0 ms total)
1:
1: [----------] 3 tests from TestConversionToRosTime
1: [ RUN      ] TestConversionToRosTime.Zero
1: [       OK ] TestConversionToRosTime.Zero (0 ms)
1: [ RUN      ] TestConversionToRosTime.NonZero
1: [       OK ] TestConversionToRosTime.NonZero (0 ms)
1: [ RUN      ] TestConversionToRosTime.BidirectionalConversionIsConsistent
1: [       OK ] TestConversionToRosTime.BidirectionalConversionIsConsistent (0 ms)
1: [----------] 3 tests from TestConversionToRosTime (0 ms total)
1:
1: [----------] 1 test from TestConversionToRosRaw
1: [ RUN      ] TestConversionToRosRaw.CopyFields
1: [       OK ] TestConversionToRosRaw.CopyFields (0 ms)
1: [----------] 1 test from TestConversionToRosRaw (0 ms total)
1:
1: [----------] 2 tests from TestConversionToRosEncoding
1: [ RUN      ] TestConversionToRosEncoding.RGBtoRGB8
1: [       OK ] TestConversionToRosEncoding.RGBtoRGB8 (0 ms)
1: [ RUN      ] TestConversionToRosEncoding.BGRtoBGR8
1: [       OK ] TestConversionToRosEncoding.BGRtoBGR8 (0 ms)
1: [----------] 2 tests from TestConversionToRosEncoding (0 ms total)
1:
1: [----------] 1 test from TestConversionToRosCompressed
1: [ RUN      ] TestConversionToRosCompressed.CopyFields
1: [       OK ] TestConversionToRosCompressed.CopyFields (0 ms)
1: [----------] 1 test from TestConversionToRosCompressed (0 ms total)
1:
1: [----------] 3 tests from TestConversionToRosFormat
1: [ RUN      ] TestConversionToRosFormat.RawThrowsInvalidArgument
1: [       OK ] TestConversionToRosFormat.RawThrowsInvalidArgument (0 ms)
1: [ RUN      ] TestConversionToRosFormat.Jpeg
1: [       OK ] TestConversionToRosFormat.Jpeg (0 ms)
1: [ RUN      ] TestConversionToRosFormat.Png
1: [       OK ] TestConversionToRosFormat.Png (0 ms)
1: [----------] 3 tests from TestConversionToRosFormat (0 ms total)
1:
1: [----------] 1 test from TestConversionToRosFFmpeg
1: [ RUN      ] TestConversionToRosFFmpeg.CopyFieldsAndVideo
1: [       OK ] TestConversionToRosFFmpeg.CopyFieldsAndVideo (0 ms)
1: [----------] 1 test from TestConversionToRosFFmpeg (0 ms total)
1:
1: [----------] 1 test from TestConversionToRosFFmpegEncoding
1: [ RUN      ] TestConversionToRosFFmpegEncoding.ValidEncodings
1: [       OK ] TestConversionToRosFFmpegEncoding.ValidEncodings (0 ms)
1: [----------] 1 test from TestConversionToRosFFmpegEncoding (0 ms total)
1:
1: [----------] 1 test from TestConversionToRosInfo
1: [ RUN      ] TestConversionToRosInfo.CopyFields
1: [       OK ] TestConversionToRosInfo.CopyFields (0 ms)
1: [----------] 1 test from TestConversionToRosInfo (0 ms total)
1:
1: [----------] 3 tests from TestConversionToRosDistortionModel
1: [ RUN      ] TestConversionToRosDistortionModel.PlumbBob
1: [       OK ] TestConversionToRosDistortionModel.PlumbBob (0 ms)
1: [ RUN      ] TestConversionToRosDistortionModel.Equidistant
1: [       OK ] TestConversionToRosDistortionModel.Equidistant (0 ms)
1: [ RUN      ] TestConversionToRosDistortionModel.RationalPolynomial
1: [       OK ] TestConversionToRosDistortionModel.RationalPolynomial (0 ms)
1: [----------] 3 tests from TestConversionToRosDistortionModel (0 ms total)
1:
1: [----------] 1 test from TestConversionToRosRoi
1: [ RUN      ] TestConversionToRosRoi.CopyFields
1: [       OK ] TestConversionToRosRoi.CopyFields (0 ms)
1: [----------] 1 test from TestConversionToRosRoi (0 ms total)
1:
1: [----------] 2 tests from TestSplitStringByCommaAndSemicolon
1: [ RUN      ] TestSplitStringByCommaAndSemicolon.Simple
1: [       OK ] TestSplitStringByCommaAndSemicolon.Simple (0 ms)
1: [ RUN      ] TestSplitStringByCommaAndSemicolon.NoDelimiter
1: [       OK ] TestSplitStringByCommaAndSemicolon.NoDelimiter (0 ms)
1: [----------] 2 tests from TestSplitStringByCommaAndSemicolon (0 ms total)
1:
1: [----------] 3 tests from TestFromRosFfmpegEncoding
1: [ RUN      ] TestFromRosFfmpegEncoding.Simple
1: [       OK ] TestFromRosFfmpegEncoding.Simple (0 ms)
1: [ RUN      ] TestFromRosFfmpegEncoding.CommaAndSemicolon
1: [       OK ] TestFromRosFfmpegEncoding.CommaAndSemicolon (0 ms)
1: [ RUN      ] TestFromRosFfmpegEncoding.UnsupportedThrows
1: [       OK ] TestFromRosFfmpegEncoding.UnsupportedThrows (0 ms)
1: [----------] 3 tests from TestFromRosFfmpegEncoding (0 ms total)
1:
1: [----------] 1 test from TestFromRosFfmpeg
1: [ RUN      ] TestFromRosFfmpeg.ConvertMessage
1: [       OK ] TestFromRosFfmpeg.ConvertMessage (0 ms)
1: [----------] 1 test from TestFromRosFfmpeg (0 ms total)
1:
1: [----------] Global test environment tear-down
1: [==========] 37 tests from 19 test suites ran. (0 ms total)
1: [  PASSED  ] 37 tests.
1: -- run_test.py: return code 0
1: -- run_test.py: inject classname prefix into gtest result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros.gtest.xml'
1: -- run_test.py: verify result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/conversion.cpp_accelerated_image_processor_ros.gtest.xml'
1/3 Test #1: conversion.cpp_accelerated_image_processor_ros ...   Passed    0.05 sec
test 2
    Start 2: parameter.cpp_accelerated_image_processor_ros

2: Test command: /usr/bin/python3 "-u" "/opt/ros/humble/share/ament_cmake_test/cmake/run_test.py" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros.gtest.xml" "--package-name" "accelerated_image_processor_ros" "--output-file" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/ament_cmake_gtest/parameter.cpp_accelerated_image_processor_ros.txt" "--command" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros" "--gtest_ou
tput=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros.gtest.xml"
2: Test timeout computed to be: 60
2: -- run_test.py: invoking following command in '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros':
2:  - /hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros --gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros.gtest.xml
2: [==========] Running 4 tests from 2 test suites.
2: [----------] Global test environment set-up.
2: [----------] 2 tests from TestParameterFetchParametersWithoutPrefix
2: [ RUN      ] TestParameterFetchParametersWithoutPrefix.NoOverridesUsesDefaults
2: [       OK ] TestParameterFetchParametersWithoutPrefix.NoOverridesUsesDefaults (206 ms)
2: [ RUN      ] TestParameterFetchParametersWithoutPrefix.AppliesOverrides
2: [       OK ] TestParameterFetchParametersWithoutPrefix.AppliesOverrides (207 ms)
2: [----------] 2 tests from TestParameterFetchParametersWithoutPrefix (413 ms total)
2:
2: [----------] 2 tests from TestParameterFetchParametersWithPrefix
2: [ RUN      ] TestParameterFetchParametersWithPrefix.AppliesOverridesAndSuccessesToFetchWithCorrectPrefix
2: [       OK ] TestParameterFetchParametersWithPrefix.AppliesOverridesAndSuccessesToFetchWithCorrectPrefix (205 ms)
2: [ RUN      ] TestParameterFetchParametersWithPrefix.AppliesOverridesButFailsToFetchWithWrongPrefix
2: [       OK ] TestParameterFetchParametersWithPrefix.AppliesOverridesButFailsToFetchWithWrongPrefix (207 ms)
2: [----------] 2 tests from TestParameterFetchParametersWithPrefix (412 ms total)
2:
2: [----------] Global test environment tear-down
2: [==========] 4 tests from 2 test suites ran. (825 ms total)
2: [  PASSED  ] 4 tests.
2: -- run_test.py: return code 0
2: -- run_test.py: inject classname prefix into gtest result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros.gtest.xml'
2: -- run_test.py: verify result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/parameter.cpp_accelerated_image_processor_ros.gtest.xml'
2/3 Test #2: parameter.cpp_accelerated_image_processor_ros ....   Passed    0.88 sec
test 3
    Start 3: qos.cpp_accelerated_image_processor_ros

3: Test command: /usr/bin/python3 "-u" "/opt/ros/humble/share/ament_cmake_test/cmake/run_test.py" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros.gtest.xml" "--package-name" "accelerated_image_processor_ros" "--output-file" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/ament_cmake_gtest/qos.cpp_accelerated_image_processor_ros.txt" "--command" "/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros" "--gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros.gtest.xml"
3: Test timeout computed to be: 60
3: -- run_test.py: invoking following command in '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros':
3:  - /hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros --gtest_output=xml:/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros.gtest.xml
3: [==========] Running 4 tests from 1 test suite.
3: [----------] Global test environment set-up.
3: [----------] 4 tests from TestQoSFindQos
3: [ RUN      ] TestQoSFindQos.ReturnsNulloptWhenNoPublishers
3: [INFO 1771909485.251036250] [aip_qos_unit_no_publishers]: Waiting for topic: /aip_qos_unit/no_publishers ...
3: [       OK ] TestQoSFindQos.ReturnsNulloptWhenNoPublishers (206 ms)
3: [ RUN      ] TestQoSFindQos.ReturnsNulloptWhenMultiplePublishers
3: [       OK ] TestQoSFindQos.ReturnsNulloptWhenMultiplePublishers (408 ms)
3: [ RUN      ] TestQoSFindQos.SucceedsWithSinglePublisherAndReturnsProfile
3: [INFO 1771909486.376367939] [aip_qos_it_query]: QoS is acquired for topic: /aip_qos_it/single_publisher
3: [       OK ] TestQoSFindQos.SucceedsWithSinglePublisherAndReturnsProfile (716 ms)
3: [ RUN      ] TestQoSFindQos.FailsWhenTwoDifferentNodesPublishSameTopic
3: [ERROR 1771909487.382421942] [aip_qos_it_query2]: Multiple publishers found for topic: /aip_qos_it/two_publishers. Cannot determine proper QoS
3: [       OK ] TestQoSFindQos.FailsWhenTwoDifferentNodesPublishSameTopic (1007 ms)
3: [----------] 4 tests from TestQoSFindQos (2337 ms total)
3:
3: [----------] Global test environment tear-down
3: [==========] 4 tests from 1 test suite ran. (2337 ms total)
3: [  PASSED  ] 4 tests.
3: -- run_test.py: return code 0
3: -- run_test.py: inject classname prefix into gtest result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros.gtest.xml'
3: -- run_test.py: verify result file '/hdd/dev/v4l2_camera/build/accelerated_image_processor_ros/test_results/accelerated_image_processor_ros/qos.cpp_accelerated_image_processor_ros.gtest.xml'
3/3 Test #3: qos.cpp_accelerated_image_processor_ros ..........   Passed    2.39 sec

100% tests passed, 0 tests failed out of 3

Label Time Summary:
gtest    =   3.33 sec*proc (3 tests)

Total Test time (real) =   3.33 sec
---
Finished <<< accelerated_image_processor_ros [3.36s]

Summary: 1 package finished [3.49s]

Remarks

Pre-Review Checklist for the PR Author

PR Author should check the checkboxes below when creating the PR.

  • Assign PR to reviewer

Checklist for the PR Reviewer

Reviewers should check the checkboxes below before approval.

  • Commits are properly organized and messages are according to the guideline
  • (Optional) Unit tests have been written for new behavior
  • PR title describes the changes

Post-Review Checklist for the PR Author

PR Author should check the checkboxes below before merging.

  • All open points are addressed and tracked via issues or tickets

CI Checks

  • Build and test for PR: Required to pass before the merge.

Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
…ecompression

Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
…er decompression related functions

Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces video decoder support for the accelerated image processor, complementing the video encoder support added in PR #29. The implementation uses FFmpeg libraries to decode H.264, H.265, and AV1 video streams with CUDA hardware acceleration.

Changes:

  • New accelerated_image_processor_decompression package with FFmpeg-based video decompression
  • New decompress_node ROS node to subscribe to compressed video topics and publish decompressed images
  • QoS and conversion utilities to support topic type detection and FFmpeg message handling

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 19 comments.

Show a summary per file
File Description
src/accelerated_image_processor_decompression/src/video_decompressor/ffmpeg.cpp Core FFmpeg video decoder implementation with CUDA acceleration
src/accelerated_image_processor_decompression/include/accelerated_image_processor_decompression/video_decompressor.hpp Video decompressor base class interface
src/accelerated_image_processor_decompression/include/accelerated_image_processor_decompression/builder.hpp Builder pattern for creating decompressor instances
src/accelerated_image_processor_decompression/src/builder.cpp Builder implementation
src/accelerated_image_processor_decompression/test/ffmpeg_video_decompressor.cpp Unit tests for FFmpeg video decompressor
src/accelerated_image_processor_decompression/test/test_utility.hpp Test utility for generating encoded video frames
src/accelerated_image_processor_decompression/test/builder.cpp Unit tests for builder pattern
src/accelerated_image_processor_decompression/CMakeLists.txt Build configuration for decompression package
src/accelerated_image_processor_decompression/package.xml Package dependencies
src/accelerated_image_processor_ros/src/decompress_node.cpp ROS node implementation for video decompression
src/accelerated_image_processor_ros/src/decompress_node.hpp ROS node header
src/accelerated_image_processor_ros/src/conversion.cpp Conversion functions for FFmpeg messages
src/accelerated_image_processor_ros/include/accelerated_image_processor_ros/conversion.hpp Conversion function declarations
src/accelerated_image_processor_ros/src/qos.cpp QoS helper functions including topic type detection
src/accelerated_image_processor_ros/include/accelerated_image_processor_ros/qos.hpp QoS helper function declarations
src/accelerated_image_processor_ros/test/conversion.cpp Tests for conversion functions
src/accelerated_image_processor_ros/launch/decompress.launch.xml Launch file for decompress node
src/accelerated_image_processor_ros/config/decompress.param.yaml Configuration parameters
src/accelerated_image_processor_ros/CMakeLists.txt Updated build configuration
src/accelerated_image_processor_ros/package.xml Added decompression dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/accelerated_image_processor_ros/src/decompress_node.cpp Outdated
Comment thread src/accelerated_image_processor_decompression/src/video_decompressor/ffmpeg.cpp Outdated
Comment thread src/accelerated_image_processor_decompression/src/video_decompressor/ffmpeg.cpp Outdated
Comment on lines +112 to +123
void cleanup_decoder()
{
if (packet_) {
av_packet_free(&packet_);
}
if (codec_ctx_) {
avcodec_free_context(&codec_ctx_);
}
if (hw_device_ctx_) {
av_buffer_unref(&hw_device_ctx_);
}
}

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

Memory leak: The decoded_frame_ allocated by av_frame_alloc() is never freed. The cleanup_decoder() function should call av_frame_free(&decoded_frame_) to properly release this resource. While av_frame_unref() releases the reference to the frame data, it doesn't free the AVFrame structure itself.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed in 04ffdc2

Comment on lines +86 to +180
explicit FfmpegTestDataProvider()
{
// Embed frame count into the frame contents
// NOTE: Large counter shown on the right middle depicts second of the video
// NOTE: To embed frame count into the pixel value (so that we can confirm the count is as
// expected programatically),
// `std::string filter_descr = "geq=lum='mod(N*10, 255)',format=pix_fmts=yuv420p";`
// is another option. This increases pixel brightness by 10 (back to 0 if the frame number
// reaches 255) frame by frame

std::string filter_descr =
"drawtext=text='%{n}':fontsize=150:fontcolor=white:x=100:y=100,format=pix_fmts=yuv420p";

AVFilterGraph * graph = avfilter_graph_alloc();
AVFilterContext * src_ctx = nullptr;
AVFilterContext * sink_ctx = nullptr;

const AVFilter * src = avfilter_get_by_name("testsrc");
const AVFilter * sink = avfilter_get_by_name("buffersink");
AVFilterInOut * inputs = avfilter_inout_alloc();
AVFilterInOut * outputs = avfilter_inout_alloc();

av_opt_set_int(src_ctx, "sample_aspect_ratio", 0, 0);
av_opt_set_int(sink_ctx, "sample_aspect_ratio", 0, 0);

// avfilter_graph_create_filter(&src_ctx, src, "src", filter_descr.c_str(), nullptr, graph);
std::string src_filter_descr = "size=" + std::to_string(WIDTH) + "x" + std::to_string(HEIGHT) +
":rate=" + std::to_string(FPS);
avfilter_graph_create_filter(&src_ctx, src, "src", src_filter_descr.c_str(), nullptr, graph);
avfilter_graph_create_filter(&sink_ctx, sink, "sink", nullptr, nullptr, graph);

outputs->name = av_strdup("in");
outputs->filter_ctx = src_ctx;
outputs->pad_idx = 0;
outputs->next = nullptr;

inputs->name = av_strdup("out");
inputs->filter_ctx = sink_ctx;
inputs->pad_idx = 0;
inputs->next = nullptr;

// av_log_set_level(AV_LOG_DEBUG);

if (avfilter_graph_parse_ptr(graph, filter_descr.c_str(), &inputs, &outputs, nullptr) < 0) {
throw std::runtime_error("Failed to parse filter graph");
}
if (avfilter_graph_config(graph, nullptr) < 0) {
throw std::runtime_error("Failed to configure filter graph");
}

// 2. Find encoder
const AVCodec * codec = avcodec_find_encoder_by_name(CodecName::value);
if (!codec) {
throw std::runtime_error("Codec not found");
}
encoder_ctx_ = avcodec_alloc_context3(codec);
encoder_ctx_->width = WIDTH;
encoder_ctx_->height = HEIGHT;
encoder_ctx_->time_base = AVRational{1, FPS};
encoder_ctx_->framerate = AVRational{FPS, 1};
encoder_ctx_->gop_size = 10;
encoder_ctx_->max_b_frames = 0;
encoder_ctx_->pix_fmt = AV_PIX_FMT_YUV420P;
encoder_ctx_->bit_rate = BITRATE;

// Enable encoder acceleration options to reduce test duration
if (std::string(CodecName::value) == "libx264" || std::string(CodecName::value) == "libx265") {
av_opt_set(encoder_ctx_->priv_data, "tune", "zerolatency", 0);
av_opt_set(encoder_ctx_->priv_data, "preset", "ultrafast", 0);
} else if (std::string(CodecName::value) == "libaom-av1") {
// accelerated options for AV1
// Without these, the test cases for AV1 takes over 2min, which causes colcon test timeout
// cpu-used: It can specify 0--8. larger value lower compression (8 is fastest)
av_opt_set(encoder_ctx_->priv_data, "cpu-used", "8", 0);
// usage: set `realtime` to minimize delay and processing time
av_opt_set(encoder_ctx_->priv_data, "usage", "realtime", 0);
}

if (avcodec_open2(encoder_ctx_, codec, nullptr) < 0) {
throw std::runtime_error("Could not open encoder");
}

// 3. Prepare packet buffer
pkt_ = av_packet_alloc();
frame_ = av_frame_alloc();
frame_->format = encoder_ctx_->pix_fmt;
frame_->width = encoder_ctx_->width;
frame_->height = encoder_ctx_->height;
av_frame_get_buffer(frame_, 32);

// 4. Store graph for later use
graph_ = graph;
src_ctx_ = src_ctx;
sink_ctx_ = sink_ctx;
}

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

Resource leak in constructor: The AVFilterInOut objects allocated at lines 105-106 with avfilter_inout_alloc() are never freed. These should be freed with avfilter_inout_free() after avfilter_graph_parse_ptr() is called. Additionally, if any of the exceptions are thrown (lines 130, 133, 139, 165), previously allocated resources (graph, inputs, outputs, encoder_ctx_, etc.) will leak. Consider using RAII wrappers or adding proper cleanup on all error paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

addressed in 04ffdc2

Comment thread src/accelerated_image_processor_decompression/src/video_decompressor/ffmpeg.cpp Outdated
Comment thread src/accelerated_image_processor_ros/launch/decompress.launch.xml Outdated

@ktro2828 ktro2828 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thank you for your PR! The code and tests looks good to me. I also confirmed all of tests passed.
It seems OK to merge after fixing typos, and I'd appreciate if you add README!

I have two questions, is my understanding correct?

  1. To get decompressed image, we need at least two or more processing.
  2. If we try to receive the results of FfmpegVideoDecompressor::process(...) after N processes, we will only get a maximum of N-1 decompressed images. If we want to get all of them, we should register a postprocess function and process them there.

Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
…ption

Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
Signed-off-by: Manato HIRABAYASHI <manato.hirabayashi@tier4.jp>
@manato

manato commented Feb 27, 2026

Copy link
Copy Markdown
Collaborator Author

@ktro2828
Thank you very much for your review!

It seems OK to merge after fixing typos, and I'd appreciate if you add README!

I added the README for the decompression package in 920dbf4 !

I have two questions, is my understanding correct?

  1. To get decompressed image, we need at least two or more processing.

Thanks for the confirmation!
It depends on the type of input frame. If the input frame is an I frame (i.e., key frame), the decompressed image is supposed to be acquired without any other additional frames. In cases of P frame (i.e., non-key frame), the corresponding I frame is required. But during the typical decoding workload, it is basically fed to the decoder beforehand, so once a P frame is fed to the decoder, the decoded result is supposed to be acquired soon.

  1. If we try to receive the results of FfmpegVideoDecompressor::process(...) after N processes, we will only get a maximum of N-1 decompressed images. If we want to get all of them, we should register a postprocess function and process them there.

Yes, FFMPEGPacket CAN contain multiple frames in one topic. To handle such cases, FfmpegVideoDecompressor::process_packet (the actual function that is called from process to handle the packets) returns std::vector<common::Image>, and postprocess will be called for the each element of the vector one by one.

FYI: I implemented accelerated_image_processor_compression::JetsonVideoCompressor so that it send one packet (i.e., one ROS topic) for one frame. Since FfmpegVideoDecompressor::process returns the first element of the vector, handling the returned value from FfmpegVideoDecompressor::process is equivalent to handle all results as long as the input packets are sent from accelerated_image_processor_compression::JetsonVideoCompressor.

FYI2: In the current implementation of accelerated_image_processor_compression::JetsonVideoCompressor, the very first encoded packet is for P frame. This is because the I frame for that frame corresponds to an initial dummy frame, which is discarded. The number of discarded frames is I_frame_interval(default: 10) - buffer_length(default: 4). So if you use the default configuration, you would get N - 6 valid frames.

@manato
manato merged commit b0c77ab into main Mar 2, 2026
@manato
manato deleted the feat/video_decoder branch March 2, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants