justinzane
justinzane

Reputation: 1987

OpenCV cv::cvtColor drops alpha channel, How to keep alpha data?

I have realized several things:

Because of these realizations, I have accepted @ypnos answer since it is clearly on the right track. Be warned that none of the code on this page works as is.

void surface_change_sat(SDL_Surface* src_surf, SDL_Surface* dst_surf, float sat_diff) {
    using namespace cv;

    SDL_LockSurface(src_surf);
    Mat src_mat = Mat(src_surf->h, src_surf->w, CV_8UC4);                // Make mat from surf
    memcpy(src_mat.data, src_surf->pixels, src_surf->h * src_surf->w * sizeof(Uint32));
    SDL_UnlockSurface(src_surf);

    Mat src_rgbI = Mat(src_mat.rows, src_mat.cols, CV_8UC3);     // split rgba into rgb and a
    Mat aI_mat = Mat(src_mat.rows, src_mat.cols, CV_8UC1);       // since hsv has no a
    Mat to_ar[] {src_rgbI, aI_mat};
    int from_to[] = { 0,0, 1,1, 2,2, 3,3 };                      // r=0, ... a=3
    mixChannels(&src_mat, 1, to_ar, 2, from_to, 4);

    Mat rgbF_mat = Mat(src_mat.rows, src_mat.cols, CV_32FC3);    // Make ints into floats
    src_rgbI.convertTo(rgbF_mat, CV_32F);

    typedef Vec<float, 3> flt_vec_t;                             // The type of pixel in hsv
    Mat hsv_mat = Mat(src_mat.rows, src_mat.cols, CV_32FC3);
    cvtColor(rgbF_mat, hsv_mat, CV_RGB2HSV, 3);                  // convert to HSV

    flt_vec_t pix_vec;
    for (MatIterator_<flt_vec_t> mat_it = hsv_mat.begin<flt_vec_t>();
         mat_it != hsv_mat.end<flt_vec_t>();
         mat_it++) {
        pix_vec = *mat_it;
        Matx<float, 3, 1> pix_matx = (Matx<float, 3, 1>)pix_vec;
        CV_Assert(pix_matx.val[1] <= 1.0f && pix_matx.val[1] >= 0.0f);
        pix_matx.val[1] += sat_diff;
        if (pix_matx.val[1] > 1.0f) { pix_matx.val[1] = 1.0f; }
        if (pix_matx.val[1] < 0.0f) { pix_matx.val[1] = 0.0f; }
    }

    cvtColor(hsv_mat, rgbF_mat, CV_HSV2RGB, 3);              // convert back to RGB

    Mat dst_mat = Mat(dst_surf->h, dst_surf->w, CV_8UC4);
    Mat dst_rgbI = Mat(dst_mat.rows, dst_mat.cols, CV_8UC3);
    rgbF_mat.convertTo(dst_rgbI, CV_8U);                     // float back to int
    Mat from_ar[] {dst_rgbI, aI_mat};
    int to_from[] = { 0,0, 1,1, 2,2, 0,3 };                  // r=0, ... a=3
    mixChannels(from_ar, 2, &dst_mat, 1, to_from, 4);        // add alpha for RGBA

    SDL_LockSurface(dst_surf);
    memcpy(dst_surf->pixels, (void*)dst_mat.data, dst_surf->h * dst_surf->w * sizeof(Uint32));
    SDL_UnlockSurface(dst_surf);
}

//-------------------Old Question -------------------------

I was getting the most mysterious memory errors until I added the CV_Asserts to the following function and realized that OpenCV was silently destroying the alpha channel in cv::Mat hsv_mat. I'm confused as to how to resolve this problem:One note, several other answers to related questions suggested thing like using shell scripts or cli tools like imagemagick. These are not helpful in this particular case. I need a standalone function with dependencies limited to opencv and the standard library.

Note: Following a suggestion in the comments, I explicitly split the alpha channel and converted from Uint8 to Float32. Now the assertions function, however, there is still a problem with invalid memory access causing a segfault.

Upvotes: 2

Views: 5619

Answers (1)

ypnos
ypnos

Reputation: 52317

Well, basically cvtColor only works with a fixed set of matrix types, including the format and number of channels. So unmixing and remixing the channels is unavoidable.

The next thing I would do is use the templated data types. They give you a better understanding of what is held within a matrix, however sometimes OpenCV manages to violate that, too (at least in older versions).

This is how I would do it:

SDL_LockSurface(surf);
cv::Mat4b rgba(surf->h, surf->w, surf->pixels);
std::vector<cv::Mat1b> channels_rgba;
cv::split(rgba, channels_rgba);

// create matrix with first three channels only
std::vector<cv::Mat1b> channels_rgb(channels_rgba.begin(),
                                    channels_rgba.begin()+3);
cv::Mat3b rgb;
cv::merge(channels_rgb, rgb);

// create floating point representation
cv::Mat3f rgbhsv;
rgbhsv = rgb; // implicit conversion

// convert to HSV
cv::cvtColor(rgbhsv, rgbhsv, CV_RGB2HSV);

for (cv::Mat3f::iterator mat_it = rgbhsv.begin();
     mat_it != rgbhsv.end(); mat_it++) {
    cv::Vec3f &pix_vec = *mat_it;
    pix_vec[0] += hue_diff;
    if (pix_vec[0] >= 360.0f || pix_vec[0] < 0.0f) {
        pix_vec[0] = (180.0f / M_PI) * (std::asin(std::sin((pix_vec[0] * M_PI / 180.0f))));
    }
}

// convert back to RGB
cv::cvtColor(rgbhsv, rgbhsv, CV_HSV2RGB);

// back to unsigned char channels
rgb = rgbhsv; // implicit conversion
cv::split(rgb, channels_rgb);

// replace first three channels only
for (size_t i = 0; i < channels_rgb.size(); ++i)
    channels_rgba[i] = channels_rgb[i];

cv::merge(channels_rgba, rgba);
SDL_UnlockSurface(surf);

Please note:

  1. I did not test this code!
  2. It might be necessary to adjust data ranges (going back to convertTo() instead of assignment operator!
  3. My last operation may not overwrite the surface data (you can go back to mixChannels which is known to not allocate any data ever)

Upvotes: 1

Related Questions