1

A library API takes an image raw pixel data as std::shared_ptr<uint8_t>.

The image files I have can be in multiple formats (e.g. .png, .jpg, .bmp), so I use cv::imread for practicality.

However, this returns the image as a cv::Mat object, which I still need to convert to std::shared_ptr<uint8_t> to comply with the library API signature.

I have tried similar solutions on the Internet, such as How to save image data in shared pointer and Creating shared_ptr from raw pointer, as well as std::make_shared, either directly from the cv::Mat

std::make_shared<uint8_t>(*mat.data);

or from a std::vector

data.assign(mat.data, mat.data + mat.total() * mat.channels()); 
std::make_shared<uint8_t>(*data.data());

But all attempts resulted in either double deletion exception, corrupted data or compilation error.

The only way I got it to work was to write the raw pixels data to a file then read it to a shared pointer with

std::shared_ptr<uint8_t> data;

std::ofstream output("temp", std::ios::out | std::ios::binary);
output.write(reinterpret_cast<char*>(mat.data), size);

std::ifstream input("temp", std::ios::in | std::ios::binary);
data.reset(new uint8_t[size], std::default_delete<uint8_t[]>());
input.read((char*)data.get(), size);

Although this works, it bothers me to have such terrible solution for this.

What is the proper way to convert cv::Mat to a shared pointer so that the data complies with the library API signature?


The full example that works, but looks terrible is

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

#include <fstream>

int main(int argc,char* argv[])
{
    std::string image_path = cv::samples::findFile("starry_night.jpg");
    cv::Mat mat = cv::imread(image_path, cv::IMREAD_COLOR);
    size_t size = mat.cols * mat.rows * mat.channels();

    std::shared_ptr<uint8_t> data;

    std::ofstream output("temp", std::ios::out | std::ios::binary);
    output.write(reinterpret_cast<char*>(mat.data), size);

    std::ifstream input("temp", std::ios::in | std::ios::binary);
    data.reset(new uint8_t[size], std::default_delete<uint8_t[]>());
    input.read((char*)data.get(), size);

    // function(std::shared_ptr<uint8_t> data);

    return 0;
}

I use the following CMakeLists.txt to compile it (required OpenCV installed)

project(help)
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(help help.cpp)
target_link_libraries(help ${OpenCV_LIBS})
fabda01
  • 3,384
  • 2
  • 31
  • 37
  • Did you try std::shared_ptr data ((uint_8t*)mat.data)? Not sure what happens with the reference counting then, e.g. whether you can and have to do something about the ownership of .data – Micka Dec 31 '22 at 23:46
  • 1
    Well if your first two examples you're trying to make a `shared_ptr` from data that appears to be owned by someone else. when all the `shared_ptr` instances that refer to it die that memory will be deleted, even though the `Mat` holds on to it. Same thing goes the other way. If the `Mat` dies I assume it will attempt to release it's resources which the `shared_ptr` instances are still trying to hold on to. – Captain Obvlious Dec 31 '22 at 23:47
  • Have a look at the different constructors here: https://en.cppreference.com/w/cpp/memory/shared_ptr/shared_ptr (and the explanations on bottom). maybe you can trick the ownership problem with one of Deleter versions, by passing a "fake Deleter" that does nothing?!? – Micka Dec 31 '22 at 23:51
  • Curious why you're saving it to a file then reloading it into a new buffer instead of just directly copying it into the new buffer. – Captain Obvlious Jan 01 '23 at 00:10
  • @Micka That is a terrible idea. Now you have two items to maintain and a shared_ptr expressing shared ownership that doesn't really have any ownership at all. Kinda defeats the purpose of even having it. – Captain Obvlious Jan 01 '23 at 00:10
  • Do you have control over the API that receives a `std::shared_ptr`? Does it truly need to receive a `std::shared_ptr`? It seems like an unusual choice for a parameter to me. – Galik Jan 01 '23 at 00:14
  • 1
    Also, unless `cv::Mat` has a way to release its ownership of the data, I think you can't do this without copying the entire data. – Galik Jan 01 '23 at 00:16
  • @CaptainObvlious No reason, this was just the way I got it to work so far. How can I directly copy it into the new buffer? I am ok with copying the data. – fabda01 Jan 01 '23 at 00:20
  • 3
    The C++ Standard Library is your friend - [`std::copy`](https://en.cppreference.com/w/cpp/algorithm/copy). – Captain Obvlious Jan 01 '23 at 00:36
  • 1
    Looking at `cv::Mat` documentation you could probably do this by creating a custom [cv::MatAllocator](https://docs.opencv.org/3.4/df/d4c/classcv_1_1MatAllocator.html) that allows you to steal its data.. – Galik Jan 01 '23 at 01:04
  • 2
    OK, there are a few *constructors* that will allow you to do this becuase they let you pass in your own data and it will never be deallocated by the `cv::Mat`. https://docs.opencv.org/3.4/d3/d63/classcv_1_1Mat.html#a51615ebf17a64c968df0bf49b4de6a3a – Galik Jan 01 '23 at 01:18
  • @CaptainObvlious if you only need the shared pointer temporarily for s function call parameter, I don't see any problem in such a hack. Best with a wrapper so that nobody else ever gets access to that shared pointer. Copying the whole image data can be very expensive if you use that often. – Micka Jan 01 '23 at 08:20

2 Answers2

2

Its impossible. Copy the matrix or read the image using another library than cv::imread to reverse the ownership semantics if it is a performance concern. OpenCV provides non-owning matrices (constructor from pointer, e.g. feed it with the output of stbi_load()) which you can use as simple view like std::span. However, you can't change that flag manually after the construction of the matrix.

Looking at the source code at modules/core/src/matrix.cpp, it may be possible to add manually the flag cv::UMatData::USER_ALLOCATED to prevent deallocation of memory.

Like that:

#include <opencv2/core.hpp>

int main()
{
    cv::Mat img(10, 10, CV_8UC1);
    
    auto ptr = std::shared_ptr<unsigned char>(img.data);
    
    if(img.u)
    {
        img.u->flags |= cv::UMatData::USER_ALLOCATED;
    }

    // Single memory deallocation at the end of main()
}

~Mat() calls release() which calls deallocate() which calls u->unmap() and after a few function calls:

          if( !(u->flags & UMatData::USER_ALLOCATED) )
          {
              fastFree(u->origdata);
              u->origdata = 0;
          }

However, it doesn't seem to work, maybe because OpenCV uses a custom allocator (fastFree()) which may not be compatible with delete called by the std::shared_ptr. This may be the reason why OpenCV don't let release the matrix.

rafoo
  • 1,506
  • 10
  • 17
  • Still not really a great option but you could always pass `fastFree` as a custom deleter to the `std::shared_ptr` or allocate the memory beforehand and pass it to the `Mat` as @Galik pointed out in their comment (see the link). – Captain Obvlious Jan 01 '23 at 19:04
  • Actually you probably won't be able to allocate the memory beforehand since `imread` handles the allocation and there doesn't seem to be a clean way to subvert that. Transferring ownership in the way you're doing it while using a custom deleter should be fine though. – Captain Obvlious Jan 01 '23 at 19:43
1

As suggested in the comments, I will copy the data so that I can keep it after deleting cv::Mat

cv::Mat mat = cv::imread(image_path, cv::IMREAD_COLOR);
size_t size = mat.cols * mat.rows * mat.channels();

std::shared_ptr<uint8_t> data(new uint8_t[size]);
std::copy(mat.data, mat.data + size, data.get());

This already looks way better than what I had before, but a better solution would be to cv::Mat release its ownership of the data.

fabda01
  • 3,384
  • 2
  • 31
  • 37
  • 1
    When using size = mat.cols * mat.rows * mat.channels() you have to make sure that the image data is continuous, otherwise you will copy padding memory and not the full image. – Micka Jan 01 '23 at 20:03