|
1 | 1 | #include <pybind11/pybind11.h>
|
2 | 2 | #include <pybind11/numpy.h>
|
3 | 3 |
|
| 4 | +#include <algorithm> |
| 5 | + |
4 | 6 | #include "_image_resample.h"
|
5 | 7 | #include "py_converters.h"
|
6 | 8 |
|
@@ -202,6 +204,70 @@ image_resample(py::array input_array,
|
202 | 204 | }
|
203 | 205 |
|
204 | 206 |
|
| 207 | +// This is used by matplotlib.testing.compare to calculate RMS and a difference image. |
| 208 | +static py::tuple |
| 209 | +calculate_rms_and_diff(py::array_t<unsigned char> expected_image, |
| 210 | + py::array_t<unsigned char> actual_image) |
| 211 | +{ |
| 212 | + for (const auto & [image, name] : {std::pair{expected_image, "Expected"}, |
| 213 | + std::pair{actual_image, "Actual"}}) |
| 214 | + { |
| 215 | + if (image.ndim() != 3) { |
| 216 | + auto exceptions = py::module_::import("matplotlib.testing.exceptions"); |
| 217 | + auto ImageComparisonFailure = exceptions.attr("ImageComparisonFailure"); |
| 218 | + py::set_error( |
| 219 | + ImageComparisonFailure, |
| 220 | + "{name} image must be 3-dimensional, but is {ndim}-dimensional"_s.format( |
| 221 | + "name"_a=name, "ndim"_a=expected_image.ndim())); |
| 222 | + throw py::error_already_set(); |
| 223 | + } |
| 224 | + } |
| 225 | + |
| 226 | + auto height = expected_image.shape(0); |
| 227 | + auto width = expected_image.shape(1); |
| 228 | + auto depth = expected_image.shape(2); |
| 229 | + |
| 230 | + if (height != actual_image.shape(0) || width != actual_image.shape(1) || |
| 231 | + depth != actual_image.shape(2)) { |
| 232 | + auto exceptions = py::module_::import("matplotlib.testing.exceptions"); |
| 233 | + auto ImageComparisonFailure = exceptions.attr("ImageComparisonFailure"); |
| 234 | + py::set_error( |
| 235 | + ImageComparisonFailure, |
| 236 | + "Image sizes do not match expected size: {expected_image.shape} "_s |
| 237 | + "actual size {actual_image.shape}"_s.format( |
| 238 | + "expected_image"_a=expected_image, "actual_image"_a=actual_image)); |
| 239 | + throw py::error_already_set(); |
| 240 | + } |
| 241 | + auto expected = expected_image.unchecked<3>(); |
| 242 | + auto actual = actual_image.unchecked<3>(); |
| 243 | + |
| 244 | + py::ssize_t diff_dims[3] = {height, width, 3}; |
| 245 | + py::array_t<unsigned char> diff_image(diff_dims); |
| 246 | + auto diff = diff_image.mutable_unchecked<3>(); |
| 247 | + |
| 248 | + double total = 0.0; |
| 249 | + for (auto i = 0; i < height; i++) { |
| 250 | + for (auto j = 0; j < width; j++) { |
| 251 | + for (auto k = 0; k < depth; k++) { |
| 252 | + auto pixel_diff = static_cast<double>(expected(i, j, k)) - |
| 253 | + static_cast<double>(actual(i, j, k)); |
| 254 | + |
| 255 | + total += pixel_diff*pixel_diff; |
| 256 | + |
| 257 | + if (k != 3) { // Hard-code a fully solid alpha channel by omitting it. |
| 258 | + diff(i, j, k) = static_cast<unsigned char>(std::clamp( |
| 259 | + abs(pixel_diff) * 10, // Expand differences in luminance domain. |
| 260 | + 0.0, 255.0)); |
| 261 | + } |
| 262 | + } |
| 263 | + } |
| 264 | + } |
| 265 | + total = total / (width * height * depth); |
| 266 | + |
| 267 | + return py::make_tuple(sqrt(total), diff_image); |
| 268 | +} |
| 269 | + |
| 270 | + |
205 | 271 | PYBIND11_MODULE(_image, m, py::mod_gil_not_used())
|
206 | 272 | {
|
207 | 273 | py::enum_<interpolation_e>(m, "_InterpolationType")
|
@@ -234,4 +300,7 @@ PYBIND11_MODULE(_image, m, py::mod_gil_not_used())
|
234 | 300 | "norm"_a = false,
|
235 | 301 | "radius"_a = 1,
|
236 | 302 | image_resample__doc__);
|
| 303 | + |
| 304 | + m.def("calculate_rms_and_diff", &calculate_rms_and_diff, |
| 305 | + "expected_image"_a, "actual_image"_a); |
237 | 306 | }
|
0 commit comments