Skip to content

Commit

Permalink
Add function to convert single channel image data to RGB image (#205)
Browse files Browse the repository at this point in the history
Added a generic helper function for converting single channel image data, e.g. thermal and depth images, to an RGB image. This is intended to be used for visualization. Optional arguments can be specified to tweak the visualization.


Signed-off-by: Ian Chen <ichen@osrfoundation.org>
Co-authored-by: Michael Carroll <michael@openrobotics.org>
  • Loading branch information
iche033 and mjcarroll authored Apr 27, 2021
1 parent cdfbe87 commit 9f26057
Show file tree
Hide file tree
Showing 2 changed files with 273 additions and 0 deletions.
75 changes: 75 additions & 0 deletions graphics/include/ignition/common/Image.hh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#ifndef IGNITION_COMMON_IMAGE_HH_
#define IGNITION_COMMON_IMAGE_HH_

#include <limits>
#include <memory>
#include <string>
#include <vector>
Expand Down Expand Up @@ -185,6 +186,80 @@ namespace ignition
/// \return true if image has a bitmap
public: bool Valid() const;

/// \brief Convert a single channel image data buffer into an RGB image.
/// During the conversion, the input image data are normalized to 8 bit
/// values i.e. [0, 255]. Optionally, specify min and max values to use
/// when normalizing the input image data. For example, if min and max
/// are set to 1 and 10, a data value 2 will be normalized to:
/// (2 - 1) / (10 - 1) * 255.
/// \param[in] _data input image data buffer
/// \param[in] _width image width
/// \param[in] _height image height
/// \param[out] _output Output RGB image
/// \param[in] _min Minimum value to be used when normalizing the input
/// image data to RGB.
/// \param[in] _max Maximum value to be used when normalizing the input
/// image data to RGB.
/// \param[in] _flip True to flip the values after normalization, i.e.
/// lower values are converted to brigher pixels.
public: template<typename T>
static void ConvertToRGBImage(const void *_data,
unsigned int _width, unsigned int _height, Image &_output,
T _min = std::numeric_limits<T>::max(),
T _max = std::numeric_limits<T>::lowest(), bool _flip = false)
{
unsigned int samples = _width * _height;
unsigned int bufferSize = samples * sizeof(T);

auto buffer = std::vector<T>(samples);
memcpy(buffer.data(), _data, bufferSize);

auto outputRgbBuffer = std::vector<uint8_t>(samples * 3);

// use min and max values found in the data if not specified
T min = std::numeric_limits<T>::max();
T max = std::numeric_limits<T>::lowest();
if (_min > max)
{
for (unsigned int i = 0; i < samples; ++i)
{
auto v = buffer[i];
// ignore inf values when computing min/max
// cast to float when calling isinf to avoid compile error on
// windows
if (v > max && !std::isinf(static_cast<float>(v)))
max = v;
if (v < min && !std::isinf(static_cast<float>(v)))
min = v;
}
}
min = math::equal(_min, std::numeric_limits<T>::max()) ? min : _min;
max = math::equal(_max, std::numeric_limits<T>::lowest()) ? max : _max;

// convert to rgb image
// color is grayscale, i.e. r == b == g
double range = static_cast<double>(max - min);
if (ignition::math::equal(range, 0.0))
range = 1.0;
unsigned int idx = 0;
for (unsigned int j = 0; j < _height; ++j)
{
for (unsigned int i = 0; i < _width; ++i)
{
auto v = buffer[idx++];
double t = static_cast<double>(v - min) / range;
if (_flip)
t = 1.0 - t;
uint8_t r = static_cast<uint8_t>(255*t);
unsigned int outIdx = j * _width * 3 + i * 3;
outputRgbBuffer[outIdx] = r;
outputRgbBuffer[outIdx + 1] = r;
outputRgbBuffer[outIdx + 2] = r;
}
}
_output.SetFromData(outputRgbBuffer.data(), _width, _height, RGB_INT8);
}

IGN_COMMON_WARN_IGNORE__DLL_INTERFACE_MISSING
/// \brief Private data pointer
private: std::unique_ptr<ImagePrivate> dataPtr;
Expand Down
198 changes: 198 additions & 0 deletions graphics/src/Image_TEST.cc
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,204 @@ TEST_F(ImageTest, ConvertPixelFormat)
Image::ConvertPixelFormat("BAYER_BGGR8"));
}

/////////////////////////////////////////////////
TEST_F(ImageTest, ConvertToRGBImage)
{
unsigned int width = 8;
unsigned int height = 8;
unsigned int size = width * height;

// test L_INT8 format
{
// create sample image data for testing
// the image is divided into 4 sections from top to bottom
// The values in the sections are 10, 20, 30, 40
auto buffer = std::vector<uint8_t>(size);
for (unsigned int i = 0; i < height; ++i)
{
uint8_t v = 10 * static_cast<int>(i / (width/ 4.0) + 1);
for (unsigned int j = 0; j < width; ++j)
{
buffer[i*width + j] = v;
}
}

common::Image output;
common::Image::ConvertToRGBImage<uint8_t>(
buffer.data(), width, height, output);

// Check RGBA data
unsigned char *data = nullptr;
unsigned int outputSize = 0;
output.Data(&data, outputSize);
EXPECT_EQ(size * 3, outputSize);
ASSERT_NE(nullptr, data);

for (unsigned int i = 0u; i < height; ++i)
{
for (unsigned int j = 0u; j < width; ++j)
{
unsigned int r = data[i * width * 3 + j * 3];
unsigned int g = data[i * width * 3 + j * 3 + 1];
unsigned int b = data[i * width * 3 + j * 3 + 2];
EXPECT_EQ(r, g);
EXPECT_EQ(r, b);
if (i < (height / 4.0))
EXPECT_EQ(0u, r);
else if (i >= (height / 4.0) && i < (height / 2.0))
EXPECT_EQ(static_cast<unsigned int>(255 / 3), r);
else if (i >= (height / 2.0) && i < (height / 4.0 * 3.0))
EXPECT_EQ(static_cast<unsigned int>(255 / 3 * 2), r);
else
EXPECT_EQ(255u, r);
}
}
}

// test L_INT16 format
{
// create sample image data for testing
// the image is divided into 4 sections from top to bottom
// The values in the sections are 100, 200, 300, 400
auto buffer = std::vector<uint16_t>(size);
for (unsigned int i = 0; i < height; ++i)
{
uint16_t v = 100 * static_cast<int>(i / (height / 4.0) + 1);
for (unsigned int j = 0; j < width; ++j)
{
buffer[i*width + j] = v;
}
}

common::Image output;
common::Image::ConvertToRGBImage<uint16_t>(
buffer.data(), width, height, output);

// Check RGB data
unsigned char *data = nullptr;
unsigned int outputSize = 0;
output.Data(&data, outputSize);
EXPECT_EQ(size * 3, outputSize);
ASSERT_NE(nullptr, data);

for (unsigned int i = 0u; i < height; ++i)
{
for (unsigned int j = 0u; j < width; ++j)
{
unsigned int r = data[i * width * 3 + j * 3];
unsigned int g = data[i * width * 3 + j * 3 + 1];
unsigned int b = data[i * width * 3 + j * 3 + 2];
EXPECT_EQ(r, g);
EXPECT_EQ(r, b);

if (i < (height / 4.0))
EXPECT_EQ(0u, r);
else if (i >= (height / 4.0) && i < (height / 2.0))
EXPECT_EQ(static_cast<unsigned int>(255 / 3), r);
else if (i >= (height / 2.0) && i < (height / 4.0 * 3.0))
EXPECT_EQ(static_cast<unsigned int>(255 / 3 * 2), r);
else
EXPECT_EQ(255u, r);
}
}
}

// test R_FLOAT32 format
{
// create sample image data for testing
// the image is divided into 4 sections from top to bottom
// The values in the sections are 0.5, 1.0, 1.5, 2.0
auto buffer = std::vector<float>(size);
for (unsigned int i = 0; i < height; ++i)
{
float v = 0.5f * static_cast<int>(i / (height / 4.0) + 1);
for (unsigned int j = 0; j < width; ++j)
{
buffer[i*width + j] = v;
}
}

common::Image output;
common::Image::ConvertToRGBImage<float>(
buffer.data(), width, height, output);

// Check RGB data
unsigned char *data = nullptr;
unsigned int outputSize = 0;
output.Data(&data, outputSize);
EXPECT_EQ(size * 3, outputSize);
ASSERT_NE(nullptr, data);

for (unsigned int i = 0u; i < height; ++i)
{
for (unsigned int j = 0u; j < width; ++j)
{
unsigned int r = data[i * width * 3 + j * 3];
unsigned int g = data[i * width * 3 + j * 3 + 1];
unsigned int b = data[i * width * 3 + j * 3 + 2];
EXPECT_EQ(r, g);
EXPECT_EQ(r, b);

if (i < (height / 4.0))
EXPECT_EQ(0u, r);
else if (i >= (height / 4.0) && i < (height / 2.0))
EXPECT_EQ(static_cast<unsigned int>(255 / 3), r);
else if (i >= (height / 2.0) && i < (height / 4.0 * 3.0))
EXPECT_EQ(static_cast<unsigned int>(255 / 3 * 2), r);
else
EXPECT_EQ(255u, r);
}
}
}

// test R_FLOAT32 format with min, max, and flip values set
{
// create sample image data for testing
// the image is divided into 4 sections from top to bottom
// The values in the sections are 0.5, 1.0, 1.5, 2.0
auto buffer = std::vector<float>(size);
for (unsigned int i = 0; i < height; ++i)
{
float v = 0.5f * static_cast<int>(i / (height / 4.0) + 1);
for (unsigned int j = 0; j < width; ++j)
{
buffer[i*width + j] = v;
}
}

float min = 0.0f;
float max = 5.0f;
common::Image output;
common::Image::ConvertToRGBImage<float>(
buffer.data(), width, height, output, min, max, true);

// Check RGB data
unsigned char *data = nullptr;
unsigned int outputSize = 0;
output.Data(&data, outputSize);
EXPECT_EQ(size * 3, outputSize);
ASSERT_NE(nullptr, data);

for (unsigned int i = 0u; i < height; ++i)
{
for (unsigned int j = 0u; j < width; ++j)
{
unsigned int r = data[i * width * 3 + j * 3];
unsigned int g = data[i * width * 3 + j * 3 + 1];
unsigned int b = data[i * width * 3 + j * 3 + 2];
EXPECT_EQ(r, g);
EXPECT_EQ(r, b);

// values should be normalized by min, max and flipped
float v = 0.5f * static_cast<int>(i / (height / 4.0) + 1);
unsigned int expectedValue = static_cast<unsigned int>(
(1.0f - ((v - min) / (max - min))) * 255);
EXPECT_EQ(expectedValue, r);
}
}
}
}

using string_int2 = std::tuple<const char *, unsigned int, unsigned int>;

class ImagePerformanceTest : public ImageTest,
Expand Down

0 comments on commit 9f26057

Please # to comment.