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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions compressed_image_transport/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ pluginlib_export_plugin_description_file(image_transport compressed_plugins.xml)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()

find_package(ament_cmake_gtest REQUIRED)
# Codec-level round-trip: exercises the JPEG/PNG codecs directly (OpenCV),
# with no ROS node or transport.
ament_add_gtest(test_compressed_roundtrip test/test_compressed_roundtrip.cpp)
target_include_directories(test_compressed_roundtrip PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(test_compressed_roundtrip
ament_cmake_ros_core::ament_ros_defaults
${OpenCV_LIBRARIES})

# End-to-end integration over the "compressed" image transport through a real
# node, plus a pluginlib-discovery check.
ament_add_gtest(test_compressed_transport test/test_compressed_transport.cpp)
target_link_libraries(test_compressed_transport
image_transport::image_transport
rclcpp::rclcpp
${sensor_msgs_TARGETS})
endif()

ament_package()
1 change: 1 addition & 0 deletions compressed_image_transport/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_gtest</test_depend>

<export>
<build_type>ament_cmake</build_type>
Expand Down
114 changes: 114 additions & 0 deletions compressed_image_transport/test/test_compressed_roundtrip.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright (c) 2026, Open Source Robotics Foundation, Inc.
// All rights reserved.
//
// 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.

// Codec-level round-trip tests: they exercise the OpenCV image codecs the
// compressed transport relies on (JPEG / PNG), with no ROS node or transport.

#include <gtest/gtest.h>

#include <vector>

#include <opencv2/core.hpp>
#include <opencv2/imgcodecs.hpp>

namespace
{

// Build a bgr8 image with a deterministic per-pixel pattern.
cv::Mat makeBgr8(int w, int h)
{
cv::Mat img(h, w, CV_8UC3);
for (int y = 0; y < h; ++y) {
for (int x = 0; x < w; ++x) {
img.at<cv::Vec3b>(y, x) = cv::Vec3b(
static_cast<uchar>((x * 7) & 0xFF),
static_cast<uchar>((y * 5) & 0xFF),
static_cast<uchar>((x + y) & 0xFF));
}
}
return img;
}

int maxAbsDiff(const cv::Mat & a, const cv::Mat & b)
{
cv::Mat diff;
cv::absdiff(a, b, diff);
double maxval = 0.0;
cv::minMaxLoc(diff.reshape(1), nullptr, &maxval);
return static_cast<int>(maxval);
}

} // namespace

// PNG is lossless: encode -> decode must reproduce the image exactly.
TEST(CompressedCodecRoundTrip, PngIsLossless)
{
const cv::Mat original = makeBgr8(32, 24);
std::vector<uchar> buffer;
const std::vector<int> params = {cv::IMWRITE_PNG_COMPRESSION, 3};
ASSERT_TRUE(cv::imencode(".png", original, buffer, params));

const cv::Mat decoded = cv::imdecode(buffer, cv::IMREAD_UNCHANGED);
ASSERT_FALSE(decoded.empty());
EXPECT_EQ(decoded.size(), original.size());
EXPECT_EQ(decoded.type(), original.type());
EXPECT_EQ(maxAbsDiff(original, decoded), 0);
}

// JPEG is lossy: structure exact, pixel values within a tolerance.
TEST(CompressedCodecRoundTrip, JpegApproximate)
{
const cv::Mat original = makeBgr8(32, 24);
std::vector<uchar> buffer;
const std::vector<int> params = {cv::IMWRITE_JPEG_QUALITY, 95};
ASSERT_TRUE(cv::imencode(".jpg", original, buffer, params));

const cv::Mat decoded = cv::imdecode(buffer, cv::IMREAD_COLOR);
ASSERT_FALSE(decoded.empty());
EXPECT_EQ(decoded.size(), original.size());
EXPECT_LE(maxAbsDiff(original, decoded), 20) << "JPEG round-trip drifted more than expected";
}

// A single-channel 16-bit image (depth-like) must survive PNG losslessly.
TEST(CompressedCodecRoundTrip, Png16BitIsLossless)
{
cv::Mat original(24, 32, CV_16UC1);
for (int y = 0; y < original.rows; ++y) {
for (int x = 0; x < original.cols; ++x) {
original.at<uint16_t>(y, x) = static_cast<uint16_t>((x * 337 + y * 71) & 0xFFFF);
}
}
std::vector<uchar> buffer;
ASSERT_TRUE(cv::imencode(".png", original, buffer));

const cv::Mat decoded = cv::imdecode(buffer, cv::IMREAD_UNCHANGED);
ASSERT_FALSE(decoded.empty());
EXPECT_EQ(decoded.type(), original.type());
EXPECT_EQ(maxAbsDiff(original, decoded), 0);
}
139 changes: 139 additions & 0 deletions compressed_image_transport/test/test_compressed_transport.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) 2026, Open Source Robotics Foundation, Inc.
// All rights reserved.
//
// 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.

#include <gtest/gtest.h>

#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <memory>
#include <string>
#include <thread>

#include <rclcpp/rclcpp.hpp>
#include <image_transport/image_transport.hpp>
#include <sensor_msgs/msg/image.hpp>

using namespace std::chrono_literals;

namespace
{

// Build a constant-colour bgr8 image (constant colour survives JPEG cleanly).
sensor_msgs::msg::Image makeBgr8(uint32_t w, uint32_t h, uint8_t b, uint8_t g, uint8_t r)
{
sensor_msgs::msg::Image img;
img.header.frame_id = "camera";
img.height = h;
img.width = w;
img.encoding = "bgr8";
img.is_bigendian = 0;
img.step = w * 3;
img.data.resize(static_cast<size_t>(img.step) * h);
for (size_t i = 0; i < img.data.size(); i += 3) {
img.data[i] = b;
img.data[i + 1] = g;
img.data[i + 2] = r;
}
return img;
}

} // namespace

class CompressedTransportTest : public ::testing::Test
{
protected:
static void SetUpTestSuite()
{
if (!rclcpp::ok()) {
rclcpp::init(0, nullptr);
}
}
static void TearDownTestSuite()
{
rclcpp::shutdown();
}
};

// The compressed transport plugin must be discoverable through pluginlib.
TEST_F(CompressedTransportTest, TransportIsLoadable)
{
bool found = false;
for (const auto & name : image_transport::getLoadableTransports()) {
if (name.find("compressed") != std::string::npos) {
found = true;
break;
}
}
EXPECT_TRUE(found) << "compressed transport not found among loadable transports";
}

// Full integration over the compressed transport (default JPEG codec, lossy):
// structure must match exactly, pixel values within a tolerance.
TEST_F(CompressedTransportTest, PublishSubscribeRoundTrip)
{
auto node = std::make_shared<rclcpp::Node>("compressed_transport_test");

sensor_msgs::msg::Image::ConstSharedPtr received;
auto sub = image_transport::create_subscription(
*node, "test_image",
[&received](const sensor_msgs::msg::Image::ConstSharedPtr & msg) {received = msg;},
"compressed", rclcpp::SystemDefaultsQoS());
auto pub = image_transport::create_publisher(*node, "test_image", rclcpp::SystemDefaultsQoS());

const auto original = makeBgr8(32, 24, 10, 120, 200);

rclcpp::executors::SingleThreadedExecutor exec;
const auto deadline = std::chrono::steady_clock::now() + 15s;
while (!received && std::chrono::steady_clock::now() < deadline && rclcpp::ok()) {
pub.publish(original);
exec.spin_node_some(node);
std::this_thread::sleep_for(50ms);
}

ASSERT_TRUE(received) << "no image received through the compressed transport within the timeout";
EXPECT_EQ(received->width, original.width);
EXPECT_EQ(received->height, original.height);
EXPECT_EQ(received->encoding, original.encoding);
ASSERT_EQ(received->data.size(), original.data.size());

int max_err = 0;
for (size_t i = 0; i < original.data.size(); ++i) {
max_err = std::max(max_err, std::abs(static_cast<int>(received->data[i]) -
static_cast<int>(original.data[i])));
}
EXPECT_LE(max_err, 12) << "JPEG round-trip drifted more than expected";
}

int main(int argc, char ** argv)
{
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
11 changes: 11 additions & 0 deletions theora_image_transport/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@ if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
set(ament_cmake_cppcheck_LANGUAGE "c++")
ament_lint_auto_find_test_dependencies()

find_package(ament_cmake_gtest REQUIRED)
# theora exposes no standalone codec API (encode/decode live in the stateful
# plugin), so it is covered only by the end-to-end transport test below, which
# exercises TheoraPublisher (encode) and TheoraSubscriber (decode) through a
# real node, plus a pluginlib-discovery check.
ament_add_gtest(test_theora_transport test/test_theora_transport.cpp)
target_link_libraries(test_theora_transport
image_transport::image_transport
rclcpp::rclcpp
${sensor_msgs_TARGETS})
endif()

pluginlib_export_plugin_description_file(image_transport theora_plugins.xml)
Expand Down
1 change: 1 addition & 0 deletions theora_image_transport/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>ament_cmake_gtest</test_depend>

<member_of_group>rosidl_interface_packages</member_of_group>

Expand Down
Loading
Loading