diff --git a/doc/python/magnum.rst b/doc/python/magnum.rst index 5ab15f8..d65a2a4 100644 --- a/doc/python/magnum.rst +++ b/doc/python/magnum.rst @@ -36,6 +36,21 @@ :data TARGET_WEBGL: WebGL target :data TARGET_VK: Vulkan interoperability +.. py:class:: magnum.Image1D + + See `Image2D` for more information. + +.. py:class:: magnum.Image2D + + An owning counterpart to `ImageView2D` / `MutableImageView2D`. Holds its + own data buffer, thus doesn't have an equivalent to `ImageView2D.owner`. + Implicitly convertible to `ImageView2D` / `MutableImageView2D`, so all APIs + consuming image views work with this type as well. + +.. py:class:: magnum.Image3D + + See `Image2D` for more information. + .. py:class:: magnum.ImageView1D See `ImageView2D` for more information. diff --git a/src/python/magnum/__init__.py b/src/python/magnum/__init__.py index 1cba4f9..860c40f 100644 --- a/src/python/magnum/__init__.py +++ b/src/python/magnum/__init__.py @@ -89,6 +89,7 @@ __all__ = [ 'MeshPrimitive', 'MeshIndexType', 'PixelFormat', 'PixelStorage', + 'Image1D', 'Image2D', 'Image3D', 'ImageView1D', 'ImageView2D', 'ImageView3D', 'MutableImageView1D', 'MutableImageView2D', 'MutableImageView3D', diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index d04e455..a9ca707 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +47,29 @@ namespace py = pybind11; namespace magnum { namespace { +template void image(py::class_& c) { + c + /* Constructors. Only the ones taking the generic format and *not* + taking an Array, as Python has no way to "move" it in */ + .def(py::init(), "Construct an image placeholder") + .def(py::init(), "Construct an image placeholder") + + /* Properties */ + .def_property_readonly("storage", &T::storage, "Storage of pixel data") + .def_property_readonly("format", &T::format, "Format of pixel data") + /** @todo formatExtra() */ + .def_property_readonly("pixel_size", &T::pixelSize, "Pixel size (in bytes)") + .def_property_readonly("size", [](T& self) { + return PyDimensionTraits::from(self.size()); + }, "Image size") + .def_property_readonly("data", [](T& self) { + return Containers::pyArrayViewHolder(self.data(), self.data() ? py::cast(self) : py::none{}); + }, "Image data") + .def_property_readonly("pixels", [](T& self) { + return Containers::pyArrayViewHolder(self.pixels(), self.data() ? py::cast(self) : py::none{}); + }, "View on pixel data"); +} + template void imageView(py::class_>& c) { /* Missing APIs: @@ -53,6 +77,8 @@ template void imageView(py::class_>& c) { Type, ErasedType, Dimensions */ + py::implicitly_convertible, T>(); + c /* Constructors. The variants *not* taking an array view have to be first, otherwise things fail on systems that don't have numpy @@ -88,6 +114,9 @@ template void imageView(py::class_>& c) { .def(py::init([](PixelFormat format, const typename PyDimensionTraits::VectorType& size, const Containers::ArrayView& data) { return pyImageViewHolder(T{format, size, data}, pyObjectHolderFor(data).owner); }), "Constructor") + .def(py::init([](Image& image) { + return pyImageViewHolder(T{image}, image.data() ? py::cast(image) : py::none{}); + }), "Construct a view on an image") /* Properties */ .def_property_readonly("storage", &T::storage, "Storage of pixel data") @@ -254,6 +283,13 @@ void magnum(py::module& m) { .def_property("skip", &PixelStorage::skip, &PixelStorage::setSkip, "Pixel, row and image skip"); + py::class_ image1D{m, "Image1D", "One-dimensional image"}; + py::class_ image2D{m, "Image2D", "Two-dimensional image"}; + py::class_ image3D{m, "Image3D", "Three-dimensional image"}; + image(image1D); + image(image2D); + image(image3D); + py::class_> imageView1D{m, "ImageView1D", "One-dimensional image view"}; py::class_> imageView2D{m, "ImageView2D", "Two-dimensional image view"}; py::class_> imageView3D{m, "ImageView3D", "Three-dimensional image view"}; diff --git a/src/python/magnum/test/test.py b/src/python/magnum/test/test.py index 8526d85..55f9a70 100644 --- a/src/python/magnum/test/test.py +++ b/src/python/magnum/test/test.py @@ -47,6 +47,66 @@ class PixelStorage_(unittest.TestCase): self.assertEqual(a.image_height, 256) self.assertEqual(a.skip, Vector3i(3, 1, 2)) +class Image(unittest.TestCase): + def test_init(self): + storage = PixelStorage() + storage.alignment = 1 + a = Image2D(storage, PixelFormat.RGB8_UNORM) + self.assertEqual(a.storage.alignment, 1) + self.assertEqual(a.size, Vector2i()) + self.assertEqual(a.format, PixelFormat.RGB8_UNORM) + self.assertEqual(len(a.data), 0) + + b = Image2D(PixelFormat.R8I) + self.assertEqual(b.storage.alignment, 4) + self.assertEqual(b.size, Vector2i()) + self.assertEqual(b.format, PixelFormat.R8I) + self.assertEqual(len(b.data), 0) + + @unittest.skip("No way to create a non-empty Image at the moment") + def test_data(self): + a = Image2D(PixelFormat.R8I, Vector2i(3, 17)) # TODO + a_refcount = sys.getrefcount(a) + + data = a.data + self.assertEqual(len(data), 3*17*1) + self.assertIs(data.owner, a) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + del data + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_data_empty(self): + a = Image2D(PixelFormat.R8I) + a_refcount = sys.getrefcount(a) + + data = a.data + self.assertEqual(len(data), 0) + self.assertIs(data.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + + @unittest.skip("No way to create a non-empty Image at the moment") + def test_pixels(self): + a = Image2D(PixelFormat.RG32UI, Vector2i(3, 17)) # TODO + a_refcount = sys.getrefcount(a) + + pixels = a.pixels + self.assertEqual(pixels.size, (3, 17, 8)) + self.assertIs(pixels.owner, a) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + del pixels + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_pixels_empty(self): + a = Image2D(PixelFormat.R8I) + a_refcount = sys.getrefcount(a) + + pixels = a.pixels + self.assertEqual(pixels.size, (0, 0, 1)) + self.assertIs(pixels.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + class ImageView(unittest.TestCase): def test_init(self): # 2x4 RGB pixels, padded for alignment @@ -135,6 +195,41 @@ class ImageView(unittest.TestCase): self.assertIs(b.owner, data) self.assertEqual(sys.getrefcount(data), data_refcount + 2) + @unittest.skip("No way to create a non-empty Image at the moment") + def test_init_image(self): + a = Image2D(PixelFormat.R32F, Vector2i(3, 17)) # TODO + a_refcount = sys.getrefcount(a) + + view = ImageView2D(a) + self.assertEqual(view.size, (3, 17)) + self.assertIs(view.owner, a) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + del view + self.assertEqual(sys.getrefcount(a), a_refcount) + + mview = MutableImageView2D(a) + self.assertEqual(mview.size, (3, 17)) + self.assertIs(mview.owner, a) + self.assertEqual(sys.getrefcount(a), a_refcount + 1) + + del mview + self.assertEqual(sys.getrefcount(a), a_refcount) + + def test_init_image_empty(self): + a = Image2D(PixelFormat.R32F) + a_refcount = sys.getrefcount(a) + + view = ImageView2D(a) + self.assertEqual(view.size, (0, 0)) + self.assertIs(view.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + + mview = MutableImageView2D(a) + self.assertEqual(mview.size, (0, 0)) + self.assertIs(mview.owner, None) + self.assertEqual(sys.getrefcount(a), a_refcount) + def test_set_data(self): # 2x4 RGB pixels, padded for alignment data = (b'rgbRGB '