/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "img-precomp.h"  // Precompiled headers
//
#include <mrpt/core/cpu.h>
#include <mrpt/core/get_env.h>
#include <mrpt/core/round.h>  // for round()
#include <mrpt/img/CImage.h>
#include <mrpt/io/CFileInputStream.h>
#include <mrpt/io/CFileOutputStream.h>
#include <mrpt/io/CMemoryStream.h>
#include <mrpt/io/lazy_load_path.h>
#include <mrpt/io/vector_loadsave.h>
#include <mrpt/math/CMatrixF.h>
#include <mrpt/math/fourier.h>
#include <mrpt/math/utils.h>  // for roundup()
#include <mrpt/serialization/CArchive.h>
#include <mrpt/system/CTicTac.h>
#include <mrpt/system/CTimeLogger.h>
#include <mrpt/system/filesystem.h>
#include <mrpt/system/memory.h>

#include <iostream>

// Universal include for all versions of OpenCV
#include <mrpt/3rdparty/do_opencv_includes.h>

#include "CImage_impl.h"

#if MRPT_HAS_MATLAB
#include <mexplus/mxarray.h>
#endif

// Prototypes of SSE2/SSE3/SSSE3 optimized functions:
#include "CImage.SSEx.h"

using namespace mrpt;
using namespace mrpt::img;
using namespace mrpt::math;
using namespace mrpt::system;
using namespace std;

// This must be added to any CSerializable class implementation file.
IMPLEMENTS_SERIALIZABLE(CImage, CSerializable, mrpt::img)

static bool DISABLE_JPEG_COMPRESSION_value = true;
static int SERIALIZATION_JPEG_QUALITY_value = 95;

const thread_local bool MRPT_DEBUG_IMG_LAZY_LOAD =
    mrpt::get_env<bool>("MRPT_DEBUG_IMG_LAZY_LOAD", false);

void CImage::DISABLE_JPEG_COMPRESSION(bool val) { DISABLE_JPEG_COMPRESSION_value = val; }
bool CImage::DISABLE_JPEG_COMPRESSION() { return DISABLE_JPEG_COMPRESSION_value; }
void CImage::SERIALIZATION_JPEG_QUALITY(int q) { SERIALIZATION_JPEG_QUALITY_value = q; }
int CImage::SERIALIZATION_JPEG_QUALITY() { return SERIALIZATION_JPEG_QUALITY_value; }

CExceptionExternalImageNotFound::CExceptionExternalImageNotFound(const std::string& s) :
    std::runtime_error(s)
{
}

const std::string& CImage::getImagesPathBase() { return mrpt::io::getLazyLoadPathBase(); }
void CImage::setImagesPathBase(const std::string& path) { mrpt::io::setLazyLoadPathBase(path); }

// Do performance time logging?
#define IMAGE_ALLOC_PERFLOG 0

#if IMAGE_ALLOC_PERFLOG
mrpt::img::CTimeLogger alloc_tims;
#endif

#if MRPT_HAS_OPENCV
namespace
{
int interpolationMethod2Cv(TInterpolationMethod i)
{
  // clang-format off
	switch (i)
	{
	    case IMG_INTERP_NN:     return cv::INTER_NEAREST;
	    case IMG_INTERP_LINEAR: return cv::INTER_LINEAR;
	    case IMG_INTERP_CUBIC:  return cv::INTER_CUBIC;
	    case IMG_INTERP_AREA:   return cv::INTER_AREA;
	};
  // clang-format on
  return -1;
}
}  // namespace

template <typename RET = uint32_t>
constexpr RET pixelDepth2CvDepth(PixelDepth d)
{
  // clang-format off
	switch (d)
	{
	    case PixelDepth::D8U:  return static_cast<RET>(CV_8U);
	    case PixelDepth::D8S:  return static_cast<RET>(CV_8S);
	    case PixelDepth::D16U: return static_cast<RET>(CV_16U);
	    case PixelDepth::D16S: return static_cast<RET>(CV_16S);
	    case PixelDepth::D32S: return static_cast<RET>(CV_32S);
	    case PixelDepth::D32F: return static_cast<RET>(CV_32F);
	    case PixelDepth::D64F: return static_cast<RET>(CV_64F);
	}
  // clang-format on
  return std::numeric_limits<RET>::max();
}
template <typename RET = uint32_t>
RET pixelDepth2IPLCvDepth(PixelDepth d)
{
  // clang-format off
	switch (d)
	{
	    case PixelDepth::D8U:  return static_cast<RET>(IPL_DEPTH_8U);
	    case PixelDepth::D8S:  return static_cast<RET>(IPL_DEPTH_8S);
	    case PixelDepth::D16U: return static_cast<RET>(IPL_DEPTH_16U);
	    case PixelDepth::D16S: return static_cast<RET>(IPL_DEPTH_16S);
	    case PixelDepth::D32S: return static_cast<RET>(IPL_DEPTH_32S);
	    case PixelDepth::D32F: return static_cast<RET>(IPL_DEPTH_32F);
	    case PixelDepth::D64F: return static_cast<RET>(IPL_DEPTH_64F);
	}
  // clang-format on
  return std::numeric_limits<RET>::max();
}

namespace
{
PixelDepth cvDepth2PixelDepth(int64_t d)
{
  // clang-format off
	switch (d)
	{
	    case CV_8U:  return PixelDepth::D8U;
	    case CV_8S:  return PixelDepth::D8S;
	    case CV_16U: return PixelDepth::D16U;
	    case CV_16S: return PixelDepth::D16S;
	    case CV_32S: return PixelDepth::D32S;
	    case CV_32F: return PixelDepth::D32F;
	    case CV_64F: return PixelDepth::D64F;
	}
  // clang-format on
  return PixelDepth::D8U;
}
}  // namespace
#endif  // MRPT_HAS_OPENCV

// Default ctor
CImage::CImage() : m_impl(mrpt::make_impl<CImage::Impl>()) {}

// Ctor with size
CImage::CImage(unsigned int width, unsigned int height, TImageChannels nChannels) : CImage()
{
  MRPT_START
  resize(width, height, nChannels);
  MRPT_END
}

void CImage::swap(CImage& o)
{
  std::swap(m_impl, o.m_impl);
  std::swap(m_imgIsExternalStorage, o.m_imgIsExternalStorage);
  std::swap(m_externalFile, o.m_externalFile);
}

void CImage::copyFromForceLoad(const CImage& o)
{
  *this = o;
  forceLoad();
}

CImage::CImage(const cv::Mat& img, copy_type_t copy_type) : CImage()
{
#if MRPT_HAS_OPENCV
  MRPT_START
  if (copy_type == DEEP_COPY)
    m_impl->img = img.clone();
  else
    m_impl->img = img;
  MRPT_END
#endif
}

CImage::CImage(const CImage& img, copy_type_t copy_type) : CImage()
{
#if MRPT_HAS_OPENCV
  MRPT_START

  // Also, copy our custom fields, only if making a shallow copy!
  m_imgIsExternalStorage = img.m_imgIsExternalStorage;
  m_externalFile = img.m_externalFile;

  // this new image is *not* lazy-load.
  if (copy_type == DEEP_COPY && !img.asCvMatRef().empty())
  {
    // deep copy
    m_impl->img = img.asCvMatRef().clone();
  }
  else
  {
    // shallow copy
    m_impl->img = img.m_impl->img;
  }
  MRPT_END
#endif
}

CImage CImage::makeDeepCopy() const
{
#if MRPT_HAS_OPENCV
  CImage ret(*this);
  ret.makeSureImageIsLoaded();
  ret.m_impl->img = m_impl->img.clone();
  return ret;
#else
  THROW_EXCEPTION("Operation not supported: build MRPT against OpenCV!");
#endif
}

void CImage::asCvMat(cv::Mat& out_img, copy_type_t copy_type) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  if (copy_type == DEEP_COPY)
    out_img = m_impl->img.clone();
  else
    out_img = m_impl->img;
#endif
}

cv::Mat& CImage::asCvMatRef()
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  return m_impl->img;
#else
  THROW_EXCEPTION("Operation not supported: build MRPT against OpenCV!");
#endif
}

const cv::Mat& CImage::asCvMatRef() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  return m_impl->img;
#else
  THROW_EXCEPTION("Operation not supported: build MRPT against OpenCV!");
#endif
}

void CImage::resize(
    std::size_t width, std::size_t height, TImageChannels nChannels, PixelDepth depth)
{
  MRPT_START

#if MRPT_HAS_OPENCV
  // Dont call makeSureImageIsLoaded() here,
  // since it will throw if resize() is called from a ctor, where it's
  // legit for the img to be uninitialized.

  // If we're resizing to exactly the current size, do nothing:
  if (static_cast<unsigned>(m_impl->img.cols) == width &&
      static_cast<unsigned>(m_impl->img.rows) == height && m_impl->img.channels() == nChannels &&
      m_impl->img.depth() == static_cast<int>(pixelDepth2CvDepth(depth)))
  {
    // Nothing to do:
    return;
  }

#if IMAGE_ALLOC_PERFLOG
  const std::string sLog = mrpt::format("cvCreateImage %ux%u", width, height);
  alloc_tims.enter(sLog.c_str());
#endif

  static_assert(pixelDepth2CvDepth<int>(PixelDepth::D8U) + CV_8UC(3) == CV_8UC3);

  m_impl->img = cv::Mat(
      static_cast<int>(height), static_cast<int>(width),
      pixelDepth2CvDepth<int>(depth) + ((nChannels - 1) << CV_CN_SHIFT));

#if IMAGE_ALLOC_PERFLOG
  alloc_tims.leave(sLog.c_str());
#endif

#else
  THROW_EXCEPTION("MRPT has been compiled with MRPT_HAS_OPENCV=0 !");
#endif
  MRPT_END
}

PixelDepth CImage::getPixelDepth() const
{
  MRPT_START
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  return cvDepth2PixelDepth(m_impl->img.depth());
#else
  THROW_EXCEPTION("MRPT has been compiled with MRPT_HAS_OPENCV=0 !");
#endif
  MRPT_END
}

mrpt::img::CImage CImage::LoadFromFile(const std::string& fileName, int isColor)
{
  CImage im;
  bool ok = im.loadFromFile(fileName, isColor);
  if (!ok) THROW_EXCEPTION_FMT("Error loading image from '%s'", fileName.c_str());
  return im;
}

bool CImage::loadFromFile(const std::string& fileName, int isColor)
{
  MRPT_START

#if MRPT_HAS_OPENCV
#ifdef HAVE_OPENCV_IMGCODECS
  std::vector<uint8_t> fileData;
  if (!mrpt::io::loadBinaryFile(fileData, fileName)) return false;
  const cv::Mat data(fileData.size(), 1, CV_8UC1, fileData.data());

  // Reuse the buffer (save memory allocations) if possible:
  if (m_impl->img.empty())
    m_impl->img = cv::imdecode(data, isColor);
  else
    cv::imdecode(data, isColor, &m_impl->img);
#else
  IplImage* newImg = cvLoadImage(fileName.c_str(), isColor);
  if (!newImg) return false;
  m_impl->img = cv::cvarrToMat(newImg);
#endif

  m_imgIsExternalStorage = false;
  m_externalFile.clear();

  if (m_impl->img.empty()) return false;

  return true;
#else
  THROW_EXCEPTION("MRPT has been compiled with MRPT_HAS_OPENCV=0 !");
#endif
  MRPT_END
}

bool CImage::saveToFile(const std::string& fileName, int jpeg_quality) const
{
  MRPT_START
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  ASSERT_(!m_impl->img.empty());

#ifdef HAVE_OPENCV_IMGCODECS
  const std::vector<int> params = {cv::IMWRITE_JPEG_QUALITY, jpeg_quality};
  return cv::imwrite(fileName, m_impl->img, params);
#else
  int p[3] = {CV_IMWRITE_JPEG_QUALITY, jpeg_quality, 0};
  _IplImage ipl = m_impl->img;
  return (0 != cvSaveImage(fileName.c_str(), &ipl, p));
#endif
#else
  THROW_EXCEPTION("MRPT has been compiled with MRPT_HAS_OPENCV=0 !");
#endif
  MRPT_END
}

void CImage::internal_fromIPL(const IplImage* iplImage, copy_type_t c)
{
  MRPT_START
#if MRPT_HAS_OPENCV
  ASSERT_(iplImage != nullptr);
  clear();
  m_impl->img = cv::cvarrToMat(iplImage, c == DEEP_COPY ? true : false /*copyData*/);
#else
  THROW_EXCEPTION("MRPT has been compiled with MRPT_HAS_OPENCV=0 !");
#endif
  MRPT_END
}

void CImage::loadFromMemoryBuffer(
    unsigned int width, unsigned int height, bool color, unsigned char* rawpixels, bool swapRedBlue)
{
  MRPT_START

#if MRPT_HAS_OPENCV
  resize(width, height, color ? CH_RGB : CH_GRAY);
  m_imgIsExternalStorage = false;
  m_externalFile.clear();

  auto* imgData = m_impl->img.data;
  const auto imgWidthStep = m_impl->img.step[0];

  if (color && swapRedBlue)
  {
    // Do copy & swap at once:
    unsigned char* ptr_src = rawpixels;
    auto* ptr_dest = imgData;
    const int bytes_per_row_out = imgWidthStep;

    for (int h = height; h--;)
    {
      for (unsigned int i = 0; i < width; i++, ptr_src += 3, ptr_dest += 3)
      {
        unsigned char t0 = ptr_src[0], t1 = ptr_src[1], t2 = ptr_src[2];
        ptr_dest[2] = t0;
        ptr_dest[1] = t1;
        ptr_dest[0] = t2;
      }
      ptr_dest += bytes_per_row_out - width * 3;
    }
  }
  else
  {
    if (imgWidthStep == static_cast<size_t>(m_impl->img.cols * m_impl->img.channels()))
    {
      // Copy the image data:
      memcpy(imgData, rawpixels, m_impl->img.dataend - m_impl->img.data);
    }
    else
    {
      // Copy the image row by row:
      unsigned char* ptr_src = rawpixels;
      auto* ptr_dest = imgData;
      int bytes_per_row = width * (color ? 3 : 1);
      int bytes_per_row_out = imgWidthStep;
      for (unsigned int y = 0; y < height; y++)
      {
        memcpy(ptr_dest, ptr_src, bytes_per_row);
        ptr_src += bytes_per_row;
        ptr_dest += bytes_per_row_out;
      }
    }
  }
#else
  THROW_EXCEPTION("MRPT has been compiled with MRPT_HAS_OPENCV=0 !");
#endif
  MRPT_END
}

unsigned char* CImage::operator()(unsigned int ucol, unsigned int urow, unsigned int uchannel) const
{
#if MRPT_HAS_OPENCV

#if defined(_DEBUG) || (MRPT_ALWAYS_CHECKS_DEBUG)
  MRPT_START
#endif

  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  const auto col = static_cast<int>(ucol);
  const auto row = static_cast<int>(urow);
  const auto channel = static_cast<int>(uchannel);

#if defined(_DEBUG) || (MRPT_ALWAYS_CHECKS_DEBUG)
  ASSERT_(m_impl && !m_impl->img.empty());
  if (row >= m_impl->img.rows || col >= m_impl->img.cols || channel >= m_impl->img.channels())
  {
    THROW_EXCEPTION(format(
        "Pixel coordinates/channel out of bounds: row=%u/%u col=%u/%u "
        "chan=%u/%u",
        row, m_impl->img.rows, col, m_impl->img.cols, channel, m_impl->img.channels()));
  }
#endif
  auto p = (&m_impl->img.at<uint8_t>(row, m_impl->img.channels() * col)) + channel;
  return const_cast<unsigned char*>(p);
#if defined(_DEBUG) || (MRPT_ALWAYS_CHECKS_DEBUG)
  MRPT_END
#endif

#else
  THROW_EXCEPTION("MRPT was compiled without OpenCV");
#endif
}

uint8_t* CImage::internal_get(int col, int row, uint8_t channel) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  auto p = (&m_impl->img.at<uint8_t>(row, m_impl->img.channels() * col)) + channel;
  return const_cast<uint8_t*>(p);
#else
  return nullptr;
#endif
}

uint8_t CImage::serializeGetVersion() const
{
#if !MRPT_HAS_OPENCV
  return 100;
#else
  return 9;
#endif
}
void CImage::serializeTo(mrpt::serialization::CArchive& out) const
{
#if !MRPT_HAS_OPENCV
  out << m_imgIsExternalStorage;
  if (m_imgIsExternalStorage) out << m_externalFile;
// Nothing else to serialize!
#else

  // Added in version 6: possibility of being stored offline:
  out << m_imgIsExternalStorage;

  if (m_imgIsExternalStorage)
  {
    out << m_externalFile;
    return;
  }
  // Normal image loaded in memory:
  ASSERT_(m_impl);

  const bool hasColor = m_impl->img.empty() ? false : isColor();

  out << hasColor;

  // Version >2: Color->JPEG, GrayScale->BYTE's array!
  const int32_t width = m_impl->img.cols;
  const int32_t height = m_impl->img.rows;
  if (!hasColor)
  {
    // GRAY-SCALE: Raw bytes:
    // Version 3: ZIP compression!
    // Version 4: Skip zip if the image size <= 16Kb
    int32_t origin = 0;  // not used mrpt v1.9.9
    uint32_t imageSize = height * m_impl->img.step[0];
    // Version 10: depth
    int32_t depth = m_impl->img.depth();

    out << width << height << origin << imageSize << int32_t(cvDepth2PixelDepth(depth));

    // Version 5: Use CImage::DISABLE_ZIP_COMPRESSION
    // Dec 2019: Remove this feature since it's not worth.
    // We still spend 1 byte for this constant bool just not to
    // bump the serialization number.
    bool imageStoredAsZip = false;

    out << imageStoredAsZip;

    if (imageSize > 0 && m_impl->img.data != nullptr) out.WriteBuffer(m_impl->img.data, imageSize);
  }
  else
  {
    // COLOR: High quality JPEG image

    // v7: If size is 0xN or Nx0, don't call
    // "saveToStreamAsJPEG"!!

    // v8: If DISABLE_JPEG_COMPRESSION
    if (!CImage::DISABLE_JPEG_COMPRESSION())
    {
      // normal behavior: compress images:
      out << width << height;

      if (width >= 1 && height >= 1)
      {
        // Save to temporary memory stream:
        mrpt::io::CMemoryStream aux;
        saveToStreamAsJPEG(aux, CImage::SERIALIZATION_JPEG_QUALITY());

        const auto nBytes = static_cast<uint32_t>(aux.getTotalBytesCount());

        out << nBytes;
        out.WriteBuffer(aux.getRawBufferData(), nBytes);
      }
    }
    else
    {  // (New in v8)
      // Don't JPEG-compress behavior:
      // Use negative image sizes to signal this behavior:
      const int32_t neg_width = -width;
      const int32_t neg_height = -height;

      out << neg_width << neg_height;

      // Dump raw image data:
      const auto bytes_per_row = width * 3;

      out.WriteBuffer(m_impl->img.data, bytes_per_row * height);
    }
  }

#endif
}

void CImage::serializeFrom(mrpt::serialization::CArchive& in, uint8_t version)
{
#if !MRPT_HAS_OPENCV
  if (version == 100)
  {
    in >> m_imgIsExternalStorage;
    if (m_imgIsExternalStorage)
      in >> m_externalFile;
    else
    {
      THROW_EXCEPTION(
          "[CImage] Cannot deserialize image since MRPT has been "
          "compiled without OpenCV");
    }
  }
#else
  // First, free current image.
  clear();

  switch (version)
  {
    case 100:  // Saved from an MRPT build without OpenCV:
    {
      in >> m_imgIsExternalStorage;
      if (m_imgIsExternalStorage) in >> m_externalFile;
    }
    break;
    case 0:
    {
      uint32_t width, height, nChannels, imgLength;
      uint8_t originTopLeft;

      in >> width >> height >> nChannels >> originTopLeft >> imgLength;

      resize(width, height, static_cast<TImageChannels>(nChannels));
      in.ReadBuffer(m_impl->img.data, imgLength);
    }
    break;
    case 1:
    {
      // Version 1: High quality JPEG image
      uint32_t nBytes;
      in >> nBytes;
      std::vector<uint8_t> buf(nBytes);
      in.ReadBuffer(buf.data(), nBytes);

      mrpt::io::CMemoryStream aux;
      aux.assignMemoryNotOwn(buf.data(), buf.size());
      aux.Seek(0);
      loadFromStreamAsJPEG(aux);
    }
    break;
    case 2:
    case 3:
    case 4:
    case 5:
    case 6:
    case 7:
    case 8:
    case 9:
    {
      // Version 6: 	m_imgIsExternalStorage ??
      if (version >= 6)
        in >> m_imgIsExternalStorage;
      else
        m_imgIsExternalStorage = false;

      if (m_imgIsExternalStorage)
      {
        // Just the file name:
        in >> m_externalFile;
      }
      else
      {  // Normal, the whole image data:

        // Version 2: Color->JPEG, GrayScale->BYTE's array!
        uint8_t hasColor;
        in >> hasColor;
        if (!hasColor)
        {
          // GRAY SCALE:
          int32_t width, height, origin, imageSize;
          in >> width >> height >> origin >> imageSize;
          PixelDepth depth(PixelDepth::D8U);
          if (version >= 9)
          {
            int32_t tempdepth;
            in >> tempdepth;
            depth = PixelDepth(tempdepth);
          }
          resize(static_cast<uint32_t>(width), static_cast<uint32_t>(height), CH_GRAY, depth);
          ASSERT_EQUAL_(
              static_cast<uint32_t>(imageSize),
              static_cast<uint32_t>(height) * m_impl->img.step[0]);

          if (version == 2)
          {
            // RAW BYTES:
            in.ReadBuffer(m_impl->img.data, imageSize);
          }
          else
          {
            // Version 3: ZIP compression!
            bool imageIsZIP = true;

            // Version 4: Skip zip if the image size <= 16Kb
            // Version 5: Use CImage::DISABLE_ZIP_COMPRESSION
            if (version == 4 && imageSize <= 16 * 1024) imageIsZIP = false;

            if (version >= 5)
            {
              // It is stored int the stream:
              in >> imageIsZIP;
            }

            if (imageIsZIP)
            {
              uint32_t zipDataLen;
              in >> zipDataLen;
              THROW_EXCEPTION(
                  "ZIP image deserialization not supported "
                  "anymore");
            }
            else
            {
              // Raw bytes:
              if (imageSize) in.ReadBuffer(m_impl->img.data, imageSize);
            }
          }
        }
        else
        {
          bool loadJPEG = true;

          if (version >= 7)
          {
            int32_t width, height;
            in >> width >> height;

            if (width >= 1 && height >= 1)
            {
              loadJPEG = true;
            }
            else
            {
              loadJPEG = false;

              if (width < 0 && height < 0)
              {
                // v8: raw image:
                const int32_t real_w = -width;
                const int32_t real_h = -height;

                resize(real_w, real_h, CH_RGB);

                auto& img = m_impl->img;
                const size_t bytes_per_row = img.cols * 3;
                for (int y = 0; y < img.rows; y++)
                {
                  const size_t nRead = in.ReadBuffer(img.ptr<void>(y), bytes_per_row);
                  if (nRead != bytes_per_row)
                    THROW_EXCEPTION(
                        "Error: Truncated data stream "
                        "while parsing raw image?");
                }
              }
              else
              {
                // it's a 0xN or Nx0 image: just resize and
                // load nothing:
                resize(width, height, CH_RGB);
              }
            }
          }

          // COLOR IMAGE: JPEG
          if (loadJPEG)
          {
            uint32_t nBytes;
            in >> nBytes;

            std::vector<uint8_t> buf(nBytes);
            in.ReadBuffer(buf.data(), nBytes);

            mrpt::io::CMemoryStream aux;
            aux.assignMemoryNotOwn(buf.data(), buf.size());
            aux.Seek(0);

            loadFromStreamAsJPEG(aux);
          }
        }
      }
    }
    break;
    default:
      MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(version);
  };
#endif
}

/*---------------------------------------------------------------
Implements the writing to a mxArray for Matlab
---------------------------------------------------------------*/
#if MRPT_HAS_MATLAB
// Add to implement mexplus::from template specialization
IMPLEMENTS_MEXPLUS_FROM(mrpt::img::CImage)
#endif

mxArray* CImage::writeToMatlab() const
{
#if MRPT_HAS_MATLAB
  return mexplus::from(this->asCvMatRef());
#else
  THROW_EXCEPTION("MRPT built without MATLAB/Mex support");
#endif
}

void CImage::getSize(TImageSize& s) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  s.x = m_impl->img.cols;
  s.y = m_impl->img.rows;
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

size_t CImage::getWidth() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  return m_impl->img.cols;
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

std::string CImage::getChannelsOrder() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  // modern opencv versions used these fixed order (see opencv
  // core/src/array.cpp)
  const int chCount = m_impl->img.channels();
  ASSERT_GE_(chCount, 1);
  ASSERT_LE_(chCount, 4);
  const std::array<const char*, 4> orderNames = {"GRAY", "", "BGR", "BGRA"};
  return std::string(orderNames.at(chCount - 1));
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

size_t CImage::getRowStride() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  return m_impl->img.step[0];
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

size_t CImage::getHeight() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  return m_impl->img.rows;
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

bool CImage::isColor() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  return m_impl->img.channels() >= 3;
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

int CImage::channelCount() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  return m_impl->img.channels();
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

bool CImage::isEmpty() const
{
#if MRPT_HAS_OPENCV
  return !m_imgIsExternalStorage && m_impl->img.empty();
#else
  return true;
#endif
}

TImageChannels CImage::getChannelCount() const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  return static_cast<TImageChannels>(m_impl->img.channels());
#else
  THROW_EXCEPTION("MRPT built without OpenCV support");
#endif
}

bool CImage::isOriginTopLeft() const
{
  return true;  // As of mrpt v1.9.9
}

float CImage::getAsFloat(unsigned int col, unsigned int row, unsigned int channel) const
{
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  // [0,255]->[0,1]
  return (*(*this)(col, row, channel)) / 255.0f;
}

float CImage::getAsFloat(unsigned int col, unsigned int row) const
{
  // Is a RGB image??
  if (isColor())
  {
    // Luminance: Y = 0.3R + 0.59G + 0.11B
    unsigned char* pixels = (*this)(col, row, 0);
    return (pixels[0] * 0.3f + pixels[1] * 0.59f + pixels[2] * 0.11f) / 255.0f;
  }
  else
  {
    // [0,255]->[0,1]
    return (*(*this)(col, row, 0 /* Channel 0:Gray level */)) / 255.0f;
  }
}

/*---------------------------------------------------------------
          getMaxAsFloat
---------------------------------------------------------------*/
float CImage::getMaxAsFloat() const
{
  int x, y, cx = getWidth(), cy = getHeight();

  float maxPixel = 0;

  for (x = 0; x < cx; x++)
    for (y = 0; y < cy; y++) maxPixel = max(maxPixel, getAsFloat(x, y));

  return maxPixel;
}

CImage CImage::grayscale() const
{
  CImage ret;
  grayscale(ret);
  return ret;
}

// Auxiliary function for both ::grayscale() and ::grayscaleInPlace()
#if MRPT_HAS_OPENCV
namespace
{
bool my_img_to_grayscale(const cv::Mat& src, cv::Mat& dest)
{
  if (dest.size() != src.size() || dest.type() != src.type())
    dest = cv::Mat(src.rows, src.cols, CV_8UC1);

    // If possible, use SSE optimized version:
#if MRPT_ARCH_INTEL_COMPATIBLE
  if ((src.step[0] & 0x0f) == 0 && (dest.step[0] & 0x0f) == 0 &&
      mrpt::cpu::supports(mrpt::cpu::feature::SSSE3))
  {
    image_SSSE3_bgr_to_gray_8u(
        src.ptr<uint8_t>(), dest.ptr<uint8_t>(), src.cols, src.rows, src.step[0], dest.step[0]);
    return true;
  }
#endif

  // OpenCV Method:
  cv::cvtColor(src, dest, cv::COLOR_BGR2GRAY);
  return false;
}
}  // namespace
#endif

bool CImage::grayscale(CImage& ret) const
{
#if MRPT_HAS_OPENCV
  // The image is already grayscale??
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  if (m_impl->img.channels() == 1)
  {
    ret = *this;  // shallow copy
    return true;
  }
  else
  {
    // Convert to a single luminance channel image
    cv::Mat src = m_impl->img;
    // Detect in-place op and make deep copy:
    if (src.data == ret.m_impl->img.data) src = src.clone();

    return my_img_to_grayscale(src, ret.m_impl->img);
  }
#else
  THROW_EXCEPTION("Operation not supported: build MRPT against OpenCV!");
#endif
}

bool CImage::scaleHalf(CImage& out, TInterpolationMethod interp) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  // Get this image size:
  auto& img = m_impl->img;
  const int w = img.cols, h = img.rows;

  // Create target image:
  out.resize(w >> 1, h >> 1, getChannelCount());
  auto& img_out = out.m_impl->img;

// If possible, use SSE optimized version:
#if MRPT_ARCH_INTEL_COMPATIBLE
  if (img.channels() == 3 && interp == IMG_INTERP_NN &&
      mrpt::cpu::supports(mrpt::cpu::feature::SSSE3))
  {
    image_SSSE3_scale_half_3c8u(img.data, img_out.data, w, h, img.step[0], img_out.step[0]);
    return true;
  }

  if (img.channels() == 1 && mrpt::cpu::supports(mrpt::cpu::feature::SSE2))
  {
    if (interp == IMG_INTERP_NN)
    {
      image_SSE2_scale_half_1c8u(img.data, img_out.data, w, h, img.step[0], img_out.step[0]);
      return true;
    }
    else if (interp == IMG_INTERP_LINEAR)
    {
      image_SSE2_scale_half_smooth_1c8u(img.data, img_out.data, w, h, img.step[0], img_out.step[0]);
      return true;
    }
  }
#endif

  // Fall back to slow method:
  cv::resize(img, img_out, img_out.size(), 0, 0, interpolationMethod2Cv(interp));
  return false;
#else
  THROW_EXCEPTION("Operation not supported: build MRPT against OpenCV!");
#endif
}

void CImage::scaleDouble(CImage& out, TInterpolationMethod interp) const
{
  out = *this;
  const TImageSize siz = this->getSize();
  out.scaleImage(out, siz.x * 2, siz.y * 2, interp);
}

void CImage::loadFromMemoryBuffer(
    unsigned int width,
    unsigned int height,
    unsigned int bytesPerRow,
    unsigned char* red,
    unsigned char* green,
    unsigned char* blue)
{
#if MRPT_HAS_OPENCV
  MRPT_START

  resize(width, height, CH_RGB, PixelDepth::D8U);

  // Copy the image data:
  for (unsigned int y = 0; y < height; y++)
  {
    // The target pixels:
    auto* dest = m_impl->img.ptr<uint8_t>(y);

    // Source channels:
    unsigned char* srcR = red + bytesPerRow * y;
    unsigned char* srcG = green + bytesPerRow * y;
    unsigned char* srcB = blue + bytesPerRow * y;

    for (unsigned int x = 0; x < width; x++)
    {
      *(dest++) = *(srcB++);
      *(dest++) = *(srcG++);
      *(dest++) = *(srcR++);
    }  // end of x
  }    // end of y

  MRPT_END
#endif
}

void CImage::setPixel(int x, int y, size_t color)
{
#if MRPT_HAS_OPENCV

#if defined(_DEBUG) || (MRPT_ALWAYS_CHECKS_DEBUG)
  MRPT_START
#endif

  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  auto& img = m_impl->img;

  ASSERT_(this->getPixelDepth() == mrpt::img::PixelDepth::D8U);

  if (x >= 0 && y >= 0 && y < img.rows && x < img.cols)
  {
    // The pixel coordinates is valid:
    if (img.channels() == 1)
    {
      img.ptr<uint8_t>(y)[x] = static_cast<uint8_t>(color);
    }
    else
    {
#if defined(_DEBUG) || (MRPT_ALWAYS_CHECKS_DEBUG)
      ASSERT_(img.channels() == 3);
#endif
      auto* dest = &img.ptr<uint8_t>(y)[3 * x];
      const auto* src = reinterpret_cast<uint8_t*>(&color);
      // Copy the color:
      *dest++ = *src++;  // R
      *dest++ = *src++;  // G
      *dest++ = *src++;  // B
    }
  }

#if defined(_DEBUG) || (MRPT_ALWAYS_CHECKS_DEBUG)
  MRPT_END
#endif

#endif
}

void CImage::line(
    int x0,
    int y0,
    int x1,
    int y1,
    const mrpt::img::TColor color,
    unsigned int width,
    [[maybe_unused]] TPenStyle penStyle)
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  cv::line(
      m_impl->img, cv::Point(x0, y0), cv::Point(x1, y1), CV_RGB(color.R, color.G, color.B),
      static_cast<int>(width));
#endif
}

void CImage::drawCircle(
    int x, int y, int radius, const mrpt::img::TColor& color, unsigned int width)
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  cv::circle(
      m_impl->img, cv::Point(x, y), radius, CV_RGB(color.R, color.G, color.B),
      static_cast<int>(width));
#endif
}

void CImage::filledRectangle(int x0, int y0, int x1, int y1, const mrpt::img::TColor color)
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  cv::rectangle(m_impl->img, {x0, y0}, {x1, y1}, CV_RGB(color.R, color.G, color.B), cv::FILLED);
#endif
}

void CImage::drawImage(int x, int y, const mrpt::img::CImage& img)
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  img.makeSureImageIsLoaded();

  cv::Rect roi(cv::Point(x, y), cv::Size(img.getWidth(), img.getHeight()));
  cv::Mat dest = m_impl->img(roi);
  img.m_impl->img.copyTo(dest);
#endif
}

void CImage::update_patch(const CImage& patch, const unsigned int col_, const unsigned int row_)
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  const auto& src = m_impl->img;
  auto& dest = patch.m_impl->img;

  src(cv::Rect(col_, row_, dest.cols, dest.rows)).copyTo(dest);
#endif
}

void CImage::extract_patch(
    CImage& patch,
    const unsigned int col_,
    const unsigned int row_,
    const unsigned int col_num,
    const unsigned int row_num) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  const auto& src = m_impl->img;
  auto& dest = patch.m_impl->img;

  src(cv::Rect(col_, row_, col_num, row_num)).copyTo(dest);
#endif
}

float CImage::correlate(const CImage& img2, int width_init, int height_init) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  if ((img2.getWidth() + width_init > getWidth()) | (img2.getHeight() + height_init > getHeight()))
    THROW_EXCEPTION("Correlation Error!, image to correlate out of bounds");

  float x1, x2;
  float syy = 0.0f, sxy = 0.0f, sxx = 0.0f, m1 = 0.0f, m2 = 0.0f,
        n = (float)(img2.getHeight() * img2.getWidth());

  // find the means
  for (size_t i = 0; i < img2.getHeight(); i++)
  {
    for (size_t j = 0; j < img2.getWidth(); j++)
    {
      m1 += *(*this)(j + width_init, i + height_init);
      m2 += *img2(j, i);
    }
  }
  m1 /= n;
  m2 /= n;

  for (size_t i = 0; i < img2.getHeight(); i++)
  {
    for (size_t j = 0; j < img2.getWidth(); j++)
    {
      x1 = *(*this)(j + width_init, i + height_init) - m1;
      x2 = *img2(j, i) - m2;
      sxx += x1 * x1;
      syy += x2 * x2;
      sxy += x1 * x2;
    }
  }

  return sxy / sqrt(sxx * syy);
#else
  return 0;
#endif
}

void CImage::normalize()
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  cv::normalize(m_impl->img, m_impl->img, 255, 0, cv::NORM_MINMAX);
#endif
}

void CImage::getAsMatrix(
    CMatrixFloat& outMatrix,
    bool doResize,
    int x_min,
    int y_min,
    int x_max,
    int y_max,
    bool normalize_01) const
{
#if MRPT_HAS_OPENCV
  MRPT_START
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  const auto& img = m_impl->img;

  // Set sizes:
  if (x_max == -1) x_max = img.cols - 1;
  if (y_max == -1) y_max = img.rows - 1;

  ASSERT_(x_min >= 0 && x_min < img.cols && x_min < x_max);
  ASSERT_(y_min >= 0 && y_min < img.rows && y_min < y_max);

  int lx = (x_max - x_min + 1);
  int ly = (y_max - y_min + 1);

  if (doResize || outMatrix.rows() < ly || outMatrix.cols() < lx)
    outMatrix.setSize(y_max - y_min + 1, x_max - x_min + 1);

  const bool is_color = isColor();

  // Luminance: Y = 0.3R + 0.59G + 0.11B
  for (int y = 0; y < ly; y++)
  {
    const uint8_t* pixels = ptr<uint8_t>(x_min, y_min + y);
    for (int x = 0; x < lx; x++)
    {
      float aux;
      if (is_color)
      {
        aux = *pixels++ * 0.3f;
        aux += *pixels++ * 0.59f;
        aux += *pixels++ * 0.11f;
      }
      else
      {
        aux = (*pixels++);
      }
      outMatrix.coeffRef(y, x) = aux;
    }
  }
  if (normalize_01) outMatrix *= (1.0f / 255);

  MRPT_END
#endif
}

void CImage::getAsMatrix(
    CMatrix_u8& outMatrix, bool doResize, int x_min, int y_min, int x_max, int y_max) const
{
#if MRPT_HAS_OPENCV
  MRPT_START
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  const auto& img = m_impl->img;

  // Set sizes:
  if (x_max == -1) x_max = img.cols - 1;
  if (y_max == -1) y_max = img.rows - 1;

  ASSERT_(x_min >= 0 && x_min < img.cols && x_min < x_max);
  ASSERT_(y_min >= 0 && y_min < img.rows && y_min < y_max);

  int lx = (x_max - x_min + 1);
  int ly = (y_max - y_min + 1);

  if (doResize || outMatrix.rows() < ly || outMatrix.cols() < lx)
    outMatrix.setSize(y_max - y_min + 1, x_max - x_min + 1);

  const bool is_color = isColor();

  // Luminance: Y = 0.3R + 0.59G + 0.11B
  for (int y = 0; y < ly; y++)
  {
    const uint8_t* pixels = ptr<uint8_t>(x_min, y_min + y);
    for (int x = 0; x < lx; x++)
    {
      if (is_color)
      {
        unsigned int aux = *pixels++ * 3000;  // 0.3f;
        aux += *pixels++ * 5900;              // 0.59f;
        aux += *pixels++ * 1100;              // 0.11f;
        outMatrix.coeffRef(y, x) = static_cast<uint8_t>(aux / 1000);
      }
      else
      {
        outMatrix.coeffRef(y, x) = (*pixels++);
      }
    }
  }

  MRPT_END
#endif
}
void CImage::getAsRGBMatrices(
    mrpt::math::CMatrixFloat& R,
    mrpt::math::CMatrixFloat& G,
    mrpt::math::CMatrixFloat& B,
    bool doResize,
    int x_min,
    int y_min,
    int x_max,
    int y_max) const
{
#if MRPT_HAS_OPENCV
  MRPT_START

  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  const auto& img = m_impl->img;

  // Set sizes:
  if (x_max == -1) x_max = img.cols - 1;
  if (y_max == -1) y_max = img.rows - 1;

  ASSERT_(x_min >= 0 && x_min < img.cols && x_min < x_max);
  ASSERT_(y_min >= 0 && y_min < img.rows && y_min < y_max);

  int lx = (x_max - x_min + 1);
  int ly = (y_max - y_min + 1);

  if (doResize || R.rows() < ly || R.cols() < lx) R.setSize(ly, lx);
  if (doResize || G.rows() < ly || G.cols() < lx) G.setSize(ly, lx);
  if (doResize || B.rows() < ly || B.cols() < lx) B.setSize(ly, lx);

  const bool is_color = isColor();
  for (int y = 0; y < ly; y++)
  {
    const uint8_t* pixels = ptr<uint8_t>(x_min, y_min + y);
    for (int x = 0; x < lx; x++)
    {
      if (is_color)
      {
        R.coeffRef(y, x) = u8tof(*pixels++);
        G.coeffRef(y, x) = u8tof(*pixels++);
        B.coeffRef(y, x) = u8tof(*pixels++);
      }
      else
      {
        R.coeffRef(y, x) = G.coeffRef(y, x) = B.coeffRef(y, x) = u8tof(*pixels++);
      }
    }
  }

  MRPT_END
#endif
}

void CImage::getAsRGBMatrices(
    mrpt::math::CMatrix_u8& R,
    mrpt::math::CMatrix_u8& G,
    mrpt::math::CMatrix_u8& B,
    bool doResize,
    int x_min,
    int y_min,
    int x_max,
    int y_max) const
{
#if MRPT_HAS_OPENCV
  MRPT_START

  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  const auto& img = m_impl->img;

  // Set sizes:
  if (x_max == -1) x_max = img.cols - 1;
  if (y_max == -1) y_max = img.rows - 1;

  ASSERT_(x_min >= 0 && x_min < img.cols && x_min < x_max);
  ASSERT_(y_min >= 0 && y_min < img.rows && y_min < y_max);

  int lx = (x_max - x_min + 1);
  int ly = (y_max - y_min + 1);

  if (doResize || R.rows() < ly || R.cols() < lx) R.setSize(ly, lx);
  if (doResize || G.rows() < ly || G.cols() < lx) G.setSize(ly, lx);
  if (doResize || B.rows() < ly || B.cols() < lx) B.setSize(ly, lx);

  const bool is_color = isColor();
  for (int y = 0; y < ly; y++)
  {
    const uint8_t* pixels = ptr<uint8_t>(x_min, y_min + y);
    for (int x = 0; x < lx; x++)
    {
      if (is_color)
      {
        R.coeffRef(y, x) = *pixels++;
        G.coeffRef(y, x) = *pixels++;
        B.coeffRef(y, x) = *pixels++;
      }
      else
      {
        R.coeffRef(y, x) = G.coeffRef(y, x) = B.coeffRef(y, x) = *pixels++;
      }
    }
  }

  MRPT_END
#endif
}

void CImage::cross_correlation_FFT(
    const CImage& in_img,
    CMatrixFloat& out_corr,
    int u_search_ini,
    int v_search_ini,
    int u_search_size,
    int v_search_size,
    float biasThisImg,
    float biasInImg) const
{
  MRPT_START

  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  // Set limits:
  if (u_search_ini == -1) u_search_ini = 0;
  if (v_search_ini == -1) v_search_ini = 0;
  if (u_search_size == -1) u_search_size = static_cast<int>(getWidth());
  if (v_search_size == -1) v_search_size = static_cast<int>(getHeight());

  int u_search_end = u_search_ini + u_search_size - 1;
  int v_search_end = v_search_ini + v_search_size - 1;

  ASSERT_(u_search_end < static_cast<int>(getWidth()));
  ASSERT_(v_search_end < static_cast<int>(getHeight()));

  // Find smallest valid size:
  size_t x, y;
  size_t actual_lx = std::max(static_cast<size_t>(u_search_size), in_img.getWidth());
  size_t actual_ly = std::max(static_cast<size_t>(v_search_size), in_img.getHeight());
  size_t lx = mrpt::round2up<size_t>(actual_lx);
  size_t ly = mrpt::round2up<size_t>(actual_ly);

  CMatrixF i1(ly, lx), i2(ly, lx);

  // We fill the images with the bias, such as when we substract the bias
  // later on,
  //  those pixels not really occupied by the image really becomes zero:
  i1.fill(biasInImg);
  i2.fill(biasThisImg);

  // Get as matrixes, padded with zeros up to power-of-two sizes:
  getAsMatrix(
      i2, false, u_search_ini, v_search_ini, u_search_ini + u_search_size - 1,
      v_search_ini + v_search_size - 1);
  in_img.getAsMatrix(i1, false);

  // Remove the bias now:
  i2 -= biasThisImg;
  i1 -= biasInImg;

  // FFT:
  CMatrixF I1_R, I1_I, I2_R, I2_I, ZEROS(ly, lx);
  math::dft2_complex(i1, ZEROS, I1_R, I1_I);
  math::dft2_complex(i2, ZEROS, I2_R, I2_I);

  // Compute the COMPLEX division of I2 by I1:
  for (y = 0; y < ly; y++)
    for (x = 0; x < lx; x++)
    {
      float r1 = I1_R(y, x);
      float r2 = I2_R(y, x);

      float ii1 = I1_I(y, x);
      float ii2 = I2_I(y, x);

      float den = square(r1) + square(ii1);
      I2_R(y, x) = (r1 * r2 + ii1 * ii2) / den;
      I2_I(y, x) = (ii2 * r1 - r2 * ii1) / den;
    }

  // IFFT:
  CMatrixF res_R, res_I;
  math::idft2_complex(I2_R, I2_I, res_R, res_I);

  out_corr.setSize(actual_ly, actual_lx);
  for (y = 0; y < actual_ly; y++)
    for (x = 0; x < actual_lx; x++)
      out_corr(y, x) = sqrt(square(res_R(y, x)) + square(res_I(y, x)));

  MRPT_END
}

void CImage::getAsMatrixTiled(CMatrixFloat& outMatrix) const
{
#if MRPT_HAS_OPENCV
  MRPT_START

  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  const auto& img = m_impl->img;

  // The size of the matrix:
  const auto matrix_lx = outMatrix.cols();
  const auto matrix_ly = outMatrix.rows();

  if (isColor())
  {
    // Luminance: Y = 0.3R + 0.59G + 0.11B
    for (CMatrixFloat::Index y = 0; y < matrix_ly; y++)
    {
      unsigned char* min_pixels = (*this)(0, y % img.rows, 0);
      unsigned char* max_pixels = min_pixels + img.cols * 3;
      unsigned char* pixels = min_pixels;
      float aux;
      for (CMatrixFloat::Index x = 0; x < matrix_lx; x++)
      {
        aux = *pixels++ * 0.30f;
        aux += *pixels++ * 0.59f;
        aux += *pixels++ * 0.11f;
        outMatrix(y, x) = aux;
        if (pixels >= max_pixels) pixels = min_pixels;
      }
    }
  }
  else
  {
    for (CMatrixFloat::Index y = 0; y < matrix_ly; y++)
    {
      unsigned char* min_pixels = (*this)(0, y % img.rows, 0);
      unsigned char* max_pixels = min_pixels + img.cols;
      unsigned char* pixels = min_pixels;
      for (CMatrixFloat::Index x = 0; x < matrix_lx; x++)
      {
        outMatrix(y, x) = *pixels++;
        if (pixels >= max_pixels) pixels = min_pixels;
      }
    }
  }

  MRPT_END
#endif
}

void CImage::clear()
{
  // Reset to defaults:
  *this = CImage();
}

void CImage::setExternalStorage(const std::string& fileName) noexcept
{
  clear();
  m_externalFile = fileName;
  m_imgIsExternalStorage = true;
}

void CImage::unload() const noexcept
{
#if MRPT_HAS_OPENCV
  if (m_imgIsExternalStorage)
  {
    if (MRPT_DEBUG_IMG_LAZY_LOAD)
      std::cout << "[CImage::unload()] Called on this=" << reinterpret_cast<const void*>(this)
                << std::endl;

    const_cast<cv::Mat&>(m_impl->img) = cv::Mat();
  }
#endif
}

void CImage::makeSureImageIsLoaded(bool allowNonInitialized) const
{
#if MRPT_HAS_OPENCV
  if (!m_impl->img.empty()) return;  // OK, continue
#endif

  if (m_imgIsExternalStorage)
  {
    // Load the file:
    string wholeFile;
    getExternalStorageFileAbsolutePath(wholeFile);

    const std::string tmpFile = m_externalFile;

    bool ret = const_cast<CImage*>(this)->loadFromFile(wholeFile);

    // These are removed by "loadFromFile", and that's good, just fix it
    // here and carry on.
    m_imgIsExternalStorage = true;
    m_externalFile = tmpFile;

    if (!ret)
      THROW_TYPED_EXCEPTION_FMT(
          CExceptionExternalImageNotFound, "Error loading externally-stored image from: %s",
          wholeFile.c_str());

    if (MRPT_DEBUG_IMG_LAZY_LOAD)
      std::cout << "[CImage] Loaded lazy-load image file '" << wholeFile
                << "' on this=" << reinterpret_cast<const void*>(this) << std::endl;
  }
  else if (!allowNonInitialized)
  {
    THROW_EXCEPTION(
        "Trying to access uninitialized image in a non "
        "externally-stored "
        "image.");
  }
}

void CImage::getExternalStorageFileAbsolutePath(std::string& out_path) const
{
  out_path = mrpt::io::lazy_load_absolute_path(m_externalFile);
}

void CImage::flipVertical()
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  cv::flip(m_impl->img, m_impl->img, 0 /* x-axis */);
#endif
}

void CImage::flipHorizontal()
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();
  cv::flip(m_impl->img, m_impl->img, 1 /* y-axis */);
#endif
}

void CImage::swapRB()
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  cv::cvtColor(m_impl->img, m_impl->img, cv::COLOR_RGB2BGR);
#endif
}

void CImage::rectifyImageInPlace(void* mapX, void* mapY)
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  auto& srcImg = m_impl->img;
  cv::Mat outImg(srcImg.rows, srcImg.cols, srcImg.type());

  auto mapXm = static_cast<cv::Mat*>(mapX);
  auto mapYm = static_cast<cv::Mat*>(mapX);

  cv::remap(srcImg, outImg, *mapXm, *mapYm, cv::INTER_CUBIC);

  clear();
  srcImg = outImg;
#endif
}

void CImage::undistort(CImage& out_img, const mrpt::img::TCamera& cameraParams) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  ASSERTMSG_(out_img.m_impl->img.data != m_impl->img.data, "In-place undistort() not supported");

  auto& srcImg = const_cast<cv::Mat&>(m_impl->img);
  // This will avoid re-alloc if size already matches.
  out_img.resize(srcImg.cols, srcImg.rows, getChannelCount());

  const auto& intrMat = cameraParams.intrinsicParams;
  const auto& dist = cameraParams.dist;

  cv::Mat distM(1, dist.size(), CV_64F, const_cast<double*>(&dist[0]));
  cv::Mat inMat(3, 3, CV_64F);

  for (int i = 0; i < 3; i++)
    for (int j = 0; j < 3; j++) inMat.at<double>(i, j) = intrMat(i, j);

  cv::undistort(srcImg, out_img.m_impl->img, inMat, distM);
#endif
}

void CImage::filterMedian(CImage& out_img, int W) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  auto srcImg = const_cast<cv::Mat&>(m_impl->img);
  if (this == &out_img)
    srcImg = srcImg.clone();
  else
    out_img.resize(srcImg.cols, srcImg.rows, getChannelCount());

  cv::medianBlur(srcImg, out_img.m_impl->img, W);
#endif
}

void CImage::filterGaussian(CImage& out_img, int W, int H, double sigma) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally
  auto srcImg = const_cast<cv::Mat&>(m_impl->img);
  if (this == &out_img)
    srcImg = srcImg.clone();
  else
    out_img.resize(srcImg.cols, srcImg.rows, getChannelCount());

  cv::GaussianBlur(srcImg, out_img.m_impl->img, cv::Size(W, H), sigma);
#endif
}

void CImage::scaleImage(
    CImage& out_img, unsigned int width, unsigned int height, TInterpolationMethod interp) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  auto srcImg = m_impl->img;
  // Detect in-place operation and make a deep copy if needed:
  if (out_img.m_impl->img.data == srcImg.data) srcImg = srcImg.clone();

  // Already done?
  if (srcImg.cols == static_cast<int>(width) && srcImg.rows == static_cast<int>(height))
  {
    out_img.m_impl->img = srcImg;
    return;
  }
  out_img.resize(width, height, getChannelCount());

  // Resize:
  cv::resize(
      srcImg, out_img.m_impl->img, out_img.m_impl->img.size(), 0, 0,
      interpolationMethod2Cv(interp));
#endif
}

void CImage::rotateImage(
    CImage& out_img, double ang, unsigned int cx, unsigned int cy, double scale) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();  // For delayed loaded images stored externally

  auto srcImg = m_impl->img;
  // Detect in-place operation and make a deep copy if needed:
  if (out_img.m_impl->img.data == srcImg.data) srcImg = srcImg.clone();

  // quick rotation?
  if (std::abs(M_PI * 0.5 - std::abs(ang)) < 1e-3 || std::abs(M_PI - std::abs(ang)) < 1e-3)
  {
    int rotCode = 0;
    if (std::abs(M_PI * 0.5 - ang) < 1e-3)
      rotCode = cv::ROTATE_90_COUNTERCLOCKWISE;
    else if (std::abs(-M_PI * 0.5 - ang) < 1e-3)
      rotCode = cv::ROTATE_90_CLOCKWISE;
    else if (std::abs(M_PI - ang) < 1e-3)
      rotCode = cv::ROTATE_180;

    cv::rotate(srcImg, out_img.m_impl->img, rotCode);
    return;
  }
  // else: general rotation:

  out_img.resize(getWidth(), getHeight(), getChannelCount());

  // Based on the blog entry:
  // http://blog.weisu.org/2007/12/opencv-image-rotate-and-zoom-rotation.html

  // Apply rotation & scale:
  double m[2 * 3] = {scale * cos(ang), -scale * sin(ang), 1.0 * cx,
                     scale * sin(ang), scale * cos(ang),  1.0 * cy};
  cv::Mat M(2, 3, CV_64F, m);

  double dx = (srcImg.cols - 1) * 0.5;
  double dy = (srcImg.rows - 1) * 0.5;
  m[2] -= m[0] * dx + m[1] * dy;
  m[5] -= m[3] * dx + m[4] * dy;

  cv::warpAffine(
      srcImg, out_img.m_impl->img, M, out_img.m_impl->img.size(),
      cv::INTER_LINEAR + cv::WARP_INVERSE_MAP, cv::BORDER_REPLICATE);
#endif
}

bool CImage::drawChessboardCorners(
    const std::vector<TPixelCoordf>& cornerCoords,
    unsigned int check_size_x,
    unsigned int check_size_y,
    unsigned int lines_width,
    unsigned int r)
{
#if MRPT_HAS_OPENCV

  if (cornerCoords.size() != check_size_x * check_size_y) return false;

  auto& img = m_impl->img;

  unsigned int x, y, i;
  cv::Point prev_pt = cvPoint(0, 0);
  const int line_max = 8;
  cv::Scalar line_colors[8];

  line_colors[0] = CV_RGB(255, 0, 0);
  line_colors[1] = CV_RGB(255, 128, 0);
  line_colors[2] = CV_RGB(255, 128, 0);
  line_colors[3] = CV_RGB(200, 200, 0);
  line_colors[4] = CV_RGB(0, 255, 0);
  line_colors[5] = CV_RGB(0, 200, 200);
  line_colors[6] = CV_RGB(0, 0, 255);
  line_colors[7] = CV_RGB(255, 0, 255);

  CCanvas::selectTextFont("10x20");

  for (y = 0, i = 0; y < check_size_y; y++)
  {
    const auto color = line_colors[y % line_max];
    for (x = 0; x < check_size_x; x++, i++)
    {
      cv::Point pt;
      pt.x = cvRound(cornerCoords[i].x);
      pt.y = cvRound(cornerCoords[i].y);

      if (i != 0) cv::line(img, prev_pt, pt, color, lines_width);

      cv::line(img, cvPoint(pt.x - r, pt.y - r), cvPoint(pt.x + r, pt.y + r), color, lines_width);
      cv::line(img, cvPoint(pt.x - r, pt.y + r), cvPoint(pt.x + r, pt.y - r), color, lines_width);

      if (r > 0) cv::circle(img, pt, r + 1, color);
      prev_pt = pt;

      // Text label with the corner index in the first and last
      // corners:
      if (i == 0 || i == cornerCoords.size() - 1)
        CCanvas::textOut(pt.x + 5, pt.y - 5, mrpt::format("%u", i), mrpt::img::TColor::blue());
    }
  }

  return true;
#else
  return false;
#endif
}

CImage CImage::colorImage() const
{
  CImage ret;
  colorImage(ret);
  return ret;
}

void CImage::colorImage(CImage& ret) const
{
#if MRPT_HAS_OPENCV
  if (this->isColor())
  {
    if (&ret != this) ret = *this;
    return;
  }

  auto srcImg = m_impl->img;
  // Detect in-place op. and make deep copy:
  if (srcImg.data == ret.m_impl->img.data) srcImg = srcImg.clone();

  ret.resize(getWidth(), getHeight(), CH_RGB);

  cv::cvtColor(srcImg, ret.m_impl->img, cv::COLOR_GRAY2BGR);
#endif
}

void CImage::joinImagesHorz(const CImage& img1, const CImage& img2)
{
#if MRPT_HAS_OPENCV
  ASSERT_(img1.getHeight() == img2.getHeight());

  auto im1 = img1.m_impl->img, im2 = img2.m_impl->img;
  ASSERT_(im1.type() == im2.type());

  this->resize(im1.cols + im2.cols, im1.rows, img1.getChannelCount());

  im1.copyTo(m_impl->img(cv::Rect(0, 0, im1.cols, im1.rows)));
  im2.copyTo(m_impl->img(cv::Rect(im1.cols, 0, im2.cols, im2.rows)));
#endif
}  // end

void CImage::equalizeHist(CImage& out_img) const
{
#if MRPT_HAS_OPENCV
  // Convert to a single luminance channel image
  auto srcImg = m_impl->img;
  if (this != &out_img) out_img.resize(srcImg.cols, srcImg.rows, getChannelCount());
  auto outImg = out_img.m_impl->img;

  if (srcImg.channels() == 1)
    cv::equalizeHist(srcImg, outImg);
  else
    THROW_EXCEPTION("Operation only supported for grayscale images");
#endif
}

#if MRPT_HAS_OPENCV
template <unsigned int HALF_WIN_SIZE>
void image_KLT_response_template(
    const cv::Mat& im, unsigned int x, unsigned int y, int32_t& _gxx, int32_t& _gyy, int32_t& _gxy)
{
  const auto min_x = x - HALF_WIN_SIZE;
  const auto min_y = y - HALF_WIN_SIZE;

  int32_t gxx = 0;
  int32_t gxy = 0;
  int32_t gyy = 0;

  const unsigned int WIN_SIZE = 1 + 2 * HALF_WIN_SIZE;

  unsigned int yy = min_y;
  for (unsigned int iy = WIN_SIZE; iy; --iy, ++yy)
  {
    unsigned int xx = min_x;
    for (unsigned int ix = WIN_SIZE; ix; --ix, ++xx)
    {
      const int32_t dx = static_cast<int32_t>(im.at<uint8_t>(yy, xx + 1)) -
                         static_cast<int32_t>(im.at<uint8_t>(yy, xx - 1));
      const int32_t dy = static_cast<int32_t>(im.at<uint8_t>(yy + 1, xx)) -
                         static_cast<int32_t>(im.at<uint8_t>(yy - 1, xx));
      gxx += dx * dx;
      gxy += dx * dy;
      gyy += dy * dy;
    }
  }
  _gxx = gxx;
  _gyy = gyy;
  _gxy = gxy;
}
#endif

float CImage::KLT_response(
    const unsigned int x, const unsigned int y, const unsigned int half_window_size) const
{
#if MRPT_HAS_OPENCV

  const auto& im1 = m_impl->img;
  const auto img_w = static_cast<unsigned int>(im1.cols),
             img_h = static_cast<unsigned int>(im1.rows);

  // If any of those predefined values worked, do the generic way:
  const unsigned int min_x = x - half_window_size;
  const unsigned int max_x = x + half_window_size;
  const unsigned int min_y = y - half_window_size;
  const unsigned int max_y = y + half_window_size;

  // Since min_* are "unsigned", checking "<" will detect negative
  // numbers:
  ASSERTMSG_(
      min_x < img_w && max_x < img_w && min_y < img_h && max_y < img_h,
      "Window is out of image bounds");

  // Gradient sums: Use integers since they're much faster than
  // doubles/floats!!
  int32_t gxx = 0;
  int32_t gxy = 0;
  int32_t gyy = 0;

  switch (half_window_size)
  {
    case 2:
      image_KLT_response_template<2>(im1, x, y, gxx, gyy, gxy);
      break;
    case 3:
      image_KLT_response_template<3>(im1, x, y, gxx, gyy, gxy);
      break;
    case 4:
      image_KLT_response_template<4>(im1, x, y, gxx, gyy, gxy);
      break;
    case 5:
      image_KLT_response_template<5>(im1, x, y, gxx, gyy, gxy);
      break;
    case 6:
      image_KLT_response_template<6>(im1, x, y, gxx, gyy, gxy);
      break;
    case 7:
      image_KLT_response_template<7>(im1, x, y, gxx, gyy, gxy);
      break;
    case 8:
      image_KLT_response_template<8>(im1, x, y, gxx, gyy, gxy);
      break;
    case 9:
      image_KLT_response_template<9>(im1, x, y, gxx, gyy, gxy);
      break;
    case 10:
      image_KLT_response_template<10>(im1, x, y, gxx, gyy, gxy);
      break;
    case 11:
      image_KLT_response_template<11>(im1, x, y, gxx, gyy, gxy);
      break;
    case 12:
      image_KLT_response_template<12>(im1, x, y, gxx, gyy, gxy);
      break;
    case 13:
      image_KLT_response_template<13>(im1, x, y, gxx, gyy, gxy);
      break;
    case 14:
      image_KLT_response_template<14>(im1, x, y, gxx, gyy, gxy);
      break;
    case 15:
      image_KLT_response_template<15>(im1, x, y, gxx, gyy, gxy);
      break;
    case 16:
      image_KLT_response_template<16>(im1, x, y, gxx, gyy, gxy);
      break;
    case 32:
      image_KLT_response_template<32>(im1, x, y, gxx, gyy, gxy);
      break;

    default:
      for (unsigned int yy = min_y; yy <= max_y; yy++)
      {
        for (unsigned int xx = min_x; xx <= max_x; xx++)
        {
          const int32_t dx = static_cast<int32_t>(im1.at<uint8_t>(yy, xx + 1)) -
                             static_cast<int32_t>(im1.at<uint8_t>(yy, xx - 1));
          const int32_t dy = static_cast<int32_t>(im1.at<uint8_t>(yy + 1, xx)) -
                             static_cast<int32_t>(im1.at<uint8_t>(yy - 1, xx));

          gxx += dx * dx;
          gxy += dx * dy;
          gyy += dy * dy;
        }
      }
      break;
  }
  // Convert to float's and normalize in the way:
  const float K = 0.5f / ((max_y - min_y + 1) * (max_x - min_x + 1));
  const float Gxx = gxx * K;
  const float Gxy = gxy * K;
  const float Gyy = gyy * K;

  // Return the minimum eigenvalue of:
  //    ( gxx  gxy )
  //    ( gxy  gyy )
  // See, for example:
  // mrpt::math::detail::eigenVectorsMatrix_special_2x2():
  const float t = Gxx + Gyy;               // Trace
  const float de = Gxx * Gyy - Gxy * Gxy;  // Det
  const float discr = std::max<float>(.0f, t * t - 4.0f * de);
  // The smallest eigenvalue is:
  return 0.5f * (t - std::sqrt(discr));
#else
  return 0;
#endif
}

// Load from TGA files. Used in loadFromFile()
// Contains code from
// https://github.com/tjohnman/Simple-Targa-Library/blob/master/src/simpleTGA.cpp
// (FreeBSD license)
bool CImage::loadTGA(
    const std::string& fileName, mrpt::img::CImage& out_RGB, mrpt::img::CImage& out_alpha)
{
#if MRPT_HAS_OPENCV
  std::fstream stream;
  stream.open(fileName.c_str(), std::fstream::in | std::fstream::binary);
  if (!stream.is_open())
  {
    std::cerr << "[CImage::loadTGA] Couldn't open file '" << fileName << "'.\n";
    return false;
  }

  stream.seekg(0, std::ios_base::end);
  // long length = stream.tellg();
  stream.seekg(0, std::ios_base::beg);

  // Simple uncompressed true-color image
  char dumpBuffer[12];
  char trueColorHeader[] = "\0\0\2\0\0\0\0\0\0\0\0\0";
  stream.read(dumpBuffer, 12);
  if (memcmp(dumpBuffer, trueColorHeader, 12) != 0)
  {
    std::cerr << "[CImage::loadTGA] Unsupported format or invalid file.\n";
    return false;
  }

  unsigned short width, height;
  unsigned char bpp;

  stream.read((char*)&width, 2);
  stream.read((char*)&height, 2);
  bpp = stream.get();
  if (bpp != 32)
  {
    std::cerr << "[CImage::loadTGA] Only 32 bpp format supported!\n";
    return false;
  }

  unsigned char desc;
  desc = stream.get();
  if (desc != 8 && desc != 32)
  {
    std::cerr << "[CImage::loadTGA] Unsupported format or invalid file.\n";
    return false;
  }
  const bool origin_is_low_corner = (desc == 8);

  // Data section
  std::vector<uint8_t> bytes(width * height * 4);
  stream.read((char*)&bytes[0], width * height * 4);
  stream.close();

  // Move data to images:
  out_RGB.resize(width, height, CH_RGB);
  out_alpha.resize(width, height, CH_GRAY);

  size_t idx = 0;
  for (int r = 0; r < height; r++)
  {
    const auto actual_row = origin_is_low_corner ? (height - 1 - r) : r;
    auto& img = out_RGB.m_impl->img;
    auto data = img.ptr<uint8_t>(actual_row);

    auto& img_alpha = out_alpha.m_impl->img;
    auto data_alpha = img_alpha.ptr<uint8_t>(actual_row);

    for (unsigned int c = 0; c < width; c++)
    {
      *data++ = bytes[idx++];        // R
      *data++ = bytes[idx++];        // G
      *data++ = bytes[idx++];        // B
      *data_alpha++ = bytes[idx++];  // A
    }
  }

  return true;
#else
  return false;
#endif  // MRPT_HAS_OPENCV
}

void CImage::getAsIplImage(IplImage* dest) const
{
#if MRPT_HAS_OPENCV
  makeSureImageIsLoaded();

#if MRPT_OPENCV_VERSION_NUM < 0x030000
  ASSERT_(dest != nullptr);
  *dest = cvIplImage(m_impl->img);
#else
  THROW_EXCEPTION("Method not supported in OpenCV>=3.0");
#endif
#endif
}

std::ostream& mrpt::img::operator<<(std::ostream& o, const TPixelCoordf& p)
{
  o << "(" << p.x << "," << p.y << ")";
  return o;
}
std::ostream& mrpt::img::operator<<(std::ostream& o, const TPixelCoord& p)
{
  o << "(" << p.x << "," << p.y << ")";
  return o;
}
