Browse Source

python: typed access to Image*.pixels.

Basically mirroring what's done for MeshData and SceneData already.
next
Vladimír Vondruš 3 years ago
parent
commit
55d5445ebf
  1. 48
      doc/python/magnum.rst
  2. 35
      doc/python/magnum.trade.rst
  3. 1
      src/Corrade/Containers/StridedArrayViewPythonBindings.h
  4. 7
      src/Magnum/StridedArrayViewPythonBindings.h
  5. 194
      src/python/magnum/acessorsForPixelFormat.h
  6. 23
      src/python/magnum/magnum.cpp
  7. BIN
      src/python/magnum/test/dxt10-depth32f-stencil8ui.dds
  8. 251
      src/python/magnum/test/test.py
  9. 13
      src/python/magnum/test/test_gl_gl.py
  10. 113
      src/python/magnum/test/test_trade.py
  11. 38
      src/python/magnum/trade.cpp

48
doc/python/magnum.rst

@ -46,7 +46,8 @@
An owning counterpart to :ref:`ImageView2D` / :ref:`MutableImageView2D`.
Holds its own data buffer, thus doesn't have an equivalent to
:ref:`ImageView2D.owner`. Implicitly convertible to :ref:`ImageView2D` /
:ref:`ImageView2D.owner`. The :ref:`data` and :ref:`pixels` views allow
mutable access. Implicitly convertible to :ref:`ImageView2D` /
:ref:`MutableImageView2D`, so all APIs consuming image views work with this
type as well.
@ -75,21 +76,60 @@
The :ref:`owner` is :py:`None` if the view is empty.
`Pixel data access`_
====================
The class makes use of Python's dynamic nature and provides direct access
to pixel data in their concrete types via :ref:`pixels`. The returned views
point to the underlying image data, element access coverts to a type
corresponding to a particular :ref:`PixelFormat` and for
performance-oriented access the view implements a buffer protocol with a
corresponding type annotation.
Normalized formats (such as :ref:`PixelFormat.RGB8_UNORM` but also
:ref:`PixelFormat.RGBA8_SRGB`) are unpacked to a corresponding
floating-point representation in element access and packed from a
floating-point representation in mutable acess. The type annotation is
however still matching the original type (such as :py:`'3B'` / :py:`'4B'`
in these cases), so code consuming these via the buffer protocol needs to
handle the normalization explicitly if needed.
..
>>> from magnum import *
>>> import numpy as np
>>> import array
.. code:: pycon
>>> data = array.array('B', [0xf3, 0x2a, 0x80, 0x23, 0x00, 0xff, 0x00, 0xff])
>>> image = ImageView2D(PixelFormat.RGBA8_SRGB, (2, 1), data)
>>> image.pixels[0, 0] # sRGB -> float conversion
Vector(0.896269, 0.0231534, 0.215861, 0.137255)
>>> np.array(image.pixels, copy=False)[0]
array([[243, 42, 128, 35],
[ 0, 255, 0, 255]], dtype=uint8)
.. py:class:: magnum.ImageView3D
See :ref:`ImageView2D` for more information.
.. py:class:: magnum.MutableImageView1D
See :ref:`ImageView2D` for more information.
See :ref:`ImageView2D` for more information. The only difference to the
non-mutable variant is that it's possible to modify the image through
:ref:`data` and :ref:`pixels`.
.. py:class:: magnum.MutableImageView2D
See :ref:`ImageView2D` for more information.
See :ref:`ImageView2D` for more information. The only difference to the
non-mutable variant is that it's possible to modify the image through
:ref:`data` and :ref:`pixels`.
.. py:class:: magnum.MutableImageView3D
See :ref:`ImageView2D` for more information.
See :ref:`ImageView2D` for more information. The only difference to the
non-mutable variant is that it's possible to modify the image through
:ref:`data` and :ref:`pixels`.
.. py:function:: magnum.ImageView1D.__init__(self, arg0: magnum.ImageView1D)
:raise RuntimeError: If :ref:`trade.ImageData1D.is_compressed` is :py:`True`

35
doc/python/magnum.trade.rst

@ -34,6 +34,18 @@
:ref:`ImageView2D` / :ref:`MutableImageView2D`, so all APIs consuming image
views work with this type as well.
`Pixel data access`_
====================
The class makes use of Python's dynamic nature and provides direct access
to pixel data in their concrete types via :ref:`pixels`. See
:ref:`ImageView2D` documentation for more information and usage example.
Compared to :ref:`Image2D` and :ref:`ImageView2D` / :ref:`MutableImageView2D`,
the :ref:`data` and :ref:`pixels` views are immutable and mutable access
is provided depending on the value of :ref:`data_flags` via
:ref:`mutable_data` and :ref:`mutable_pixels`.
.. py:class:: magnum.trade.ImageData3D
See :ref:`ImageData2D` for more information.
@ -45,6 +57,16 @@
.. py:property:: magnum.trade.ImageData3D.storage
:raise AttributeError: If :ref:`is_compressed` is :py:`True`
.. py:property:: magnum.trade.ImageData1D.mutable_data
:raise AttributeError: If :ref:`data_flags` doesn't contain
:ref:`DataFlags.MUTABLE`
.. py:property:: magnum.trade.ImageData2D.mutable_data
:raise AttributeError: If :ref:`data_flags` doesn't contain
:ref:`DataFlags.MUTABLE`
.. py:property:: magnum.trade.ImageData3D.mutable_data
:raise AttributeError: If :ref:`data_flags` doesn't contain
:ref:`DataFlags.MUTABLE`
.. py:property:: magnum.trade.ImageData1D.format
:raise AttributeError: If :ref:`is_compressed` is :py:`True`
.. py:property:: magnum.trade.ImageData2D.format
@ -66,6 +88,19 @@
.. py:property:: magnum.trade.ImageData3D.pixels
:raise AttributeError: If :ref:`is_compressed` is :py:`True`
.. py:property:: magnum.trade.ImageData1D.mutable_pixels
:raise AttributeError: If :ref:`is_compressed` is :py:`True`
:raise AttributeError: If :ref:`data_flags` doesn't contain
:ref:`DataFlags.MUTABLE`
.. py:property:: magnum.trade.ImageData2D.mutable_pixels
:raise AttributeError: If :ref:`is_compressed` is :py:`True`
:raise AttributeError: If :ref:`data_flags` doesn't contain
:ref:`DataFlags.MUTABLE`
.. py:property:: magnum.trade.ImageData3D.mutable_pixels
:raise AttributeError: If :ref:`is_compressed` is :py:`True`
:raise AttributeError: If :ref:`data_flags` doesn't contain
:ref:`DataFlags.MUTABLE`
.. py:enum:: magnum.trade.MeshAttribute
The equivalent to C++ :dox:`Trade::meshAttributeCustom()` is creating an

1
src/Corrade/Containers/StridedArrayViewPythonBindings.h

@ -51,7 +51,6 @@ template<> constexpr const char* pythonFormatString<std::uint32_t>() { return "I
template<> constexpr const char* pythonFormatString<std::int64_t>() { return "q"; }
template<> constexpr const char* pythonFormatString<std::uint64_t>() { return "Q"; }
/** @todo how to represent std::size_t? conflicts with uint32_t/uint64_t above */
/** @todo half? take from Magnum? */
template<> constexpr const char* pythonFormatString<float>() { return "f"; }
template<> constexpr const char* pythonFormatString<double>() { return "d"; }

7
src/Magnum/StridedArrayViewPythonBindings.h

@ -25,9 +25,10 @@
DEALINGS IN THE SOFTWARE.
*/
#include <Corrade/Containers/StridedArrayViewPythonBindings.h>
#include <Magnum/Magnum.h>
#include "Corrade/Containers/StridedArrayViewPythonBindings.h"
namespace Corrade { namespace Containers { namespace Implementation {
/* This expands Containers::Implementation::pythonFormatString() with Magnum
@ -37,7 +38,9 @@ namespace Corrade { namespace Containers { namespace Implementation {
#define _c(type, string) \
template<> constexpr const char* pythonFormatString<Magnum::type>() { return string; }
_c(Half, "e")
_c(Vector2, "2f")
_c(Vector2h, "2e")
_c(Vector2d, "2d")
_c(Vector2ub, "2B")
_c(Vector2b, "2b")
@ -46,6 +49,7 @@ _c(Vector2s, "2h")
_c(Vector2ui, "2I")
_c(Vector2i, "2i")
_c(Vector3, "3f")
_c(Vector3h, "3e")
_c(Vector3d, "3d")
_c(Vector3ub, "3B")
_c(Vector3b, "3b")
@ -54,6 +58,7 @@ _c(Vector3s, "3h")
_c(Vector3ui, "3I")
_c(Vector3i, "3i")
_c(Vector4, "4f")
_c(Vector4h, "4e")
_c(Vector4d, "4d")
_c(Vector4ub, "4B")
_c(Vector4b, "4b")

194
src/python/magnum/acessorsForPixelFormat.h

@ -0,0 +1,194 @@
#ifndef magnum_accessorsForPixelFormat_h
#define magnum_accessorsForPixelFormat_h
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022 Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <pybind11/pybind11.h>
#include <Corrade/Containers/Triple.h>
#include <Magnum/PixelFormat.h>
#include <Magnum/Math/Color.h>
#include <Magnum/Math/Half.h>
#include <Magnum/Math/Packing.h>
#include "Magnum/StridedArrayViewPythonBindings.h"
#include "magnum/bootstrap.h"
/* This is used by both the root magnum module (Image, ImageView) and
magnum.trade.ImageData. There's no easy way to call an exported symbol of
another module due to Python isolating their namespaces so it's duplicated
in both modules. */
namespace magnum { namespace {
Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> accessorsForPixelFormat(const PixelFormat format) {
switch(format) {
#define _c(format, type) \
case PixelFormat::format: return { \
Containers::Implementation::pythonFormatString<type>(), \
[](const char* item) { \
return py::cast(*reinterpret_cast<const type*>(item)); \
}, \
[](char* item, py::handle object) { \
*reinterpret_cast<type*>(item) = py::cast<type>(object); \
}};
/* Types (such as half-floats) that need to be cast before passed
from/to pybind that doesn't understand the type directly */
#define _cc(format, type, castType) \
case PixelFormat::format: return { \
Containers::Implementation::pythonFormatString<type>(), \
[](const char* item) { \
return py::cast(castType(*reinterpret_cast<const type*>(item))); \
}, \
[](char* item, py::handle object) { \
*reinterpret_cast<type*>(item) = type(py::cast<castType>(object)); \
}};
/* Normalized types that need to be packed/unpacked before passed
from/to pybind */
#define _cNormalized(format, type, unpackType) \
case PixelFormat::format: return { \
Containers::Implementation::pythonFormatString<type>(), \
[](const char* item) { \
return py::cast(Math::unpack<unpackType>(*reinterpret_cast<const type*>(item))); \
}, \
[](char* item, py::handle object) { \
*reinterpret_cast<type*>(item) = Math::pack<type>(py::cast<unpackType>(object)); \
}};
/* LCOV_EXCL_START */
_cNormalized(R8Unorm, UnsignedByte, Float)
_cNormalized(RG8Unorm, Vector2ub, Vector2)
_cNormalized(RGB8Unorm, Vector3ub, Vector3)
_cNormalized(RGBA8Unorm, Vector4ub, Vector4)
_cNormalized(R8Snorm, Byte, Float)
_cNormalized(RG8Snorm, Vector2b, Vector2)
_cNormalized(RGB8Snorm, Vector3b, Vector3)
_cNormalized(RGBA8Snorm, Vector4b, Vector4)
/* LCOV_EXCL_STOP */
case PixelFormat::R8Srgb: return {
Containers::Implementation::pythonFormatString<UnsignedByte>(),
/** @todo have an (internal) API to convert just R/RG sRGB channels */
[](const char* item) {
return py::cast(Color3::fromSrgb(Vector3ub{*reinterpret_cast<const UnsignedByte*>(item), 0, 0}).r());
},
[](char* item, py::handle object) {
*reinterpret_cast<UnsignedByte*>(item) = Color3{py::cast<Float>(object), 0.0f, 0.0f}.toSrgb<UnsignedByte>().r();
}};
case PixelFormat::RG8Srgb: return {
Containers::Implementation::pythonFormatString<Vector2ub>(),
/** @todo have an (internal) API to convert just R/RG sRGB channels */
[](const char* item) {
return py::cast(Color3::fromSrgb(Vector3ub{*reinterpret_cast<const Vector2ub*>(item), 0}).rg());
},
[](char* item, py::handle object) {
*reinterpret_cast<Vector2ub*>(item) = Color3{py::cast<Vector2>(object), 0.0f}.toSrgb<UnsignedByte>().rg();
}};
case PixelFormat::RGB8Srgb: return {
Containers::Implementation::pythonFormatString<Vector3ub>(),
[](const char* item) {
return py::cast(Color3::fromSrgb(*reinterpret_cast<const Vector3ub*>(item)));
},
[](char* item, py::handle object) {
*reinterpret_cast<Vector3ub*>(item) = py::cast<Color3>(object).toSrgb<UnsignedByte>();
}};
case PixelFormat::RGBA8Srgb: return {
Containers::Implementation::pythonFormatString<Vector4ub>(),
[](const char* item) {
return py::cast(Color4::fromSrgbAlpha(*reinterpret_cast<const Vector4ub*>(item)));
},
[](char* item, py::handle object) {
*reinterpret_cast<Vector4ub*>(item) = py::cast<Color4>(object).toSrgbAlpha<UnsignedByte>();
}};
/* LCOV_EXCL_START */
_cc(R8UI, UnsignedByte, UnsignedInt)
_cc(RG8UI, Vector2ub, Vector2ui)
_cc(RGB8UI, Vector3ub, Vector3ui)
_cc(RGBA8UI, Vector4ub, Vector4ui)
_cc(R8I, Byte, Int)
_cc(RG8I, Vector2b, Vector2i)
_cc(RGB8I, Vector3b, Vector3i)
_cc(RGBA8I, Vector4b, Vector4i)
_cNormalized(R16Unorm, UnsignedShort, Float)
_cNormalized(RG16Unorm, Vector2us, Vector2)
_cNormalized(RGB16Unorm, Vector3us, Vector3)
_cNormalized(RGBA16Unorm, Vector4us, Vector4)
_cNormalized(R16Snorm, Short, Float)
_cNormalized(RG16Snorm, Vector2s, Vector2)
_cNormalized(RGB16Snorm, Vector3s, Vector3)
_cNormalized(RGBA16Snorm, Vector4s, Vector4)
_cc(R16UI, UnsignedShort, UnsignedInt)
_cc(RG16UI, Vector2us, Vector2ui)
_cc(RGB16UI, Vector3us, Vector3ui)
_cc(RGBA16UI, Vector4us, Vector4ui)
_cc(R16I, Short, Int)
_cc(RG16I, Vector2s, Vector2i)
_cc(RGB16I, Vector3s, Vector3i)
_cc(RGBA16I, Vector4s, Vector4i)
_c(R32UI, UnsignedInt)
_c(RG32UI, Vector2ui)
_c(RGB32UI, Vector3ui)
_c(RGBA32UI, Vector4ui)
_c(R32I, Int)
_c(RG32I, Vector2i)
_c(RGB32I, Vector3i)
_c(RGBA32I, Vector4i)
_cc(R16F, Half, Float)
_cc(RG16F, Vector2h, Vector2)
_cc(RGB16F, Vector3h, Vector3)
_cc(RGBA16F, Vector4h, Vector4)
_c(R32F, Float)
_c(RG32F, Vector2)
_c(RGB32F, Vector3)
_c(RGBA32F, Vector4)
/* LCOV_EXCL_STOP */
#undef _c
#undef _cc
#undef _cNormalized
/** @todo handle depth/stencil types (yes, i'm lazy) */
default:
return {};
}
}
template<UnsignedInt dimensions, class T> Containers::StridedArrayView<dimensions - 1, T> flattenPixelView(const Containers::ArrayView<T> data, const Containers::StridedArrayView<dimensions, T>& pixels) {
/** @todo have some builtin API for this, this is awful (pixels<void>()?
flatten<dimensions>() that asserts the last dimension is contiguous and
of a POD type? transpose<3, 0, 1, 2>()[0], differently named to avoid
clashes, taking dimension order?) */
Containers::Size<dimensions - 1> size{NoInit};
Containers::Stride<dimensions - 1> stride{NoInit};
for(std::size_t i = 0; i != dimensions - 1; ++i) {
size[i] = pixels.size()[i];
stride[i] = pixels.stride()[i];
}
return Containers::StridedArrayView<dimensions - 1, T>{
data,
static_cast<T*>(pixels.data()),
size,
stride};
}
}}
#endif

23
src/python/magnum/magnum.cpp

@ -39,6 +39,7 @@
#include "Corrade/Containers/StridedArrayViewPythonBindings.h"
#include "Magnum/PythonBindings.h"
#include "magnum/acessorsForPixelFormat.h"
#include "magnum/bootstrap.h"
#ifdef MAGNUM_BUILD_STATIC
@ -68,8 +69,15 @@ template<class T> void image(py::class_<T>& c) {
return Containers::pyArrayViewHolder(self.data(), self.data() ? py::cast(self) : py::none{});
}, "Raw image data")
.def_property_readonly("pixels", [](T& self) {
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<T::Dimensions + 1, const char>{self.pixels()}, self.data() ? py::cast(self) : py::none{});
}, "View on pixel data");
const PixelFormat format = self.format();
const std::size_t itemsize = pixelFormatSize(format);
const Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> formatStringGetitemSetitem = accessorsForPixelFormat(format);
if(!formatStringGetitemSetitem.first()) {
PyErr_SetString(PyExc_NotImplementedError, "access to this pixel format is not implemented yet, sorry");
throw py::error_already_set{};
}
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<T::Dimensions, char>{flattenPixelView(self.data(), self.pixels()), formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, self.data() ? py::cast(self) : py::none{});
}, "Pixel data");
}
template<class T> void imageView(py::class_<T, PyImageViewHolder<T>>& c) {
@ -138,8 +146,15 @@ template<class T> void imageView(py::class_<T, PyImageViewHolder<T>>& c) {
pyObjectHolderFor<Containers::PyArrayViewHolder>(data).owner;
}, "Raw image data")
.def_property_readonly("pixels", [](T& self) {
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<T::Dimensions + 1, typename T::Type>{self.pixels()}, pyObjectHolderFor<PyImageViewHolder>(self).owner);
}, "View on pixel data")
const PixelFormat format = self.format();
const std::size_t itemsize = pixelFormatSize(format);
const Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> formatStringGetitemSetitem = accessorsForPixelFormat(format);
if(!formatStringGetitemSetitem.first()) {
PyErr_SetString(PyExc_NotImplementedError, "access to this pixel format is not implemented yet, sorry");
throw py::error_already_set{};
}
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<T::Dimensions, typename T::Type>{flattenPixelView(self.data(), self.pixels()), formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, pyObjectHolderFor<PyImageViewHolder>(self).owner);
}, "Pixel data")
.def_property_readonly("owner", [](T& self) {
return pyObjectHolderFor<PyImageViewHolder>(self).owner;

BIN
src/python/magnum/test/dxt10-depth32f-stencil8ui.dds

Binary file not shown.

251
src/python/magnum/test/test.py

@ -23,6 +23,7 @@
# DEALINGS IN THE SOFTWARE.
#
import array
import sys
import unittest
@ -48,23 +49,21 @@ class PixelStorage_(unittest.TestCase):
self.assertEqual(a.skip, Vector3i(3, 1, 2))
class Image(unittest.TestCase):
def test_init(self):
def test_init_empty(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):
def test_data_access(self):
# Tested in test_gl_gl.Framebuffer.test_read_image instead
a = Image2D(PixelFormat.R8I, Vector2i(3, 17)) # TODO
a_refcount = sys.getrefcount(a)
@ -77,7 +76,7 @@ class Image(unittest.TestCase):
del data
self.assertEqual(sys.getrefcount(a), a_refcount)
def test_data_empty(self):
def test_data_access_empty(self):
a = Image2D(PixelFormat.R8I)
a_refcount = sys.getrefcount(a)
@ -87,28 +86,38 @@ class Image(unittest.TestCase):
self.assertEqual(sys.getrefcount(a), a_refcount)
@unittest.skip("No way to create a non-empty Image at the moment")
def test_pixels(self):
def test_pixels_access(self):
# Tested in test_gl_gl.Framebuffer.test_read_image instead
a = Image2D(PixelFormat.RG32UI, Vector2i(3, 17)) # TODO
a_refcount = sys.getrefcount(a)
pixels = a.pixels
self.assertEqual(pixels.size, (3, 17, 8))
self.assertEqual(pixels.size, (3, 17))
self.assertEqual(pixels.stride, (17*8, 8))
self.assertEqual(pixels.format, '2I')
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)
def test_pixels_access_empty(self):
a = Image2D(PixelFormat.RGB8I)
a_refcount = sys.getrefcount(a)
pixels = a.pixels
self.assertEqual(pixels.size, (0, 0, 1))
self.assertEqual(pixels.size, (0, 0))
self.assertEqual(pixels.stride, (0, 3))
self.assertEqual(pixels.format, '3b')
self.assertIs(pixels.owner, None)
self.assertEqual(sys.getrefcount(a), a_refcount)
def test_pixels_access_unsupported_format(self):
a = Image2D(PixelFormat.DEPTH32F)
with self.assertRaisesRegex(NotImplementedError, "access to this pixel format is not implemented yet, sorry"):
a.pixels
class ImageView(unittest.TestCase):
def test_init(self):
# 2x4 RGB pixels, padded for alignment
@ -123,16 +132,11 @@ class ImageView(unittest.TestCase):
self.assertEqual(a.size, Vector2i(2, 4))
self.assertEqual(a.format, PixelFormat.RGB8_UNORM)
self.assertEqual(a.pixel_size, 3)
self.assertEqual(len(a.data), 32)
self.assertIs(a.owner, data)
self.assertEqual(a.owner, data)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
# Third row, second pixel, green channel
self.assertEqual(a.pixels[2][1][1], 'E')
# Deleting the original data shouldn't make the image invalid
del data
self.assertEqual(a.pixels[2][1][1], 'E')
del a
self.assertEqual(sys.getrefcount(data), data_refcount)
def test_init_storage(self):
# 2x2x2 RGB pixels
@ -147,16 +151,18 @@ class ImageView(unittest.TestCase):
a = ImageView3D(storage, PixelFormat.RGB8_UNORM, (2, 2, 2), data)
self.assertEqual(a.storage.alignment, 2)
self.assertEqual(a.size, Vector3i(2, 2, 2))
self.assertEqual(len(a.data), 24)
self.assertEqual(a.format, PixelFormat.RGB8_UNORM)
self.assertEqual(a.pixel_size, 3)
self.assertEqual(a.owner, data)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
# Second image, first row, second pixel, green channel
self.assertEqual(a.pixels[1][0][1][1], 'E')
del a
self.assertEqual(sys.getrefcount(data), data_refcount)
def test_init_empty(self):
a = MutableImageView1D(PixelFormat.RG16UI, 32)
self.assertEqual(a.storage.alignment, 4)
self.assertEqual(a.size, 32)
self.assertEqual(len(a.data), 0)
self.assertEqual(a.owner, None)
storage = PixelStorage()
@ -164,7 +170,6 @@ class ImageView(unittest.TestCase):
b = ImageView2D(storage, PixelFormat.R32F, (8, 8))
self.assertEqual(b.storage.alignment, 2)
self.assertEqual(b.size, Vector2i(8, 8))
self.assertEqual(len(b.data), 0)
self.assertEqual(b.owner, None)
def test_init_mutable(self):
@ -178,16 +183,6 @@ class ImageView(unittest.TestCase):
a = MutableImageView2D(PixelFormat.RGB8_UNORM, (2, 4), data)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
a.pixels[1, 1, 1] = '_'
a.pixels[1, 0, 1] = '_'
a.pixels[2, 1, 1] = '_'
a.pixels[2, 0, 1] = '_'
self.assertEqual(data, b'rgbRGB '
b'a_cA_C '
b'd_fD_F '
b'ijkIJK ')
# Back to immutable
b = ImageView2D(a)
self.assertEqual(b.size, Vector2i(2, 4))
@ -233,6 +228,61 @@ class ImageView(unittest.TestCase):
self.assertIs(mview.owner, None)
self.assertEqual(sys.getrefcount(a), a_refcount)
def test_data_access(self):
# 2x4 RGB pixels, padded for alignment
data = (b'rgbRGB '
b'abcABC '
b'defDEF '
b'ijkIJK ')
data_refcount = sys.getrefcount(data)
a = ImageView2D(PixelFormat.RGB8_UNORM, (2, 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], 'b')
self.assertEqual(a_data[20], 'E')
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):
# 2x4 RGB pixels, padded for alignment
data = bytearray(b'rgbRGB '
b'abcABC '
b'defDEF '
b'ijkIJK ')
data_refcount = sys.getrefcount(data)
a = MutableImageView2D(PixelFormat.RGB8_UNORM, (2, 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], 'b')
self.assertEqual(a_data[20], 'E')
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'rgbRGB '
b'a_cABC '
b'defD_F '
b'ijkIJK ')
del a_data
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
def test_set_data(self):
# 2x4 RGB pixels, padded for alignment
data = (b'rgbRGB '
@ -262,3 +312,138 @@ class ImageView(unittest.TestCase):
self.assertIs(a.owner, data2)
self.assertEqual(sys.getrefcount(data), data_refcount)
self.assertEqual(sys.getrefcount(data2), data2_refcount + 1)
def test_pixels_access(self):
# 2x4 RGB pixels, padded for alignment
data = (b'rgbRGB '
b'abcABC '
b'defDEF '
b'ijkIJK ')
data_refcount = sys.getrefcount(data)
a = ImageView2D(PixelFormat.RGB8UI, (2, 4), data)
a_refcount = sys.getrefcount(a)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
pixels = a.pixels
self.assertEqual(pixels.size, (4, 2))
self.assertEqual(pixels.stride, (8, 3))
self.assertEqual(pixels.format, '3B')
self.assertEqual(pixels[1, 0].g, ord('b'))
self.assertEqual(pixels[2, 1].g, ord('E'))
self.assertIs(pixels.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 pixels
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
def test_mutable_pixels_access(self):
# 2x4 RGB pixels, padded for alignment
data = bytearray(b'rgbRGB '
b'abcABC '
b'defDEF '
b'ijkIJK ')
data_refcount = sys.getrefcount(data)
a = MutableImageView2D(PixelFormat.RGB8UI, (2, 4), data)
a_refcount = sys.getrefcount(a)
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
pixels = a.pixels
self.assertEqual(pixels.size, (4, 2))
self.assertEqual(pixels.stride, (8, 3))
self.assertEqual(pixels.format, '3B')
self.assertEqual(pixels[1, 0].g, ord('b'))
self.assertEqual(pixels[2, 1].g, ord('E'))
self.assertIs(pixels.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)
pixels[1, 0] = Vector3(ord('a'), ord('_'), ord('c'))
pixels[2, 1] = Vector3(ord('D'), ord('_'), ord('F'))
self.assertEqual(data, b'rgbRGB '
b'a_cABC '
b'defD_F '
b'ijkIJK ')
del pixels
self.assertEqual(sys.getrefcount(data), data_refcount + 1)
def test_pixels_access_direct(self):
data = array.array('f', [1.0, 2.0, 3.0, 4.0])
a = MutableImageView2D(PixelFormat.RG32F, (2, 1), data)
pixels = a.pixels
self.assertEqual(pixels.size, (1, 2))
self.assertEqual(pixels.stride, (16, 8))
self.assertEqual(pixels.format, '2f')
self.assertEqual(pixels[0, 1], Vector2(3.0, 4.0))
pixels[0, 1] *= 0.25
self.assertEqual(pixels[0, 1], Vector2(0.75, 1.0))
def test_pixels_access_cast(self):
# 0.0, 1.0, 2.0, 3.0; values taken from Magnum's HalfTest.cpp
# TODO clean up once array supports half-floats (ugh)
data = array.array('H', [0x0000, 0x3c00, 0x4000, 0x4200])
a = MutableImageView2D(PixelFormat.RG16F, (2, 1), data)
pixels = a.pixels
self.assertEqual(pixels.size, (1, 2))
self.assertEqual(pixels.stride, (8, 4))
self.assertEqual(pixels.format, '2e')
self.assertEqual(pixels[0, 1], Vector2(2.0, 3.0))
pixels[0, 1] *= 0.25
self.assertEqual(pixels[0, 1], Vector2(0.5, 0.75))
def test_pixels_access_srgb(self):
# Values taken from Magnum's ColorTest::fromIntegralSrgb()
data1 = array.array('B', [0xf3, 0x2a, 0x80, 0x23, 0xff, 0x00, 0xff, 0x00])
data2 = array.array('B', [0xf3, 0x2a, 0x80, 0xff, 0x00, 0xff, 0, 0])
data3 = array.array('B', [0xf3, 0x2a, 0xff, 0x00, 0, 0, 0, 0])
data4 = array.array('B', [0xf3, 0xff, 0, 0, 0, 0, 0, 0])
rgba = MutableImageView2D(PixelFormat.RGBA8_SRGB, (2, 1), data1)
rgb = MutableImageView2D(PixelFormat.RGB8_SRGB, (2, 1), data2)
rg = MutableImageView2D(PixelFormat.RG8_SRGB, (2, 1), data3)
r = MutableImageView2D(PixelFormat.R8_SRGB, (2, 1), data4)
rgba_pixels = rgba.pixels
rgb_pixels = rgb.pixels
rg_pixels = rg.pixels
r_pixels = r.pixels
self.assertEqual(rgba_pixels.format, '4B')
self.assertEqual(rgb_pixels.format, '3B')
self.assertEqual(rg_pixels.format, '2B')
self.assertEqual(r_pixels.format, 'B')
self.assertEqual(rgba_pixels[0, 0], Vector4(0.896269, 0.0231534, 0.215861, 0.137255))
self.assertEqual(rgb_pixels[0, 0], Vector3(0.896269, 0.0231534, 0.215861))
self.assertEqual(rg_pixels[0, 0], Vector2(0.896269, 0.0231534))
# Python compares floats with an unnecessary precision compared to
# Magnum's op== on vector types
self.assertEqual(r_pixels[0, 0], 0.8962693810462952)
self.assertEqual(rgba_pixels[0, 1], Vector4(1.0, 0.0, 1.0, 0.0))
self.assertEqual(rgb_pixels[0, 1], Vector3(1.0, 0.0, 1.0))
self.assertEqual(rg_pixels[0, 1], Vector2(1.0, 0.0))
self.assertEqual(r_pixels[0, 1], 1.0)
rgba_pixels[0, 0] *= 0.5
rgb_pixels[0, 0] *= 0.5
rg_pixels[0, 0] *= 0.5
r_pixels[0, 0] *= 0.5
self.assertEqual(rgba_pixels[0, 0], Vector4(0.450786, 0.0116122, 0.107023, 0.0705882))
self.assertEqual(rgb_pixels[0, 0], Vector3(0.450786, 0.0116122, 0.107023))
self.assertEqual(rg_pixels[0, 0], Vector2(0.450786, 0.0116122))
# Python compares floats with an unnecessary precision compared to
# Magnum's op== on vector types
self.assertEqual(r_pixels[0, 0], 0.4507858455181122)
def test_pixels_access_unsupported_format(self):
data = array.array('f', [1.0, 2.0, 3.0, 4.0])
a = ImageView2D(PixelFormat.DEPTH32F, (2, 2), data)
with self.assertRaisesRegex(NotImplementedError, "access to this pixel format is not implemented yet, sorry"):
a.pixels

13
src/python/magnum/test/test_gl_gl.py

@ -221,11 +221,13 @@ class Framebuffer(GLTestCase):
self.assertEqual(sys.getrefcount(a), a_refcount)
pixels = a.pixels
self.assertEqual(pixels.size, (2, 2))
self.assertEqual(pixels.stride, (8, 4))
self.assertEqual(pixels.format, '4B')
self.assertIs(pixels.owner, a)
# Rounding errors in the 8-bit representation
self.assertEqual(pixels[0, 0], Color4(1, 0.501961, 0.74902))
self.assertEqual(sys.getrefcount(a), a_refcount + 1)
self.assertEqual(ord(a.pixels[0, 0, 0]), 0xff)
self.assertEqual(ord(a.pixels[0, 1, 1]), 0x80)
self.assertEqual(ord(a.pixels[1, 0, 2]), 0xbf)
del pixels
self.assertEqual(sys.getrefcount(a), a_refcount)
@ -259,9 +261,8 @@ class Framebuffer(GLTestCase):
a = MutableImageView2D(PixelFormat.RGBA8_UNORM, (2, 2), bytearray(16))
framebuffer.read(Range2Di.from_size((1, 1), (2, 2)), a)
self.assertEqual(a.size, Vector2i(2, 2))
self.assertEqual(ord(a.pixels[0, 0, 0]), 0xff)
self.assertEqual(ord(a.pixels[0, 1, 1]), 0x80)
self.assertEqual(ord(a.pixels[1, 0, 2]), 0xbf)
# Rounding errors in the 8-bit representation
self.assertEqual(a.pixels[0, 0], Color4(1, 0.501961, 0.74902))
class Mesh(GLTestCase):
def test_init(self):

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

@ -41,23 +41,11 @@ class ImageData(unittest.TestCase):
importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png"))
image = importer.image2d(0)
image_refcount = sys.getrefcount(image)
self.assertFalse(image.is_compressed)
self.assertEqual(image.storage.alignment, 1) # libPNG has 4 tho
self.assertEqual(image.format, PixelFormat.RGB8_UNORM)
self.assertEqual(image.pixel_size, 3)
self.assertEqual(image.size, Vector2i(3, 2))
# TODO: ugh, report as bytes, not chars
self.assertEqual(ord(image.pixels[1, 2, 2]), 181)
self.assertEqual(ord(image.data[9 + 6 + 2]), 181) # libPNG has 12 +
data = image.data
self.assertEqual(len(data), 3*3*2)
self.assertIs(data.owner, image)
self.assertEqual(sys.getrefcount(image), image_refcount + 1)
del data
self.assertEqual(sys.getrefcount(image), image_refcount)
def test_compressed(self):
# The only way to get an image instance is through a manager
@ -101,6 +89,107 @@ class ImageData(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "image is compressed"):
mutable_view = MutableImageView2D(image)
def test_data_access(self):
# The only way to get an image instance is through a manager
importer = trade.ImporterManager().load_and_instantiate('StbImageImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png"))
image = importer.image2d(0)
image_refcount = sys.getrefcount(image)
self.assertEqual(image.storage.alignment, 1) # libPNG has 4 tho
self.assertEqual(image.format, PixelFormat.RGB8_UNORM)
self.assertEqual(image.size, Vector2i(3, 2))
data = image.data
self.assertEqual(len(data), 3*3*2)
self.assertEqual(ord(data[9 + 6 + 2]), 181) # libPNG has 12 +
self.assertIs(data.owner, image)
self.assertEqual(sys.getrefcount(image), image_refcount + 1)
del data
self.assertEqual(sys.getrefcount(image), image_refcount)
mutable_data = image.data
self.assertEqual(len(mutable_data), 3*3*2)
self.assertEqual(ord(mutable_data[9 + 6 + 2]), 181) # libPNG has 12 +
self.assertIs(mutable_data.owner, image)
self.assertEqual(sys.getrefcount(image), image_refcount + 1)
del mutable_data
self.assertEqual(sys.getrefcount(image), image_refcount)
def test_mutable_data_access(self):
# The only way to get an image instance is through a manager
importer = trade.ImporterManager().load_and_instantiate('StbImageImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png"))
image = importer.image2d(0)
self.assertEqual(image.data_flags, trade.DataFlags.OWNED|trade.DataFlags.MUTABLE)
data = image.data
mutable_data = image.mutable_data
# TODO: ugh, report as bytes, not chars
self.assertEqual(ord(data[13]), 254)
self.assertEqual(ord(mutable_data[13]), 254)
mutable_data[13] = chr(76)
self.assertEqual(data[13], chr(76))
def test_pixels_access(self):
# The only way to get an image instance is through a manager
importer = trade.ImporterManager().load_and_instantiate('StbImageImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png"))
image = importer.image2d(0)
image_refcount = sys.getrefcount(image)
self.assertEqual(image.storage.alignment, 1) # libPNG has 4 tho
self.assertEqual(image.format, PixelFormat.RGB8_UNORM)
self.assertEqual(image.size, Vector2i(3, 2))
pixels = image.pixels
self.assertEqual(pixels.size, (2, 3))
self.assertEqual(pixels.stride, (9, 3))
self.assertEqual(pixels.format, '3B')
self.assertEqual(pixels[0, 2], Color3(0.792157, 0.996078, 0.466667))
self.assertEqual(pixels[1, 0], Color3(0.870588, 0.678431, 0.709804))
self.assertIs(pixels.owner, image)
self.assertEqual(sys.getrefcount(image), image_refcount + 1)
del pixels
self.assertEqual(sys.getrefcount(image), image_refcount)
def test_mutable_pixels_access(self):
# The only way to get an image instance is through a manager
importer = trade.ImporterManager().load_and_instantiate('StbImageImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), "rgb.png"))
image = importer.image2d(0)
self.assertEqual(image.data_flags, trade.DataFlags.OWNED|trade.DataFlags.MUTABLE)
pixels = image.pixels
mutable_pixels = image.mutable_pixels
self.assertEqual(pixels[0, 2], Color3(0.792157, 0.996078, 0.466667))
self.assertEqual(mutable_pixels[0, 2], Color3(0.792157, 0.996078, 0.466667))
mutable_pixels[0, 2] *= 0.5
self.assertEqual(pixels[0, 2], Color3(0.396078, 0.498039, 0.235294))
def test_data_access_not_mutable(self):
pass
# TODO implement once there's a way to get immutable ImageData, either
# by "deserializing" a binary blob, or by mmapping a KTX file etc.
def test_pixels_access_unsupported_format(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__), "dxt10-depth32f-stencil8ui.dds"))
image = importer.image2d(0)
self.assertEqual(image.format, PixelFormat.DEPTH32F_STENCIL8UI)
with self.assertRaisesRegex(NotImplementedError, "access to this pixel format is not implemented yet, sorry"):
image.pixels
class MeshData(unittest.TestCase):
def test_custom_attribute(self):
# Creating a custom attribute

38
src/python/magnum/trade.cpp

@ -50,6 +50,7 @@
#include "corrade/EnumOperators.h"
#include "corrade/pluginmanager.h"
#include "magnum/acessorsForPixelFormat.h"
#include "magnum/bootstrap.h"
#ifdef CORRADE_TARGET_WINDOWS
@ -173,6 +174,17 @@ template<UnsignedInt dimensions, class T> PyObject* implicitlyConvertibleToImage
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);
const Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> formatStringGetitemSetitem = accessorsForPixelFormat(format);
if(!formatStringGetitemSetitem.first()) {
PyErr_SetString(PyExc_NotImplementedError, "access to this pixel format is not implemented yet, sorry");
throw py::error_already_set{};
}
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<dimensions, T>{flattenPixelView(data, pixels), formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, py::cast(image));
}
template<UnsignedInt dimensions> void imageData(py::class_<Trade::ImageData<dimensions>>& c) {
/*
Missing APIs:
@ -209,6 +221,9 @@ template<UnsignedInt dimensions> void imageData(py::class_<Trade::ImageData<dime
/* There are no constructors at the moment --- expecting those types
get only created by importers. (It would also need the Array type
and movability figured out, postponing that to later.) */
.def_property_readonly("data_flags", [](Trade::ImageData<dimensions>& self) {
return Trade::DataFlag(Containers::enumCastUnderlyingType(self.dataFlags()));
}, "Data flags")
/* Properties */
.def_property_readonly("is_compressed", &Trade::ImageData<dimensions>::isCompressed, "Whether the image is compressed")
@ -242,14 +257,31 @@ template<UnsignedInt dimensions> void imageData(py::class_<Trade::ImageData<dime
.def_property_readonly("data", [](Trade::ImageData<dimensions>& self) {
return Containers::pyArrayViewHolder(self.data(), py::cast(self));
}, "Raw image data")
.def_property_readonly("mutable_data", [](Trade::ImageData<dimensions>& self) {
if(!(self.dataFlags() & Trade::DataFlag::Mutable)) {
PyErr_SetString(PyExc_AttributeError, "image data is not mutable");
throw py::error_already_set{};
}
return Containers::pyArrayViewHolder(self.mutableData(), py::cast(self));
}, "Mutable raw image data")
.def_property_readonly("pixels", [](Trade::ImageData<dimensions>& self) {
if(self.isCompressed()) {
PyErr_SetString(PyExc_AttributeError, "image is compressed");
throw py::error_already_set{};
}
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<dimensions + 1, const char>{self.pixels()}, py::cast(self));
}, "View on pixel data");
return imagePixelsView(self, self.data(), self.pixels());
}, "Pixel data")
.def_property_readonly("mutable_pixels", [](Trade::ImageData<dimensions>& self) {
if(self.isCompressed()) {
PyErr_SetString(PyExc_AttributeError, "image is compressed");
throw py::error_already_set{};
}
if(!(self.dataFlags() & Trade::DataFlag::Mutable)) {
PyErr_SetString(PyExc_AttributeError, "image data is not mutable");
throw py::error_already_set{};
}
return imagePixelsView(self, self.mutableData(), self.mutablePixels());
}, "Mutable pixel data");
}
/* For some reason having ...Args as the second (and not last) template

Loading…
Cancel
Save