primlib 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
primlib/utils.py ADDED
@@ -0,0 +1,53 @@
1
+ import matplotlib.pyplot as plt
2
+ import numpy as np
3
+ from numpy.typing import ArrayLike
4
+
5
+
6
+ def scalar_to_color(
7
+ scalars: ArrayLike,
8
+ cmap: str = "viridis",
9
+ min_max: tuple[float, float] | None = None,
10
+ nan_color: tuple[float, float, float] = (1, 0, 0),
11
+ ) -> np.ndarray:
12
+ scalars = np.asarray(scalars)
13
+ mask_valid = ~np.isnan(scalars)
14
+ if min_max is None:
15
+ min_max = (np.min(scalars[mask_valid]), np.max(scalars[mask_valid]))
16
+ scalars_norm = np.clip(
17
+ (scalars[mask_valid] - min_max[0]) / (min_max[1] - min_max[0]), 0, 1
18
+ )
19
+ cm = plt.get_cmap(cmap)
20
+ colors = np.empty((scalars.size, 3))
21
+ colors[mask_valid, :] = cm(scalars_norm)[:, 0:3]
22
+ colors[~mask_valid, :] = nan_color
23
+ return colors
24
+
25
+
26
+ def scalar_to_bicolor(
27
+ scalars: ArrayLike,
28
+ cmaps: tuple[str, str] = ("Blues", "Reds"),
29
+ vmax: float | None = None,
30
+ nan_color: tuple[float, float, float] = (0, 1, 0),
31
+ ) -> np.ndarray:
32
+ scalars = np.asarray(scalars)
33
+ mask_valid = ~np.isnan(scalars)
34
+ mask_pos = scalars >= 0
35
+ mask_neg = ~mask_pos
36
+ neg = scalars[mask_valid & mask_neg]
37
+ pos = scalars[mask_valid & mask_pos]
38
+ if vmax is None:
39
+ if neg.size == 0:
40
+ vmax = np.max(pos)
41
+ elif pos.size == 0:
42
+ vmax = np.max(-neg)
43
+ else:
44
+ vmax = np.max((np.max(pos), np.max(-neg)))
45
+ scalars_norm_neg = np.clip(-neg / vmax, 0, 1)
46
+ scalars_norm_pos = np.clip(pos / vmax, 0, 1)
47
+ cm_neg = plt.get_cmap(cmaps[0])
48
+ cm_pos = plt.get_cmap(cmaps[1])
49
+ colors = np.empty((scalars.size, 3))
50
+ colors[mask_valid & mask_neg, :] = cm_neg(scalars_norm_neg)[:, 0:3]
51
+ colors[mask_valid & mask_pos, :] = cm_pos(scalars_norm_pos)[:, 0:3]
52
+ colors[~mask_valid, :] = nan_color
53
+ return colors
primlib/vertices.py ADDED
@@ -0,0 +1,604 @@
1
+ import copy
2
+ import logging
3
+ import os
4
+ from typing import Any, Self
5
+
6
+ import numpy as np
7
+ from numpy.typing import ArrayLike
8
+ from plyfile import PlyData, PlyElement
9
+ from scipy.spatial import KDTree
10
+ from trafolib.trafo3d import Trafo3d
11
+
12
+ from primlib.utils import scalar_to_bicolor, scalar_to_color
13
+
14
+
15
+ class Vertices:
16
+ """
17
+ A class representing a set of vertices, normals and colors
18
+
19
+ Can be used as a base class for point clouds, line sets or triangle meshes.
20
+ """
21
+
22
+ __slots__ = ("_colors", "_normals", "_vertices")
23
+
24
+ def __init__(
25
+ self,
26
+ vertices: ArrayLike | None = None,
27
+ normals: ArrayLike | None = None,
28
+ colors: ArrayLike | None = None,
29
+ ) -> None:
30
+ # Define types and initialize
31
+ self._vertices: np.ndarray = np.zeros((0, 3))
32
+ self._normals: np.ndarray = np.zeros((0, 3))
33
+ self._colors: np.ndarray = np.zeros((0, 3))
34
+ # Initialize using parameters and setters
35
+ self.vertices = vertices
36
+ self.normals = normals
37
+ self.colors = colors
38
+
39
+ def __array__(
40
+ self,
41
+ dtype: Any = None,
42
+ copy: bool | None = None,
43
+ ) -> np.ndarray:
44
+ """Convert vertices to a NumPy array.
45
+
46
+ :param dtype: Desired data type of the resulting array.
47
+ If None, the data type is inferred from the vertices.
48
+ :param copy: Whether to return a copy of the array.
49
+ If None, the copy behaviour is determined by NumPy.
50
+ :returns: NumPy array containing the vertices.
51
+ """
52
+ if copy is True:
53
+ return np.array(self._vertices, dtype=dtype, copy=True)
54
+ return np.asarray(self._vertices, dtype=dtype)
55
+
56
+ def __getitem__(self, item: Any) -> np.ndarray:
57
+ """Get vertices by index or slice.
58
+
59
+ Supports integer indexing, slicing, and advanced NumPy indexing.
60
+
61
+ :param item: Index, slice, or array of indices used to select vertices.
62
+ :returns: Selected vertex or vertices as a NumPy array.
63
+ """
64
+ return self._vertices[item]
65
+
66
+ def __eq__(self, other: object) -> bool:
67
+ """Check two vertices sets for equality.
68
+
69
+ :param other: Other object
70
+ :returns: True if vertices sets are equal
71
+ """
72
+ if not isinstance(other, Vertices):
73
+ return NotImplemented
74
+ return (
75
+ # Vertices
76
+ self._vertices.shape == other._vertices.shape
77
+ and np.allclose(self._vertices, other._vertices)
78
+ # Normals
79
+ and self._normals.shape == other._normals.shape
80
+ and np.allclose(self._normals, other._normals)
81
+ # Colors
82
+ and self._colors.shape == other._colors.shape
83
+ and np.allclose(
84
+ self._colors,
85
+ other._colors,
86
+ atol=1.0 / 255.0,
87
+ )
88
+ )
89
+
90
+ def __ne__(self, other: object) -> bool:
91
+ """Check two vertices sets for inequality
92
+
93
+ :param other: Other object
94
+ :returns: True, if vertices sets are not equal
95
+ """
96
+ return not self.__eq__(other)
97
+
98
+ def __iadd__(self, other: "Vertices") -> Self:
99
+ """Concatenate another vertices set in-place
100
+
101
+ :param other: Other vertices set
102
+ :returns: This vertices set (self)
103
+ """
104
+ if not isinstance(other, Vertices):
105
+ return NotImplemented
106
+ if self.has_normals != other.has_normals or self.has_colors != other.has_colors:
107
+ raise ValueError("Both vertices sets must have the same properties")
108
+ self._vertices = np.vstack((self._vertices, other._vertices))
109
+ if self.has_normals:
110
+ self._normals = np.vstack((self._normals, other._normals))
111
+ if self.has_colors:
112
+ self._colors = np.vstack((self._colors, other._colors))
113
+ return self
114
+
115
+ def __add__(self, other: "Vertices") -> Self:
116
+ """Concatenate two vertices sets
117
+
118
+ :param other: Other vertices set
119
+ :returns: New vertices set
120
+ """
121
+ if not isinstance(other, Vertices):
122
+ return NotImplemented
123
+ result = self.copy()
124
+ result += other
125
+ return result
126
+
127
+ def copy(self) -> Self:
128
+ """Get copy of vertices
129
+
130
+ :returns: Copy
131
+ """
132
+ return copy.deepcopy(self)
133
+
134
+ def __copy__(self) -> Self:
135
+ """Return a shallow copy of the object
136
+
137
+ Required by the `copy` module to implement custom shallow copy behavior.
138
+
139
+ :returns: A new instance containing references to the original
140
+ instance's attributes.
141
+ """
142
+ cls = type(self)
143
+ result = cls.__new__(cls)
144
+ for base in cls.__mro__:
145
+ slots = getattr(base, "__slots__", ())
146
+ if isinstance(slots, str):
147
+ slots = (slots,)
148
+ for name in slots:
149
+ if name in ("__dict__", "__weakref__"):
150
+ continue
151
+ if hasattr(self, name):
152
+ setattr(result, name, copy.copy(getattr(self, name)))
153
+ return result
154
+
155
+ def __deepcopy__(self, memo) -> "Vertices":
156
+ """Return a deep copy of the object
157
+
158
+ Required by the `copy` module to implement custom deep copy behavior.
159
+
160
+ :param memo: A dictionary tracking already copied objects to prevent
161
+ infinite loops from recursive references
162
+ :returns: A new, completely independent copy of the instance.
163
+ """
164
+ cls = type(self)
165
+ result = cls.__new__(cls)
166
+ memo[id(self)] = result
167
+ for base in cls.__mro__:
168
+ slots = getattr(base, "__slots__", ())
169
+ if isinstance(slots, str):
170
+ slots = (slots,)
171
+ for name in slots:
172
+ if name in ("__dict__", "__weakref__"):
173
+ continue
174
+ if hasattr(self, name):
175
+ setattr(result, name, copy.deepcopy(getattr(self, name), memo))
176
+ return result
177
+
178
+ @property
179
+ def vertices(self) -> np.ndarray:
180
+ """Get vertices
181
+
182
+ Vertices are points in 3d.
183
+
184
+ :returns: Vertices, shape (n, 3), type float
185
+ """
186
+ return self._vertices
187
+
188
+ @vertices.setter
189
+ def vertices(self, value: ArrayLike | None) -> None:
190
+ """Set vertices
191
+
192
+ Vertices are points in 3d.
193
+
194
+ If there are normals or colors defined in the vertices set,
195
+ then the number of vertices has to match their number.
196
+
197
+ :param value: Vertices, shape (n, 3), type float
198
+ """
199
+ if value is None:
200
+ value = np.zeros((0, 3))
201
+ else:
202
+ value = np.asarray(value, dtype=float)
203
+ if value.ndim != 2 or value.shape[1] != 3:
204
+ raise Exception("Provide vertices of shape (n, 3)")
205
+ if self.has_normals and value.shape[0] != self._normals.shape[0]:
206
+ raise Exception(
207
+ "vertices set already has normals, provide proper amount of vertices"
208
+ )
209
+ if self.has_colors and value.shape[0] != self._colors.shape[0]:
210
+ raise Exception(
211
+ "vertices set already has colors, provide proper amount of vertices"
212
+ )
213
+ self._vertices = value
214
+
215
+ @property
216
+ def normals(self) -> np.ndarray:
217
+ """Get normals
218
+
219
+ Normals are 3d vectors that are perpendicular to neighborhood of the
220
+ vertex. For a point cloud that can mean perpendicular to a plane
221
+ sampled by the vertex and its neighbors. For a triangle mesh the
222
+ vertex normal can be the mean of the face normals of all faces
223
+ adjacent to the vector. Normals are normalized, i.e. they have a
224
+ length of 1.0.
225
+
226
+ :returns: Normals, shape (n, 3)
227
+ """
228
+ return self._normals
229
+
230
+ @normals.setter
231
+ def normals(self, value: ArrayLike | None) -> None:
232
+ """Set normals
233
+
234
+ If there are vertices defined in the vertices set, then the number
235
+ of normals has to match the number of vertices.
236
+
237
+ Normals should have a length > 0, otherwise an exception is
238
+ thrown. Normals should have unit length. If not, normals will
239
+ be normalized and a warning is given.
240
+
241
+ :param value: Normals, shape (n, 3)
242
+ """
243
+ if value is None:
244
+ value = np.zeros((0, 3))
245
+ else:
246
+ value = np.asarray(value, dtype=float)
247
+ if value.ndim != 2 or value.shape[1] != 3:
248
+ raise Exception("Provide normals of shape (n, 3)")
249
+ if value.shape[0] > 0 and value.shape[0] != self._vertices.shape[0]:
250
+ raise Exception("Provide as many normals as vertices")
251
+ lengths = np.sqrt(value[:, 0] ** 2 + value[:, 1] ** 2 + value[:, 2] ** 2)
252
+ if np.any(np.isclose(lengths, 0.0)):
253
+ raise Exception("Some normals have zero lengths")
254
+ if not np.allclose(lengths, 1.0):
255
+ logging.warning("Some normals are not normalized. Normalizing...")
256
+ value = value / lengths[:, None]
257
+ self._normals = value
258
+
259
+ @property
260
+ def has_normals(self) -> bool:
261
+ """Check if vertices set has normals
262
+
263
+ :returns: True, if vertices set has normals
264
+ """
265
+ return self._normals.shape[0] > 0
266
+
267
+ @property
268
+ def colors(self) -> np.ndarray:
269
+ """Get colors
270
+
271
+ Color are 3-tuples of RGB values in range [0..1] of type float.
272
+
273
+ :returns: Colors
274
+ """
275
+ return self._colors
276
+
277
+ @colors.setter
278
+ def colors(self, value: ArrayLike | None) -> None:
279
+ """Set colors
280
+
281
+ If there are vertices defined in the vertices set, then the number
282
+ of colors has to match the number of the vertices.
283
+
284
+ Colors are RGB values in range [0..1]. If values outside that
285
+ range are provided, values will be clipped and a warning is given.
286
+
287
+ :param value: Colors, shape (n, 3)
288
+ """
289
+ if value is None:
290
+ colors = np.zeros((0, 3))
291
+ else:
292
+ colors = np.asarray(value, dtype=float)
293
+ if colors.ndim != 2 or colors.shape[1] != 3:
294
+ raise ValueError("Provide colors of shape (n, 3)")
295
+ if colors.shape[0] > 0 and colors.shape[0] != self._vertices.shape[0]:
296
+ raise ValueError("Provide as many colors as vertices")
297
+ if not np.all(np.logical_and(colors >= 0.0, colors <= 1.0)):
298
+ logging.warning("Some colors are out of bounds. Clipping to [0,1] ...")
299
+ colors = np.clip(colors, 0.0, 1.0)
300
+ self._colors = colors
301
+
302
+ @property
303
+ def has_colors(self) -> bool:
304
+ """Check if vertices set has colors
305
+
306
+ :returns: True, if vertices set has colors
307
+ """
308
+ return self._colors.shape[0] > 0
309
+
310
+ @staticmethod
311
+ def _read_ply_data(path: str | os.PathLike) -> PlyData:
312
+ """Helper method to read PlyData from file
313
+
314
+ :param path: Path to file to read from
315
+ :returns: PlyData
316
+ """
317
+ try:
318
+ return PlyData.read(path)
319
+ except Exception as ex:
320
+ raise Exception(str(type(ex))) from ex
321
+
322
+ @staticmethod
323
+ def _write_ply_data(path: str | os.PathLike, elements: list[PlyElement]):
324
+ """Helper method to write PlyData to file
325
+
326
+ :param path: Path to file to write to
327
+ :param elements: PlyData
328
+ """
329
+ plydata = PlyData(elements, text=False)
330
+ try:
331
+ plydata.write(path)
332
+ except Exception as ex:
333
+ raise Exception(str(type(ex))) from ex
334
+
335
+ @staticmethod
336
+ def _load_vertex_data(
337
+ plydata: PlyData,
338
+ ) -> tuple[np.ndarray | None, np.ndarray | None, np.ndarray | None]:
339
+ """Helper method to read vertex data
340
+
341
+ :param plydata: PlyData
342
+ :returns: Vertices, normals and colors
343
+ """
344
+ vertex_data = plydata["vertex"]
345
+ properties = {p.name: p.val_dtype for p in vertex_data.properties}
346
+ # Vertices
347
+ vertices = (
348
+ np.vstack([vertex_data["x"], vertex_data["y"], vertex_data["z"]])
349
+ .astype(float)
350
+ .T
351
+ )
352
+ # Normals (if existing)
353
+ normals = None
354
+ normal_keys = ["nx", "ny", "nz"]
355
+ if all(k in properties for k in normal_keys):
356
+ normals = (
357
+ np.vstack([vertex_data["nx"], vertex_data["ny"], vertex_data["nz"]])
358
+ .astype(float)
359
+ .T
360
+ )
361
+ # Colors (if existing)
362
+ colors = None
363
+ color_keys_rgb = ["red", "green", "blue"]
364
+ if all(k in properties for k in color_keys_rgb):
365
+ if properties["red"] == "u1":
366
+ colors = np.vstack(
367
+ [vertex_data["red"], vertex_data["green"], vertex_data["blue"]]
368
+ ).T
369
+ colors = colors.astype(float) / 255.0
370
+ elif properties["red"] == "f4":
371
+ colors = np.vstack(
372
+ [vertex_data["red"], vertex_data["green"], vertex_data["blue"]]
373
+ ).T
374
+ else:
375
+ raise Exception(f"Unknown color format: {properties['red']}")
376
+ return vertices, normals, colors
377
+
378
+ def _save_vertex_data(self) -> PlyElement:
379
+ """Helper method to save vertex data"""
380
+ # Collect dtypes
381
+ dtypes = [("x", "f8"), ("y", "f8"), ("z", "f8")]
382
+ if self.has_normals:
383
+ dtypes.extend([("nx", "f8"), ("ny", "f8"), ("nz", "f8")])
384
+ if self.has_colors:
385
+ dtypes.extend([("red", "u1"), ("green", "u1"), ("blue", "u1")])
386
+ # Collect data
387
+ element_data = np.empty(self._vertices.shape[0], dtype=dtypes)
388
+ element_data["x"] = self._vertices[:, 0]
389
+ element_data["y"] = self._vertices[:, 1]
390
+ element_data["z"] = self._vertices[:, 2]
391
+ if self.has_normals:
392
+ element_data["nx"] = self._normals[:, 0]
393
+ element_data["ny"] = self._normals[:, 1]
394
+ element_data["nz"] = self._normals[:, 2]
395
+ if self.has_colors:
396
+ element_data["red"] = np.round(self._colors[:, 0] * 255.0).astype(np.uint8)
397
+ element_data["green"] = np.round(self._colors[:, 1] * 255.0).astype(
398
+ np.uint8
399
+ )
400
+ element_data["blue"] = np.round(self._colors[:, 2] * 255.0).astype(np.uint8)
401
+ return PlyElement.describe(element_data, "vertex")
402
+
403
+ @classmethod
404
+ def from_ply(cls, path: str | os.PathLike) -> "Vertices":
405
+ """Create vertices set from PLY file
406
+
407
+ :param path: Path to PLY file to read
408
+ :returns: Resulting vertices set
409
+ """
410
+ plydata = Vertices._read_ply_data(path)
411
+ vertices, normals, colors = Vertices._load_vertex_data(plydata)
412
+ return cls(vertices, normals, colors)
413
+
414
+ def save_ply(self, path: str | os.PathLike) -> None:
415
+ """Save vertices set to PLY file
416
+
417
+ :param path: Path to PLY file to write
418
+ """
419
+ vertex_element = self._save_vertex_data()
420
+ Vertices._write_ply_data(path, [vertex_element])
421
+
422
+ def min(self) -> np.ndarray:
423
+ """Get minimum of all coordinates
424
+
425
+ :raises Exception: Raised if vertices set empty
426
+ :returns: Minimum of all coordinates, shape (3,)
427
+ """
428
+ if self._vertices.shape[0] == 0:
429
+ raise Exception("Cannot compute min for empty vertices set")
430
+ return np.min(self._vertices, axis=0)
431
+
432
+ def max(self) -> np.ndarray:
433
+ """Get maximum of all coordinates
434
+
435
+ :raises Exception: Raised if vertices set empty
436
+ :returns: Maximum of all coordinates, shape (3,)
437
+ """
438
+ if self._vertices.shape[0] == 0:
439
+ raise Exception("Cannot compute max for empty vertices set")
440
+ return np.max(self._vertices, axis=0)
441
+
442
+ def bounds(self) -> np.ndarray:
443
+ """Get bounds (min and max) of vertices set
444
+
445
+ :raises Exception: Raised if vertices set empty
446
+ :returns: Min and max values for all coordinates, shape (2, 3)
447
+ """
448
+ if self._vertices.shape[0] == 0:
449
+ raise Exception("Cannot compute bounds for empty vertices set")
450
+ return np.vstack(
451
+ (np.min(self._vertices, axis=0), np.max(self._vertices, axis=0))
452
+ )
453
+
454
+ def range(self) -> np.ndarray:
455
+ """Get ranges for all coordinates
456
+
457
+ :raises Exception: Raised if vertices set empty
458
+ :returns: Ranges (max-min) for all coordinates
459
+ """
460
+ if self._vertices.shape[0] == 0:
461
+ raise Exception("Cannot compute range for empty vertices set")
462
+ return self.max() - self.min()
463
+
464
+ def mean(self) -> np.ndarray:
465
+ """Get mean for all coordinates
466
+ :raises Exception: Raised if vertices set empty
467
+ :returns: Mean for all coordinates
468
+ """
469
+ if self._vertices.shape[0] == 0:
470
+ raise Exception("Cannot compute mean for empty vertices set")
471
+ return np.mean(self._vertices, axis=0)
472
+
473
+ def center(self) -> np.ndarray:
474
+ """Get center of gravity
475
+
476
+ Same as self.mean.
477
+
478
+ :raises Exception: Raised if vertices set empty
479
+ :returns: Center of gravity
480
+ """
481
+ return self.mean()
482
+
483
+ def demean(self) -> None:
484
+ """Translate vertices set that center of gravity is at (0, 0, 0)."""
485
+ if self._vertices.shape[0] == 0:
486
+ return
487
+ self._vertices -= np.mean(self._vertices, axis=0)
488
+
489
+ def scale(self, factor: float | ArrayLike, center: ArrayLike | None = None) -> None:
490
+ """Scale vertices
491
+
492
+ :param factor: Scaling factor, either scalar or shape (3,)
493
+ :param center: Center, shape (3,)
494
+ """
495
+ if center is None:
496
+ self._vertices = np.asarray(factor) * self._vertices
497
+ else:
498
+ center = np.asarray(center)
499
+ if not (center.ndim == 1 and center.size == 3):
500
+ raise Exception("Provide center of shape (3, )")
501
+ self._vertices = (np.asarray(factor) * (self._vertices - center)) + center
502
+
503
+ def translate(self, offset: ArrayLike) -> None:
504
+ """Translate vertices
505
+
506
+ :param offset: Translation offset, shape (3,)
507
+ """
508
+ offset = np.asarray(offset)
509
+ if not (offset.ndim == 1 and offset.size == 3):
510
+ raise Exception("Provide offset of shape (3, )")
511
+ self._vertices += offset
512
+
513
+ def transform(self, trafo: Trafo3d) -> None:
514
+ """Transform vertices
515
+
516
+ :param trafo: 6d transformation
517
+ """
518
+ self._vertices = trafo * self._vertices
519
+ if self.has_normals:
520
+ self._normals = self._normals @ trafo.rotation_matrix.T
521
+
522
+ def kdtree(self) -> KDTree:
523
+ """Get KDTree of vertices
524
+
525
+ Use for fast next-neighbor queries.
526
+
527
+ :returns: KDTree of type scipy.spatial.KDTree
528
+ """
529
+ return KDTree(self._vertices)
530
+
531
+ def filter_region_of_interest(
532
+ self,
533
+ bounds: ArrayLike,
534
+ trafo: Trafo3d | None = None,
535
+ ) -> np.ndarray:
536
+ """Filter vertices set
537
+
538
+ :param bounds: Min and max values for all coordinates, shape (2, 3)
539
+ :param trafo: 6d Pose of the bounding box
540
+ :returns: Inlier mask
541
+ """
542
+ bounds = np.array(bounds)
543
+ if bounds.shape != (2, 3):
544
+ raise Exception(
545
+ "Provide bounds as ((xmin, ymin, zmin), "
546
+ "(xmax, ymax, zmax)), shape (2, 3)"
547
+ )
548
+ P = self._vertices
549
+ if trafo is not None:
550
+ P = trafo.inverse * P
551
+ return np.all(P >= bounds[0], axis=1) & np.all(P <= bounds[1], axis=1)
552
+
553
+ def paint_uniform_color(self, color: ArrayLike) -> None:
554
+ """Colorize vertices set with uniform color
555
+
556
+ Sets colors to a single user defined color.
557
+
558
+ :param color: Color, RGB values in range [0..1], shape (3,), type float
559
+ """
560
+ color = np.asarray(color)
561
+ if not (color.ndim == 1 and color.size == 3):
562
+ raise Exception("Provide color of shape (3, )")
563
+ if not np.all(np.logical_and(color >= 0.0, color <= 1.0)):
564
+ logging.warning("Color are out of bounds. Clipping to [0,1] ...")
565
+ color = np.clip(color, 0.0, 1.0)
566
+ self._colors = np.tile(color, (self._vertices.shape[0], 1))
567
+
568
+ def colorize_vertices_by_scalar(
569
+ self,
570
+ scalars: ArrayLike,
571
+ cmap: str = "viridis",
572
+ min_max: tuple[float, float] | None = None,
573
+ nan_color: tuple[float, float, float] = (1, 0, 0),
574
+ ) -> None:
575
+ """Colorizes vertices by scalar
576
+
577
+ :param scalars: Scalar values, length has to match number of vertices
578
+ :param cmap: Matplotlib color map string
579
+ :param min_max: Minimum and maximum
580
+ :param nan_color: Color for NaN values, RGB values [0..1]
581
+ """
582
+ scalars = np.asarray(scalars)
583
+ if scalars.ndim != 1 or scalars.size != self._vertices.shape[0]:
584
+ raise Exception("Provide corrent number of values to colorize vertices")
585
+ self._colors = scalar_to_color(scalars, cmap, min_max, nan_color)
586
+
587
+ def colorize_vertices_by_scalar_bicolor(
588
+ self,
589
+ scalars: ArrayLike,
590
+ cmaps: tuple[str, str] = ("Blues", "Reds"),
591
+ vmax: float | None = None,
592
+ nan_color: tuple[float, float, float] = (0, 1, 0),
593
+ ) -> None:
594
+ """Colorizes mesh vertices by scalar, pos/neg in separate colors
595
+
596
+ :param scalars: Scalar values, length has to match number of vertices
597
+ :param cmaps: Matplotlib color map strings for pos/neg
598
+ :param vmax: Maximum (pos/neg) value
599
+ :param nan_color: Color for NaN values, RGB values [0..1]
600
+ """
601
+ scalars = np.asarray(scalars)
602
+ if scalars.ndim != 1 or scalars.size != self._vertices.shape[0]:
603
+ raise Exception("Provide corrent number of values to colorize vertices")
604
+ self._colors = scalar_to_bicolor(scalars, cmaps, vmax, nan_color)