diff --git a/src/bindings/python/PyCPUProcessor.cpp b/src/bindings/python/PyCPUProcessor.cpp index 84b91427b..e5786ba24 100644 --- a/src/bindings/python/PyCPUProcessor.cpp +++ b/src/bindings/python/PyCPUProcessor.cpp @@ -96,7 +96,10 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. py::buffer_info info = data.request(); checkBufferDivisible(info, 3); - // Interpret as single row of RGB pixels + // --- detect C-contiguous --- + checkCContiguousArray(info); + + // --- proceed normally --- BitDepth bitDepth = getBufferBitDepth(info); py::gil_scoped_release release; @@ -115,6 +118,7 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. chanStrideBytes, xStrideBytes, yStrideBytes); + self->apply(img); }, "data"_a, @@ -171,6 +175,8 @@ float values is returned, leaving the input list unchanged. py::buffer_info info = data.request(); checkBufferDivisible(info, 4); + // --- detect C-contiguous --- + checkCContiguousArray(info); // Interpret as single row of RGBA pixels BitDepth bitDepth = getBufferBitDepth(info); diff --git a/src/bindings/python/PyUtils.cpp b/src/bindings/python/PyUtils.cpp index 23be75068..fbf1afc31 100644 --- a/src/bindings/python/PyUtils.cpp +++ b/src/bindings/python/PyUtils.cpp @@ -179,6 +179,27 @@ void checkBufferType(const py::buffer_info & info, BitDepth bitDepth) checkBufferType(info, bitDepthToDtype(bitDepth)); } +void checkCContiguousArray(const py::buffer_info & info) +{ + bool isC = true; + ptrdiff_t itemsize = info.itemsize; + auto shape = info.shape; + auto strides = info.strides; + py::ssize_t ndim = info.ndim; + + ptrdiff_t expected = itemsize; + for (py::ssize_t i = ndim - 1; i >= 0; --i) + { + if (strides[i] != expected) { isC = false; break; } + expected *= shape[i]; + } + + if (!isC) + { + throw std::runtime_error("function only supports C-contiguous (row-major) arrays"); + } +} + void checkBufferDivisible(const py::buffer_info & info, py::ssize_t numChannels) { if (info.size % numChannels != 0) diff --git a/src/bindings/python/PyUtils.h b/src/bindings/python/PyUtils.h index f5d39ee91..8e0ca44ef 100644 --- a/src/bindings/python/PyUtils.h +++ b/src/bindings/python/PyUtils.h @@ -89,6 +89,9 @@ unsigned long getBufferLut3DGridSize(const py::buffer_info & info); // Throw if vector size is not divisible by channel count void checkVectorDivisible(const std::vector & pixel, size_t numChannels); +// Throw if array is not C-contiguous +void checkCContiguousArray(const py::buffer_info & info); + } // namespace OCIO_NAMESPACE #endif // INCLUDED_OCIO_PYUTILS_H diff --git a/tests/python/CPUProcessorTest.py b/tests/python/CPUProcessorTest.py index 4fc5e3c15..128281cfe 100644 --- a/tests/python/CPUProcessorTest.py +++ b/tests/python/CPUProcessorTest.py @@ -19,7 +19,7 @@ class CPUProcessorTest(unittest.TestCase): - FLOAT_DELTA = 1e+5 + FLOAT_DELTA = 1e-5 UINT_DELTA = 1 @classmethod @@ -385,6 +385,28 @@ def test_apply_rgb_list(self): delta=self.FLOAT_DELTA ) + def test_apply_rgb_buffer_column_major(self): + if not np: + logger.warning("NumPy not found. Skipping test!") + return + + for arr, cpu_proc_fwd in [ + (self.float_rgb_2d, self.default_cpu_proc_fwd), + (self.float_rgb_3d, self.default_cpu_proc_fwd), + (self.half_rgb_2d, self.half_cpu_proc_fwd), + (self.half_rgb_3d, self.half_cpu_proc_fwd), + (self.uint16_rgb_2d, self.uint16_cpu_proc_fwd), + (self.uint16_rgb_3d, self.uint16_cpu_proc_fwd), + (self.uint8_rgb_2d, self.uint8_cpu_proc_fwd), + (self.uint8_rgb_3d, self.uint8_cpu_proc_fwd), + ]: + # Transpose to F-order (column-major) + arr_copy = arr.copy().T + + # Expect runtime error for non-C-contiguous array + with self.assertRaises(RuntimeError): + cpu_proc_fwd.applyRGB(arr_copy) + def test_apply_rgb_buffer(self): if not np: logger.warning("NumPy not found. Skipping test!") @@ -627,3 +649,48 @@ def test_apply_rgba_buffer(self): arr.flat[i], delta=self.UINT_DELTA ) + + def test_apply_rgba_buffer_column_major(self): + if not np: + logger.warning("NumPy not found. Skipping test!") + return + + for arr, cpu_proc_fwd in [ + ( + self.float_rgba_2d, + self.default_cpu_proc_fwd + ), + ( + self.float_rgba_3d, + self.default_cpu_proc_fwd + ), + ( + self.half_rgba_2d, + self.half_cpu_proc_fwd + ), + ( + self.half_rgba_3d, + self.half_cpu_proc_fwd + ), + ( + self.uint16_rgba_2d, + self.uint16_cpu_proc_fwd + ), + ( + self.uint16_rgba_3d, + self.uint16_cpu_proc_fwd + ), + ( + self.uint8_rgba_2d, + self.uint8_cpu_proc_fwd + ), + ( + self.uint8_rgba_3d, + self.uint8_cpu_proc_fwd, + ), + ]: + # Transpose to F-order (column-major) + arr_copy = arr.copy().T + # Expect runtime error for non-C-contiguous array + with self.assertRaises(RuntimeError): + cpu_proc_fwd.applyRGBA(arr_copy)