Deserializing array of string with boost and are bigger than expected (after serializing from cv::cuda::GpuMat)

Viewed 102

I'm trying to serialize and deserialize an array of cv::cuda::GpuMat (pitched arrays of byte / uchar). In the deserialize part, I don't need to recover a GpuMat, but a byte array.

I use those two classes:

#pragma once
#include <fstream>
#include <string>
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <opencv2/core/cuda.hpp>

using namespace std;
using namespace cv;
using namespace cuda;

#ifndef uchar
#define uchar unsigned char
#endif

class SerieFlow
{
    friend class boost::serialization::access;
    
    template <class Archive>
    void serialize(Archive& ar, const unsigned int version)
    {
        ar & flowX;
        ar & flowY;
    }

    GpuMat* FlowPlanes{}; // GpuMat[2]
public:
    string flowX{};
    string flowY{};

    void PrepData()
    {
        Mat matX = Mat(FlowPlanes[0]);
        Mat matY = Mat(FlowPlanes[1]);
        flowX = *new string((const char*)matX.col(0).data);
        flowY = *new string((const char*)matY.col(0).data);
        matX.release();
        matY.release();
    }
    
    SerieFlow() = default;
    SerieFlow(GpuMat* flowPlanes) : FlowPlanes(flowPlanes)
    {
        PrepData();
    }
};

class SerieFlowFile
{
public:
    void Save(SerieFlow content, string filename)
    {
        ofstream stream(filename);
        {
            boost::archive::text_oarchive archive(stream);
            archive << content;
        }
    }
    
    SerieFlow Open(string filename)
    {
        SerieFlow content;
        {
            ifstream stream(filename);
            boost::archive::text_iarchive archive(stream);
            archive >> content;
        }
        return content;
    }
};

And I unit test the results with:

TEST_METHOD(ReadDeserializeTest)
{
    string filename{ "WriteRead.sflow" };
    if (filesystem::exists(filename))
        std::remove(filename.c_str());
    unsigned char data[8] = { 7, 4, 2, 6, 7, 18, 29, 111 };
    string dataAsString((const char*)data, 8);
    vector<unsigned char> expectedData{ dataAsString.begin(), dataAsString.end() };
    Mat mat{ 8, 1, CV_8UC1, data };
    GpuMat gpuMat1(mat);
    GpuMat gpuMat2(mat);
    GpuMat gpuMatArray[2] = { gpuMat1 , gpuMat2 };
    SerieFlow sflow(gpuMatArray);
    SerieFlowFile sut{};
    sut.Save(sflow, filename);

    SerieFlowFile sut2{};
    auto sflow2 = sut2.Open(filename);
    vector<unsigned char> resultFlowX{ sflow2.flowX.begin(),sflow2.flowX.end() };
    vector<unsigned char> resultFlowY{ sflow2.flowY.begin(),sflow2.flowY.end() };

    stringstream ss{};
    ss << "flowX size: " << resultFlowX.size();
    ss << " flowY size: " << resultFlowY.size() << endl;
    Logger::WriteMessage(ss.str().c_str());

    stringstream resultFlowXStream{};
    stringstream resultFlowYStream{};
    copy(resultFlowX.begin(), resultFlowX.end(), std::ostream_iterator<int>(resultFlowXStream, " "));
    copy(resultFlowY.begin(), resultFlowY.end(), std::ostream_iterator<int>(resultFlowYStream, " "));
    Logger::WriteMessage(resultFlowXStream.str().c_str());
    Logger::WriteMessage(resultFlowYStream.str().c_str());

    Assert::IsTrue(resultFlowX == resultFlowY, L"flowX and flowY are not the same.");
    Assert::IsTrue(expectedData == resultFlowX, L"resultFlowX is not correct.");
    Assert::IsTrue(expectedData == resultFlowY, L"resultFlowY is not correct.");
}

However I get:

flowX size: 52 flowY size: 36

7 4 2 6 7 18 29 111 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 253 253 253 253

and

7 4 2 6 7 18 29 111 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 205 253 253 253 253

I'm expecting both to be of size 8 and to give:

7 4 2 6 7 18 29 111

Is it normal to get this much padding?

I'm using string as a convenient way to manipulate a byte array, compare, etc.

I suspect I have a problem with string because my raw array from the file is not null terminated; how should I serialize and deserialize those arrays ? How come this is a problem if I null terminated the string at serialization (string dataAsString((const char*)data, 8);) ?

I'm using boost 1.72 as nuget package and opencv 4.51, and test with VS2019 toolset 1.42 in x64.

2 Answers
  1. Don't use the memory leak operator:

     X a = *new X(); // guaranteed unrecoverable memory leak¹
    
  2. The place where you used it is riddled with other problems:

        flowX = *new string((const char*)matX.col(0).data);
        flowY = *new string((const char*)matY.col(0).data);
    
  3. Firstly, (const char*)data is a reinterpret-cast. It's rarely safe to use.

    flowX = std::string(reinterpret_cast<const char*>(matX.col(0).data), length);
    flowY = std::string(reinterpret_cast<const char*>(matY.col(0).data), length);
    
  4. You interpret that as a raw, C-style string. C strings need to be NUL-terminated. There is no reason to assume they are. Specify a length.

    flowX = std::string(reinterpret_cast<const char*>(matX.col(0).data), length);
    flowY = std::string(reinterpret_cast<const char*>(matY.col(0).data), length);
    

    Or indeed, use flowX.assign

    flowX.assign(reinterpret_cast<const char*>(matX.col(0).data), length);
    flowY.assign(reinterpret_cast<const char*>(matY.col(0).data), length);
    
  5. This line

    cv::Mat matX = Mat(FlowPlanes[0]);
    

    needlessly copy-constructs

    cv::Nat matX(FlowPlanes[0]);
    
  6. There seems to be no reason to copy the matrices anyway

    void PrepData() {
        flowX.assign(reinterpret_cast<const char*>(FlowPlanes[0].col(0).data), length);
        flowY.assign(reinterpret_cast<const char*>(FlowPlanes[1].col(0).data), length);
    }
    
  7. In fact, since nothing else ever uses flowPlanes, just initialize in the constructor, as usual:

    std::string flowX;
    std::string flowY;
    
    SerieFlow() = default;
    explicit SerieFlow(cv::cuda::GpuMat* flowPlanes)
        : flowX(reinterpret_cast<const char*>(flowPlanes[0].col(0).data), length),
          flowY(reinterpret_cast<const char*>(flowPlanes[1].col(0).data), length)
    { }
    
  8. Many other conversion confusions can be simplified:

    unsigned char data[8] = { 7, 4, 2, 6, 7, 18, 29, 111 };
    std::string dataAsString(reinterpret_cast<const char*>(data), 8);
    

    To

    std::string dataAsString { 7, 4, 2, 6, 7, 18, 29, 111 };
    

    And

    std::string dataAsString { 7, 4, 2, 6, 7, 18, 29, 111 };
    std::vector<unsigned char> expectedData{ dataAsString.begin(),
                                             dataAsString.end() };
    

    To simply...

    std::vector<unsigned char> data { 7, 4, 2, 6, 7, 18, 29, 111 };
    cv::Mat mat(data.size(), 1, CV_8UC1, data.data());
    
  9. No need for temporaries to create the array:

    cv::cuda::GpuMat gpuMat1(mat);
    cv::cuda::GpuMat gpuMat2(mat);
    cv::cuda::GpuMat gpuMatArray[2] = { gpuMat1, gpuMat2 };
    

    Can be

    GpuMat gpuMatArray[2] = { GpuMat{mat}, GpuMat{mat} };
    
  10. In general the whole conversion to std::string is unnecessary and what creates the incompatibilities with uchar vs char. Just keep it vector<uchar>?

    using cv::cuda::GpuMat;
    using uchar = std::uint8_t;
    
    using TwoMat = std::array<GpuMat, 2>;
    
    class SerieFlow {
        friend class boost::serialization::access;
        template <class Archive> void serialize(Archive& ar, unsigned /*unused*/) {
            ar& flowX & flowY;
        }
    
        static auto to_vector(GpuMat const& mat) {
            assert(mat.elemSize() == 1);
            auto n = mat.size().area() * mat.elemSize();
            return std::vector<uchar>(mat.data, mat.data+n);
        }
      public:
        std::vector<uchar> flowX, flowY;
    
        SerieFlow() = default;
        explicit SerieFlow(TwoMat const planes)
          : flowX(to_vector(planes[0])),
            flowY(to_vector(planes[1])) {}
    };
    
    struct SerieFlowFile {
        static void Save(const SerieFlow& content, const std::string& filename) {
            std::ofstream stream(filename);
            boost::archive::text_oarchive archive(stream);
            archive << content;
        }
    
        static SerieFlow Open(const std::string& filename) {
            std::ifstream stream(filename);
            boost::archive::text_iarchive archive(stream);
            SerieFlow content;
            archive >> content;
            return content;
        }
    };
    
    void ReadDeserializeTest() {
        std::filesystem::path filename = "WriteRead.sflow";
        if (exists(filename)) {
            remove(filename);
        }
    
        std::vector<unsigned char> data { 7, 4, 2, 6, 7, 18, 29, 111 };
        cv::Mat mat(data.size(), 1, CV_8UC1, data.data());
        TwoMat gpuMatArray { GpuMat{mat}, GpuMat{mat} };
    
        SerieFlow sflow(gpuMatArray);
        SerieFlowFile::Save(sflow, filename);
    
        auto roundtrip = SerieFlowFile::Open(filename);
    
        auto check = [&data](auto& flowX, auto label) {
            std::cout << label << " size: " << flowX.size();
            copy(flowX.begin(), flowX.end(), std::ostream_iterator<int>(std::cout, " "));
            std::cout << "\n" << (data == flowX? "CORRECT":"INCORRECT") << std::endl;
        };
    
        check(roundtrip.flowX, "flowX");
        check(roundtrip.flowY, "flowY");
    }
    
    int main() {
        ReadDeserializeTest();
    }
    

Out Of The Box: Just Serialize GpuMat

Why don't you add serialization for the GpuMat type directly?

#include <boost/serialization/array.hpp>
#include <boost/serialization/array_wrapper.hpp>

namespace boost::serialization {
    template <typename Ar> void save(Ar& ar, GpuMat const& mat, unsigned) {
        int r = mat.rows;
        int c = mat.cols;
        ar & r & c & make_array(&mat.data[0], r*c);
    }

    template <typename Ar> void load(Ar& ar, GpuMat& mat, unsigned) {
        int r, c;
        ar & r & c;
        mat = GpuMat(r, c, CV_8UC1);
        ar & make_array(&mat.data[0], r*c);
    }
}

BOOST_SERIALIZATION_SPLIT_FREE(GpuMat)

This removes all the copying and should immediatly fix the issue of not dealing with the actual runtime shape of the matrix (you just assumed it will always be 1 row).

Now the whole thing can be implemented as just

using Flows = std::array<GpuMat, 2>;

struct SerieFlowFile {
    static void Save(const Flows& content, const std::string& filename) {
        std::ofstream stream(filename);
        boost::archive::text_oarchive archive(stream);
        archive << content;
    }

    static Flows Open(const std::string& filename) {
        std::ifstream stream(filename);
        boost::archive::text_iarchive archive(stream);
        Flows content;
        archive >> content;
        return content;
    }
};

Which compiles with the following test main:

void ReadDeserializeTest() {
    std::vector<uchar> data { 7, 4, 2, 6, 7, 18, 29, 111 };

    Flows gpuMatArray {
        GpuMat (8, 1, CV_8UC1, data.data()),
        GpuMat (4, 1, CV_8UC1, data.data()),
    };

    SerieFlowFile::Save(gpuMatArray, "WriteRead.sflow");
    auto roundtrip = SerieFlowFile::Open("WriteRead.sflow");

    static auto as_vec = [](GpuMat const& mat) {
        return std::vector(&mat.data[0], &mat.data[mat.cols * mat.rows]);
    };

    auto check = [&] (int index) {
        auto const& v = as_vec(roundtrip[index]);
        auto eq = boost::equal(as_vec(gpuMatArray[index]), v);
        fmt::print("#{} size: {} {} {}\n", index, v.size(), v, (eq? "CORRECT":"INCORRECT"));
    };

    check(0);
    check(1);
}

See it Live On Compiler Explorer

//#pragma once
#include <array>
#include <cstdint>

using uchar = std::uint8_t;

#ifndef NO_OPENCV
    #include <opencv2/core/cuda.hpp>
    //using cv::cuda::GpuMat;
    using GpuMat = cv::Mat;
#else
    #include <memory>
    #include <algorithm>
    namespace {
        enum {CV_8UC1};
        struct FakeMat {
            FakeMat(int r=1, int c=1, int=CV_8UC1, void* init = nullptr)
                : rows(r), cols(c), data(r && c? std::make_unique<uchar[]>(r*c) : nullptr)
            {
                if (init && data) {
                    std::copy_n(static_cast<uchar const*>(init), rows*cols, data.get());
                }
            }
            FakeMat(FakeMat const& rhs)
                : rows(rhs.rows), cols(rhs.cols), data(rhs.data? std::make_unique<uchar[]>(rows*cols) : nullptr)
            {
                if (data && rhs.data) {
                    std::copy_n(rhs.data.get(), rows*cols, data.get());
                }
            }

            FakeMat(FakeMat&&) = default;
            FakeMat& operator=(FakeMat&&) = default;

            int rows, cols;
            std::unique_ptr<uchar[]> data;
        };
    }

    using GpuMat = FakeMat;
#endif

#include <boost/serialization/array.hpp>
#include <boost/serialization/array_wrapper.hpp>

namespace boost::serialization {
    template <typename Ar> void save(Ar& ar, GpuMat const& mat, unsigned) {
        int r = mat.rows;
        int c = mat.cols;
        ar & r & c & make_array(&mat.data[0], r*c);
    }

    template <typename Ar> void load(Ar& ar, GpuMat& mat, unsigned) {
        int r, c;
        ar & r & c;
        mat = GpuMat(r, c, CV_8UC1);
        ar & make_array(&mat.data[0], r*c);
    }
}

BOOST_SERIALIZATION_SPLIT_FREE(GpuMat)

#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
#include <fstream>

using Flows = std::array<GpuMat, 2>;

struct SerieFlowFile {
    static void Save(const Flows& content, const std::string& filename) {
        std::ofstream stream(filename);
        boost::archive::text_oarchive archive(stream);
        archive << content;
    }

    static Flows Open(const std::string& filename) {
        std::ifstream stream(filename);
        boost::archive::text_iarchive archive(stream);
        Flows content;
        archive >> content;
        return content;
    }
};

#include <boost/range.hpp>
#include <fmt/ranges.h>
void ReadDeserializeTest() {
    std::vector<uchar> data { 7, 4, 2, 6, 7, 18, 29, 111 };

    Flows gpuMatArray {
        GpuMat (8, 1, CV_8UC1, data.data()),
        GpuMat (4, 1, CV_8UC1, data.data()+2),
    };

    SerieFlowFile::Save(gpuMatArray, "WriteRead.sflow");
    auto roundtrip = SerieFlowFile::Open("WriteRead.sflow");

    static auto as_vec = [](GpuMat const& mat) {
        return std::vector(&mat.data[0], &mat.data[mat.cols * mat.rows]);
    };

    auto check = [&] (int index) {
        auto const& v = as_vec(roundtrip[index]);
        auto eq = boost::equal(as_vec(gpuMatArray[index]), v);
        fmt::print("#{} size: {} {} {}\n", index, v.size(), v, (eq? "CORRECT":"INCORRECT"));
    };

    check(0);
    check(1);
}

int main() {
    ReadDeserializeTest();
}

Prints

#0 size: 8 {7, 4, 2, 6, 7, 18, 29, 111} CORRECT
#1 size: 4 {2, 6, 7, 18} CORRECT

In PrepData, string are initialized without a given length, leading to arbitrary long string (until null '\0' is reached).

The correct code is :

string strX((const char*)matX.col(0).data, matX.rows);
string strY((const char*)matY.col(0).data, matY.rows);
Related