Skip to content

TST: Calculate RMS and diff image in C++ #29102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions lib/matplotlib/testing/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from PIL import Image

import matplotlib as mpl
from matplotlib import cbook
from matplotlib import cbook, _image
from matplotlib.testing.exceptions import ImageComparisonFailure

_log = logging.getLogger(__name__)
Expand Down Expand Up @@ -398,7 +398,7 @@ def compare_images(expected, actual, tol, in_decorator=False):

The two given filenames may point to files which are convertible to
PNG via the `.converter` dictionary. The underlying RMS is calculated
with the `.calculate_rms` function.
in a similar way to the `.calculate_rms` function.

Parameters
----------
Expand Down Expand Up @@ -469,17 +469,12 @@ def compare_images(expected, actual, tol, in_decorator=False):
if np.array_equal(expected_image, actual_image):
return None

# convert to signed integers, so that the images can be subtracted without
# overflow
expected_image = expected_image.astype(np.int16)
actual_image = actual_image.astype(np.int16)

rms = calculate_rms(expected_image, actual_image)
rms, abs_diff = _image.calculate_rms_and_diff(expected_image, actual_image)

if rms <= tol:
return None

save_diff_image(expected, actual, diff_image)
Image.fromarray(abs_diff).save(diff_image, format="png")

results = dict(rms=rms, expected=str(expected),
actual=str(actual), diff=str(diff_image), tol=tol)
Expand Down
75 changes: 75 additions & 0 deletions src/_image_wrapper.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>

#include <algorithm>

#include "_image_resample.h"
#include "py_converters.h"

Expand Down Expand Up @@ -200,6 +202,76 @@
}


// This is used by matplotlib.testing.compare to calculate RMS and a difference image.
static py::tuple
calculate_rms_and_diff(py::array_t<unsigned char> expected_image,
py::array_t<unsigned char> actual_image)
{
if (expected_image.ndim() != 3) {
auto exceptions = py::module_::import("matplotlib.testing.exceptions");

Check warning on line 211 in src/_image_wrapper.cpp

View check run for this annotation

Codecov / codecov/patch

src/_image_wrapper.cpp#L211

Added line #L211 was not covered by tests
auto ImageComparisonFailure = exceptions.attr("ImageComparisonFailure");
py::set_error(
ImageComparisonFailure,
"Expected image must be 3-dimensional, but is {ndim}-dimensional"_s.format(
"ndim"_a=expected_image.ndim()));
throw py::error_already_set();
}

if (actual_image.ndim() != 3) {
auto exceptions = py::module_::import("matplotlib.testing.exceptions");

Check warning on line 221 in src/_image_wrapper.cpp

View check run for this annotation

Codecov / codecov/patch

src/_image_wrapper.cpp#L221

Added line #L221 was not covered by tests
auto ImageComparisonFailure = exceptions.attr("ImageComparisonFailure");
py::set_error(
ImageComparisonFailure,
"Actual image must be 3-dimensional, but is {ndim}-dimensional"_s.format(
"ndim"_a=actual_image.ndim()));
throw py::error_already_set();
}

auto height = expected_image.shape(0);
auto width = expected_image.shape(1);
auto depth = expected_image.shape(2);

if (height != actual_image.shape(0) || width != actual_image.shape(1) ||
depth != actual_image.shape(2)) {
auto exceptions = py::module_::import("matplotlib.testing.exceptions");

Check warning on line 236 in src/_image_wrapper.cpp

View check run for this annotation

Codecov / codecov/patch

src/_image_wrapper.cpp#L236

Added line #L236 was not covered by tests
auto ImageComparisonFailure = exceptions.attr("ImageComparisonFailure");
py::set_error(
ImageComparisonFailure,
"Image sizes do not match expected size: {expected_image.shape} "_s
"actual size {actual_image.shape}"_s.format(
"expected_image"_a=expected_image, "actual_image"_a=actual_image));
throw py::error_already_set();
}
auto expected = expected_image.unchecked<3>();
auto actual = actual_image.unchecked<3>();

py::ssize_t diff_dims[3] = {height, width, 3};
py::array_t<unsigned char> diff_image(diff_dims);
auto diff = diff_image.mutable_unchecked<3>();

double total = 0.0;
for (auto i = 0; i < height; i++) {
for (auto j = 0; j < width; j++) {
for (auto k = 0; k < depth; k++) {
auto pixel_diff = static_cast<double>(expected(i, j, k)) -
static_cast<double>(actual(i, j, k));

total += pixel_diff*pixel_diff;

if (k != 3) { // Hard-code a fully solid alpha channel by omitting it.
diff(i, j, k) = static_cast<unsigned char>(std::clamp(
abs(pixel_diff) * 10, // Expand differences in luminance domain.
0.0, 255.0));
}
}
}
}
total = total / (width * height * depth);

return py::make_tuple(sqrt(total), diff_image);
}


PYBIND11_MODULE(_image, m, py::mod_gil_not_used())
{
py::enum_<interpolation_e>(m, "_InterpolationType")
Expand Down Expand Up @@ -232,4 +304,7 @@
"norm"_a = false,
"radius"_a = 1,
image_resample__doc__);

m.def("calculate_rms_and_diff", &calculate_rms_and_diff,
"expected_image"_a, "actual_image"_a);
}
Loading