From c736fab654d85fc90e565250cd737fa790241f50 Mon Sep 17 00:00:00 2001 From: Jeremy Lorelli Date: Sat, 31 Aug 2024 00:08:06 -0700 Subject: [PATCH] common: Rewrote image conversion, other misc improovements/refactors --- CMakeLists.txt | 28 ++++++++ src/cli/action_convert.cpp | 16 +++-- src/cli/action_extract.cpp | 2 +- src/cli/action_pack.cpp | 35 +++++---- src/common/image.cpp | 98 +++++++++++++++++--------- src/common/image.hpp | 90 +++++++++-------------- src/common/lwiconv.hpp | 141 +++++++++++++++++++++++++++++++++++++ src/common/pack.cpp | 2 +- src/tests/image_tests.cpp | 123 ++++++++++++++++++++++++++++++++ 9 files changed, 428 insertions(+), 107 deletions(-) create mode 100644 src/common/lwiconv.hpp create mode 100644 src/tests/image_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bea9fd6..f9e1681 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD 20) # Project settings option(BUILD_GUI "Build the VTFViewer GUI" ON) +option(BUILD_TESTS "Build test binaries" ON) # Global flags, mainly for UNIX. Use $ORIGIN rpath & -fPIC set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -18,6 +19,7 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") # Build vtflib as static lib add_subdirectory(external/vtflib) add_subdirectory(external/fmtlib) +add_subdirectory(external/gtest) add_definitions(-DVTFLIB_STATIC=1) @@ -99,6 +101,32 @@ if (BUILD_GUI) ) endif() +############################## +# Tests +############################## + +if (BUILD_TESTS) + add_executable( + tests + + src/tests/image_tests.cpp + ) + + target_link_libraries( + tests PRIVATE + + gtest + vtflib_static + com + ) + + target_include_directories( + tests PRIVATE + + src + ) +endif() + ############################## # Version header ############################## diff --git a/src/cli/action_convert.cpp b/src/cli/action_convert.cpp index a71f2da..42bd600 100644 --- a/src/cli/action_convert.cpp +++ b/src/cli/action_convert.cpp @@ -319,15 +319,15 @@ bool ActionConvert::process_file( std::max(formatInfo.uiRedBitsPerPixel, formatInfo.uiGreenBitsPerPixel), std::max(formatInfo.uiBlueBitsPerPixel, formatInfo.uiAlphaBitsPerPixel)); if (maxBpp > 16) { - procChanType = imglib::Float; + procChanType = imglib::ChannelType::Float; return IMAGE_FORMAT_RGBA32323232F; } else if (maxBpp > 8) { - procChanType = imglib::UInt16; - return IMAGE_FORMAT_RGBA16161616F; + procChanType = imglib::ChannelType::UInt16; + return IMAGE_FORMAT_RGBA16161616; } else { - procChanType = imglib::UInt8; + procChanType = imglib::ChannelType::UInt8; return IMAGE_FORMAT_RGBA8888; } }(); @@ -521,6 +521,14 @@ bool ActionConvert::add_image_data( return false; } + // Hack for VTFLib; Ensure we have an alpha channel because that's well supported in that horrible code + if (image->channels() < 4 && image->type() != imglib::ChannelType::UInt8) { + if (!image->convert(image->type(), 4)) { + std::cerr << fmt::format("Failed to convert {}\n", imageSrc.c_str()); + return false; + } + } + // Add the raw image data return add_image_data_raw( file, image->data(), format, image->vtf_format(), image->width(), image->height(), create); diff --git a/src/cli/action_extract.cpp b/src/cli/action_extract.cpp index 9df8055..0511e81 100644 --- a/src/cli/action_extract.cpp +++ b/src/cli/action_extract.cpp @@ -217,7 +217,7 @@ bool ActionExtract::extract_file( return false; } - imglib::Image image(imageData, destIsFloat ? imglib::Float : imglib::UInt8, comps, w, h, true); + imglib::Image image(imageData, destIsFloat ? imglib::ChannelType::Float : imglib::ChannelType::UInt8, comps, w, h, true); if (!image.save(outFile.string().c_str(), targetFmt)) { std::cerr << fmt::format("Could not save image to '{}'!\n", outFile.string()); return false; diff --git a/src/cli/action_pack.cpp b/src/cli/action_pack.cpp index 09517c1..3146d71 100644 --- a/src/cli/action_pack.cpp +++ b/src/cli/action_pack.cpp @@ -212,7 +212,7 @@ int ActionPack::exec(const OptionList& opts) { // static std::shared_ptr load_image(const std::filesystem::path& path) { - auto img = imglib::Image::load(path); + auto img = imglib::Image::load(path); // Force UInt8 for packed images if (!img) std::cerr << fmt::format("Could not load image '{}'\n", path.string()); return img; @@ -236,20 +236,23 @@ static void determine_size(int* w, int* h, const std::shared_ptr // // Resize images if required and converts too! // -static void resize_if_required(const std::shared_ptr& image, int w, int h) { +static bool resize_if_required(const std::shared_ptr& image, int w, int h) { if (!image) - return; + return true; // @TODO: For now we're just going to force 8 bit per channel. // Sometimes we do get 16bpc images, mainly for height data, but we're cramming that into a RGBA8888 texture // anyways. It'd be best to eventually support RGBA16F normals for instances where you need precise height data. - if (image->type() != imglib::UInt8) { - assert(image->convert(imglib::UInt8)); + if (image->type() != imglib::ChannelType::UInt8) { + if (!image->convert(imglib::ChannelType::UInt8)) { + std::cerr << "Failed to convert image\n"; + return false; + } } if (image->width() == w && image->height() == h) - return; - assert(image->resize(w, h)); + return true; + return image->resize(w, h); } // @@ -296,10 +299,14 @@ bool ActionPack::pack_mrao( } // Resize images if required - resize_if_required(roughnessData, w, h); - resize_if_required(metalnessData, w, h); - resize_if_required(aoData, w, h); - resize_if_required(tmaskData, w, h); + if (!resize_if_required(roughnessData, w, h)) + return false; + if (!resize_if_required(metalnessData, w, h)) + return false; + if (!resize_if_required(aoData, w, h)) + return false; + if (!resize_if_required(tmaskData, w, h)) + return false; // Packing config pack::ChannelPack_t pack[] = { @@ -400,8 +407,10 @@ bool ActionPack::pack_normal( } // Resize images if required - resize_if_required(normalData, w, h); - resize_if_required(heightData, w, h); + if (!resize_if_required(normalData, w, h)) + return false; + if (!resize_if_required(heightData, w, h)) + return false; // Convert normal to DX if necessary if (isGL) diff --git a/src/common/image.cpp b/src/common/image.cpp index eadd814..85e47f7 100644 --- a/src/common/image.cpp +++ b/src/common/image.cpp @@ -2,6 +2,7 @@ #include "image.hpp" #include "util.hpp" #include "strtools.hpp" +#include "lwiconv.hpp" #include #include @@ -52,16 +53,16 @@ Image::~Image() { free(m_data); } -std::shared_ptr Image::load(const char* path) { +std::shared_ptr Image::load(const char* path, ChannelType convertOnLoad) { FILE* fp = fopen(path, "rb"); if (!fp) return nullptr; - auto img = load(fp); + auto img = load(fp, convertOnLoad); fclose(fp); return img; } -std::shared_ptr Image::load(FILE* fp) { +std::shared_ptr Image::load(FILE* fp, ChannelType convertOnLoad) { auto info = image_info(fp); auto image = std::make_shared(); if (info.type == ChannelType::Float) { @@ -73,9 +74,15 @@ std::shared_ptr Image::load(FILE* fp) { else { image->m_data = stbi_load_from_file(fp, &image->m_width, &image->m_height, &image->m_comps, info.comps); } + image->m_type = info.type; if (!image->m_data) return nullptr; + + if (convertOnLoad != ChannelType::None && convertOnLoad != info.type) + if (!image->convert(convertOnLoad)) + return nullptr; // Convert on load failed + return image; } @@ -86,6 +93,8 @@ void Image::clear() { } bool Image::save(const char* file, FileFormat format) { + using enum ChannelType; + if (!file || !m_data || (format != Tga && format != Png && format != Jpeg && format != Bmp && format != Hdr)) return false; @@ -97,7 +106,7 @@ bool Image::save(const char* file, FileFormat format) { if (m_type != Float) { dataToUse = malloc(m_width * m_height * sizeof(float) * m_comps); dataIsOurs = true; - if (!convert_formats(m_data, dataToUse, m_type, Float, m_comps, m_width, m_height)) { + if (!convert_formats(m_data, dataToUse, m_type, Float, m_width, m_height, m_comps, m_comps, pixel_size(), imglib::pixel_size(Float, m_comps))) { free(dataToUse); return false; } @@ -115,7 +124,7 @@ bool Image::save(const char* file, FileFormat format) { if (m_type != UInt8) { dataToUse = malloc(m_width * m_height * sizeof(uint8_t) * m_comps); dataIsOurs = true; - if (!convert_formats(m_data, dataToUse, m_type, UInt8, m_comps, m_width, m_height)) { + if (!convert_formats(m_data, dataToUse, m_type, UInt8, m_width, m_height, m_comps, m_comps, pixel_size(), imglib::pixel_size(UInt8, m_comps))) { free(dataToUse); return false; } @@ -157,10 +166,10 @@ bool Image::resize(int newW, int newH) { VTFImageFormat Image::vtf_format() const { switch (m_type) { - case imglib::UInt16: - // @TODO: How to handle RGBA16? DONT i guess - return IMAGE_FORMAT_RGBA16161616F; - case imglib::Float: + case ChannelType::UInt16: + // @TODO: How to handle RGB16? DONT i guess + return IMAGE_FORMAT_RGBA16161616; + case ChannelType::Float: return (m_comps == 3) ? IMAGE_FORMAT_RGB323232F : (m_comps == 1 ? IMAGE_FORMAT_R32F : IMAGE_FORMAT_RGBA32323232F); default: @@ -170,6 +179,7 @@ VTFImageFormat Image::vtf_format() const { bool imglib::resize( void* indata, void** useroutdata, ChannelType srcType, int comps, int w, int h, int newW, int newH) { + using enum ChannelType; stbir_datatype type; switch (srcType) { case Float: @@ -215,36 +225,46 @@ size_t imglib::bytes_for_image(int w, int h, ChannelType type, int comps) { return w * h * comps * bpc; } -template bool convert_formats_internal( - const void* srcData, void* dstData, ChannelType srcChanType, ChannelType dstChanType, int w, int h) { - static_assert(COMPS > 0 && COMPS <= 4, "Comps is out of range"); + const void* srcData, void* dstData, ChannelType srcChanType, ChannelType dstChanType, int w, int h, int inComps, int outComps, int inStride, int outStride, const lwiconv::PixelF& pdef) { + using enum ChannelType; if (srcChanType == UInt8) { // RGBX32 if (dstChanType == Float) - convert_8_to_32(srcData, dstData, w, h); + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); // RGBX16 else if (dstChanType == UInt16) - convert_8_to_16(srcData, dstData, w, h); + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); + // RGBX8 (just adding/removing channel(s)) + else if (dstChanType == UInt8) + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); return true; } // RGBX32 -> RGBX[8|16] else if (srcChanType == Float) { // RGBX16 if (dstChanType == UInt16) - convert_32_to_16(srcData, dstData, w, h); + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); // RGBX8 else if (dstChanType == UInt8) - convert_32_to_8(srcData, dstData, w, h); + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); + // RGBX32 (just adding/removing channel(s)) + else if (dstChanType == Float) + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); return true; } - // RGBX16 + // RGBX16 -> RGBX[8|32F] else if (srcChanType == UInt16) { + // RGBX8 if (dstChanType == UInt8) - convert_16_to_8(srcData, dstData, w, h); + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); + // RGBX32 else if (dstChanType == Float) - convert_16_to_32(srcData, dstData, w, h); + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); + // RGBX16 (just adding/removing channel(s)) + else if (dstChanType == UInt16) + lwiconv::convert_generic(srcData, dstData, w, h, inComps, outComps, inStride, outStride, pdef); return true; } @@ -252,26 +272,19 @@ bool convert_formats_internal( } bool imglib::convert_formats( - const void* srcData, void* dstData, ChannelType srcChanType, ChannelType dstChanType, int comps, int w, int h) { + const void* srcData, void* dstData, ChannelType srcChanType, ChannelType dstChanType, int w, int h, int inComps, int outComps, int inStride, int outStride, const lwiconv::PixelF& pdef) { // No conv needed - if (srcChanType == dstChanType) + if (srcChanType == dstChanType && inComps == outComps) return true; - if (comps == 4) - return convert_formats_internal<4>(srcData, dstData, srcChanType, dstChanType, w, h); - else if (comps == 3) - return convert_formats_internal<3>(srcData, dstData, srcChanType, dstChanType, w, h); - else if (comps == 2) - return convert_formats_internal<2>(srcData, dstData, srcChanType, dstChanType, w, h); - else if (comps == 1) - return convert_formats_internal<1>(srcData, dstData, srcChanType, dstChanType, w, h); - return false; + return convert_formats_internal(srcData, dstData, srcChanType, dstChanType, w, h, inComps, outComps, inStride, outStride, pdef); } -bool Image::convert(ChannelType dstChanType) { - void* dst = malloc(imglib::bytes_for_image(m_width, m_height, m_type, m_comps)); +bool Image::convert(ChannelType dstChanType, int channels, const lwiconv::PixelF& pdef) { + channels = channels <= 0 ? m_comps : channels; + void* dst = malloc(imglib::bytes_for_image(m_width, m_height, m_type, channels)); - if (!convert_formats(m_data, dst, m_type, dstChanType, m_comps, m_width, m_height)) { + if (!convert_formats(m_data, dst, m_type, dstChanType, m_width, m_height, m_comps, channels, pixel_size(), channels * channel_size(dstChanType), pdef)) { free(dst); return false; } @@ -302,6 +315,7 @@ static bool process_image_internal(void* indata, int comps, int w, int h, ProcFl } bool Image::process(ProcFlags flags) { + using enum ChannelType; switch (m_type) { case UInt8: return process_image_internal(m_data, m_comps, m_width, m_height, flags); @@ -374,3 +388,21 @@ static ImageInfo_t image_info(FILE* fp) { return info; } + +size_t imglib::pixel_size(ChannelType type, int channels) { + return channels * channel_size(type); +} + +size_t imglib::channel_size(ChannelType type) { + switch(type) { + case imglib::ChannelType::UInt8: + return 1; + case imglib::ChannelType::UInt16: + return 2; + case imglib::ChannelType::Float: + return 4; + default: + assert(0); + return 1; + } +} diff --git a/src/common/image.hpp b/src/common/image.hpp index db3d9fc..2fb4be0 100644 --- a/src/common/image.hpp +++ b/src/common/image.hpp @@ -6,15 +6,20 @@ #include #include +#include "lwiconv.hpp" + #include "VTFLib.h" namespace imglib { + constexpr int MAX_CHANNELS = 4; + /** * Per-channel data type */ - enum ChannelType { + enum class ChannelType { + None = -1, UInt8, UInt16, Float // Generally a linear FP number (32-BPC) @@ -44,6 +49,16 @@ namespace imglib using ProcFlags = uint32_t; inline constexpr ProcFlags PROC_GL_TO_DX_NORM = (1 << 0); + /** + * Returns the number of bytes per pixel for the format + */ + size_t pixel_size(ChannelType type, int channels); + + /** + * Returns the size of the channel type + */ + size_t channel_size(ChannelType type); + class Image { public: Image() = default; @@ -63,16 +78,16 @@ namespace imglib ~Image(); - static inline std::shared_ptr load(const std::filesystem::path& path) { - return load(path.string().c_str()); + static inline std::shared_ptr load(const std::filesystem::path& path, ChannelType convertOnLoad = ChannelType::None) { + return load(path.string().c_str(), convertOnLoad); } /** * Loads the image from the specified file * Optionally FILE* can be specified directly */ - static std::shared_ptr load(const char* file); - static std::shared_ptr load(FILE* fp); + static std::shared_ptr load(const char* file, ChannelType convertOnLoad = ChannelType::None); + static std::shared_ptr load(FILE* fp, ChannelType convertOnLoad = ChannelType::None); /** * @brief Clear internal data store, frees up some memory @@ -104,8 +119,11 @@ namespace imglib /** * Convert this image to the specified channel type * Not all conversions are supported, so check the return value! + * @param type New channel type + * @param channels New channel count. If < 0, it is defaulted to m_comps + * @param pdef Default pixel fill for uninitialized pixels */ - bool convert(ChannelType type); + bool convert(ChannelType type, int channels = -1, const lwiconv::PixelF& pdef = {0,0,0,1}); /** * Returns the VTF format which matches up to the data we have internally here @@ -115,18 +133,23 @@ namespace imglib int width() const { return m_width; } + int height() const { return m_height; } + int frames() const { return m_frames; } + int channels() const { return m_comps; } + ChannelType type() const { return m_type; } + const void* data() const { return m_data; } @@ -139,11 +162,16 @@ namespace imglib T* data() { return static_cast(m_data); } + template const T* data() const { return static_cast(m_data); } + size_t pixel_size() const { + return imglib::pixel_size(m_type, m_comps); + } + private: int m_width = 0; int m_height = 0; @@ -193,55 +221,7 @@ namespace imglib */ bool convert_formats( - const void* srcData, void* dstData, ChannelType srcChanType, ChannelType dstChanType, int comps, int w, int h); - - template - void convert_16_to_8(const void* rgb16, void* rgb8, int w, int h) { - const uint16_t* src = static_cast(rgb16); - uint8_t* dst = static_cast(rgb8); - for (int i = 0; i < w * h * COMPS; ++i) - dst[i] = src[i] * (255.f / 65535.f); - } - - template - void convert_32_to_8(const void* rgb32, void* rgb8, int w, int h) { - const float* src = static_cast(rgb32); - uint8_t* dst = static_cast(rgb8); - for (int i = 0; i < w * h * COMPS; ++i) - dst[i] = src[i] * (255.f); - } - - template - void convert_8_to_32(const void* rgb8, void* rgb32, int w, int h) { - const uint8_t* src = static_cast(rgb8); - float* dst = static_cast(rgb32); - for (int i = 0; i < w * h * COMPS; ++i) - dst[i] = src[i] / (255.f); - } - - template - void convert_16_to_32(const void* rgb16, void* rgb32, int w, int h) { - const uint16_t* src = static_cast(rgb16); - float* dst = static_cast(rgb32); - for (int i = 0; i < w * h * COMPS; ++i) - dst[i] = src[i] / (65535.f); - } - - template - void convert_32_to_16(const void* rgb32, void* rgb16, int w, int h) { - const float* src = static_cast(rgb32); - uint16_t* dst = static_cast(rgb16); - for (int i = 0; i < w * h * COMPS; ++i) - dst[i] = src[i] * 65535.f; - } - - template - void convert_8_to_16(const void* rgb8, void* rgb16, int w, int h) { - auto* src = static_cast(rgb8); - auto* dst = static_cast(rgb16); - for (int i = 0; i < w * h * COMPS; ++i) - dst[i] = src[i] * (65535.f / 255.f); - } + const void* srcData, void* dstData, ChannelType srcChanType, ChannelType dstChanType, int w, int h, int inComps, int outComps, int inStride, int outStride, const lwiconv::PixelF& pdefaults = {0, 0, 0, 1}); /** * Get a compatible VTF image format for the image data diff --git a/src/common/lwiconv.hpp b/src/common/lwiconv.hpp new file mode 100644 index 0000000..982e66f --- /dev/null +++ b/src/common/lwiconv.hpp @@ -0,0 +1,141 @@ +/** + * lwiconv: Lightweight Image Conversion library. + * Not designed to be particularly fast, just simple. + */ +#pragma once + +#include +#include + +namespace lwiconv +{ +constexpr int MAX_CHANNELS = 4; + +/** + * Just represents a pixel. Assumed to be normalized unsigned float format [0-1] + */ +struct PixelF { + float d[MAX_CHANNELS]; +}; + +namespace detail { + +template +inline float tofloat(const T& t); + +template<> inline float tofloat(const uint8_t& t) { + return float(t) / float(UINT8_MAX); +} + +template<> inline float tofloat(const uint16_t& t) { + return float(t) / float(UINT16_MAX); +} + +template<> inline float tofloat(const float& t) { + return t; +} + +template +inline T fromfloat(float p); + +template<> inline uint8_t fromfloat(float p) { + return uint8_t(p * UINT8_MAX); +} + +template<> inline uint16_t fromfloat(float p) { + return uint16_t(p * UINT16_MAX); +} + +template<> inline float fromfloat(float p) { + return p; +} + +template +inline PixelF pixel_from_data(const T* pin, const PixelF& defs) { + if constexpr (COMPS == 1) + return {{tofloat(*pin), defs.d[1], defs.d[2], defs.d[3]}}; + else if constexpr (COMPS == 2) + return {{tofloat(*pin), tofloat(pin[1]), defs.d[2], defs.d[3]}}; + else if constexpr (COMPS == 3) + return {{tofloat(*pin), tofloat(pin[1]), tofloat(pin[2]), defs.d[3]}}; + else if constexpr (COMPS == 4) + return {{tofloat(*pin), tofloat(pin[1]), tofloat(pin[2]), tofloat(pin[3])}}; +} + +template +inline void pixel_to_data(T* pout, const PixelF& p) { + static_assert(COMPS <= MAX_CHANNELS && COMPS > 0); + if constexpr (COMPS == 1) { + pout[0] = fromfloat(p.d[0]); + } + else if constexpr (COMPS == 2) { + pout[0] = fromfloat(p.d[0]); + pout[1] = fromfloat(p.d[1]); + } + else if constexpr (COMPS == 3) { + pout[0] = fromfloat(p.d[0]); + pout[1] = fromfloat(p.d[1]); + pout[2] = fromfloat(p.d[2]); + } + else if constexpr (COMPS == 4) { + pout[0] = fromfloat(p.d[0]); + pout[1] = fromfloat(p.d[1]); + pout[2] = fromfloat(p.d[2]); + pout[3] = fromfloat(p.d[3]); + } +} + +} + +/** + * \brief Convert buffer from one color format to another + * The input and output buffers are assumed to be the same dimensions. + * \param in Pointer to the input buffer + * \param out Pointer to the output buffer + * \param w Width of the image + * \param h Height of the image + * \param inC Number of input channels + * \param outC Number of output channels + * \param inStride Input stride, in bytes. If set <= 0, it will be computed for you based on inC + * \param outStride Output stride, in bytes. If set <= 0, it will be computed for you based on outC + * \param channelDefaults If inC < outC, the missing channel data from each input pixel will be defaulted to this. For example, if you're converting from + * an RGB_888 -> RGBA_8888 image, supplying {0,0,0,1} here will default the resulting alpha channel to 255 + */ +template +static void convert_generic(const void* in, void* out, int w, int h, int inC, int outC, int inStride = -1, int outStride = -1, const PixelF& channelDefaults = {0,0,0,0}) { + const Tin* pin = static_cast(in); + Tout* pout = static_cast(out); + + // Compute stride if not provided + if (inStride <= 0) + inStride = inC * sizeof(Tin); + if (outStride <= 0) + outStride = outC * sizeof(Tout); + + using fnInConv = PixelF (*)(const Tin*, const PixelF&); + constexpr fnInConv inConvFuncs[MAX_CHANNELS] = { + detail::pixel_from_data, + detail::pixel_from_data, + detail::pixel_from_data, + detail::pixel_from_data, + }; + const fnInConv inConv = inConvFuncs[inC-1]; + + using fnOutConv = void (*)(Tout*, const PixelF&); + constexpr fnOutConv outConvFuncs[MAX_CHANNELS] = { + detail::pixel_to_data, + detail::pixel_to_data, + detail::pixel_to_data, + detail::pixel_to_data, + }; + const fnOutConv outConv = outConvFuncs[outC-1]; + + const size_t target = w * h; + const size_t isb = inStride / sizeof(Tin); + const size_t osb = outStride / sizeof(Tout); + + for (size_t i = 0; i < target; ++i, pin += isb, pout += osb) + outConv(pout, inConv(pin, channelDefaults)); +} + +} // lwiconv diff --git a/src/common/pack.cpp b/src/common/pack.cpp index d4f6482..745dabd 100644 --- a/src/common/pack.cpp +++ b/src/common/pack.cpp @@ -56,7 +56,7 @@ pack::pack_image(int destChannels, ChannelPack_t* channels, int numChannels, int } // Allocate image - std::shared_ptr result = std::make_shared(imglib::UInt8, destChannels, w, h, false); + std::shared_ptr result = std::make_shared(imglib::ChannelType::UInt8, destChannels, w, h, false); if (numChannels == 1) pack_channel<1>(result->data(), destChannels, channels, w, h); diff --git a/src/tests/image_tests.cpp b/src/tests/image_tests.cpp new file mode 100644 index 0000000..53a2023 --- /dev/null +++ b/src/tests/image_tests.cpp @@ -0,0 +1,123 @@ + +#include +#include +#include + +#include "gtest/gtest.h" + +#include "common/lwiconv.hpp" + +using namespace lwiconv; + +template +static void fillPattern(T* buf, const T (&pattern)[MAX_CHANNELS], int w, int h, int channels) { + for (int i = 0; i < w * h; ++i) { + for (int j = 0; j < channels; ++j) + buf[i * channels + j] = pattern[j]; + } +} + +template +struct Buffer { + T* data; + int w, h, c; + Buffer(int w, int h, int channels) { + data = (T*)malloc(w * h * channels * sizeof(T)); + this->w = w; + this->h = h; + this->c = channels; + } + + ~Buffer() { + free(data); + } + + void fill(const T(&pattern)[MAX_CHANNELS]) { + fillPattern((T*)data, pattern, w, h, c); + } + + void check(const T(&pattern)[MAX_CHANNELS]) { + for (int i = 0; i < w * h; ++i) { + for (int j = 0; j < c; ++j) { + if (data[i*c+j] != pattern[j]) + abort(); + ASSERT_EQ(data[i*c+j], pattern[j]); + } + } + } +}; + +template +void runTest(int width, int height, int inChannels, int outChannels, const Tin (&ipattern)[MAX_CHANNELS], const Tout (&opattern)[MAX_CHANNELS], int inBufChannels = -1, int inStride = -1, int outStride = -1, + const PixelF& channelDefaults = {0,0,0,1}) +{ + Buffer b(width, height, inBufChannels < 0 ? inChannels : inBufChannels); + b.fill(ipattern); + + Buffer o(width, height, outChannels); + + convert_generic(b.data, o.data, width, height, inChannels, outChannels, inStride, outStride, channelDefaults); + + o.check(opattern); +} + +TEST(ImageTests, Basic8To16) +{ + runTest(32, 32, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(32, 16, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(32, 128, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(32, 7, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(2, 7, 4, 4, {0, 0xFF, 0xFF, 0}, {0, 0xFFFF, 0xFFFF, 0}); + runTest(2, 1, 4, 4, {0, 0, 0xFF, 0}, {0, 0, 0xFFFF, 0}); + runTest(45, 177, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(1024, 999, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); +} + +TEST(ImageTests, DiffChannels8To16) +{ + runTest(32, 32, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(32, 16, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(32, 128,4, 3, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(32, 7, 4, 2, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0, 0}); + runTest(2, 7, 4, 3, {0, 0xFF, 0xFF, 0xFF},{0, 0xFFFF, 0xFFFF, 0}); + runTest(2, 1, 2, 4, {0, 0, 0xFF, 0}, {0, 0, 0, 0xFFFF}, 4, sizeof(uint8_t) * 4, -1, {0, 0, 0, 1.f}); + runTest(45, 177, 3, 4, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0xFFFF, 0xFFFF}, 4, sizeof(uint8_t) * 4, -1, {0, 0, 0, 1.f}); + runTest(1024, 999, 2, 2, {0xFF, 0, 0xFF, 0}, {0xFFFF, 0, 0, 0}); +} + +TEST(ImageTests, Basic8To32) +{ + runTest(32, 32, 4, 4, {0xFF, 0, 0xFF, 0}, {1.0f, 0, 1.0f, 0}); + runTest(32, 32, 4, 4, {0xFF, 0, 128, 0}, {1.0f, 0, 128.f / 255.f, 0}); +} + +TEST(ImageTests, Basic32To16) +{ + runTest(32, 32, 4, 4, {1.0f, 0, 1.0f, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(5, 32, 4, 4, {1.0f, 0, 1.0f, 0}, {0xFFFF, 0, 0xFFFF, 0}); + runTest(55, 55, 4, 4, {1.0f, 0.5f, 1.0f, 0}, {0xFFFF, uint16_t(0.5f * 0xFFFF), 0xFFFF, 0}); +} + +TEST(ImageTests, Basic32To8) +{ + runTest(32, 32, 4, 4, {1.0f, 0, 1.0f, 0}, {0xFF, 0, 0xFF, 0}); + runTest(5, 32, 4, 4, {1.0f, 0, 1.0f, 0}, {0xFF, 0, 0xFF, 0}); + runTest(55, 55, 4, 4, {1.0f, 0.5f, 1.0f, 0}, {0xFF, uint16_t(0.5f * 0xFF), 0xFF, 0}); +} + +TEST(ImageTests, DiffChannels32To8) +{ + runTest(32, 32, 2, 4, {0, 0, 0, 0}, {0, 0, 0xFF, 0xFF}, 4, sizeof(uint8_t) * 4, -1, {0, 0, 1.f, 1.f}); + runTest(32, 32, 3, 4, {0, 0, 0, 0}, {0, 0, 0, 0xFF}, 4, sizeof(uint8_t) * 4, -1, {0, 0, 1.f, 1.f}); + runTest(32, 32, 1, 4, {0, 0, 0, 0}, {0, 0xFF, 0, 0xFF}, 4, sizeof(uint8_t) * 4, -1, {0, 1.f, 0, 1.f}); +} + +TEST(ImageTests, Basic8To8) +{ + runTest(32, 32, 4, 4, {0xFF, 0, 0xFF, 0}, {0xFF, 0, 0xFF, 0}); + runTest(32, 32, 4, 4, {128, 0, 0xFF, 99}, {128, 0, 0xFF, 99}); +} + +int main(int argc, char** argv) { + return RUN_ALL_TESTS(); +}