Skip to content

Commit c191699

Browse files
committed
add colors support for point clouds
1 parent c2d34db commit c191699

File tree

6 files changed

+395
-40
lines changed

6 files changed

+395
-40
lines changed

docs/examples.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ Scalar Field
2727
.. literalinclude:: ../script_examples/scalar_field.py
2828
:language: Python
2929

30+
PointCloud Colors
31+
-----------------
32+
33+
.. literalinclude:: ../scripts_examples/point-cloud-colors.py
34+
:language: Python
35+
3036
Polyline
3137
---------
3238

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
An example on how to create a point cloud and add colors
3+
And how to get colors of a point clouds
4+
"""
5+
import pycc
6+
import numpy as np
7+
8+
CC = pycc.GetInstance()
9+
10+
points = np.array([
11+
[0, 0, 0],
12+
[1, 1, 0],
13+
[2, 2, 0]
14+
])
15+
cloud = pycc.ccPointCloud(points[:, 0], points[:, 1], points[:, 2])
16+
cloud.setName("Colored Cloud 1")
17+
cloud.setPointSize(9) # So that the point are visible
18+
19+
# This point cloud does not yet have colors:
20+
assert cloud.hasColors() == False
21+
assert cloud.colors() is None
22+
23+
red = pycc.Rgb(255, 0, 0)
24+
green = pycc.Rgb(0, 255, 0)
25+
blue = pycc.Rgb(0, 0, 255)
26+
27+
# This needs to be called after points have been added
28+
cloud.resizeTheRGBTable()
29+
30+
cloud.setPointColor(0, red)
31+
cloud.setPointColor(1, green)
32+
cloud.setPointColor(2, blue)
33+
# So that the colors are show by default
34+
# without having to select in the UI
35+
cloud.showColors(True)
36+
37+
print(f"First point color: {cloud.getPointColor(0)}")
38+
color = cloud.getPointColor(1)
39+
print(f"Second point r: {color.r}, g: {color.g}, b: {color.b}, a: {color.a}")
40+
41+
# Get the colors as a numpy array
42+
colors = cloud.colors()
43+
print(colors)
44+
# This array is a view so modifications made on it will reflect on
45+
# the point cloud
46+
colors[0][:] = [255, 0, 255, 255]
47+
48+
CC.addToDB(cloud)
49+
50+
# Another way is to use the setColors function
51+
points = np.array([
52+
[3, 3, 0],
53+
[4, 4, 0],
54+
[5, 5, 0]
55+
])
56+
cloud = pycc.ccPointCloud(points[:, 0], points[:, 1], points[:, 2])
57+
cloud.setName("Colored Cloud 2")
58+
cloud.setPointSize(9) # So that the point are visible
59+
pointColors = np.array([[255, 0, 0], [0, 255, 0], [0, 0, 255]])
60+
cloud.setColors(pointColors)
61+
cloud.showColors(True)
62+
# We can then also get colors as an array
63+
colors = cloud.colors()
64+
colors[0][:] = [255, 0, 255, 255]
65+
66+
CC.addToDB(cloud)

script_examples/polyline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
VERTICES = np.array([
77
[-0.5, -0.5, 0],
88
[1, 1, 0],
9-
[2, 2, 0]
9+
[2, 2, 0]
1010
])
1111
vertices = pycc.ccPointCloud(VERTICES[:, 0], VERTICES[:, 1], VERTICES[:, 2])
1212
polyline = pycc.ccPolyline(vertices)

wrapper/pycc/src/qcc_db/ccPointCloud.cpp

Lines changed: 231 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
#include <ccScalarField.h>
2626
#include <climits>
2727
#include <cstdint>
28+
#include <pybind11/detail/common.h>
2829
#include <pybind11/pytypes.h>
30+
#include <stdexcept>
2931
#include <type_traits>
3032
#include <vector>
3133

@@ -43,6 +45,60 @@ static_assert(std::is_same<PointCoordinateType, float>::value,
4345
static_assert(sizeof(CCVector3) == sizeof(PointCoordinateType) * 3,
4446
"Unexpected layout for CCVector3");
4547

48+
void ccPointCloud_setColors(ccPointCloud &self,
49+
const py::array_t<ColorCompType> &r,
50+
const py::array_t<ColorCompType> &g,
51+
const py::array_t<ColorCompType> &b,
52+
const py::array_t<ColorCompType> *a)
53+
{
54+
if (r.ndim() != 1 || g.ndim() != 1 || b.ndim() != 1 || (a && a->ndim() != 1))
55+
{
56+
throw py::value_error("arrays must be one dimensional");
57+
}
58+
59+
if (r.size() != g.size() || r.size() != b.size() || a && r.size() != a->size())
60+
{
61+
throw py::value_error("r, g, b must have the same size");
62+
}
63+
64+
if (r.size() != self.size())
65+
{
66+
throw py::value_error("The number of colors must match the number of points");
67+
}
68+
69+
if (!self.resizeTheRGBTable())
70+
{
71+
throw std::runtime_error("failed to allocate rgb table");
72+
}
73+
74+
py::iterator a_begin;
75+
if (a)
76+
{
77+
a_begin = a->begin();
78+
}
79+
80+
auto r_it = r.begin();
81+
auto g_it = g.begin();
82+
auto b_it = b.begin();
83+
auto *a_it = a ? &a_begin : nullptr;
84+
85+
for (size_t i = 0; i < self.size(); ++i)
86+
{
87+
const ccColor::Rgba value(r_it->cast<ColorCompType>(),
88+
g_it->cast<ColorCompType>(),
89+
b_it->cast<ColorCompType>(),
90+
a_it ? a_it->cast<ColorCompType>() : ccColor::MAX);
91+
self.setPointColor(i, value);
92+
++r_it;
93+
++g_it;
94+
++b_it;
95+
if (a_it)
96+
{
97+
++(*a_it);
98+
}
99+
}
100+
}
101+
46102
void define_ccPointCloud(py::module &m)
47103
{
48104
DEFINE_POINTCLOUDTPL(ccGenericPointCloud, QString, m, "__ccGenericPointCloudTpl");
@@ -123,8 +179,8 @@ void define_ccPointCloud(py::module &m)
123179
"reference"_a)
124180
// features allocation/resize
125181
.def("reserveThePointsTable", &ccPointCloud::reserveThePointsTable, "_numberOfPoints"_a)
126-
.def("reserveTheRGBTable", &ccPointCloud::reserveThePointsTable)
127-
.def("reserveTheRGBTable", &ccPointCloud::reserveThePointsTable, "fillWithWhite"_a = false)
182+
.def("reserveTheRGBTable", &ccPointCloud::reserveTheRGBTable)
183+
.def("resizeTheRGBTable", &ccPointCloud::resizeTheRGBTable, "fillWithWhite"_a = false)
128184
.def("reserveTheNormsTable", &ccPointCloud::reserveTheNormsTable)
129185
.def("resizeTheNormsTable", &ccPointCloud::resizeTheNormsTable)
130186
.def("shrinkToFit", &ccPointCloud::shrinkToFit)
@@ -161,14 +217,180 @@ void define_ccPointCloud(py::module &m)
161217
&ccPointCloud::orientNormalsWithFM,
162218
"level"_a,
163219
"pDlg"_a = nullptr)
164-
// Special functions added by pycc
165220
.def("addPoints", &PyCC::addPointsFromArrays<ccPointCloud>)
166-
.def("setColor",
167-
[](ccPointCloud &self,
168-
ColorCompType r,
169-
ColorCompType g,
170-
ColorCompType b,
171-
ColorCompType a) { self.setColor(r, g, b, a); })
221+
.def(
222+
"setColor",
223+
[](ccPointCloud &self,
224+
ColorCompType r,
225+
ColorCompType g,
226+
ColorCompType b,
227+
ColorCompType a) { self.setColor(r, g, b, a); },
228+
"r"_a,
229+
"g"_a,
230+
"b"_a,
231+
"a"_a = ccColor::MAX,
232+
R"doc(
233+
Sets the color for the whole point cloud
234+
)doc")
235+
.def(
236+
"setPointColor",
237+
[](ccPointCloud &self, const unsigned index, const ccColor::Rgb &color)
238+
{
239+
if (!self.hasColors())
240+
{
241+
throw std::runtime_error("colors needs to be enabled first");
242+
}
243+
if (index >= self.size())
244+
{
245+
throw py::index_error("invalid index");
246+
}
247+
self.setPointColor(index, color);
248+
},
249+
"index"_a,
250+
"color"_a,
251+
R"doc(
252+
Sets the color of the point at the given index with the new value
253+
254+
Colors must have been enabled before
255+
)doc")
256+
.def(
257+
"setPointColor",
258+
[](ccPointCloud &self, const unsigned index, const ccColor::Rgba &color)
259+
{
260+
if (!self.hasColors())
261+
{
262+
throw std::runtime_error("colors needs to be enabled first");
263+
}
264+
265+
if (index >= self.size())
266+
{
267+
throw py::index_error("invalid index");
268+
}
269+
self.setPointColor(index, color);
270+
},
271+
"index"_a,
272+
"color"_a,
273+
R"doc(
274+
Sets the color of the point at the given index with the new value
275+
276+
Colors must have been enabled before
277+
)doc")
278+
.def(
279+
"getPointColor",
280+
[](ccPointCloud &self, const unsigned index)
281+
{
282+
if (!self.hasColors())
283+
{
284+
throw std::runtime_error("colors needs to be enabled first");
285+
}
286+
287+
if (index >= self.size())
288+
{
289+
throw py::index_error("invalid index");
290+
}
291+
return self.getPointColor(index);
292+
},
293+
"index"_a,
294+
R"doc(
295+
Returns the color of the point at the given index
296+
297+
Colors must have been enabled before
298+
)doc")
299+
.def("setColors",
300+
&ccPointCloud_setColors,
301+
"r"_a,
302+
"g"_a,
303+
"b"_a,
304+
"a"_a = nullptr,
305+
R"doc(
306+
Sets the colors in the point cloud with the given r, g, b arrays
307+
(and the optinal alpha array).
308+
309+
Enables colors if not done already
310+
)doc")
311+
.def(
312+
"setColors",
313+
[](ccPointCloud &self, const py::array_t<ColorCompType> &colors)
314+
{
315+
if (colors.ndim() != 2)
316+
{
317+
throw py::value_error("color array must be 2 dimensional");
318+
}
319+
320+
if (colors.shape(0) != self.size())
321+
{
322+
throw py::value_error("The number of colors must match the number of points");
323+
}
324+
325+
if (colors.shape(1) != 3 && colors.shape(1) != 4)
326+
{
327+
throw py::value_error("Colors must be RGB or RGBA");
328+
}
329+
330+
if (!self.resizeTheRGBTable())
331+
{
332+
throw std::runtime_error("failed to allocate rgb table");
333+
}
334+
335+
if (colors.shape(1) == 3)
336+
{
337+
auto rgb = colors.unchecked<2>();
338+
for (size_t i = 0; i < self.size(); ++i)
339+
{
340+
self.setPointColor(i, ccColor::Rgb(rgb(i, 0), rgb(i, 1), rgb(i, 2)));
341+
}
342+
}
343+
else if (colors.shape(1) == 4)
344+
{
345+
auto rgba = colors.unchecked<2>();
346+
for (size_t i = 0; i < self.size(); ++i)
347+
{
348+
self.setPointColor(
349+
i, ccColor::Rgba(rgba(i, 0), rgba(i, 1), rgba(i, 2), rgba(i, 3)));
350+
}
351+
}
352+
},
353+
"colors"_a,
354+
R"doc(
355+
Sets the colors in the point cloud with the given r, g, b arrays
356+
(and the optinal alpha array).
357+
358+
Enables colors if not done already
359+
)doc")
360+
.def(
361+
"colors",
362+
[](ccPointCloud &self) -> py::object
363+
{
364+
RGBAColorsTableType *colorsTable = self.rgbaColors();
365+
if (colorsTable == nullptr)
366+
{
367+
return py::none();
368+
}
369+
370+
ccColor::Rgba *colors = colorsTable->data();
371+
if (colors == nullptr)
372+
{
373+
return py::none();
374+
}
375+
376+
static_assert(CHAR_BIT == 8, "A char must have 8 bits");
377+
static_assert(sizeof(ColorCompType) == 1, "ColorCompType must have 8 bit");
378+
static_assert(sizeof(ccColor::Rgba) == 4 * sizeof(ColorCompType), "");
379+
static_assert(alignof(uint8_t) == alignof(ccColor::Rgba), "Not same alignment");
380+
381+
auto *ptr = reinterpret_cast<uint8_t *>(colors);
382+
383+
auto capsule = py::capsule(ptr, [](void *) {});
384+
py::array a(py::dtype("4u1"), colorsTable->size(), ptr, capsule);
385+
386+
return a;
387+
},
388+
R"doc(
389+
"Returns a the colors as a "view" in a numpy array
390+
if the point cloud does not have colors, None is returned
391+
392+
As this is a "view", modifications made to this will reflect on the point cloud
393+
)doc")
172394
.def("setPointNormal", &ccPointCloud::setPointNormal, "index"_a, "normal"_a)
173395
.def("addNorm", &ccPointCloud::addNorm, "normal"_a)
174396
.def("addNormals",
@@ -207,33 +429,6 @@ void define_ccPointCloud(py::module &m)
207429
.def("showNormalsAsLines", &ccPointCloud::showNormalsAsLines, "state"_a)
208430
.def("colorize", &ccPointCloud::colorize)
209431
.def("crop2D", &ccPointCloud::crop2D, "poly"_a, "orthodDim"_a, "inside"_a = true)
210-
.def("colors",
211-
[](ccPointCloud &self) -> py::object
212-
{
213-
RGBAColorsTableType *colorsTable = self.rgbaColors();
214-
if (colorsTable == nullptr)
215-
{
216-
return py::none();
217-
}
218-
219-
ccColor::Rgba *colors = colorsTable->data();
220-
if (colors == nullptr)
221-
{
222-
return py::none();
223-
}
224-
225-
static_assert(CHAR_BIT == 8, "A char must have 8 bits");
226-
static_assert(sizeof(ColorCompType) == 1, "ColorCompType must have 8 bit");
227-
static_assert(sizeof(ccColor::Rgba) == 4 * sizeof(ColorCompType), "");
228-
static_assert(alignof(uint8_t) == alignof(ccColor::Rgba), "Not same alignment");
229-
230-
auto *ptr = reinterpret_cast<uint8_t *>(colors);
231-
232-
auto capsule = py::capsule(ptr, [](void *) {});
233-
py::array a(py::dtype("4u1"), colorsTable->size(), ptr, capsule);
234-
235-
return a;
236-
})
237432
.def("points",
238433
[](ccPointCloud &self)
239434
{

0 commit comments

Comments
 (0)