From c3fc9146c65e6c46922939e2943c2c791109623a Mon Sep 17 00:00:00 2001 From: Afsaneh Sheikhmiri Date: Fri, 26 Sep 2025 14:23:34 -0400 Subject: [PATCH 1/3] adding unit test for applyRGB buffer, check in the applyRGB if the buffer is C order or not, for now throw error Signed-off-by: Afsaneh Sheikhmiri --- src/bindings/python/PyCPUProcessor.cpp | 13 ++++ tests/python/CPUProcessorTest.py | 83 ++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/bindings/python/PyCPUProcessor.cpp b/src/bindings/python/PyCPUProcessor.cpp index 84b91427bf..42b3bafbb0 100644 --- a/src/bindings/python/PyCPUProcessor.cpp +++ b/src/bindings/python/PyCPUProcessor.cpp @@ -100,6 +100,19 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. BitDepth bitDepth = getBufferBitDepth(info); py::gil_scoped_release release; + bool isCContiguous = true; + if (info.ndim >= 2) + { + // last dimension stride should be itemsize + if (info.strides.back() != info.itemsize) + { + isCContiguous = false; + } + } + if (!isCContiguous) + { + throw std::runtime_error("applyRGB only supports C-contiguous (row-major) arrays"); + } long numChannels = 3; long width = (long)info.size / numChannels; diff --git a/tests/python/CPUProcessorTest.py b/tests/python/CPUProcessorTest.py index 4fc5e3c157..a9674f9b21 100644 --- a/tests/python/CPUProcessorTest.py +++ b/tests/python/CPUProcessorTest.py @@ -385,6 +385,89 @@ 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, cpu_proc_inv in [ + ( + self.float_rgb_2d, + self.default_cpu_proc_fwd, + self.default_cpu_proc_inv + ), + ( + self.float_rgb_3d, + self.default_cpu_proc_fwd, + self.default_cpu_proc_inv + ), + ( + self.half_rgb_2d, + self.half_cpu_proc_fwd, + self.half_cpu_proc_inv + ), + ( + self.half_rgb_3d, + self.half_cpu_proc_fwd, + self.half_cpu_proc_inv + ), + ( + self.uint16_rgb_2d, + self.uint16_cpu_proc_fwd, + self.uint16_cpu_proc_inv + ), + ( + self.uint16_rgb_3d, + self.uint16_cpu_proc_fwd, + self.uint16_cpu_proc_inv + ), + ( + self.uint8_rgb_2d, + self.uint8_cpu_proc_fwd, + self.uint8_cpu_proc_inv + ), + ( + self.uint8_rgb_3d, + self.uint8_cpu_proc_fwd, + self.uint8_cpu_proc_inv + ), + ]: + # Transpose to column-major format and + # Process duplicate array + arr_copy = arr.copy().T + + cpu_proc_fwd.applyRGB(arr_copy) + for i in range(arr_copy.size): + if arr.dtype in (np.float32, np.float16): + self.assertAlmostEqual( + arr_copy.flat[i], + arr.flat[i] * 0.5, + delta=self.FLOAT_DELTA + ) + else: + self.assertAlmostEqual( + arr_copy.flat[i], + arr.flat[i] // 2, + delta=self.UINT_DELTA + ) + + # Inverse transform roundtrips values in place + cpu_proc_inv.applyRGB(arr_copy) + + for i in range(arr_copy.size): + if arr.dtype in (np.float32, np.float16): + self.assertAlmostEqual( + arr_copy.flat[i], + arr.flat[i], + delta=self.FLOAT_DELTA + ) + else: + self.assertAlmostEqual( + arr_copy.flat[i], + arr.flat[i], + delta=self.UINT_DELTA + ) + def test_apply_rgb_buffer(self): if not np: logger.warning("NumPy not found. Skipping test!") From f56ef65a39b1f234fc54016068396241d3ec2a88 Mon Sep 17 00:00:00 2001 From: Afsaneh Sheikhmiri Date: Sun, 28 Sep 2025 19:00:56 -0400 Subject: [PATCH 2/3] Adsk Contrib - Detect C/F order, if not C order : RuntimeError shown, test is still not passing Signed-off-by: Afsaneh Sheikhmiri --- src/bindings/python/PyCPUProcessor.cpp | 33 ++++++----- tests/python/CPUProcessorTest.py | 80 +++++++++----------------- 2 files changed, 47 insertions(+), 66 deletions(-) diff --git a/src/bindings/python/PyCPUProcessor.cpp b/src/bindings/python/PyCPUProcessor.cpp index 42b3bafbb0..46c3ce3bbd 100644 --- a/src/bindings/python/PyCPUProcessor.cpp +++ b/src/bindings/python/PyCPUProcessor.cpp @@ -92,28 +92,34 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. )doc") .def("applyRGB", [](CPUProcessorRcPtr & self, py::buffer & data) - { + { py::buffer_info info = data.request(); checkBufferDivisible(info, 3); - // Interpret as single row of RGB pixels - BitDepth bitDepth = getBufferBitDepth(info); + // --- detect C-contiguous --- + bool isC = true; + ptrdiff_t itemsize = info.itemsize; + auto shape = info.shape; + auto strides = info.strides; + py::ssize_t ndim = info.ndim; - py::gil_scoped_release release; - bool isCContiguous = true; - if (info.ndim >= 2) + ptrdiff_t expected = itemsize; + for (py::ssize_t i = ndim - 1; i >= 0; --i) { - // last dimension stride should be itemsize - if (info.strides.back() != info.itemsize) - { - isCContiguous = false; - } + if (strides[i] != expected) { isC = false; break; } + expected *= shape[i]; } - if (!isCContiguous) + + if (!isC) { throw std::runtime_error("applyRGB only supports C-contiguous (row-major) arrays"); } + // --- proceed normally --- + BitDepth bitDepth = getBufferBitDepth(info); + + py::gil_scoped_release release; + long numChannels = 3; long width = (long)info.size / numChannels; long height = 1; @@ -128,8 +134,9 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. chanStrideBytes, xStrideBytes, yStrideBytes); + self->apply(img); - }, + }, "data"_a, R"doc( Apply to a packed RGB array adhering to the Python buffer protocol. diff --git a/tests/python/CPUProcessorTest.py b/tests/python/CPUProcessorTest.py index a9674f9b21..51456148e1 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 @@ -391,79 +391,53 @@ def test_apply_rgb_buffer_column_major(self): return for arr, cpu_proc_fwd, cpu_proc_inv in [ - ( - self.float_rgb_2d, - self.default_cpu_proc_fwd, - self.default_cpu_proc_inv - ), - ( - self.float_rgb_3d, - self.default_cpu_proc_fwd, - self.default_cpu_proc_inv - ), - ( - self.half_rgb_2d, - self.half_cpu_proc_fwd, - self.half_cpu_proc_inv - ), - ( - self.half_rgb_3d, - self.half_cpu_proc_fwd, - self.half_cpu_proc_inv - ), - ( - self.uint16_rgb_2d, - self.uint16_cpu_proc_fwd, - self.uint16_cpu_proc_inv - ), - ( - self.uint16_rgb_3d, - self.uint16_cpu_proc_fwd, - self.uint16_cpu_proc_inv - ), - ( - self.uint8_rgb_2d, - self.uint8_cpu_proc_fwd, - self.uint8_cpu_proc_inv - ), - ( - self.uint8_rgb_3d, - self.uint8_cpu_proc_fwd, - self.uint8_cpu_proc_inv - ), + (self.float_rgb_2d, self.default_cpu_proc_fwd, self.default_cpu_proc_inv), + (self.float_rgb_3d, self.default_cpu_proc_fwd, self.default_cpu_proc_inv), + (self.half_rgb_2d, self.half_cpu_proc_fwd, self.half_cpu_proc_inv), + (self.half_rgb_3d, self.half_cpu_proc_fwd, self.half_cpu_proc_inv), + (self.uint16_rgb_2d, self.uint16_cpu_proc_fwd, self.uint16_cpu_proc_inv), + (self.uint16_rgb_3d, self.uint16_cpu_proc_fwd, self.uint16_cpu_proc_inv), + (self.uint8_rgb_2d, self.uint8_cpu_proc_fwd, self.uint8_cpu_proc_inv), + (self.uint8_rgb_3d, self.uint8_cpu_proc_fwd, self.uint8_cpu_proc_inv), ]: - # Transpose to column-major format and - # Process duplicate array + # Transpose to F-order (column-major) arr_copy = arr.copy().T - cpu_proc_fwd.applyRGB(arr_copy) - for i in range(arr_copy.size): + # Expect runtime error for non-C-contiguous array + with self.assertRaises(RuntimeError): + cpu_proc_fwd.applyRGB(arr_copy) + + # Convert back to C-order and retry + arr_copy_c = np.ascontiguousarray(arr_copy) + cpu_proc_fwd.applyRGB(arr_copy_c) + + # Check forward transform + for i in range(arr_copy_c.size): if arr.dtype in (np.float32, np.float16): self.assertAlmostEqual( - arr_copy.flat[i], + arr_copy_c.flat[i], arr.flat[i] * 0.5, delta=self.FLOAT_DELTA ) else: self.assertAlmostEqual( - arr_copy.flat[i], + arr_copy_c.flat[i], arr.flat[i] // 2, delta=self.UINT_DELTA ) - # Inverse transform roundtrips values in place - cpu_proc_inv.applyRGB(arr_copy) - - for i in range(arr_copy.size): + # Inverse transform + cpu_proc_inv.applyRGB(arr_copy_c) + for i in range(arr_copy_c.size): if arr.dtype in (np.float32, np.float16): self.assertAlmostEqual( - arr_copy.flat[i], + arr_copy_c.flat[i], arr.flat[i], delta=self.FLOAT_DELTA ) else: self.assertAlmostEqual( - arr_copy.flat[i], + arr_copy_c.flat[i], arr.flat[i], delta=self.UINT_DELTA ) From 1cfb236fa7b00bfae6635f35f32a84435092f904 Mon Sep 17 00:00:00 2001 From: Afsaneh Sheikhmiri Date: Mon, 6 Oct 2025 12:08:01 -0400 Subject: [PATCH 3/3] remove 1d arrays from the test, since even after Transpose, 1d arrays are still contiguous Signed-off-by: Afsaneh Sheikhmiri --- src/bindings/python/PyCPUProcessor.cpp | 24 ++----- src/bindings/python/PyUtils.cpp | 21 ++++++ src/bindings/python/PyUtils.h | 3 + tests/python/CPUProcessorTest.py | 98 ++++++++++++++------------ 4 files changed, 83 insertions(+), 63 deletions(-) diff --git a/src/bindings/python/PyCPUProcessor.cpp b/src/bindings/python/PyCPUProcessor.cpp index 46c3ce3bbd..e5786ba244 100644 --- a/src/bindings/python/PyCPUProcessor.cpp +++ b/src/bindings/python/PyCPUProcessor.cpp @@ -92,28 +92,12 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. )doc") .def("applyRGB", [](CPUProcessorRcPtr & self, py::buffer & data) - { + { py::buffer_info info = data.request(); checkBufferDivisible(info, 3); // --- detect C-contiguous --- - 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("applyRGB only supports C-contiguous (row-major) arrays"); - } + checkCContiguousArray(info); // --- proceed normally --- BitDepth bitDepth = getBufferBitDepth(info); @@ -136,7 +120,7 @@ written to the dstImgDesc image, leaving srcImgDesc unchanged. yStrideBytes); self->apply(img); - }, + }, "data"_a, R"doc( Apply to a packed RGB array adhering to the Python buffer protocol. @@ -191,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 23be750685..fbf1afc31e 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 f5d39ee911..8e0ca44ef5 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 51456148e1..128281cfe9 100644 --- a/tests/python/CPUProcessorTest.py +++ b/tests/python/CPUProcessorTest.py @@ -390,15 +390,15 @@ def test_apply_rgb_buffer_column_major(self): logger.warning("NumPy not found. Skipping test!") return - for arr, cpu_proc_fwd, cpu_proc_inv in [ - (self.float_rgb_2d, self.default_cpu_proc_fwd, self.default_cpu_proc_inv), - (self.float_rgb_3d, self.default_cpu_proc_fwd, self.default_cpu_proc_inv), - (self.half_rgb_2d, self.half_cpu_proc_fwd, self.half_cpu_proc_inv), - (self.half_rgb_3d, self.half_cpu_proc_fwd, self.half_cpu_proc_inv), - (self.uint16_rgb_2d, self.uint16_cpu_proc_fwd, self.uint16_cpu_proc_inv), - (self.uint16_rgb_3d, self.uint16_cpu_proc_fwd, self.uint16_cpu_proc_inv), - (self.uint8_rgb_2d, self.uint8_cpu_proc_fwd, self.uint8_cpu_proc_inv), - (self.uint8_rgb_3d, self.uint8_cpu_proc_fwd, self.uint8_cpu_proc_inv), + 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 @@ -407,41 +407,6 @@ def test_apply_rgb_buffer_column_major(self): with self.assertRaises(RuntimeError): cpu_proc_fwd.applyRGB(arr_copy) - # Convert back to C-order and retry - arr_copy_c = np.ascontiguousarray(arr_copy) - cpu_proc_fwd.applyRGB(arr_copy_c) - - # Check forward transform - for i in range(arr_copy_c.size): - if arr.dtype in (np.float32, np.float16): - self.assertAlmostEqual( - arr_copy_c.flat[i], - arr.flat[i] * 0.5, - delta=self.FLOAT_DELTA - ) - else: - self.assertAlmostEqual( - arr_copy_c.flat[i], - arr.flat[i] // 2, - delta=self.UINT_DELTA - ) - - # Inverse transform - cpu_proc_inv.applyRGB(arr_copy_c) - for i in range(arr_copy_c.size): - if arr.dtype in (np.float32, np.float16): - self.assertAlmostEqual( - arr_copy_c.flat[i], - arr.flat[i], - delta=self.FLOAT_DELTA - ) - else: - self.assertAlmostEqual( - arr_copy_c.flat[i], - arr.flat[i], - delta=self.UINT_DELTA - ) - def test_apply_rgb_buffer(self): if not np: logger.warning("NumPy not found. Skipping test!") @@ -684,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)