Skip to content

Fix bug at blobFromImagesWithParams#24128

Merged
asmorkalov merged 1 commit intoopencv:4.xfrom
CSBVision:CSBVision-patch-1
Sep 6, 2023
Merged

Fix bug at blobFromImagesWithParams#24128
asmorkalov merged 1 commit intoopencv:4.xfrom
CSBVision:CSBVision-patch-1

Conversation

@CSBVision
Copy link
Copy Markdown
Contributor

@CSBVision CSBVision commented Aug 8, 2023

blobFromImagesWithParams(image-s) apply swap scalefactor for every images when swapRB is true. This patch moves the swap scalefactor code out of the loop, that fix the above issue.

We would like to improve the recently introduced function blobFromImagesWithParams with respect to three minor issues that we noticed:

  • Currently, mean and scalefactor are recreated for every image, even though they should be reused. Maybe the compiler is smart enough to optimize this, still we think the variable should be created / swapped before the main loop.
  • Next, we would like to improve the function's robustness with respect to inconsistent arguments, i.e. if a single-channel image is passed but swapRB is true, as explained below.
  • Finally, we made some minor modifications to follow the coding guidelines (if - else if - else instead of nested else).

Function wise, there are two improvements from this:

  1. Currently, if the user passes a single-channel image in combination with mean and scalefactor values, but unintentionally sets swapRB to true, the whole result can be silently zeroed. The mean and scalefactor arrays will only contain a single value (whereas the other indices' values remain 0), but swapRB will swap out the true mean / scalefactor values and swap in zeros for these instead. There is no warning or assertion, only the whole image will be zeroed by the following multiplication. Therefore, we suggest to only swap these values if the image is not a single-channel image, i.e. mean and scalefactor contain correct values for all channels, therefore add if (nch > 2).
  2. Next, a related issue exists in combination with DNN_LAYOUT_NHWC. If the user passes this in combination with a single-channel image and swapRB is true, the function produces the following error:
OpenCV\modules\core\src\copy.cpp:320: error: (-215:Assertion failed) channels() == CV_MAT_CN(dtype) in function 'cv::Mat::copyTo'

This results from first converting the image to RGB (even though it is grayscale instead of the expected BGR layout) and, thereafter, copying the resulting three-channel image into the single-channel blob. This is avoided by checking nch > 2 here, too, which is also consistent to the function's implementation of the DNN_LAYOUT_NCHW case.

Both changes can be summarized such that swapRB only has an effect if the image has at least three channels. Most importantly, nothing changes at all if the arguments are consistent.

Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
  • The PR is proposed to the proper branch
  • There is a reference to the original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

@CSBVision
Copy link
Copy Markdown
Contributor Author

Furthermore, we think there is also a bug in the current implementation that is fixed by our changes. If the user passes multiple three-channel images in combination with swapRB == true and RGB scales, scalefactor will be swapped during every iteration of the main loop. Consequently, for images with even indices, the correct scales will be used, while for all images with odd indices, the red and blue factors are incorrectly swapped.

@LaurentBerger
Copy link
Copy Markdown
Contributor

LaurentBerger commented Aug 8, 2023

Furthermore, we think there is also a bug in the current implementation that is fixed by our changes.

I don't undestand. Image2BlobParams is const in blobFromImageWithParams

I tried VS2022 + windows11

    Mat image = imread(samples::findFile("lena.jpg"));
        Image2BlobParams paramBlob;
        paramBlob.datalayout = DNN_LAYOUT_NCHW;
        paramBlob.ddepth = CV_32F;
        paramBlob.mean = Scalar(4, 5, 6);
        paramBlob.scalefactor = Scalar(1, 2, 3);
        paramBlob.size = Size(1024, 1024);
        paramBlob.swapRB = true;
        paramBlob.paddingmode = DNN_PMODE_NULL;
        cout << "Init " << paramBlob.scalefactor << endl;
        Mat blobOpencv = blobFromImageWithParams(image, paramBlob);
        cout << "First call " << paramBlob.scalefactor << endl;
        blobOpencv = blobFromImageWithParams(image, paramBlob);
        cout << "Second call " << paramBlob.scalefactor << endl;

and result is

Init [1, 2, 3, 0]
Init [4, 5, 6, 0]
First call [1, 2, 3, 0]
First call [4, 5, 6, 0]
Second call [1, 2, 3, 0]
Second call [4, 5, 6, 0]
[```

@CSBVision
Copy link
Copy Markdown
Contributor Author

Correct, but the actually applied weights are those defined by

Scalar scalefactor = param.scalefactor;

which is a non-const local variable inside blobFromImagesWithParams. Here, red and blue values are swapped during each iteration, which is only relevant if you pass a collection of images instead of a single one. If you only use blobFromImageWithParams (notice the missing 's' - blobFromImage and blobFromImages), your input is forwarded to blobFromImagesWithParams, but the main loop is only executed once.
Inside each iteration we currently have

Scalar mean = param.mean;
if (param.swapRB)
{
    std::swap(mean[0], mean[2]);
    std::swap(scalefactor[0], scalefactor[2]);
}

which recreates mean (i.e. no issue here) but reuses scalefactor and keeps swapping.

@LaurentBerger
Copy link
Copy Markdown
Contributor

Ok only scaleFactor variables is outside the loop. yes there is a problem :

{
        Mat image = imread(samples::findFile("lena.jpg"));
        Mat roi(image(Rect(0, 0, 4, 2)));
        vector<Mat> tab = { roi.clone(), roi.clone(), roi.clone() };
        Image2BlobParams paramBlob;
        paramBlob.datalayout = DNN_LAYOUT_NHWC;
        paramBlob.ddepth = CV_32F;
        paramBlob.mean = Scalar(4, 5, 6);
        paramBlob.scalefactor = Scalar(1, 2, 3);
        paramBlob.size = Size(4, 2);
        paramBlob.swapRB = true;
        paramBlob.paddingmode = DNN_PMODE_NULL;
        cout << "Init " << paramBlob.scalefactor << endl;
        cout << tab[0] << endl;
        Mat blobOpencv = blobFromImagesWithParams(tab, paramBlob);
        displayBlobSize(blobOpencv, "blobOpencv");
        Mat x;
        x = blobOpencv.reshape(3, vector<int> {3,8});
        cout << "Original " << tab[2] << endl;
        for (int i = 0; i < tab.size(); i++)
        {
            cout << "blob " << i << endl;
            cout << x.row(i).reshape(0, 2) << endl;
        }

result is

Original
 [128, 138, 225, 127, 137, 224, 126, 136, 224, 125, 135, 223;
 127, 137, 224, 127, 137, 224, 127, 137, 224, 126, 136, 224]
blob 0
[221, 266, 366, 220, 264, 363, 220, 262, 360, 219, 260, 357;
 220, 264, 363, 220, 264, 363, 220, 264, 363, 220, 262, 360]
blob 1
[663, 266, 122, 660, 264, 121, 660, 262, 120, 657, 260, 119;
 660, 264, 121, 660, 264, 121, 660, 264, 121, 660, 262, 120]
blob 2
[221, 266, 366, 220, 264, 363, 220, 262, 360, 219, 260, 357;
 220, 264, 363, 220, 264, 363, 220, 264, 363, 220, 262, 360]

Copy link
Copy Markdown
Member

@zihaomu zihaomu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👍 Thanks for your contribution.

@CSBVision
Copy link
Copy Markdown
Contributor Author

Thanks for confirming - but this bug will be fixed by the PR as well 👍

@zihaomu zihaomu changed the title Update dnn_utils.cpp Fix bug at blobFromImagesWithParams Aug 8, 2023
@zihaomu zihaomu added the bug label Aug 8, 2023
@zihaomu
Copy link
Copy Markdown
Member

zihaomu commented Aug 8, 2023

Hi @CSBVision, can you add a test at https://github.com/opencv/opencv/blob/4.x/modules/dnn/test/test_misc.cpp#L66 so that we will not miss this case again in the future?

@CSBVision
Copy link
Copy Markdown
Contributor Author

This is our proposal for a test:

Mat blobs = blobFromImagesWithParams(std::vector<Mat> { img, img }, param);
vector<Range> ranges;
ranges.push_back(Range(0, 1));
ranges.push_back(Range(0, blobs.size[1]));
ranges.push_back(Range(0, blobs.size[2]));
ranges.push_back(Range(0, blobs.size[3]));
Mat blob0 = blobs(ranges);
ranges[0] = Range(1, 2);
Mat blob1 = blobs(ranges);
Mat diff = blob0 - blob1;
diff = diff.reshape(1, 1);
double minVal, maxVal;
minMaxLoc(diff, &minVal, &maxVal);
EXPECT_EQ(minVal, maxVal);

Is this test fine for you?
If so, we would update the PR.

@zihaomu
Copy link
Copy Markdown
Member

zihaomu commented Aug 9, 2023

Hi @CSBVision, how about adding a new test case for this issue?
It's a good time to add the test for blobFromImagesWithParams(image-s).

TEST(blobFromImagesWithParams_4ch, multi_image)
{
    Mat img(10, 10, CV_8UC4, cv::Scalar(0,1,2,3));
    ...
}

@CSBVision
Copy link
Copy Markdown
Contributor Author

Then we a arrive at the following additional function:

TEST(blobFromImagesWithParams_4ch, multi_image)
{
    Mat img(10, 10, CV_8UC4, cv::Scalar(0, 1, 2, 3));
    std::vector<double> factorVec = { 0.1, 0.2, 0.3, 0.4 };

    Scalar scalefactor(factorVec[0], factorVec[1], factorVec[2], factorVec[3]);

    Image2BlobParams param;
    param.scalefactor = scalefactor;
    param.datalayout = DNN_LAYOUT_NHWC;

    Mat blobs = blobFromImagesWithParams(std::vector<Mat> { img, img }, param);
    vector<Range> ranges;
    ranges.push_back(Range(0, 1));
    ranges.push_back(Range(0, blobs.size[1]));
    ranges.push_back(Range(0, blobs.size[2]));
    ranges.push_back(Range(0, blobs.size[3]));
    Mat blob0 = blobs(ranges);
    ranges[0] = Range(1, 2);
    Mat blob1 = blobs(ranges);
    Mat diff = blob0 - blob1;
    diff = diff.reshape(1, 1);
    double minVal, maxVal;
    minMaxLoc(diff, &minVal, &maxVal);
    EXPECT_EQ(minVal, maxVal);
}

Should we add this to the PR?

@zihaomu
Copy link
Copy Markdown
Member

zihaomu commented Aug 9, 2023

Hi @CSBVision, thanks for update. We can use the following code the check if the two Mat are the same. Other part looks good for me.

 EXPECT_EQ(0, cvtest::norm(image0, image1, NORM_INF));

@CSBVision
Copy link
Copy Markdown
Contributor Author

Alright, just updated the PR.

{
std::swap(mean[0], mean[2]);
std::swap(scalefactor[0], scalefactor[2]);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to add CV_LOG_WARNING for the else case to highlight the issue.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about that?

if (param.swapRB)
{
    if (nch > 2)
    {
        std::swap(mean[0], mean[2]);
        std::swap(scalefactor[0], scalefactor[2]);
    }
    else
    {
        CV_LOG_WARNING(NULL, "Red/blue color swapping requires at least three image channels.");
    }
}

{
CV_Assert(scalefactor == Scalar::all(1.0) && "Scaling is not supported for CV_8U blob depth");
CV_Assert(param.mean == Scalar() && "Mean subtraction is not supported for CV_8U blob depth");
CV_Assert(mean == Scalar() && "Mean subtraction is not supported for CV_8U blob depth");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to revert param.mean here. In case if assert user should see function parameter name, but not some internal variable,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used this for consistency with the scalefactor check. If param.mean should be used, we should also use param.scalefactor, making both checks more explicit. Or what do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, both checks.

@LaurentBerger
Copy link
Copy Markdown
Contributor

May it can be usefull to define in doc if mean and scalefactor are applied before channel are swap

@CSBVision
Copy link
Copy Markdown
Contributor Author

May it can be usefull to define in doc if mean and scalefactor are applied before channel are swap

In the blobFromImage doc, it is already explained:

mean - scalar with mean values which are subtracted from channels. Values are intended to be in (mean-R, mean-G, mean-B) order if image has BGR ordering and swapRB is true.

@CSBVision CSBVision force-pushed the CSBVision-patch-1 branch 2 times, most recently from 7c91620 to 2b0ebfa Compare August 10, 2023 09:28
@zihaomu
Copy link
Copy Markdown
Member

zihaomu commented Aug 10, 2023

Hi @CSBVision

/home/ci/opencv/modules/dnn/src/dnn_utils.cpp:122:13: error: 'CV_LOG_WARNING' was not declared in this scope
  122 |             CV_LOG_WARNING(NULL, "Red/blue color swapping requires at least three image channels.");
      |             ^~~~~~~~~~~~~~

Please add the header file #include <opencv2/core/utils/logger.hpp>

@CSBVision
Copy link
Copy Markdown
Contributor Author

Thanks for pointing out. We just added the lines from the comments without recompiling. Should be fine now 👍

@zihaomu zihaomu requested a review from asmorkalov September 6, 2023 05:41
Copy link
Copy Markdown
Contributor

@asmorkalov asmorkalov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍
I changed test a bit to have slightly different images in batch.

@CSBVision
Copy link
Copy Markdown
Contributor Author

Alright, looks good👍
Btw: Would be great if you could fine the time to approve our two years old JPEG2000 PR (not linked here as it has nothing to do with this PR), too, such that we can soon delete all these old/merged branches on our side.

@asmorkalov asmorkalov merged commit 5350fba into opencv:4.x Sep 6, 2023
@asmorkalov asmorkalov self-assigned this Sep 6, 2023
@asmorkalov asmorkalov added this to the 4.9.0 milestone Sep 6, 2023
@asmorkalov asmorkalov mentioned this pull request Sep 11, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants