From cca2eaf6592314598af40212e57407b2bccbeec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Tue, 7 Feb 2023 22:07:39 +0100 Subject: [PATCH] python: expose all index/attribute property queries in trade.MeshData. Except for the actual data access, that'll be done next. Also updated the mesh test file with more useful contents. --- doc/python/magnum.trade.rst | 40 ++++++ src/python/magnum/__init__.py | 4 +- src/python/magnum/magnum.cpp | 112 ++++++++++++++++ src/python/magnum/test/convert.sh | 8 ++ src/python/magnum/test/mesh.bin | Bin 0 -> 92 bytes src/python/magnum/test/mesh.bin.in | 19 +++ src/python/magnum/test/mesh.glb | Bin 832 -> 0 bytes src/python/magnum/test/mesh.gltf | 113 ++++++++++++++++ src/python/magnum/test/test_primitives.py | 52 ++++---- src/python/magnum/test/test_trade.py | 151 ++++++++++++++++++---- src/python/magnum/trade.cpp | 130 ++++++++++++++++++- 11 files changed, 575 insertions(+), 54 deletions(-) create mode 100755 src/python/magnum/test/convert.sh create mode 100644 src/python/magnum/test/mesh.bin create mode 100644 src/python/magnum/test/mesh.bin.in delete mode 100644 src/python/magnum/test/mesh.glb create mode 100644 src/python/magnum/test/mesh.gltf diff --git a/doc/python/magnum.trade.rst b/doc/python/magnum.trade.rst index f6489cc..579745d 100644 --- a/doc/python/magnum.trade.rst +++ b/doc/python/magnum.trade.rst @@ -66,8 +66,48 @@ .. py:property:: magnum.trade.ImageData3D.pixels :raise AttributeError: If :ref:`is_compressed` is :py:`True` +.. py:class:: magnum.trade.MeshData + + Compared to the C++ API, there's no + :dox:`Trade::MeshData::findAttributeId()`, the desired workflow is instead + calling :ref:`attribute_id()` and catching an exception if not found. + .. py:property:: magnum.trade.MeshData.index_count :raise AttributeError: If :ref:`is_indexed` is :py:`False` +.. py:property:: magnum.trade.MeshData.index_type + :raise AttributeError: If :ref:`is_indexed` is :py:`False` +.. py:property:: magnum.trade.MeshData.index_offset + :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:function:: magnum.trade.MeshData.attribute_name + :raise IndexError: If :p:`id` is negative or not less than + :ref:`attribute_count()` +.. py:function:: magnum.trade.MeshData.attribute_id + :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.attribute_format + :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.attribute_offset + :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.attribute_stride + :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.attribute_array_size + :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:class:: magnum.trade.ImporterManager :summary: Manager for :ref:`AbstractImporter` plugin instances diff --git a/src/python/magnum/__init__.py b/src/python/magnum/__init__.py index 3e2278c..7bab5b6 100644 --- a/src/python/magnum/__init__.py +++ b/src/python/magnum/__init__.py @@ -78,7 +78,9 @@ __all__ = [ 'ImageView1D', 'ImageView2D', 'ImageView3D', 'MutableImageView1D', 'MutableImageView2D', 'MutableImageView3D', - 'SamplerFilter', 'SamplerMipmap', 'SamplerWrapping' + 'SamplerFilter', 'SamplerMipmap', 'SamplerWrapping', + + 'VertexFormat' # TARGET_*, BUILD_* are omitted as `from magnum import *` would pull them # to globals and this would likely cause conflicts (corrade also defines diff --git a/src/python/magnum/magnum.cpp b/src/python/magnum/magnum.cpp index 4d126b4..6bd82d7 100644 --- a/src/python/magnum/magnum.cpp +++ b/src/python/magnum/magnum.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include "Corrade/PythonBindings.h" #include "Corrade/Containers/PythonBindings.h" @@ -336,6 +337,117 @@ void magnum(py::module_& m) { .value("CLAMP_TO_EDGE", SamplerWrapping::ClampToEdge) .value("CLAMP_TO_BORDER", SamplerWrapping::ClampToBorder) .value("MIRROR_CLAMP_TO_EDGE", SamplerWrapping::MirrorClampToEdge); + + py::enum_{m, "VertexFormat", "Vertex format"} + .value("FLOAT", VertexFormat::Float) + .value("HALF", VertexFormat::Half) + .value("DOUBLE", VertexFormat::Double) + .value("UNSIGNED_BYTE", VertexFormat::UnsignedByte) + .value("UNSIGNED_BYTE_NORMALIZED", VertexFormat::UnsignedByteNormalized) + .value("BYTE", VertexFormat::Byte) + .value("BYTE_NORMALIZED", VertexFormat::ByteNormalized) + .value("UNSIGNED_SHORT", VertexFormat::UnsignedShort) + .value("UNSIGNED_SHORT_NORMALIZED", VertexFormat::UnsignedShortNormalized) + .value("SHORT", VertexFormat::Short) + .value("SHORT_NORMALIZED", VertexFormat::ShortNormalized) + .value("UNSIGNED_INT", VertexFormat::UnsignedInt) + .value("INT", VertexFormat::Int) + .value("VECTOR2", VertexFormat::Vector2) + .value("VECTOR2H", VertexFormat::Vector2h) + .value("VECTOR2D", VertexFormat::Vector2d) + .value("VECTOR2UB", VertexFormat::Vector2ub) + .value("VECTOR2UB_NORMALIZED", VertexFormat::Vector2ubNormalized) + .value("VECTOR2B", VertexFormat::Vector2b) + .value("VECTOR2B_NORMALIZED", VertexFormat::Vector2bNormalized) + .value("VECTOR2US", VertexFormat::Vector2us) + .value("VECTOR2US_NORMALIZED", VertexFormat::Vector2usNormalized) + .value("VECTOR2S", VertexFormat::Vector2s) + .value("VECTOR2S_NORMALIZED", VertexFormat::Vector2usNormalized) + .value("VECTOR2UI", VertexFormat::Vector2ui) + .value("VECTOR2I", VertexFormat::Vector2i) + .value("VECTOR3", VertexFormat::Vector3) + .value("VECTOR3H", VertexFormat::Vector3h) + .value("VECTOR3D", VertexFormat::Vector3d) + .value("VECTOR3UB", VertexFormat::Vector3ub) + .value("VECTOR3UB_NORMALIZED", VertexFormat::Vector3ubNormalized) + .value("VECTOR3B", VertexFormat::Vector3b) + .value("VECTOR3B_NORMALIZED", VertexFormat::Vector3bNormalized) + .value("VECTOR3US", VertexFormat::Vector3us) + .value("VECTOR3US_NORMALIZED", VertexFormat::Vector3usNormalized) + .value("VECTOR3S", VertexFormat::Vector3s) + .value("VECTOR3S_NORMALIZED", VertexFormat::Vector3usNormalized) + .value("VECTOR3UI", VertexFormat::Vector3ui) + .value("VECTOR3I", VertexFormat::Vector3i) + .value("VECTOR4", VertexFormat::Vector4) + .value("VECTOR4H", VertexFormat::Vector4h) + .value("VECTOR4D", VertexFormat::Vector4d) + .value("VECTOR4UB", VertexFormat::Vector4ub) + .value("VECTOR4UB_NORMALIZED", VertexFormat::Vector4ubNormalized) + .value("VECTOR4B", VertexFormat::Vector4b) + .value("VECTOR4B_NORMALIZED", VertexFormat::Vector4bNormalized) + .value("VECTOR4US", VertexFormat::Vector4us) + .value("VECTOR4US_NORMALIZED", VertexFormat::Vector4usNormalized) + .value("VECTOR4S", VertexFormat::Vector4s) + .value("VECTOR4S_NORMALIZED", VertexFormat::Vector4usNormalized) + .value("VECTOR4UI", VertexFormat::Vector4ui) + .value("VECTOR4I", VertexFormat::Vector4i) + .value("MATRIX2X2", VertexFormat::Matrix2x2) + .value("MATRIX2X2H", VertexFormat::Matrix2x2h) + .value("MATRIX2X2D", VertexFormat::Matrix2x2d) + .value("MATRIX2X2B_NORMALIZED", VertexFormat::Matrix2x2bNormalized) + .value("MATRIX2X2S_NORMALIZED", VertexFormat::Matrix2x2sNormalized) + .value("MATRIX2X3", VertexFormat::Matrix2x3) + .value("MATRIX2X3H", VertexFormat::Matrix2x3h) + .value("MATRIX2X3D", VertexFormat::Matrix2x3d) + .value("MATRIX2X3B_NORMALIZED", VertexFormat::Matrix2x3bNormalized) + .value("MATRIX2X3S_NORMALIZED", VertexFormat::Matrix2x3sNormalized) + .value("MATRIX2X4", VertexFormat::Matrix2x4) + .value("MATRIX2X4H", VertexFormat::Matrix2x4h) + .value("MATRIX2X4D", VertexFormat::Matrix2x4d) + .value("MATRIX2X4B_NORMALIZED", VertexFormat::Matrix2x4bNormalized) + .value("MATRIX2X4S_NORMALIZED", VertexFormat::Matrix2x4sNormalized) + .value("MATRIX2X2B_NORMALIZED_ALIGNED", VertexFormat::Matrix2x2bNormalizedAligned) + .value("MATRIX2X3H_ALIGNED", VertexFormat::Matrix2x3hAligned) + .value("MATRIX2X3B_NORMALIZED_ALIGNED", VertexFormat::Matrix2x3bNormalizedAligned) + .value("MATRIX2X3S_NORMALIZED_ALIGNED", VertexFormat::Matrix2x3sNormalizedAligned) + .value("MATRIX3X2", VertexFormat::Matrix3x2) + .value("MATRIX3X2H", VertexFormat::Matrix3x2h) + .value("MATRIX3X2D", VertexFormat::Matrix3x2d) + .value("MATRIX3X2B_NORMALIZED", VertexFormat::Matrix3x2bNormalized) + .value("MATRIX3X2S_NORMALIZED", VertexFormat::Matrix3x2sNormalized) + .value("MATRIX3X3", VertexFormat::Matrix3x3) + .value("MATRIX3X3H", VertexFormat::Matrix3x3h) + .value("MATRIX3X3D", VertexFormat::Matrix3x3d) + .value("MATRIX3X3B_NORMALIZED", VertexFormat::Matrix3x3bNormalized) + .value("MATRIX3X3S_NORMALIZED", VertexFormat::Matrix3x3sNormalized) + .value("MATRIX3X4", VertexFormat::Matrix3x4) + .value("MATRIX3X4H", VertexFormat::Matrix3x4h) + .value("MATRIX3X4D", VertexFormat::Matrix3x4d) + .value("MATRIX3X4B_NORMALIZED", VertexFormat::Matrix3x4bNormalized) + .value("MATRIX3X4S_NORMALIZED", VertexFormat::Matrix3x4sNormalized) + .value("MATRIX3X2B_NORMALIZED_ALIGNED", VertexFormat::Matrix3x2bNormalizedAligned) + .value("MATRIX3X3H_ALIGNED", VertexFormat::Matrix3x3hAligned) + .value("MATRIX3X3B_NORMALIZED_ALIGNED", VertexFormat::Matrix3x3bNormalizedAligned) + .value("MATRIX3X3S_NORMALIZED_ALIGNED", VertexFormat::Matrix3x3sNormalizedAligned) + .value("MATRIX4X2", VertexFormat::Matrix4x2) + .value("MATRIX4X2H", VertexFormat::Matrix4x2h) + .value("MATRIX4X2D", VertexFormat::Matrix4x2d) + .value("MATRIX4X2B_NORMALIZED", VertexFormat::Matrix4x2bNormalized) + .value("MATRIX4X2S_NORMALIZED", VertexFormat::Matrix4x2sNormalized) + .value("MATRIX4X3", VertexFormat::Matrix4x3) + .value("MATRIX4X3H", VertexFormat::Matrix4x3h) + .value("MATRIX4X3D", VertexFormat::Matrix4x3d) + .value("MATRIX4X3B_NORMALIZED", VertexFormat::Matrix4x3bNormalized) + .value("MATRIX4X3S_NORMALIZED", VertexFormat::Matrix4x3sNormalized) + .value("MATRIX4X4", VertexFormat::Matrix4x4) + .value("MATRIX4X4H", VertexFormat::Matrix4x4h) + .value("MATRIX4X4D", VertexFormat::Matrix4x4d) + .value("MATRIX4X4B_NORMALIZED", VertexFormat::Matrix4x4bNormalized) + .value("MATRIX4X4S_NORMALIZED", VertexFormat::Matrix4x4sNormalized) + .value("MATRIX4X2B_NORMALIZED_ALIGNED", VertexFormat::Matrix4x2bNormalizedAligned) + .value("MATRIX4X3H_ALIGNED", VertexFormat::Matrix4x3hAligned) + .value("MATRIX4X3B_NORMALIZED_ALIGNED", VertexFormat::Matrix4x3bNormalizedAligned) + .value("MATRIX4X3S_NORMALIZED_ALIGNED", VertexFormat::Matrix4x3sNormalizedAligned); } }} diff --git a/src/python/magnum/test/convert.sh b/src/python/magnum/test/convert.sh new file mode 100755 index 0000000..9899d47 --- /dev/null +++ b/src/python/magnum/test/convert.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +# in -> bin +for i in *.bin.in; do + ../../../../../magnum-plugins/src/MagnumPlugins/GltfImporter/Test/in2bin.py $i +done diff --git a/src/python/magnum/test/mesh.bin b/src/python/magnum/test/mesh.bin new file mode 100644 index 0000000000000000000000000000000000000000..d25c1a9d042ec50a424292457f86574ff9ce05ac GIT binary patch literal 92 zcmZQzU|?WkU<8s4`=QwG?3pvRXV3W9{WnfyxB--bfChUYZ9i+~Opv^taoT?dVFr*! VkT_Vc9YkKzKK;KjgKv+`Y5-*y8LI#Q literal 0 HcmV?d00001 diff --git a/src/python/magnum/test/mesh.bin.in b/src/python/magnum/test/mesh.bin.in new file mode 100644 index 0000000..59fe2e7 --- /dev/null +++ b/src/python/magnum/test/mesh.bin.in @@ -0,0 +1,19 @@ +type = '>$+Y*{xH~3<HxhI9?b{SdivGgqF{Hh zo9}u*BWVYY!@MSEV-!H4;|_+dU_v$~LmbsbT204bAaJ%C1(~2wFBZy$8V)yvO@8up zm!JKE-@Gkqdu$xce)s>`=w#h{(@Wx`J&~-;L{Ih;-SO3OoMc7NrL?_Xye(ShDyyws z>@%_bCN;B7RS%)=v{j>(fu;Bj#h39aRB^{|N+uyk@wU$lLf$_ySRN&he*j`+hPd%D fo#qzv`SoI6&Mo5{<`6$MG)*%QHyp #include "Corrade/Containers/PythonBindings.h" +#include "Corrade/Containers/OptionalPythonBindings.h" #include "Corrade/Containers/StridedArrayViewPythonBindings.h" #include "Magnum/PythonBindings.h" @@ -305,11 +306,24 @@ void trade(py::module_& m) { /* AbstractImporter depends on this */ py::module_::import("corrade.pluginmanager"); + py::enum_{m, "MeshAttribute", "Mesh attribute name"} + .value("POSITION", Trade::MeshAttribute::Position) + .value("TANGENT", Trade::MeshAttribute::Tangent) + .value("BITANGENT", Trade::MeshAttribute::Bitangent) + .value("NORMAL", Trade::MeshAttribute::Normal) + .value("TEXTURE_COORDINATES", Trade::MeshAttribute::TextureCoordinates) + .value("COLOR", Trade::MeshAttribute::Color) + .value("JOINT_IDS", Trade::MeshAttribute::JointIds) + .value("WEIGHTS", Trade::MeshAttribute::Weights) + .value("OBJECT_ID", Trade::MeshAttribute::ObjectId); + py::class_{m, "MeshData", "Mesh data"} .def_property_readonly("primitive", &Trade::MeshData::primitive, "Primitive") .def_property_readonly("index_data", [](Trade::MeshData& self) { return Containers::pyArrayViewHolder(self.indexData(), py::cast(self)); }, "Raw index data") + /** @todo direct access to MeshAttributeData, once making custom + MeshData is desired */ .def_property_readonly("vertex_data", [](Trade::MeshData& self) { return Containers::pyArrayViewHolder(self.vertexData(), py::cast(self)); }, "Raw vertex data") @@ -319,11 +333,109 @@ void trade(py::module_& m) { PyErr_SetString(PyExc_AttributeError, "mesh is not indexed"); throw py::error_already_set{}; } - return self.indexCount(); }, "Index count") + .def_property_readonly("index_type", [](Trade::MeshData& self) { + if(!self.isIndexed()) { + PyErr_SetString(PyExc_AttributeError, "mesh is not indexed"); + throw py::error_already_set{}; + } + return self.indexType(); + }, "Index type") + .def_property_readonly("index_offset", [](Trade::MeshData& self) { + if(!self.isIndexed()) { + PyErr_SetString(PyExc_AttributeError, "mesh is not indexed"); + throw py::error_already_set{}; + } + return self.indexOffset(); + }, "Index offset") + .def_property_readonly("index_stride", [](Trade::MeshData& self) { + if(!self.isIndexed()) { + PyErr_SetString(PyExc_AttributeError, "mesh is not indexed"); + throw py::error_already_set{}; + } + return self.indexStride(); + }, "Index stride") .def_property_readonly("vertex_count", &Trade::MeshData::vertexCount, "Vertex count") - .def_property_readonly("attribute_count", static_cast(&Trade::MeshData::attributeCount), "Attribute array count"); + /* Has to be a function instead of a property because there's an + overload taking a name */ + .def("attribute_count", static_cast(&Trade::MeshData::attributeCount), "Attribute array count") + /** @todo direct access to MeshAttributeData, once making custom + MeshData is desired */ + .def("attribute_name", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + return self.attributeName(id); + }, "Attribute name", py::arg("id")) + .def("attribute_id", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + return self.attributeId(id); + }, "Attribute ID in a set of attributes of the same name", py::arg("id")) + .def("attribute_format", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + return self.attributeFormat(id); + }, "Attribute format", py::arg("id")) + .def("attribute_offset", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + return self.attributeOffset(id); + }, "Attribute offset", py::arg("id")) + .def("attribute_stride", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + return self.attributeStride(id); + }, "Attribute stride", py::arg("id")) + .def("attribute_array_size", [](Trade::MeshData& self, UnsignedInt id) { + if(id >= self.attributeCount()) { + PyErr_SetString(PyExc_IndexError, ""); + throw py::error_already_set{}; + } + return self.attributeArraySize(id); + }, "Attribute array size", py::arg("id")) + .def("has_attribute", &Trade::MeshData::hasAttribute, "Whether the mesh has given attribute", py::arg("name")) + .def("attribute_count", static_cast(&Trade::MeshData::attributeCount), "Count of given named attribute", py::arg("name")) + .def("attribute_id", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + if(const Containers::Optional found = self.findAttributeId(name, id)) + return *found; + PyErr_SetString(PyExc_KeyError, ""); + throw py::error_already_set{}; + }, "Absolute ID of a named attribute", py::arg("name"), py::arg("id") = 0) + .def("attribute_format", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + if(const Containers::Optional found = self.findAttributeId(name, id)) + return self.attributeFormat(*found); + PyErr_SetString(PyExc_KeyError, ""); + throw py::error_already_set{}; + }, "Format of a named attribute", py::arg("name"), py::arg("id") = 0) + .def("attribute_offset", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + if(const Containers::Optional found = self.findAttributeId(name, id)) + return self.attributeOffset(*found); + PyErr_SetString(PyExc_KeyError, ""); + throw py::error_already_set{}; + }, "Offset of a named attribute", py::arg("name"), py::arg("id") = 0) + .def("attribute_stride", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + if(const Containers::Optional found = self.findAttributeId(name, id)) + return self.attributeStride(*found); + PyErr_SetString(PyExc_KeyError, ""); + throw py::error_already_set{}; + }, "Stride of a named attribute", py::arg("name"), py::arg("id") = 0) + .def("attribute_array_size", [](Trade::MeshData& self, Trade::MeshAttribute name, UnsignedInt id) { + if(const Containers::Optional found = self.findAttributeId(name, id)) + 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); py::class_ imageData1D{m, "ImageData1D", "One-dimensional image data"}; py::class_ imageData2D{m, "ImageData2D", "Two-dimensional image data"}; @@ -367,7 +479,19 @@ void trade(py::module_& m) { .def("mesh_name", checkOpenedBoundsReturnsString<&Trade::AbstractImporter::meshName, &Trade::AbstractImporter::meshCount>, "Mesh name", py::arg("id")) .def("mesh", checkOpenedBoundsResult, "Mesh", py::arg("id"), py::arg("level") = 0) .def("mesh", checkOpenedBoundsResultString, "Mesh", py::arg("name"), py::arg("level") = 0) - /** @todo mesh_attribute_for_name / mesh_attribute_name */ + /** @todo drop std::string in favor of our own string caster */ + .def("mesh_attribute_for_name", [](Trade::AbstractImporter& self, const std::string& name) -> Containers::Optional { + const Trade::MeshAttribute attribute = self.meshAttributeForName(name); + if(attribute == Trade::MeshAttribute{}) + return {}; + return attribute; + }, "Mesh attribute for given name", py::arg("name")) + /** @todo drop std::string in favor of our own string caster */ + .def("mesh_attribute_name", [](Trade::AbstractImporter& self, Trade::MeshAttribute name) -> Containers::Optional { + if(const Containers::String attribute = self.meshAttributeName(name)) + return std::string{attribute}; + return {}; + }, "String name for given mesh attribute", py::arg("name")) .def_property_readonly("image1d_count", checkOpened, "One-dimensional image count") .def_property_readonly("image2d_count", checkOpened, "Two-dimensional image count")