Browse Source

python: direct access to trade.MeshData indices and attributes.

In concrete types. Yes. Well, some of them for now, I still need to
sleep on normalized and matrix types, how it makes the most sense to
expose those. Oh, and array attributes as well.
next
Vladimír Vondruš 3 years ago
parent
commit
d106fc09d7
  1. 44
      doc/python/magnum.trade.rst
  2. 182
      src/python/magnum/test/test_trade.py
  3. 195
      src/python/magnum/trade.cpp

44
doc/python/magnum.trade.rst

@ -72,6 +72,35 @@
:dox:`Trade::MeshData::findAttributeId()`, the desired workflow is instead
calling :ref:`attribute_id()` and catching an exception if not found.
`Index and attribute data access`_
==================================
The class makes use of Python's dynamic nature and provides direct access
to index and attribute data in their concrete types via :ref:`indices` and
:ref:`attribute()`. The returned views point to the underlying mesh data,
element access coverts to a type corresponding to a particular
:ref:`VertexFormat` and for performance-oriented access the view implements
a buffer protocol with a corresponding type annotation:
..
>>> from magnum import primitives, trade
>>> import numpy as np
.. code:: pycon
>>> mesh = primitives.cube_solid()
>>> list(mesh.indices)[:10]
[0, 1, 2, 0, 2, 3, 4, 5, 6, 4]
>>> list(mesh.attribute(trade.MeshAttribute.POSITION))[:3]
[Vector(-1, -1, 1), Vector(1, -1, 1), Vector(1, 1, 1)]
>>> np.array(mesh.attribute(trade.MeshAttribute.NORMAL), copy=False)[2]
(0., 0., 1.)
Depending on the value of :ref:`index_data_flags` / :ref:`vertex_data_flags`
it's also possible to access the data in a mutable way via
:ref:`mutable_indices` and :ref:`mutable_attribute()`, for example to
perform a static transformation of the mesh before passing it to OpenGL.
.. py:property:: magnum.trade.MeshData.mutable_index_data
:raise AttributeError: If :ref:`index_data_flags` doesn't contain
:ref:`DataFlag.MUTABLE`
@ -86,6 +115,9 @@
:raise AttributeError: If :ref:`is_indexed` is :py:`False`
.. py:property:: magnum.trade.MeshData.index_stride
:raise AttributeError: If :ref:`is_indexed` is :py:`False`
.. py:property:: magnum.trade.MeshData.mutable_indices
:raise AttributeError: If :ref:`index_data_flags` doesn't contain
:ref:`DataFlag.MUTABLE`
.. py:function:: magnum.trade.MeshData.attribute_name
:raise IndexError: If :p:`id` is negative or not less than
:ref:`attribute_count()`
@ -114,6 +146,18 @@
:ref:`attribute_count()`
:raise KeyError: If :p:`id` is negative or not less than
:ref:`attribute_count()` for :p:`name`
.. py:function:: magnum.trade.MeshData.attribute
:raise IndexError: If :p:`id` is negative or not less than
:ref:`attribute_count()`
:raise KeyError: If :p:`id` is negative or not less than
:ref:`attribute_count()` for :p:`name`
.. py:function:: magnum.trade.MeshData.mutable_attribute
:raise IndexError: If :p:`id` is negative or not less than
:ref:`attribute_count()`
:raise KeyError: If :p:`id` is negative or not less than
:ref:`attribute_count()` for :p:`name`
:raise AttributeError: If :ref:`vertex_data_flags` doesn't contain
:ref:`DataFlag.MUTABLE`
.. py:class:: magnum.trade.ImporterManager
:summary: Manager for :ref:`AbstractImporter` plugin instances

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

@ -202,6 +202,95 @@ class MeshData(unittest.TestCase):
del mutable_vertex_data
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
def test_indices_access(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
mesh = importer.mesh(0)
mesh_refcount = sys.getrefcount(mesh)
indices = mesh.indices
self.assertEqual(indices.size, (3, ))
self.assertEqual(indices.stride, (2, ))
self.assertEqual(indices.format, 'H')
self.assertEqual(list(indices), [0, 2, 1])
self.assertIs(indices.owner, mesh)
self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1)
del indices
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
mutable_indices = mesh.mutable_indices
self.assertEqual(mutable_indices.size, (3, ))
self.assertEqual(mutable_indices.stride, (2, ))
self.assertEqual(mutable_indices.format, 'H')
self.assertEqual(list(mutable_indices), [0, 2, 1])
self.assertIs(mutable_indices.owner, mesh)
self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1)
del mutable_indices
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
def test_attribute_access(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
mesh = importer.mesh(0)
mesh_refcount = sys.getrefcount(mesh)
self.assertEqual(mesh.attribute_id(trade.MeshAttribute.POSITION), 3)
positions = mesh.attribute(3)
self.assertEqual(positions.size, (3, ))
self.assertEqual(positions.stride, (28, ))
self.assertEqual(positions.format, 'fff')
self.assertEqual(list(positions), [
Vector3(-1, -1, 0.25),
Vector3(0, 1, 0.5),
Vector3(1, -1, 0.25)
])
self.assertIs(positions.owner, mesh)
self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1)
del positions
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
object_ids = mesh.attribute(trade.MeshAttribute.OBJECT_ID)
self.assertEqual(object_ids.size, (3, ))
self.assertEqual(object_ids.stride, (28, ))
self.assertEqual(object_ids.format, 'I')
self.assertEqual(list(object_ids), [216, 16777235, 2872872013])
self.assertIs(object_ids.owner, mesh)
self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1)
del object_ids
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
mutable_positions = mesh.mutable_attribute(3)
self.assertEqual(mutable_positions.size, (3, ))
self.assertEqual(mutable_positions.stride, (28, ))
self.assertEqual(mutable_positions.format, 'fff')
self.assertEqual(list(mutable_positions), [
Vector3(-1, -1, 0.25),
Vector3(0, 1, 0.5),
Vector3(1, -1, 0.25)
])
self.assertIs(mutable_positions.owner, mesh)
self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1)
del mutable_positions
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
mutable_object_ids = mesh.mutable_attribute(trade.MeshAttribute.OBJECT_ID)
self.assertEqual(mutable_object_ids.size, (3, ))
self.assertEqual(mutable_object_ids.stride, (28, ))
self.assertEqual(mutable_object_ids.format, 'I')
self.assertEqual(list(mutable_object_ids), [216, 16777235, 2872872013])
self.assertIs(mutable_object_ids.owner, mesh)
self.assertEqual(sys.getrefcount(mesh), mesh_refcount + 1)
del mutable_object_ids
self.assertEqual(sys.getrefcount(mesh), mesh_refcount)
def test_mutable_index_data_access(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
@ -236,6 +325,45 @@ class MeshData(unittest.TestCase):
mutable_vertex_data[21] = chr(76)
self.assertEqual(vertex_data[21], chr(76))
def test_mutable_indices_access(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
mesh = importer.mesh(0)
self.assertEqual(mesh.index_data_flags, trade.DataFlag.OWNED|trade.DataFlag.MUTABLE)
indices = mesh.indices
mutable_indices = mesh.mutable_indices
self.assertEqual(indices[1], 2)
self.assertEqual(mutable_indices[1], 2)
mutable_indices[1] = 76
self.assertEqual(indices[1], 76)
def test_mutable_attributes_access(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
mesh = importer.mesh(0)
self.assertEqual(mesh.index_data_flags, trade.DataFlag.OWNED|trade.DataFlag.MUTABLE)
self.assertEqual(mesh.attribute_id(trade.MeshAttribute.POSITION), 3)
positions = mesh.attribute(3)
mutable_positions = mesh.mutable_attribute(3)
self.assertEqual(positions[1], Vector3(0, 1, 0.5))
self.assertEqual(mutable_positions[1], Vector3(0, 1, 0.5))
mutable_positions[1] *= 2
self.assertEqual(positions[1], Vector3(0, 2, 1))
object_ids = mesh.attribute(trade.MeshAttribute.OBJECT_ID)
mutable_object_ids = mesh.mutable_attribute(trade.MeshAttribute.OBJECT_ID)
self.assertEqual(object_ids[1], 16777235)
self.assertEqual(mutable_object_ids[1], 16777235)
mutable_object_ids[1] //= 1000
self.assertEqual(object_ids[1], 16777)
def test_data_access_not_mutable(self):
mesh = primitives.cube_solid()
# TODO split this once there's a mesh where only one or the other would
@ -245,8 +373,14 @@ class MeshData(unittest.TestCase):
with self.assertRaisesRegex(AttributeError, "mesh index data is not mutable"):
mesh.mutable_index_data
with self.assertRaisesRegex(AttributeError, "mesh index data is not mutable"):
mesh.mutable_indices
with self.assertRaisesRegex(AttributeError, "mesh vertex data is not mutable"):
mesh.mutable_vertex_data
with self.assertRaisesRegex(AttributeError, "mesh vertex data is not mutable"):
mesh.mutable_attribute(0)
with self.assertRaisesRegex(AttributeError, "mesh vertex data is not mutable"):
mesh.mutable_attribute(trade.MeshAttribute.POSITION)
def test_nonindexed(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
@ -267,6 +401,10 @@ class MeshData(unittest.TestCase):
mesh.index_offset
with self.assertRaisesRegex(AttributeError, "mesh is not indexed"):
mesh.index_stride
with self.assertRaisesRegex(AttributeError, "mesh is not indexed"):
mesh.indices
with self.assertRaisesRegex(AttributeError, "mesh is not indexed"):
mesh.mutable_indices
def test_attribute_oob(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
@ -287,6 +425,10 @@ class MeshData(unittest.TestCase):
mesh.attribute_stride(mesh.attribute_count())
with self.assertRaises(IndexError):
mesh.attribute_array_size(mesh.attribute_count())
with self.assertRaises(IndexError):
mesh.attribute(mesh.attribute_count())
with self.assertRaises(IndexError):
mesh.mutable_attribute(mesh.attribute_count())
# Access by nonexistent name
with self.assertRaises(KeyError):
@ -299,6 +441,10 @@ class MeshData(unittest.TestCase):
mesh.attribute_stride(trade.MeshAttribute.TANGENT)
with self.assertRaises(KeyError):
mesh.attribute_array_size(trade.MeshAttribute.TANGENT)
with self.assertRaises(KeyError):
mesh.attribute(trade.MeshAttribute.TANGENT)
with self.assertRaises(KeyError):
mesh.mutable_attribute(trade.MeshAttribute.TANGENT)
# Access by existing name + OOB ID
with self.assertRaises(KeyError):
@ -311,6 +457,42 @@ class MeshData(unittest.TestCase):
mesh.attribute_stride(trade.MeshAttribute.TEXTURE_COORDINATES, 2)
with self.assertRaises(KeyError):
mesh.attribute_array_size(trade.MeshAttribute.TEXTURE_COORDINATES, 2)
with self.assertRaises(KeyError):
mesh.attribute(trade.MeshAttribute.TEXTURE_COORDINATES, 2)
with self.assertRaises(KeyError):
mesh.mutable_attribute(trade.MeshAttribute.TEXTURE_COORDINATES, 2)
def test_attribute_access_array(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
mesh = importer.mesh(0)
self.assertEqual(mesh.attribute_id(trade.MeshAttribute.JOINT_IDS), 1)
with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"):
mesh.attribute(1)
with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"):
mesh.mutable_attribute(1)
with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"):
mesh.attribute(trade.MeshAttribute.JOINT_IDS)
with self.assertRaisesRegex(NotImplementedError, "array attributes not implemented yet, sorry"):
mesh.mutable_attribute(trade.MeshAttribute.JOINT_IDS)
def test_attribute_access_unsupported_format(self):
importer = trade.ImporterManager().load_and_instantiate('GltfImporter')
importer.open_file(os.path.join(os.path.dirname(__file__), 'mesh.gltf'))
mesh = importer.mesh(0)
self.assertEqual(mesh.attribute_id(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE")), 8)
with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"):
mesh.attribute(8)
with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"):
mesh.mutable_attribute(8)
with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"):
mesh.attribute(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE"))
with self.assertRaisesRegex(NotImplementedError, "access to this vertex format is not implemented yet, sorry"):
mesh.mutable_attribute(importer.mesh_attribute_for_name("_CUSTOM_ATTRIBUTE"))
class Importer(unittest.TestCase):
def test(self):

195
src/python/magnum/trade.cpp

@ -27,7 +27,10 @@
#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/StringStl.h> /** @todo drop once we have our string casters */
#include <Corrade/Containers/Triple.h>
#include <Magnum/ImageView.h>
#include <Magnum/Math/Half.h>
#include <Magnum/Math/Matrix4.h>
#include <Magnum/Trade/AbstractImporter.h>
#include <Magnum/Trade/AbstractImageConverter.h>
#include <Magnum/Trade/AbstractSceneConverter.h>
@ -299,6 +302,115 @@ template<class T, bool(Trade::AbstractSceneConverter::*f)(const T&, Containers::
}
}
Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> accessorsForMeshIndexType(const MeshIndexType type) {
switch(type) {
#define _c(type, string) \
case MeshIndexType::type: return { \
string, \
[](const char* item) { \
return py::cast(*reinterpret_cast<const type*>(item)); \
}, \
[](char* item, py::handle object) { \
*reinterpret_cast<type*>(item) = py::cast<type>(object); \
}};
_c(UnsignedByte, "B")
_c(UnsignedShort, "H")
_c(UnsignedInt, "I")
#undef _c
}
return {};
}
Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> accessorsForVertexFormat(const VertexFormat format) {
switch(format) {
#define _c(format, string) \
case VertexFormat::format: return { \
string, \
[](const char* item) { \
return py::cast(*reinterpret_cast<const format*>(item)); \
}, \
[](char* item, py::handle object) { \
*reinterpret_cast<format*>(item) = py::cast<format>(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, castType, string) \
case VertexFormat::format: return { \
string, \
[](const char* item) { \
return py::cast(format(*reinterpret_cast<const castType*>(item))); \
}, \
[](char* item, py::handle object) { \
*reinterpret_cast<format*>(item) = format(py::cast<castType>(object)); \
}};
_c(Float, "f")
_c(Double, "d")
_cc(UnsignedByte, UnsignedInt, "B")
_cc(Byte, Int, "b")
_cc(UnsignedShort, UnsignedInt, "H")
_cc(Short, Int, "h")
_c(UnsignedInt, "I")
_c(Int, "i")
_c(Vector2, "ff")
_c(Vector2d, "dd")
_cc(Vector2ub, Vector2ui, "BB")
_cc(Vector2b, Vector2i, "bb")
_cc(Vector2us, Vector2ui, "HH")
_cc(Vector2s, Vector2i, "hh")
_c(Vector2ui, "II")
_c(Vector2i, "ii")
_c(Vector3, "fff")
_c(Vector3d, "ddd")
_cc(Vector3ub, Vector3ui, "BBB")
_cc(Vector3b, Vector3i, "bbb")
_cc(Vector3us, Vector3ui, "HHH")
_cc(Vector3s, Vector3i, "hhh")
_c(Vector3ui, "III")
_c(Vector3i, "iii")
_c(Vector4, "ffff")
_c(Vector4d, "dddd")
_cc(Vector4ub, Vector4ui, "BBBB")
_cc(Vector4b, Vector4i, "bbbb")
_cc(Vector4us, Vector4ui, "HHHH")
_cc(Vector4s, Vector4i, "hhhh")
_c(Vector4ui, "IIII")
_c(Vector4i, "iiii")
#undef _c
#undef _cc
/** @todo handle half, normalized and matrix types */
default:
return {};
}
return {};
}
template<class T> Containers::PyArrayViewHolder<Containers::PyStridedArrayView<1, T>> meshIndicesView(Trade::MeshData& mesh, const Containers::StridedArrayView2D<T>& data) {
const MeshIndexType type = mesh.indexType();
const std::size_t itemsize = meshIndexTypeSize(type);
const Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> formatStringGetitemSetitem = accessorsForMeshIndexType(type);
/** @todo update this once there are plugins that can give back custom
index types */
CORRADE_INTERNAL_ASSERT(formatStringGetitemSetitem.first());
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<1, T>{data.template transposed<0, 1>()[0], formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, py::cast(mesh));
}
template<class T> Containers::PyArrayViewHolder<Containers::PyStridedArrayView<1, T>> meshAttributeView(Trade::MeshData& mesh, const UnsignedInt id, const Containers::StridedArrayView2D<T>& data) {
const VertexFormat format = mesh.attributeFormat(id);
const std::size_t itemsize = vertexFormatSize(format);
const Containers::Triple<const char*, py::object(*)(const char*), void(*)(char*, py::handle)> formatStringGetitemSetitem = accessorsForVertexFormat(format);
if(!formatStringGetitemSetitem.first()) {
PyErr_SetString(PyExc_NotImplementedError, "access to this vertex format is not implemented yet, sorry");
throw py::error_already_set{};
}
return Containers::pyArrayViewHolder(Containers::PyStridedArrayView<1, T>{data.template transposed<0, 1>()[0], formatStringGetitemSetitem.first(), itemsize, formatStringGetitemSetitem.second(), formatStringGetitemSetitem.third()}, py::cast(mesh));
}
}
void trade(py::module_& m) {
@ -384,6 +496,24 @@ void trade(py::module_& m) {
}
return self.indexStride();
}, "Index stride")
.def_property_readonly("indices", [](Trade::MeshData& self) {
if(!self.isIndexed()) {
PyErr_SetString(PyExc_AttributeError, "mesh is not indexed");
throw py::error_already_set{};
}
return meshIndicesView(self, self.indices());
}, "Indices")
.def_property_readonly("mutable_indices", [](Trade::MeshData& self) {
if(!self.isIndexed()) {
PyErr_SetString(PyExc_AttributeError, "mesh is not indexed");
throw py::error_already_set{};
}
if(!(self.indexDataFlags() & Trade::DataFlag::Mutable)) {
PyErr_SetString(PyExc_AttributeError, "mesh index data is not mutable");
throw py::error_already_set{};
}
return meshIndicesView(self, self.mutableIndices());
}, "Mutable indices")
.def_property_readonly("vertex_count", &Trade::MeshData::vertexCount, "Vertex count")
/* Has to be a function instead of a property because there's an
overload taking a name */
@ -463,7 +593,70 @@ void trade(py::module_& m) {
return self.attributeArraySize(*found);
PyErr_SetString(PyExc_KeyError, "");
throw py::error_already_set{};
}, "Array size of a named attribute", py::arg("name"), py::arg("id") = 0);
}, "Array size of a named attribute", py::arg("name"), py::arg("id") = 0)
.def("attribute", [](Trade::MeshData& self, UnsignedInt id) {
if(id >= self.attributeCount()) {
PyErr_SetString(PyExc_IndexError, "");
throw py::error_already_set{};
}
/** @todo handle arrays (return a 2D view, and especially annotate
the return type properly in the docs) */
if(self.attributeArraySize(id) != 0) {
PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry");
throw py::error_already_set{};
}
return meshAttributeView(self, id, self.attribute(id));
}, "Data for given attribute", py::arg("id"))
.def("mutable_attribute", [](Trade::MeshData& self, UnsignedInt id) {
if(id >= self.attributeCount()) {
PyErr_SetString(PyExc_IndexError, "");
throw py::error_already_set{};
}
if(!(self.vertexDataFlags() & Trade::DataFlag::Mutable)) {
PyErr_SetString(PyExc_AttributeError, "mesh vertex data is not mutable");
throw py::error_already_set{};
}
/** @todo handle arrays (return a 2D view, and especially annotate
the return type properly in the docs) */
if(self.attributeArraySize(id) != 0) {
PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry");
throw py::error_already_set{};
}
return meshAttributeView(self, id, self.mutableAttribute(id));
}, "Mutable data for given attribute", py::arg("id"))
.def("attribute", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) {
const Containers::Optional<UnsignedInt> found = self.findAttributeId(name, id);
if(!found) {
PyErr_SetString(PyExc_KeyError, "");
throw py::error_already_set{};
}
/** @todo handle arrays (return a 2D view, and especially annotate
the return type properly in the docs) */
if(self.attributeArraySize(*found) != 0) {
PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry");
throw py::error_already_set{};
}
return meshAttributeView(self, *found, self.attribute(*found));
}, "Data for given named attribute", py::arg("name"), py::arg("id") = 0)
.def("mutable_attribute", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) {
const Containers::Optional<UnsignedInt> found = self.findAttributeId(name, id);
if(!found) {
PyErr_SetString(PyExc_KeyError, "");
throw py::error_already_set{};
}
if(!(self.vertexDataFlags() & Trade::DataFlag::Mutable)) {
PyErr_SetString(PyExc_AttributeError, "mesh vertex data is not mutable");
throw py::error_already_set{};
}
/** @todo handle arrays (return a 2D view, and especially annotate
the return type properly in the docs) */
if(self.attributeArraySize(*found) != 0) {
PyErr_SetString(PyExc_NotImplementedError, "array attributes not implemented yet, sorry");
throw py::error_already_set{};
}
return meshAttributeView(self, *found, self.mutableAttribute(*found));
}, "Data for given named attribute", py::arg("name"), py::arg("id") = 0)
;
py::class_<Trade::ImageData1D> imageData1D{m, "ImageData1D", "One-dimensional image data"};
py::class_<Trade::ImageData2D> imageData2D{m, "ImageData2D", "Two-dimensional image data"};

Loading…
Cancel
Save