Browse Source

python: exposed CompressedImage and ImageView classes.

next
Vladimír Vondruš 3 years ago
parent
commit
c9515bbd1d
  1. 102
      doc/python/magnum.rst
  2. 2
      doc/python/pages/changelog.rst
  3. 3
      src/python/magnum/__init__.py
  4. 94
      src/python/magnum/magnum.cpp
  5. 163
      src/python/magnum/test/test.py
  6. 10
      src/python/magnum/test/test_trade.py
  7. 24
      src/python/magnum/trade.cpp

102
doc/python/magnum.rst

@ -83,6 +83,23 @@
See :ref:`Image2D` for more information.
.. py:class:: magnum.CompressedImage1D
See :ref:`CompressedImage2D` for more information.
.. py:class:: magnum.CompressedImage2D
An owning counterpart to :ref:`CompressedImageView2D` /
:ref:`MutableCompressedImageView2D`. Holds its own data buffer, thus
doesn't have an equivalent to :ref:`CompressedImageView2D.owner`. The
:ref:`data` view allows mutable access. Implicitly convertible to
:ref:`CompressedImageView2D` / :ref:`MutableCompressedImageView2D`, so all
APIs consuming image views work with this type as well.
.. py:class:: magnum.CompressedImage3D
See :ref:`CompressedImage2D` for more information.
.. py:class:: magnum.ImageView1D
See :ref:`ImageView2D` for more information.
@ -194,3 +211,88 @@
This function is used to implement implicit conversion from
:ref:`trade.ImageData3D` in the :ref:`trade` module.
.. py:class:: magnum.CompressedImageView1D
See :ref:`CompressedImageView2D` for more information.
.. py:class:: magnum.CompressedImageView2D
:TODO: remove this line once m.css stops ignoring first caption on a page
`Memory ownership and reference counting`_
==========================================
Similarly to :ref:`corrade.containers.ArrayView` (and unlike in C++), the
view keeps a reference to the original memory owner object in the
:ref:`owner` field, meaning that calling :py:`del` on the original object
will *not* invalidate the view. Slicing a view creates a new view
referencing the same original object, without any dependency on the
previous view. That means a long chained slicing operation will not cause
increased memory usage.
The :ref:`owner` is :py:`None` if the view is empty.
.. py:class:: magnum.CompressedImageView3D
See :ref:`CompressedImageView2D` for more information.
.. py:class:: magnum.MutableCompressedImageView1D
See :ref:`CompressedImageView2D` for more information. The only difference
to the non-mutable variant is that it's possible to modify the image
through :ref:`data`.
.. py:class:: magnum.MutableCompressedImageView2D
See :ref:`CompressedImageView2D` for more information. The only difference
to the non-mutable variant is that it's possible to modify the image
through :ref:`data`.
.. py:class:: magnum.MutableImageView3D
See :ref:`CompressedImageView2D` for more information. The only difference
to the non-mutable variant is that it's possible to modify the image
through :ref:`data`.
.. py:function:: magnum.CompressedImageView1D.__init__(self, arg0: magnum.CompressedImageView1D)
:raise RuntimeError: If :ref:`trade.ImageData1D.is_compressed` is
:py:`False`
This function is used to implement implicit conversion from
:ref:`trade.ImageData1D` in the :ref:`trade` module.
.. py:function:: magnum.CompressedImageView2D.__init__(self, arg0: magnum.CompressedImageView2D)
:raise RuntimeError: If :ref:`trade.ImageData2D.is_compressed` is
:py:`False`
This function is used to implement implicit conversion from
:ref:`trade.ImageData2D` in the :ref:`trade` module.
.. py:function:: magnum.CompressedImageView3D.__init__(self, arg0: magnum.CompressedImageView3D)
:raise RuntimeError: If :ref:`trade.ImageData3D.is_compressed` is
:py:`False`
This function is used to implement implicit conversion from
:ref:`trade.ImageData3D` in the :ref:`trade` module.
.. py:function:: magnum.MutableCompressedImageView1D.__init__(self, arg0: magnum.MutableCompressedImageView1D)
:raise RuntimeError: If :ref:`trade.ImageData1D.is_compressed` is
:py:`False`
This function is used to implement implicit conversion from
:ref:`trade.ImageData1D` in the :ref:`trade` module.
.. py:function:: magnum.MutableCompressedImageView2D.__init__(self, arg0: magnum.MutableCompressedImageView2D)
:raise RuntimeError: If :ref:`trade.ImageData2D.is_compressed` is
:py:`True`
This function is used to implement implicit conversion from
:ref:`trade.ImageData2D` in the :ref:`trade` module.
.. py:function:: magnum.MutableCompressedImageView3D.__init__(self, arg0: magnum.MutableCompressedImageView3D)
:raise RuntimeError: If :ref:`trade.ImageData3D.is_compressed` is
:py:`False`
This function is used to implement implicit conversion from
:ref:`trade.ImageData3D` in the :ref:`trade` module.

2
doc/python/pages/changelog.rst

@ -58,6 +58,8 @@ Changelog
- Exposed the :ref:`CompressedPixelFormat` enum, various pixel-format-related
helper APIs are now properties on :ref:`PixelFormat` and
:ref:`CompressedPixelFormat`
- Exposed :ref:`CompressedImage2D`, :ref:`CompressedImageView2D`,
:ref:`MutableCompressedImageView2D` and their 1D and 3D counterparts
- Exposed :ref:`Color3.from_xyz()`, :ref:`Color3.from_linear_rgb_int()`,
:ref:`Color3.to_xyz()`, :ref:`Color3.to_linear_rgb_int()` and equivalent
APIs on :ref:`Color4`

3
src/python/magnum/__init__.py

@ -80,8 +80,11 @@ __all__ = [
'PixelFormat', 'PixelStorage',
'CompressedPixelFormat',
'Image1D', 'Image2D', 'Image3D',
'CompressedImage1D', 'CompressedImage2D', 'CompressedImage3D',
'ImageView1D', 'ImageView2D', 'ImageView3D',
'MutableImageView1D', 'MutableImageView2D', 'MutableImageView3D',
'CompressedImageView1D', 'CompressedImageView2D', 'CompressedImageView3D',
'MutableCompressedImageView1D', 'MutableCompressedImageView2D', 'MutableCompressedImageView3D',
'SamplerFilter', 'SamplerMipmap', 'SamplerWrapping',

94
src/python/magnum/magnum.cpp

@ -170,6 +170,75 @@ template<class T> void imageViewFromMutable(py::class_<T, PyImageViewHolder<T>>&
}), "Construct from a mutable view");
}
template<class T> void compressedImage(py::class_<T>& c) {
c
/* Constructors. Only the ones that are *not* taking an Array, as
Python has no way to "move" it in */
.def(py::init(), "Construct an image placeholder")
/* Properties */
.def_property_readonly("format", &T::format, "Format of compressed pixel data")
.def_property_readonly("size", [](T& self) {
return PyDimensionTraits<T::Dimensions, Int>::from(self.size());
}, "Image size")
.def_property_readonly("data", [](T& self) {
return Containers::pyArrayViewHolder(self.data(), self.data() ? py::cast(self) : py::none{});
}, "Raw image data");
}
template<class T> void compressedImageView(py::class_<T, PyImageViewHolder<T>>& c) {
/*
Missing APIs:
Type, ErasedType, Dimensions
*/
py::implicitly_convertible<CompressedImage<T::Dimensions>, T>();
c
/* Constructors. The variants *not* taking an array view have to be
first, otherwise things fail on systems that don't have numpy
installed. See imageView() above for details */
.def(py::init([](CompressedPixelFormat format, const typename PyDimensionTraits<T::Dimensions, Int>::VectorType& size) {
return T{format, size};
}), "Construct an empty view")
.def(py::init([](CompressedPixelFormat format, const typename PyDimensionTraits<T::Dimensions, Int>::VectorType& size, const Containers::ArrayView<typename T::Type>& data) {
return pyImageViewHolder(T{format, size, data}, pyObjectHolderFor<Containers::PyArrayViewHolder>(data).owner);
}), "Constructor")
.def(py::init([](CompressedImage<T::Dimensions>& image) {
return pyImageViewHolder(T{image}, image.data() ? py::cast(image) : py::none{});
}), "Construct a view on an image")
.def(py::init([](const CompressedImageView<T::Dimensions, typename T::Type>& other) {
return pyImageViewHolder(CompressedImageView<T::Dimensions, typename T::Type>(other), pyObjectHolderFor<PyImageViewHolder>(other).owner);
}), "Construct from any type convertible to an image view")
/* Properties */
.def_property_readonly("format", &T::format, "Format of compressedpixel data")
.def_property_readonly("size", [](T& self) {
return PyDimensionTraits<T::Dimensions, Int>::from(self.size());
}, "Image size")
.def_property("data", [](T& self) {
return Containers::pyArrayViewHolder(self.data(), pyObjectHolderFor<PyImageViewHolder>(self).owner);
}, [](T& self, const Containers::ArrayView<typename T::Type>& data) {
self.setData(data);
pyObjectHolderFor<PyImageViewHolder>(self).owner =
pyObjectHolderFor<Containers::PyArrayViewHolder>(data).owner;
}, "Raw image data")
.def_property_readonly("owner", [](T& self) {
return pyObjectHolderFor<PyImageViewHolder>(self).owner;
}, "Memory owner");
}
template<class T> void compressedImageViewFromMutable(py::class_<T, PyImageViewHolder<T>>& c) {
py::implicitly_convertible<BasicMutableCompressedImageView<T::Dimensions>, T>();
c
.def(py::init([](const BasicMutableCompressedImageView<T::Dimensions>& other) {
return pyImageViewHolder(BasicCompressedImageView<T::Dimensions>(other), pyObjectHolderFor<PyImageViewHolder>(other).owner);
}), "Construct from a mutable view");
}
void magnum(py::module_& m) {
m.attr("BUILD_DEPRECATED") =
#ifdef MAGNUM_BUILD_DEPRECATED
@ -540,6 +609,13 @@ void magnum(py::module_& m) {
image(image2D);
image(image3D);
py::class_<CompressedImage1D> compressedImage1D{m, "CompressedImage1D", "One-dimensional compressed image"};
py::class_<CompressedImage2D> compressedImage2D{m, "CompressedImage2D", "Two-dimensional compressed image"};
py::class_<CompressedImage3D> compressedImage3D{m, "CompressedImage3D", "Three-dimensional compressed image"};
compressedImage(compressedImage1D);
compressedImage(compressedImage2D);
compressedImage(compressedImage3D);
py::class_<ImageView1D, PyImageViewHolder<ImageView1D>> imageView1D{m, "ImageView1D", "One-dimensional image view"};
py::class_<ImageView2D, PyImageViewHolder<ImageView2D>> imageView2D{m, "ImageView2D", "Two-dimensional image view"};
py::class_<ImageView3D, PyImageViewHolder<ImageView3D>> imageView3D{m, "ImageView3D", "Three-dimensional image view"};
@ -558,6 +634,24 @@ void magnum(py::module_& m) {
imageViewFromMutable(imageView2D);
imageViewFromMutable(imageView3D);
py::class_<CompressedImageView1D, PyImageViewHolder<CompressedImageView1D>> compressedImageView1D{m, "CompressedImageView1D", "One-dimensional compressed image view"};
py::class_<CompressedImageView2D, PyImageViewHolder<CompressedImageView2D>> compressedImageView2D{m, "CompressedImageView2D", "Two-dimensional compressed image view"};
py::class_<CompressedImageView3D, PyImageViewHolder<CompressedImageView3D>> compressedImageView3D{m, "CompressedImageView3D", "Three-dimensional compressed image view"};
py::class_<MutableCompressedImageView1D, PyImageViewHolder<MutableCompressedImageView1D>> mutableCompressedImageView1D{m, "MutableCompressedImageView1D", "One-dimensional mutable compressed image view"};
py::class_<MutableCompressedImageView2D, PyImageViewHolder<MutableCompressedImageView2D>> mutableCompressedImageView2D{m, "MutableCompressedImageView2D", "Two-dimensional mutable compressed image view"};
py::class_<MutableCompressedImageView3D, PyImageViewHolder<MutableCompressedImageView3D>> mutableCompressedImageView3D{m, "MutableCompressedImageView3D", "Three-dimensional mutable compressed image view"};
compressedImageView(compressedImageView1D);
compressedImageView(compressedImageView2D);
compressedImageView(compressedImageView3D);
compressedImageView(mutableCompressedImageView1D);
compressedImageView(mutableCompressedImageView2D);
compressedImageView(mutableCompressedImageView3D);
compressedImageViewFromMutable(compressedImageView1D);
compressedImageViewFromMutable(compressedImageView2D);
compressedImageViewFromMutable(compressedImageView3D);
py::enum_<SamplerFilter>{m, "SamplerFilter", "Texture sampler filtering"}
.value("NEAREST", SamplerFilter::Nearest)
.value("LINEAR", SamplerFilter::Linear);

163
src/python/magnum/test/test.py

@ -183,6 +183,35 @@ class Image(unittest.TestCase):
with self.assertRaisesRegex(NotImplementedError, "access to this pixel format is not implemented yet, sorry"):
a.pixels
class CompressedImage(unittest.TestCase):
def test_init_empty(self):
a = CompressedImage2D()
self.assertEqual(a.size, Vector2i())
self.assertEqual(a.format, CompressedPixelFormat(0))
@unittest.skip("No way to create a non-empty Image at the moment")
def test_data_access(self):
# Tested in test_gl_gl.Framebuffer.test_read_image instead
a = CompressedImage2D(CompressedPixelFormat.BC1_RGB_UNORM, 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_access_empty(self):
a = CompressedImage2D()
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)
class ImageView(unittest.TestCase):
def test_init(self):
# 2x4 RGB pixels, padded for alignment
@ -513,6 +542,140 @@ class ImageView(unittest.TestCase):
with self.assertRaisesRegex(NotImplementedError, "access to this pixel format is not implemented yet, sorry"):
a.pixels
class CompressedImageView(unittest.TestCase):
def test_init(self):
# Four 64-bit blocks
data = (b'01234567'
b'89abcdef'
b'FEDCBA98'
b'76543210')
data_refcount = sys.getrefcount(data)
a = CompressedImageView2D(CompressedPixelFormat.BC1_RGB_UNORM, (4, 16), data)
self.assertEqual(a.size, Vector2i(4, 16))
self.assertEqual(a.format, CompressedPixelFormat.BC1_RGB_UNORM)
self.assertEqual(a.owner, data)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
del a
self.assertEqual(sys.getrefcount(data), data_refcount)
def test_init_empty(self):
a = MutableCompressedImageView1D(CompressedPixelFormat.ASTC_4X4_RGBA_UNORM, 32)
self.assertEqual(a.size, 32)
self.assertEqual(a.owner, None)
def test_init_mutable(self):
# Four 64-bit blocks
data = bytearray(b'01234567'
b'89abcdef'
b'FEDCBA98'
b'76543210')
data_refcount = sys.getrefcount(data)
a = MutableCompressedImageView2D(CompressedPixelFormat.BC4_R_SNORM, (8, 8), data)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
# Back to immutable
b = CompressedImageView2D(a)
self.assertEqual(b.size, Vector2i(8, 8))
self.assertEqual(b.format, CompressedPixelFormat.BC4_R_SNORM)
self.assertEqual(len(b.data), 32)
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):
# TODO adapt from ImageView.test_init_image
pass
def test_init_image_empty(self):
a = CompressedImage2D()
a_refcount = sys.getrefcount(a)
view = CompressedImageView2D(a)
self.assertEqual(view.size, (0, 0))
self.assertIs(view.owner, None)
self.assertEqual(sys.getrefcount(a), a_refcount)
mview = MutableCompressedImageView2D(a)
self.assertEqual(mview.size, (0, 0))
self.assertIs(mview.owner, None)
self.assertEqual(sys.getrefcount(a), a_refcount)
def test_data_access(self):
# Two 128-bit blocks
data = (b'0123456789abcdef'
b'FEDCBA9876543210')
data_refcount = sys.getrefcount(data)
a = CompressedImageView2D(CompressedPixelFormat.BC7_RGBA_UNORM, (8, 4), data)
a_refcount = sys.getrefcount(a)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
a_data = a.data
self.assertEqual(len(a_data), 32)
self.assertEqual(a_data[9], '9')
self.assertEqual(a_data[20], 'B')
self.assertIs(a_data.owner, data)
# The data references the original data as an owner, not the view
self.assertEqual(sys.getrefcount(a), a_refcount)
self.assertEqual(sys.getrefcount(data), data_refcount + 2)
del a_data
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
def test_mutable_data_access(self):
# Two 128-bit blocks
data = bytearray(b'0123456789abcdef'
b'FEDCBA9876543210')
data_refcount = sys.getrefcount(data)
a = MutableCompressedImageView2D(CompressedPixelFormat.BC6H_RGB_SFLOAT, (4, 8), data)
a_refcount = sys.getrefcount(a)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
a_data = a.data
self.assertEqual(len(a_data), 32)
self.assertEqual(a_data[9], '9')
self.assertEqual(a_data[20], 'B')
self.assertIs(a_data.owner, data)
# The data references the original data as an owner, not the view
self.assertEqual(sys.getrefcount(a), a_refcount)
self.assertEqual(sys.getrefcount(data), data_refcount + 2)
a_data[9] = '_'
a_data[20] = '_'
self.assertEqual(data, b'012345678_abcdef'
b'FEDC_A9876543210')
del a_data
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
def test_set_data(self):
# Two 128-bit blocks
data = (b'0123456789ABCDEF'
b'fedcba9876543210')
data_refcount = sys.getrefcount(data)
a = CompressedImageView2D(CompressedPixelFormat.BC3_RGBA_SRGB, (4, 8), data)
self.assertIs(a.owner, data)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
data2 = (b'0123456789abcdef'
b'FEDCBA9876543210')
data2_refcount = sys.getrefcount(data2)
# Replacing the data should disown the original object and point to the
# new one
a.data = data2
self.assertEqual(bytes(a.data),
b'0123456789abcdef'
b'FEDCBA9876543210')
self.assertIs(a.owner, data2)
self.assertEqual(sys.getrefcount(data), data_refcount)
self.assertEqual(sys.getrefcount(data2), data2_refcount + 1)
class UtilityCopy(unittest.TestCase):
def test_1d(self):
a_data = array.array('f', [1.0, 2.0, 3.0, 4.0, 5.0, 6.0])

10
src/python/magnum/test/test_trade.py

@ -81,14 +81,20 @@ class ImageData(unittest.TestCase):
view = ImageView2D(image)
mutable_view = MutableImageView2D(image)
with self.assertRaisesRegex(RuntimeError, "image is not compressed"):
CompressedImageView2D(image)
with self.assertRaisesRegex(RuntimeError, "image is not compressed"):
MutableCompressedImageView2D(image)
def test_convert_view_compressed(self):
# The only way to get an image instance is through a manager
importer = trade.ImporterManager().load_and_instantiate('DdsImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), "rgba_dxt1.dds"))
image = importer.image2d(0)
# No compressed-image-related APIs exposed ATM, so just verifying the
# uncompressed ones fail properly
view = CompressedImageView2D(image)
mutable_view = MutableCompressedImageView2D(image)
with self.assertRaisesRegex(RuntimeError, "image is compressed"):
view = ImageView2D(image)
with self.assertRaisesRegex(RuntimeError, "image is compressed"):

24
src/python/magnum/trade.cpp

@ -177,6 +177,22 @@ template<UnsignedInt dimensions, class T> PyObject* implicitlyConvertibleToImage
return r;
}
template<UnsignedInt dimensions, class T> PyObject* implicitlyConvertibleToCompressedImageView(PyObject* obj, PyTypeObject*) {
py::detail::make_caster<Trade::ImageData<dimensions>> caster;
if(!caster.load(obj, false)) {
return nullptr;
}
Trade::ImageData<dimensions>& data = caster;
if(!data.isCompressed()) {
PyErr_SetString(PyExc_RuntimeError, "image is not compressed");
throw py::error_already_set{};
}
auto r = pyCastButNotShitty(pyImageViewHolder(CompressedImageView<dimensions, T>(data), py::reinterpret_borrow<py::object>(obj))).release().ptr();
return r;
}
template<UnsignedInt dimensions, class T> Containers::PyArrayViewHolder<Containers::PyStridedArrayView<dimensions, T>> imagePixelsView(Trade::ImageData<dimensions>& image, const Containers::ArrayView<T> data, const Containers::StridedArrayView<dimensions + 1, T>& pixels) {
const PixelFormat format = image.format();
const std::size_t itemsize = pixelFormatSize(format);
@ -218,6 +234,14 @@ template<UnsignedInt dimensions> void imageData(py::class_<Trade::ImageData<dime
auto tinfo = py::detail::get_type_info(typeid(ImageView<dimensions, const char>));
CORRADE_INTERNAL_ASSERT(tinfo);
tinfo->implicit_conversions.push_back(implicitlyConvertibleToImageView<dimensions, const char>);
} {
auto tinfo = py::detail::get_type_info(typeid(CompressedImageView<dimensions, char>));
CORRADE_INTERNAL_ASSERT(tinfo);
tinfo->implicit_conversions.push_back(implicitlyConvertibleToCompressedImageView<dimensions, char>);
} {
auto tinfo = py::detail::get_type_info(typeid(CompressedImageView<dimensions, const char>));
CORRADE_INTERNAL_ASSERT(tinfo);
tinfo->implicit_conversions.push_back(implicitlyConvertibleToCompressedImageView<dimensions, const char>);
}
c

Loading…
Cancel
Save