geone 1.3.0__py313-none-manylinux_2_35_x86_64.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.
geone/imgplot3d.py ADDED
@@ -0,0 +1,1918 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # -------------------------------------------------------------------------
5
+ # Python module: 'imgplot3d.py'
6
+ # author: Julien Straubhaar
7
+ # date: feb-2020
8
+ # -------------------------------------------------------------------------
9
+
10
+ """
11
+ Module for custom plots of images (class :class:`geone.img.Img`) in 3D (based on `pyvista`).
12
+ """
13
+
14
+ import numpy as np
15
+ import matplotlib.pyplot as plt
16
+
17
+ import matplotlib.colors as mcolors
18
+
19
+ import pyvista as pv
20
+
21
+ from geone import customcolors as ccol
22
+
23
+ # ============================================================================
24
+ class Imgplot3dError(Exception):
25
+ """
26
+ Custom exception related to `imgplot3d` module.
27
+ """
28
+ pass
29
+ # ============================================================================
30
+
31
+ # ----------------------------------------------------------------------------
32
+ def drawImage3D_surface (
33
+ im,
34
+ plotter=None,
35
+ ix0=0, ix1=None,
36
+ iy0=0, iy1=None,
37
+ iz0=0, iz1=None,
38
+ iv=0,
39
+ cmap=ccol.cmap_def,
40
+ cmin=None, cmax=None,
41
+ alpha=None,
42
+ excludedVal=None,
43
+ categ=False,
44
+ ncateg_max=30,
45
+ categVal=None,
46
+ categCol=None,
47
+ categColCycle=False,
48
+ categActive=None,
49
+ use_clip_plane=False,
50
+ show_scalar_bar=True,
51
+ show_outline=True,
52
+ show_bounds=False,
53
+ show_axes=True,
54
+ text=None,
55
+ scalar_bar_annotations=None,
56
+ scalar_bar_annotations_max=20,
57
+ scalar_bar_kwargs=None,
58
+ outline_kwargs=None,
59
+ bounds_kwargs=None,
60
+ axes_kwargs=None,
61
+ text_kwargs=None,
62
+ background_color=None,
63
+ foreground_color=None,
64
+ cpos=None,
65
+ verbose=1,
66
+ logger=None,
67
+ **kwargs):
68
+ """
69
+ Displays a 3D image as surface(s) (based on `pyvista`).
70
+
71
+ Parameters
72
+ ----------
73
+ im : :class:`geone.img.Img`
74
+ image (3D)
75
+
76
+ plotter : :class:`pyvista.Plotter`, optional
77
+ - if given (not `None`), add element to the plotter, a further call to \
78
+ `plotter.show()` will be required to show the plot
79
+ - if not given (`None`, default): a plotter is created and the plot \
80
+ is shown
81
+
82
+ ix0 : int, default: 0
83
+ index of first slice along x direction, considered for plotting
84
+
85
+ ix1 : int, optional
86
+ 1+index of last slice along x direction (`ix0 < ix1`), considered for
87
+ plotting; by default: number of cells in x direction (`ix1=im.nx`) is
88
+ used
89
+
90
+ iy0 : int, default: 0
91
+ index of first slice along y direction, considered for plotting
92
+
93
+ iy1 : int, optional
94
+ 1+index of last slice along y direction (`iy0 < iy1`), considered for
95
+ plotting; by default: number of cells in x direction (`iy1=im.ny`) is
96
+ used
97
+
98
+ iz0 : int, default: 0
99
+ index of first slice along z direction, considered for plotting
100
+
101
+ iz1 : int, optional
102
+ 1+index of last slice along z direction (`iz0 < iz1`), considered for
103
+ plotting; by default: number of cells in z direction (`iz1=im.nz`) is
104
+ used
105
+
106
+ iv : int, default: 0
107
+ index of the variable to be displayed
108
+
109
+ cmap : colormap, default: `geone.customcolors.cmap_def`
110
+ color map (can be a string, in this case the color map is obtained by
111
+ `matplotlib.pyplot.get_cmap(cmap)`)
112
+
113
+ cmin : float, optional
114
+ used only if `categ=False`:
115
+ minimal value to be displayed; by default: minimal value of the displayed
116
+ variable is used for `cmin`
117
+
118
+ cmax : float, optional
119
+ used only if `categ=False`:
120
+ maximal value to be displayed; by default: maximal value of the displayed
121
+ variable is used for `cmax`
122
+
123
+ alpha : float, optional
124
+ value of the "alpha" channel (for transparency);
125
+ by default (`None`): `alpha=1.0` is used (no transparency)
126
+
127
+ excludedVal : sequence of values, or single value, optional
128
+ values to be excluded from the plot;
129
+ note not used if `categ=True` and `categVal` is not `None`
130
+
131
+ categ : bool, default: False
132
+ indicates if the variable of the image to diplay has to be treated as a
133
+ categorical (discrete) variable (True), or continuous variable (False)
134
+
135
+ ncateg_max : int, default: 30
136
+ used only if `categ=True`:
137
+ maximal number of categories, if there are more category values and
138
+ `categVal=None`, nothing is plotted (`categ` should set to False)
139
+
140
+ categVal : sequence of values, or single value, optional
141
+ used only if `categ=True`:
142
+ explicit list of the category values to be displayed;
143
+ by default (`None`): the list of all unique values are automatically
144
+ retrieved
145
+
146
+ categCol: sequence of colors, optional
147
+ used only if `categ=True`:
148
+ sequence of colors, (given as 3-tuple (RGB code), 4-tuple (RGBA code) or
149
+ str), used for the category values that will be displayed:
150
+
151
+ - if `categVal` is not `None`: `categCol` must have the same length as \
152
+ `categVal`
153
+ - if `categVal=None`:
154
+ - first colors of `categCol` are used if its length is greater than \
155
+ or equal to the number of displayed category values,
156
+ - otherwise: the colors of `categCol` are used cyclically if \
157
+ `categColCycle=True`, and the colors taken from the color map `cmap` \
158
+ are used if `categColCycle=False`
159
+
160
+ categColCycle : bool, default: False
161
+ used only if `categ=True`:
162
+ indicates if the colors of `categCol` can be used cyclically or not
163
+ (when the number of displayed category values exceeds the length of
164
+ `categCol`)
165
+
166
+ categActive : 1D array-like of bools, optional
167
+ used only if `categ=True`, sequence of same length as `categVal`:
168
+
169
+ - `categActive[i]=True`: `categVal[i]` is displayed
170
+ - `categActive[i]=False`: `categVal[i]` is not displayed
171
+
172
+ by default (`None`): all category values `categVal` are displayed
173
+
174
+ use_clip_plane : bool, default: False
175
+ if `True`: the function `pyvista.add_mesh_clip_plane` (allowing
176
+ interactive clipping) is used instead of `pyvista.add_mesh`
177
+
178
+ show_scalar_bar : bool, default: True
179
+ indicates if scalar bar (color bar) is displayed
180
+
181
+ show_outline : bool, default: True
182
+ indicates if outline (around the image) is displayed
183
+
184
+ show_bounds : bool, default: False
185
+ indicates if bounds are displayed (box with graduation)
186
+
187
+ show_axes : bool, default: True
188
+ indicates if axes are displayed
189
+
190
+ text : str, optional
191
+ text (title) to be displayed on the figure
192
+
193
+ scalar_bar_annotations : dict, optional
194
+ annotation (ticks) on the scalar bar (color bar), used if
195
+ `show_scalar_bar=True`
196
+
197
+ scalar_bar_annotations_max : int, default: 20
198
+ maximal number of annotations (ticks) on the scalar bar (color bar)
199
+ when `categ=True` and `scalar_bar_annotations=None`
200
+
201
+ scalar_bar_kwargs : dict, optional
202
+ keyword arguments passed to function `plotter.add_scalar_bar`
203
+ (can be useful for customization, used if `show_scalar_bar=True`)
204
+ note: in subplots (multi-sub-window), key 'title' should be distinct for
205
+ each subplot
206
+
207
+ outline_kwargs : dict, optional
208
+ keyword arguments passed to function `plotter.add_mesh`
209
+ (can be useful for customization, used if `show_outline=True`)
210
+
211
+ bounds_kwargs : dict, optional
212
+ keyword arguments passed to function `plotter.show_bounds`
213
+ (can be useful for customization, used if `show_bounds=True`)
214
+
215
+ axes_kwargs : dict, optional
216
+ keyword arguments passed to function `plotter.add_axes`
217
+ (can be useful for customization, used if `show_axes=True`)
218
+
219
+ text_kwargs : dict, optional
220
+ keyword arguments passed to function `plotter.add_text`
221
+ (can be useful for customization, used if `text` is not `None`)
222
+
223
+ background_color : color, optional
224
+ background color (3-tuple (RGB code), 4-tuple (RGBA code) or str)
225
+
226
+ foreground_color : color, optional
227
+ foreground color (3-tuple (RGB code), 4-tuple (RGBA code) or str)
228
+
229
+ cpos : sequence[sequence[float]], optional
230
+ camera position (unsused if `plotter=None`);
231
+ `cpos` = [camera_location, focus_point, viewup_vector], with
232
+
233
+ - camera_location: (tuple of length 3) camera location ("eye")
234
+ - focus_point : (tuple of length 3) focus point
235
+ - viewup_vector : (tuple of length 3) viewup vector (vector \
236
+ attached to the "head" and pointed to the "sky")
237
+
238
+ note: in principle, (focus_point - camera_location) is orthogonal to
239
+ viewup_vector
240
+
241
+ verbose : int, default: 1
242
+ verbose mode, higher implies more printing (info)
243
+
244
+ logger : :class:`logging.Logger`, optional
245
+ logger (see package `logging`)
246
+ if specified, messages are written via `logger` (no print)
247
+
248
+ kwargs : dict
249
+ additional keyword arguments passed to `plotter.add_mesh[_clip_plane]`
250
+ when plotting the variable, such as
251
+
252
+ - opacity (float, or str) : \
253
+ opacity for colors; \
254
+ default: 'linear', (set 'linear_r' to invert opacity)
255
+ - show_edges (bool) : \
256
+ indicates if edges of the grid are displayed
257
+ - edge_color (color) : \
258
+ color (3-tuple (RGB code), 4-tuple (RGBA code) or str) for edges \
259
+ (used if `show_edges=True`)
260
+ - line_width (float) \
261
+ line width for edges (used if `show_edges=True`)
262
+ - etc.
263
+
264
+ Notes
265
+ -----
266
+ - 'scalar bar', and 'axes' may be not displayed in multiple-plot, bug ?
267
+ """
268
+ fname = 'drawImage3D_surface'
269
+
270
+ # Check iv
271
+ if iv < 0:
272
+ iv = im.nv + iv
273
+
274
+ if iv < 0 or iv >= im.nv:
275
+ err_msg = f'{fname}: invalid `iv` index'
276
+ if logger: logger.error(err_msg)
277
+ raise Imgplot3dError(err_msg)
278
+
279
+ # Set indices to be plotted
280
+ if ix1 is None:
281
+ ix1 = im.nx
282
+
283
+ if iy1 is None:
284
+ iy1 = im.ny
285
+
286
+ if iz1 is None:
287
+ iz1 = im.nz
288
+
289
+ if ix0 >= ix1 or ix0 < 0 or ix1 > im.nx:
290
+ err_msg = f'{fname}: invalid indices along x axis'
291
+ if logger: logger.error(err_msg)
292
+ raise Imgplot3dError(err_msg)
293
+
294
+ if iy0 >= iy1 or iy0 < 0 or iy1 > im.ny:
295
+ err_msg = f'{fname}: invalid indices along y axis'
296
+ if logger: logger.error(err_msg)
297
+ raise Imgplot3dError(err_msg)
298
+
299
+ if iz0 >= iz1 or iz0 < 0 or iz1 > im.nz:
300
+ err_msg = f'{fname}: invalid indices along z axis'
301
+ if logger: logger.error(err_msg)
302
+ raise Imgplot3dError(err_msg)
303
+
304
+ # Get the color map
305
+ if isinstance(cmap, str):
306
+ try:
307
+ cmap = plt.get_cmap(cmap)
308
+ except:
309
+ err_msg = f'{fname}: invalid `cmap` string'
310
+ if logger: logger.error(err_msg)
311
+ raise Imgplot3dError(err_msg)
312
+
313
+ # Initialization of dictionary (do not use {} as default argument, it is not re-initialized...)
314
+ if scalar_bar_annotations is None:
315
+ scalar_bar_annotations = {}
316
+
317
+ if scalar_bar_kwargs is None:
318
+ scalar_bar_kwargs = {}
319
+
320
+ if outline_kwargs is None:
321
+ outline_kwargs = {}
322
+
323
+ if bounds_kwargs is None:
324
+ bounds_kwargs = {}
325
+
326
+ if axes_kwargs is None:
327
+ axes_kwargs = {}
328
+
329
+ if text_kwargs is None:
330
+ text_kwargs = {}
331
+
332
+ # Extract what to be plotted
333
+ # zz = np.array(im.val[iv][iz0:iz1, iy0:iy1, ix0:ix1]) # np.array() to get a copy
334
+ zz = im.val[iv][iz0:iz1, iy0:iy1, ix0:ix1].flatten() # .flatten() provides a copy
335
+
336
+ if categ:
337
+ # --- Treat categorical variable ---
338
+ if categCol is not None \
339
+ and type(categCol) is not list \
340
+ and type(categCol) is not tuple:
341
+ err_msg = f'{fname}: `categCol` must be a list or a tuple (if not `None`)'
342
+ if logger: logger.error(err_msg)
343
+ raise Imgplot3dError(err_msg)
344
+
345
+ # Get array 'dval' of displayed values (at least for color bar)
346
+ if categVal is not None:
347
+ dval = np.array(categVal).reshape(-1) # force to be an 1d array
348
+
349
+ if len(np.unique(dval)) != len(dval):
350
+ err_msg = f'{fname}: `categVal` contains duplicated entries'
351
+ if logger: logger.error(err_msg)
352
+ raise Imgplot3dError(err_msg)
353
+
354
+ # Check 'categCol' (if not None)
355
+ if categCol is not None and len(categCol) != len(dval):
356
+ err_msg = f'{fname}: length of `categVal` and length of `categCol` differ'
357
+ if logger: logger.error(err_msg)
358
+ raise Imgplot3dError(err_msg)
359
+
360
+ else:
361
+ # Possibly exclude values from zz
362
+ if excludedVal is not None:
363
+ for val in np.array(excludedVal).reshape(-1):
364
+ np.putmask(zz, zz == val, np.nan)
365
+
366
+ # Get the unique values in zz
367
+ dval = np.array([v for v in np.unique(zz).reshape(-1) if ~np.isnan(v)])
368
+ if len(dval) > ncateg_max:
369
+ err_msg = f'{fname}: too many categories, set `categ=False`'
370
+ if logger: logger.error(err_msg)
371
+ raise Imgplot3dError(err_msg)
372
+
373
+ if not len(dval): # len(dval) == 0
374
+ err_msg = f'{fname}: no value to be drawn'
375
+ if logger: logger.error(err_msg)
376
+ raise Imgplot3dError(err_msg)
377
+
378
+ if categActive is not None:
379
+ if len(categActive) != len(dval):
380
+ err_msg = f'{fname}: length of `categActive` invalid (should be the same as length of `categVal`)'
381
+ if logger: logger.error(err_msg)
382
+ raise Imgplot3dError(err_msg)
383
+
384
+ else:
385
+ categActive = np.ones(len(dval), dtype='bool')
386
+
387
+ # Replace dval[i] by i in zz if categActive[i] is True otherwise by np.nan, and other values by np.nan
388
+ zz2 = np.array(zz) # copy array
389
+ zz[...] = np.nan # initialize
390
+ for i, v in enumerate(dval):
391
+ if categActive[i]:
392
+ zz[zz2 == v] = i
393
+
394
+ del zz2
395
+
396
+ # Set 'colorList': the list of colors to use
397
+ colorList = None
398
+ if categCol is not None:
399
+ if len(categCol) >= len(dval):
400
+ colorList = [categCol[i] for i in range(len(dval))]
401
+ # colorList = [mcolors.ColorConverter().to_rgba(categCol[i]) for i in range(len(dval))]
402
+
403
+ elif categColCycle:
404
+ if verbose > 0:
405
+ if logger:
406
+ logger.warning(f'{fname}: `categCol` is used cyclically (too few entries)')
407
+ else:
408
+ print(f'{fname}: WARNING: `categCol` is used cyclically (too few entries)')
409
+ colorList = [categCol[i%len(categCol)] for i in range(len(dval))]
410
+
411
+ else:
412
+ if verbose > 0:
413
+ if logger:
414
+ logger.warning(f'{fname}: `categCol` not used (too few entries)')
415
+ else:
416
+ print(f'{fname}: WARNING: `categCol` not used (too few entries)')
417
+
418
+ if colorList is None:
419
+ # Use colors from cmap
420
+ colorList = [cmap(x) for x in np.arange(len(dval)) * 1.0/(len(dval)-1)]
421
+
422
+ # Set the colormap: 'cmap'
423
+ # - Trick: duplicate last color (if len(colorList)> 1)!
424
+ if len(colorList) == 1:
425
+ colorList.append(colorList[-1])
426
+ cmap = ccol.custom_cmap(colorList, ncol=len(colorList), alpha=alpha)
427
+
428
+ # Set the min and max of the colorbar
429
+ cmin, cmax = 0, len(dval) # works, but scalar bar annotations may be shifted of +0.5, see below
430
+ # cmin, cmax = -0.5, len(dval) - 0.5 # does not work
431
+
432
+ # Set scalar bar annotations if not given
433
+ if scalar_bar_annotations == {}:
434
+ if len(dval) <= scalar_bar_annotations_max: # avoid too many annotations (very slow and useless)
435
+ for i, v in enumerate(dval):
436
+ scalar_bar_annotations[i+0.5] = f'{v:.3g}'
437
+
438
+ scalar_bar_kwargs['n_labels'] = 0
439
+ scalar_bar_kwargs['n_colors'] = len(dval)
440
+
441
+ else: # categ == False
442
+ # --- Treat continuous variable ---
443
+ # Possibly exclude values from zz
444
+ if excludedVal is not None:
445
+ for val in np.array(excludedVal).reshape(-1): # force to be an 1d array
446
+ np.putmask(zz, zz == val, np.nan)
447
+
448
+ # Set cmin and cmax if not specified
449
+ if cmin is None:
450
+ cmin = np.nanmin(zz)
451
+
452
+ if cmax is None:
453
+ cmax = np.nanmax(zz)
454
+
455
+ # Set pyvista ImageData (previously: UniformGrid)
456
+ xmin = im.ox + ix0 * im.sx
457
+ xmax = im.ox + ix1 * im.sx
458
+ xdim = ix1 - ix0 + 1
459
+
460
+ ymin = im.oy + iy0 * im.sy
461
+ ymay = im.oy + iy1 * im.sy
462
+ ydim = iy1 - iy0 + 1
463
+
464
+ zmin = im.oz + iz0 * im.sz
465
+ zmaz = im.oz + iz1 * im.sz
466
+ zdim = iz1 - iz0 + 1
467
+
468
+ # pg = pv.UniformGrid(dims=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
469
+ # pg = pv.UniformGrid(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
470
+ pg = pv.ImageData(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
471
+
472
+ pg.cell_data[im.varname[iv]] = zz #.flatten()
473
+
474
+ if plotter is not None:
475
+ pp = plotter
476
+ else:
477
+ pp = pv.Plotter()
478
+
479
+ if use_clip_plane:
480
+ add_mesh_func = pp.add_mesh_clip_plane
481
+ else:
482
+ add_mesh_func = pp.add_mesh
483
+
484
+ add_mesh_func(pg.threshold(value=(cmin, cmax)), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
485
+
486
+ if background_color is not None:
487
+ pp.background_color = background_color
488
+
489
+ if foreground_color is not None:
490
+ for d in [scalar_bar_kwargs, outline_kwargs, bounds_kwargs, axes_kwargs, text_kwargs]:
491
+ if 'color' not in d.keys():
492
+ d['color'] = foreground_color
493
+
494
+ if show_scalar_bar:
495
+ # # - old -
496
+ # # pg.cell_arrays[im.varname[iv]][...] = np.nan # trick: set all value to nan and use nan_opacity = 0 for empty plot but 'saving' the scalar bar...
497
+ # # # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=True, scalar_bar_args=scalar_bar_kwargs)
498
+ # # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=False)
499
+ # # - old -
500
+ # # Trick: set opacity=0 and nan_opacity=0 for empty plot but 'saving' the scalar bar...
501
+ # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), opacity=0., nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=False)
502
+ if 'title' not in scalar_bar_kwargs.keys():
503
+ scalar_bar_kwargs['title'] = im.varname[iv]
504
+ pp.add_scalar_bar(**scalar_bar_kwargs)
505
+
506
+ if show_outline:
507
+ pp.add_mesh(pg.outline(), **outline_kwargs)
508
+
509
+ if show_bounds:
510
+ pp.show_bounds(**bounds_kwargs)
511
+
512
+ if show_axes:
513
+ pp.add_axes(**axes_kwargs)
514
+
515
+ if text is not None:
516
+ pp.add_text(text, **text_kwargs)
517
+
518
+ if plotter is None:
519
+ pp.show(cpos=cpos)
520
+ # ----------------------------------------------------------------------------
521
+
522
+ # ----------------------------------------------------------------------------
523
+ def drawImage3D_slice (
524
+ im,
525
+ plotter=None,
526
+ ix0=0, ix1=None,
527
+ iy0=0, iy1=None,
528
+ iz0=0, iz1=None,
529
+ iv=0,
530
+ slice_normal_x=None,
531
+ slice_normal_y=None,
532
+ slice_normal_z=None,
533
+ slice_normal_custom=None,
534
+ cmap=ccol.cmap_def,
535
+ cmin=None, cmax=None,
536
+ alpha=None,
537
+ excludedVal=None,
538
+ categ=False,
539
+ ncateg_max=30,
540
+ categVal=None,
541
+ categCol=None,
542
+ categColCycle=False,
543
+ categActive=None,
544
+ show_scalar_bar=True,
545
+ show_outline=True,
546
+ show_bounds=False,
547
+ show_axes=True,
548
+ text=None,
549
+ scalar_bar_annotations=None,
550
+ scalar_bar_annotations_max=20,
551
+ scalar_bar_kwargs=None,
552
+ outline_kwargs=None,
553
+ bounds_kwargs=None,
554
+ axes_kwargs=None,
555
+ text_kwargs=None,
556
+ background_color=None,
557
+ foreground_color=None,
558
+ cpos=None,
559
+ verbose=1,
560
+ logger=None,
561
+ **kwargs):
562
+ """
563
+ Displays a 3D image as slices(s) (based on `pyvista`).
564
+
565
+ Parameters
566
+ ----------
567
+ im : :class:`geone.img.Img`
568
+ image (3D)
569
+
570
+ plotter : :class:`pyvista.Plotter`, optional
571
+ - if given (not `None`), add element to the plotter, a further call to \
572
+ `plotter.show()` will be required to show the plot
573
+ - if not given (`None`, default): a plotter is created and the plot \
574
+ is shown
575
+
576
+ ix0 : int, default: 0
577
+ index of first slice along x direction, considered for plotting
578
+
579
+ ix1 : int, optional
580
+ 1+index of last slice along x direction (`ix0 < ix1`), considered for
581
+ plotting; by default: number of cells in x direction (`ix1=im.nx`) is
582
+ used
583
+
584
+ iy0 : int, default: 0
585
+ index of first slice along y direction, considered for plotting
586
+
587
+ iy1 : int, optional
588
+ 1+index of last slice along y direction (`iy0 < iy1`), considered for
589
+ plotting; by default: number of cells in x direction (`iy1=im.ny`) is
590
+ used
591
+
592
+ iz0 : int, default: 0
593
+ index of first slice along z direction, considered for plotting
594
+
595
+ iz1 : int, optional
596
+ 1+index of last slice along z direction (`iz0 < iz1`), considered for
597
+ plotting; by default: number of cells in z direction (`iz1=im.nz`) is
598
+ used
599
+
600
+ iv : int, default: 0
601
+ index of the variable to be displayed
602
+
603
+ slice_normal_x : sequence of values, or single value, optional
604
+ values of the (float) x coordinate where a slice normal to x axis is
605
+ displayed
606
+
607
+ slice_normal_y : sequence of values, or single value, optional
608
+ values of the (float) y coordinate where a slice normal to y axis is
609
+ displayed
610
+
611
+ slice_normal_z : sequence of values, or single value, optional
612
+ values of the (float) z coordinate where a slice normal to z axis is
613
+ displayed
614
+
615
+ slice_normal_custom : (sequence of) sequence(s) of two 3-tuple, optional
616
+ definition of custom normal slice(s) to be displayed, a slice is
617
+ defined by a sequence two 3-tuple, ((vx, vy, vz), (px, py, pz)): slice
618
+ normal to the vector (vx, vy, vz) and going through the point (px, py, pz)
619
+
620
+ cmap : colormap, default: `geone.customcolors.cmap_def`
621
+ color map (can be a string, in this case the color map
622
+ `matplotlib.pyplot.get_cmap(cmap)`)
623
+
624
+ cmin : float, optional
625
+ used only if `categ=False`:
626
+ minimal value to be displayed; by default: minimal value of the displayed
627
+ variable is used for `cmin`
628
+
629
+ cmax : float, optional
630
+ used only if `categ=False`:
631
+ maximal value to be displayed; by default: maximal value of the displayed
632
+ variable is used for `cmax`
633
+
634
+ alpha : float, optional
635
+ value of the "alpha" channel (for transparency);
636
+ by default (`None`): `alpha=1.0` is used (no transparency)
637
+
638
+ excludedVal : sequence of values, or single value, optional
639
+ values to be excluded from the plot;
640
+ note not used if `categ=True` and `categVal` is not `None`
641
+
642
+ categ : bool, default: False
643
+ indicates if the variable of the image to diplay has to be treated as a
644
+ categorical (discrete) variable (True), or continuous variable (False)
645
+
646
+ ncateg_max : int, default: 30
647
+ used only if `categ=True`:
648
+ maximal number of categories, if there are more category values and
649
+ `categVal=None`, nothing is plotted (`categ` should set to False)
650
+
651
+ categVal : sequence of values, or single value, optional
652
+ used only if `categ=True`:
653
+ explicit list of the category values to be displayed;
654
+ by default (`None`): the list of all unique values are automatically
655
+ retrieved
656
+
657
+ categCol: sequence of colors, optional
658
+ used only if `categ=True`:
659
+ sequence of colors, (given as 3-tuple (RGB code), 4-tuple (RGBA code) or
660
+ str), used for the category values that will be displayed:
661
+
662
+ - if `categVal` is not `None`: `categCol` must have the same length as \
663
+ `categVal`
664
+ - if `categVal=None`:
665
+ - first colors of `categCol` are used if its length is greater than \
666
+ or equal to the number of displayed category values,
667
+ - otherwise: the colors of `categCol` are used cyclically if \
668
+ `categColCycle=True`, and the colors taken from the color map `cmap` \
669
+ are used if `categColCycle=False`
670
+
671
+ categColCycle : bool, default: False
672
+ used only if `categ=True`:
673
+ indicates if the colors of `categCol` can be used cyclically or not
674
+ (when the number of displayed category values exceeds the length of
675
+ `categCol`)
676
+
677
+ categActive : 1D array-like of bools, optional
678
+ used only if `categ=True`, sequence of same length as `categVal`:
679
+
680
+ - `categActive[i]=True`: `categVal[i]` is displayed
681
+ - `categActive[i]=False`: `categVal[i]` is not displayed
682
+
683
+ by default (`None`): all category values `categVal` are displayed
684
+
685
+ show_scalar_bar : bool, default: True
686
+ indicates if scalar bar (color bar) is displayed
687
+
688
+ show_outline : bool, default: True
689
+ indicates if outline (around the image) is displayed
690
+
691
+ show_bounds : bool, default: False
692
+ indicates if bounds are displayed (box with graduation)
693
+
694
+ show_axes : bool, default: True
695
+ indicates if axes are displayed
696
+
697
+ text : str, optional
698
+ text (title) to be displayed on the figure
699
+
700
+ scalar_bar_annotations : dict, optional
701
+ annotation (ticks) on the scalar bar (color bar), used if
702
+ `show_scalar_bar=True`
703
+ scalar_bar_annotations_max : int, default: 20
704
+ maximal number of annotations (ticks) on the scalar bar (color bar)
705
+ when `categ=True` and `scalar_bar_annotations=None`
706
+
707
+ scalar_bar_kwargs : dict, optional
708
+ keyword arguments passed to function `plotter.add_scalar_bar`
709
+ (can be useful for customization, used if `show_scalar_bar=True`)
710
+ note: in subplots (multi-sub-window), key 'title' should be distinct for
711
+ each subplot
712
+
713
+ outline_kwargs : dict, optional
714
+ keyword arguments passed to function `plotter.add_mesh`
715
+ (can be useful for customization, used if `show_outline=True`)
716
+
717
+ bounds_kwargs : dict, optional
718
+ keyword arguments passed to function `plotter.show_bounds`
719
+ (can be useful for customization, used if `show_bounds=True`)
720
+
721
+ axes_kwargs : dict, optional
722
+ keyword arguments passed to function `plotter.add_axes`
723
+ (can be useful for customization, used if `show_axes=True`)
724
+
725
+ text_kwargs : dict, optional
726
+ keyword arguments passed to function `plotter.add_text`
727
+ (can be useful for customization, used if `text` is not `None`)
728
+
729
+ background_color : color, optional
730
+ background color (3-tuple (RGB code), 4-tuple (RGBA code) or str)
731
+
732
+ foreground_color : color, optional
733
+ foreground color (3-tuple (RGB code), 4-tuple (RGBA code) or str)
734
+
735
+ cpos : sequence[sequence[float]], optional
736
+ camera position (unsused if `plotter=None`);
737
+ `cpos` = [camera_location, focus_point, viewup_vector], with
738
+
739
+ - camera_location: (tuple of length 3) camera location ("eye")
740
+ - focus_point : (tuple of length 3) focus point
741
+ - viewup_vector : (tuple of length 3) viewup vector (vector \
742
+ attached to the "head" and pointed to the "sky")
743
+
744
+ note: in principle, (focus_point - camera_location) is orthogonal to
745
+ viewup_vector
746
+
747
+ verbose : int, default: 1
748
+ verbose mode, higher implies more printing (info)
749
+
750
+ logger : :class:`logging.Logger`, optional
751
+ logger (see package `logging`)
752
+ if specified, messages are written via `logger` (no print)
753
+
754
+ kwargs : dict
755
+ additional keyword arguments passed to `plotter.add_mesh`
756
+ when plotting the variable, such as
757
+
758
+ - opacity (float, or str) : \
759
+ opacity for colors; \
760
+ default: 'linear', (set 'linear_r' to invert opacity)
761
+ - show_edges (bool) : \
762
+ indicates if edges of the grid are displayed
763
+ - edge_color (color) : \
764
+ color (3-tuple (RGB code), 4-tuple (RGBA code) or str) for edges \
765
+ (used if `show_edges=True`)
766
+ - line_width (float) \
767
+ line width for edges (used if `show_edges=True`)
768
+ - etc.
769
+
770
+ Notes
771
+ -----
772
+ - 'scalar bar', and 'axes' may be not displayed in multiple-plot, bug ?
773
+ """
774
+ fname = 'drawImage3D_slice'
775
+
776
+ # Check iv
777
+ if iv < 0:
778
+ iv = im.nv + iv
779
+
780
+ if iv < 0 or iv >= im.nv:
781
+ err_msg = f'{fname}: invalid `iv` index'
782
+ if logger: logger.error(err_msg)
783
+ raise Imgplot3dError(err_msg)
784
+
785
+ # Set indices to be plotted
786
+ if ix1 is None:
787
+ ix1 = im.nx
788
+
789
+ if iy1 is None:
790
+ iy1 = im.ny
791
+
792
+ if iz1 is None:
793
+ iz1 = im.nz
794
+
795
+ if ix0 >= ix1 or ix0 < 0 or ix1 > im.nx:
796
+ err_msg = f'{fname}: invalid indices along x axis'
797
+ if logger: logger.error(err_msg)
798
+ raise Imgplot3dError(err_msg)
799
+
800
+ if iy0 >= iy1 or iy0 < 0 or iy1 > im.ny:
801
+ err_msg = f'{fname}: invalid indices along y axis'
802
+ if logger: logger.error(err_msg)
803
+ raise Imgplot3dError(err_msg)
804
+
805
+ if iz0 >= iz1 or iz0 < 0 or iz1 > im.nz:
806
+ err_msg = f'{fname}: invalid indices along z axis'
807
+ if logger: logger.error(err_msg)
808
+ raise Imgplot3dError(err_msg)
809
+
810
+ # Get the color map
811
+ if isinstance(cmap, str):
812
+ try:
813
+ cmap = plt.get_cmap(cmap)
814
+ except:
815
+ err_msg = f'{fname}: invalid `cmap` string'
816
+ if logger: logger.error(err_msg)
817
+ raise Imgplot3dError(err_msg)
818
+
819
+ # Initialization of dictionary (do not use {} as default argument, it is not re-initialized...)
820
+ if scalar_bar_annotations is None:
821
+ scalar_bar_annotations = {}
822
+
823
+ if scalar_bar_kwargs is None:
824
+ scalar_bar_kwargs = {}
825
+
826
+ if outline_kwargs is None:
827
+ outline_kwargs = {}
828
+
829
+ if bounds_kwargs is None:
830
+ bounds_kwargs = {}
831
+
832
+ if axes_kwargs is None:
833
+ axes_kwargs = {}
834
+
835
+ if text_kwargs is None:
836
+ text_kwargs = {}
837
+
838
+ # Extract what to be plotted
839
+ # zz = np.array(im.val[iv][iz0:iz1, iy0:iy1, ix0:ix1]) # np.array() to get a copy
840
+ zz = im.val[iv][iz0:iz1, iy0:iy1, ix0:ix1].flatten() # .flatten() provides a copy
841
+
842
+ if categ:
843
+ # --- Treat categorical variable ---
844
+ if categCol is not None \
845
+ and type(categCol) is not list \
846
+ and type(categCol) is not tuple:
847
+ err_msg = f'{fname}: `categCol` must be a list or a tuple (if not `None`)'
848
+ if logger: logger.error(err_msg)
849
+ raise Imgplot3dError(err_msg)
850
+
851
+ # Get array 'dval' of displayed values (at least for color bar)
852
+ if categVal is not None:
853
+ dval = np.array(categVal).reshape(-1) # force to be an 1d array
854
+
855
+ if len(np.unique(dval)) != len(dval):
856
+ err_msg = f'{fname}: `categVal` contains duplicated entries'
857
+ if logger: logger.error(err_msg)
858
+ raise Imgplot3dError(err_msg)
859
+
860
+ # Check 'categCol' (if not None)
861
+ if categCol is not None and len(categCol) != len(dval):
862
+ err_msg = f'{fname}: length of `categVal` and length of `categCol` differ'
863
+ if logger: logger.error(err_msg)
864
+ raise Imgplot3dError(err_msg)
865
+
866
+ else:
867
+ # Possibly exclude values from zz
868
+ if excludedVal is not None:
869
+ for val in np.array(excludedVal).reshape(-1):
870
+ np.putmask(zz, zz == val, np.nan)
871
+
872
+ # Get the unique values in zz
873
+ dval = np.array([v for v in np.unique(zz).reshape(-1) if ~np.isnan(v)])
874
+ if len(dval) > ncateg_max:
875
+ err_msg = f'{fname}: too many categories, set `categ=False`'
876
+ if logger: logger.error(err_msg)
877
+ raise Imgplot3dError(err_msg)
878
+
879
+ if not len(dval): # len(dval) == 0
880
+ err_msg = f'{fname}: no value to be drawn'
881
+ if logger: logger.error(err_msg)
882
+ raise Imgplot3dError(err_msg)
883
+
884
+ if categActive is not None:
885
+ if len(categActive) != len(dval):
886
+ err_msg = f'{fname}: length of `categActive` invalid (should be the same as length of `categVal`)'
887
+ if logger: logger.error(err_msg)
888
+ raise Imgplot3dError(err_msg)
889
+
890
+ else:
891
+ categActive = np.ones(len(dval), dtype='bool')
892
+
893
+ # Replace dval[i] by i in zz if categActive[i] is True otherwise by np.nan, and other values by np.nan
894
+ zz2 = np.array(zz) # copy array
895
+ zz[...] = np.nan # initialize
896
+ for i, v in enumerate(dval):
897
+ if categActive[i]:
898
+ zz[zz2 == v] = i
899
+
900
+ del zz2
901
+
902
+ # Set 'colorList': the list of colors to use
903
+ colorList = None
904
+ if categCol is not None:
905
+ if len(categCol) >= len(dval):
906
+ colorList = [categCol[i] for i in range(len(dval))]
907
+ # colorList = [mcolors.ColorConverter().to_rgba(categCol[i]) for i in range(len(dval))]
908
+
909
+ elif categColCycle:
910
+ if verbose > 0:
911
+ if logger:
912
+ logger.warning(f'{fname}: `categCol` is used cyclically (too few entries)')
913
+ else:
914
+ print(f'{fname}: WARNING: `categCol` is used cyclically (too few entries)')
915
+ colorList = [categCol[i%len(categCol)] for i in range(len(dval))]
916
+
917
+ else:
918
+ if verbose > 0:
919
+ if logger:
920
+ logger.warning(f'{fname}: `categCol` not used (too few entries)')
921
+ else:
922
+ print(f'{fname}: WARNING: `categCol` not used (too few entries)')
923
+
924
+ if colorList is None:
925
+ # Use colors from cmap
926
+ colorList = [cmap(x) for x in np.arange(len(dval)) * 1.0/(len(dval)-1)]
927
+
928
+ # Set the colormap: 'cmap'
929
+ # - Trick: duplicate last color (if len(colorList)> 1)!
930
+ if len(colorList) == 1:
931
+ colorList.append(colorList[-1])
932
+ cmap = ccol.custom_cmap(colorList, ncol=len(colorList), alpha=alpha)
933
+
934
+ # Set the min and max of the colorbar
935
+ cmin, cmax = 0, len(dval) # works, but scalar bar annotations may be shifted of +0.5, see below
936
+ # cmin, cmax = -0.5, len(dval) - 0.5 # does not work
937
+
938
+ # Set scalar bar annotations if not given
939
+ if scalar_bar_annotations == {}:
940
+ if len(dval) <= scalar_bar_annotations_max: # avoid too many annotations (very slow and useless)
941
+ for i, v in enumerate(dval):
942
+ scalar_bar_annotations[i+0.5] = f'{v:.3g}'
943
+
944
+ scalar_bar_kwargs['n_labels'] = 0
945
+ scalar_bar_kwargs['n_colors'] = len(dval)
946
+
947
+ else: # categ == False
948
+ # --- Treat continuous variable ---
949
+ # Possibly exclude values from zz
950
+ if excludedVal is not None:
951
+ for val in np.array(excludedVal).reshape(-1): # force to be an 1d array
952
+ np.putmask(zz, zz == val, np.nan)
953
+
954
+ # Set cmin and cmax if not specified
955
+ if cmin is None:
956
+ cmin = np.nanmin(zz)
957
+
958
+ if cmax is None:
959
+ cmax = np.nanmax(zz)
960
+
961
+ # Set pyvista ImageData (previously: UniformGrid)
962
+ xmin = im.ox + ix0 * im.sx
963
+ xmax = im.ox + ix1 * im.sx
964
+ xdim = ix1 - ix0 + 1
965
+
966
+ ymin = im.oy + iy0 * im.sy
967
+ ymay = im.oy + iy1 * im.sy
968
+ ydim = iy1 - iy0 + 1
969
+
970
+ zmin = im.oz + iz0 * im.sz
971
+ zmaz = im.oz + iz1 * im.sz
972
+ zdim = iz1 - iz0 + 1
973
+
974
+ # pg = pv.UniformGrid(dims=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
975
+ # pg = pv.UniformGrid(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
976
+ pg = pv.ImageData(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
977
+
978
+ pg.cell_data[im.varname[iv]] = zz #.flatten()
979
+
980
+ if plotter is not None:
981
+ pp = plotter
982
+ else:
983
+ pp = pv.Plotter()
984
+
985
+ if slice_normal_x is not None:
986
+ for v in np.array(slice_normal_x).reshape(-1):
987
+ pp.add_mesh(pg.slice(normal=(1,0,0), origin=(v,0,0)), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
988
+
989
+ if slice_normal_y is not None:
990
+ for v in np.array(slice_normal_y).reshape(-1):
991
+ pp.add_mesh(pg.slice(normal=(0,1,0), origin=(0,v,0)), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
992
+
993
+ if slice_normal_z is not None:
994
+ for v in np.array(slice_normal_z).reshape(-1):
995
+ pp.add_mesh(pg.slice(normal=(0,0,1), origin=(0,0,v)), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
996
+
997
+ if slice_normal_custom is not None:
998
+ for nor, ori in np.array(slice_normal_custom).reshape(-1, 2, 3):
999
+ pp.add_mesh(pg.slice(normal=nor, origin=ori), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
1000
+
1001
+ if background_color is not None:
1002
+ pp.background_color = background_color
1003
+
1004
+ if foreground_color is not None:
1005
+ for d in [scalar_bar_kwargs, outline_kwargs, bounds_kwargs, axes_kwargs, text_kwargs]:
1006
+ if 'color' not in d.keys():
1007
+ d['color'] = foreground_color
1008
+
1009
+ if show_scalar_bar:
1010
+ # # - old -
1011
+ # # pg.cell_arrays[im.varname[iv]][...] = np.nan # trick: set all value to nan and use nan_opacity = 0 for empty plot but 'saving' the scalar bar...
1012
+ # # # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=True, scalar_bar_args=scalar_bar_kwargs)
1013
+ # # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=False)
1014
+ # # - old -
1015
+ # # Trick: set opacity=0 and nan_opacity=0 for empty plot but 'saving' the scalar bar...
1016
+ # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), opacity=0., nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=False)
1017
+ if 'title' not in scalar_bar_kwargs.keys():
1018
+ scalar_bar_kwargs['title'] = im.varname[iv]
1019
+ pp.add_scalar_bar(**scalar_bar_kwargs)
1020
+
1021
+ if show_outline:
1022
+ pp.add_mesh(pg.outline(), **outline_kwargs)
1023
+
1024
+ if show_bounds:
1025
+ pp.show_bounds(**bounds_kwargs)
1026
+
1027
+ if show_axes:
1028
+ pp.add_axes(**axes_kwargs)
1029
+
1030
+ if text is not None:
1031
+ pp.add_text(text, **text_kwargs)
1032
+
1033
+ if plotter is None:
1034
+ pp.show(cpos=cpos)
1035
+ # ----------------------------------------------------------------------------
1036
+
1037
+ # ----------------------------------------------------------------------------
1038
+ def drawImage3D_empty_grid (
1039
+ im,
1040
+ plotter=None,
1041
+ ix0=0, ix1=None,
1042
+ iy0=0, iy1=None,
1043
+ iz0=0, iz1=None,
1044
+ cmap=ccol.cmap_def,
1045
+ cmin=None, cmax=None,
1046
+ alpha=None,
1047
+ excludedVal=None,
1048
+ categ=False,
1049
+ ncateg_max=30,
1050
+ categVal=None,
1051
+ categCol=None,
1052
+ categColCycle=False,
1053
+ categActive=None,
1054
+ show_scalar_bar=True,
1055
+ show_outline=True,
1056
+ show_bounds=False,
1057
+ show_axes=True,
1058
+ text=None,
1059
+ scalar_bar_annotations=None,
1060
+ scalar_bar_annotations_max=20,
1061
+ scalar_bar_kwargs=None,
1062
+ outline_kwargs=None,
1063
+ bounds_kwargs=None,
1064
+ axes_kwargs=None,
1065
+ text_kwargs=None,
1066
+ background_color=None,
1067
+ foreground_color=None,
1068
+ cpos=None,
1069
+ verbose=1,
1070
+ logger=None,
1071
+ **kwargs):
1072
+ """
1073
+ Displays an empty grid from a 3D image.
1074
+
1075
+ Same parameters (if present) as in function :func:`drawImage3D_slice` are used,
1076
+ see this function. (Tricks are applied.)
1077
+ """
1078
+ fname = 'drawImage3D_empty_grid'
1079
+
1080
+ # Set indices to be plotted
1081
+ if ix1 is None:
1082
+ ix1 = im.nx
1083
+
1084
+ if iy1 is None:
1085
+ iy1 = im.ny
1086
+
1087
+ if iz1 is None:
1088
+ iz1 = im.nz
1089
+
1090
+ if ix0 >= ix1 or ix0 < 0 or ix1 > im.nx:
1091
+ err_msg = f'{fname}: invalid indices along x axis'
1092
+ if logger: logger.error(err_msg)
1093
+ raise Imgplot3dError(err_msg)
1094
+
1095
+ if iy0 >= iy1 or iy0 < 0 or iy1 > im.ny:
1096
+ err_msg = f'{fname}: invalid indices along y axis'
1097
+ if logger: logger.error(err_msg)
1098
+ raise Imgplot3dError(err_msg)
1099
+
1100
+ if iz0 >= iz1 or iz0 < 0 or iz1 > im.nz:
1101
+ err_msg = f'{fname}: invalid indices along z axis'
1102
+ if logger: logger.error(err_msg)
1103
+ raise Imgplot3dError(err_msg)
1104
+
1105
+ # Get the color map
1106
+ if isinstance(cmap, str):
1107
+ try:
1108
+ cmap = plt.get_cmap(cmap)
1109
+ except:
1110
+ err_msg = f'{fname}: invalid `cmap` string'
1111
+ if logger: logger.error(err_msg)
1112
+ raise Imgplot3dError(err_msg)
1113
+
1114
+ # Initialization of dictionary (do not use {} as default argument, it is not re-initialized...)
1115
+ if scalar_bar_annotations is None:
1116
+ scalar_bar_annotations = {}
1117
+
1118
+ if scalar_bar_kwargs is None:
1119
+ scalar_bar_kwargs = {}
1120
+
1121
+ if outline_kwargs is None:
1122
+ outline_kwargs = {}
1123
+
1124
+ if bounds_kwargs is None:
1125
+ bounds_kwargs = {}
1126
+
1127
+ if axes_kwargs is None:
1128
+ axes_kwargs = {}
1129
+
1130
+ if text_kwargs is None:
1131
+ text_kwargs = {}
1132
+
1133
+ # Extract what to be plotted
1134
+ vname = 'tmp'
1135
+ zz = np.nan * np.ones((iz1-iz0, iy1-iy0, ix1-ix0)).flatten()
1136
+
1137
+ if categ:
1138
+ # --- Treat categorical variable ---
1139
+ if categCol is not None \
1140
+ and type(categCol) is not list \
1141
+ and type(categCol) is not tuple:
1142
+ err_msg = f'{fname}: `categCol` must be a list or a tuple (if not `None`)'
1143
+ if logger: logger.error(err_msg)
1144
+ raise Imgplot3dError(err_msg)
1145
+
1146
+ # Get array 'dval' of displayed values (at least for color bar)
1147
+ if categVal is not None:
1148
+ dval = np.array(categVal).reshape(-1) # force to be an 1d array
1149
+
1150
+ if len(np.unique(dval)) != len(dval):
1151
+ err_msg = f'{fname}: `categVal` contains duplicated entries'
1152
+ if logger: logger.error(err_msg)
1153
+ raise Imgplot3dError(err_msg)
1154
+
1155
+ # Check 'categCol' (if not None)
1156
+ if categCol is not None and len(categCol) != len(dval):
1157
+ err_msg = f'{fname}: length of `categVal` and length of `categCol` differ'
1158
+ if logger: logger.error(err_msg)
1159
+ raise Imgplot3dError(err_msg)
1160
+
1161
+ else:
1162
+ # Possibly exclude values from zz
1163
+ if excludedVal is not None:
1164
+ for val in np.array(excludedVal).reshape(-1):
1165
+ np.putmask(zz, zz == val, np.nan)
1166
+
1167
+ # Get the unique values in zz
1168
+ dval = np.array([v for v in np.unique(zz).reshape(-1) if ~np.isnan(v)])
1169
+ if len(dval) > ncateg_max:
1170
+ err_msg = f'{fname}: too many categories, set `categ=False`'
1171
+ if logger: logger.error(err_msg)
1172
+ raise Imgplot3dError(err_msg)
1173
+
1174
+ if not len(dval): # len(dval) == 0
1175
+ err_msg = f'{fname}: no value to be drawn'
1176
+ if logger: logger.error(err_msg)
1177
+ raise Imgplot3dError(err_msg)
1178
+
1179
+ if categActive is not None:
1180
+ if len(categActive) != len(dval):
1181
+ err_msg = f'{fname}: length of `categActive` invalid (should be the same as length of `categVal`)'
1182
+ if logger: logger.error(err_msg)
1183
+ raise Imgplot3dError(err_msg)
1184
+ else:
1185
+ categActive = np.ones(len(dval), dtype='bool')
1186
+
1187
+ # Replace dval[i] by i in zz if categActive[i] is True otherwise by np.nan, and other values by np.nan
1188
+ zz2 = np.array(zz) # copy array
1189
+ zz[...] = np.nan # initialize
1190
+ for i, v in enumerate(dval):
1191
+ if categActive[i]:
1192
+ zz[zz2 == v] = i
1193
+
1194
+ del zz2
1195
+
1196
+ # Set 'colorList': the list of colors to use
1197
+ colorList = None
1198
+ if categCol is not None:
1199
+ if len(categCol) >= len(dval):
1200
+ colorList = [categCol[i] for i in range(len(dval))]
1201
+ # colorList = [mcolors.ColorConverter().to_rgba(categCol[i]) for i in range(len(dval))]
1202
+
1203
+ elif categColCycle:
1204
+ if verbose > 0:
1205
+ if logger:
1206
+ logger.warning(f'{fname}: `categCol` is used cyclically (too few entries)')
1207
+ else:
1208
+ print(f'{fname}: WARNING: `categCol` is used cyclically (too few entries)')
1209
+ colorList = [categCol[i%len(categCol)] for i in range(len(dval))]
1210
+
1211
+ else:
1212
+ if verbose > 0:
1213
+ if logger:
1214
+ logger.warning(f'{fname}: `categCol` not used (too few entries)')
1215
+ else:
1216
+ print(f'{fname}: WARNING: `categCol` not used (too few entries)')
1217
+
1218
+ if colorList is None:
1219
+ # Use colors from cmap
1220
+ colorList = [cmap(x) for x in np.arange(len(dval)) * 1.0/(len(dval)-1)]
1221
+
1222
+ # Set the colormap: 'cmap'
1223
+ # - Trick: duplicate last color (if len(colorList)> 1)!
1224
+ if len(colorList) == 1:
1225
+ colorList.append(colorList[-1])
1226
+ cmap = ccol.custom_cmap(colorList, ncol=len(colorList), alpha=alpha)
1227
+
1228
+ # Set the min and max of the colorbar
1229
+ cmin, cmax = 0, len(dval) # works, but scalar bar annotations may be shifted of +0.5, see below
1230
+ # cmin, cmax = -0.5, len(dval) - 0.5 # does not work
1231
+
1232
+ # Set scalar bar annotations if not given
1233
+ if scalar_bar_annotations == {}:
1234
+ if len(dval) <= scalar_bar_annotations_max: # avoid too many annotations (very slow and useless)
1235
+ for i, v in enumerate(dval):
1236
+ scalar_bar_annotations[i+0.5] = f'{v:.3g}'
1237
+
1238
+ scalar_bar_kwargs['n_labels'] = 0
1239
+ scalar_bar_kwargs['n_colors'] = len(dval)
1240
+
1241
+ else: # categ == False
1242
+ # --- Treat continuous variable ---
1243
+ # Possibly exclude values from zz
1244
+ if excludedVal is not None:
1245
+ for val in np.array(excludedVal).reshape(-1): # force to be an 1d array
1246
+ np.putmask(zz, zz == val, np.nan)
1247
+
1248
+ # Set cmin and cmax if not specified
1249
+ if cmin is None:
1250
+ cmin = np.nanmin(zz)
1251
+
1252
+ if cmax is None:
1253
+ cmax = np.nanmax(zz)
1254
+
1255
+ # Set pyvista ImageData (previously: UniformGrid)
1256
+ xmin = im.ox + ix0 * im.sx
1257
+ xmax = im.ox + ix1 * im.sx
1258
+ xdim = ix1 - ix0 + 1
1259
+
1260
+ ymin = im.oy + iy0 * im.sy
1261
+ ymay = im.oy + iy1 * im.sy
1262
+ ydim = iy1 - iy0 + 1
1263
+
1264
+ zmin = im.oz + iz0 * im.sz
1265
+ zmaz = im.oz + iz1 * im.sz
1266
+ zdim = iz1 - iz0 + 1
1267
+
1268
+ # pg = pv.UniformGrid(dims=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
1269
+ # pg = pv.UniformGrid(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
1270
+ pg = pv.ImageData(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
1271
+
1272
+ pg.cell_data[vname] = zz #.flatten()
1273
+
1274
+ if plotter is not None:
1275
+ pp = plotter
1276
+ else:
1277
+ pp = pv.Plotter()
1278
+
1279
+ # Here is the trick!
1280
+ kwargs['opacity']=0.0
1281
+ pp.add_mesh(pg.slice(normal=(1,0,0), origin=(im.ox + (ix0+0.5)*im.sx,0,0)), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
1282
+
1283
+ if background_color is not None:
1284
+ pp.background_color = background_color
1285
+
1286
+ if foreground_color is not None:
1287
+ for d in [scalar_bar_kwargs, outline_kwargs, bounds_kwargs, axes_kwargs, text_kwargs]:
1288
+ if 'color' not in d.keys():
1289
+ d['color'] = foreground_color
1290
+
1291
+ if show_scalar_bar:
1292
+ pp.add_scalar_bar(**scalar_bar_kwargs)
1293
+
1294
+ if show_outline:
1295
+ pp.add_mesh(pg.outline(), **outline_kwargs)
1296
+
1297
+ if show_bounds:
1298
+ pp.show_bounds(**bounds_kwargs)
1299
+
1300
+ if show_axes:
1301
+ pp.add_axes(**axes_kwargs)
1302
+
1303
+ if text is not None:
1304
+ pp.add_text(text, **text_kwargs)
1305
+
1306
+ if plotter is None:
1307
+ pp.show(cpos=cpos)
1308
+ # ----------------------------------------------------------------------------
1309
+
1310
+ # ----------------------------------------------------------------------------
1311
+ def drawImage3D_volume (
1312
+ im,
1313
+ plotter=None,
1314
+ ix0=0, ix1=None,
1315
+ iy0=0, iy1=None,
1316
+ iz0=0, iz1=None,
1317
+ iv=0,
1318
+ cmap=ccol.cmap_def,
1319
+ cmin=None, cmax=None,
1320
+ set_out_values_to_nan=True,
1321
+ show_scalar_bar=True,
1322
+ show_outline=True,
1323
+ show_bounds=False,
1324
+ show_axes=True,
1325
+ text=None,
1326
+ scalar_bar_annotations=None,
1327
+ scalar_bar_kwargs=None,
1328
+ outline_kwargs=None,
1329
+ bounds_kwargs=None,
1330
+ axes_kwargs=None,
1331
+ text_kwargs=None,
1332
+ background_color=None,
1333
+ foreground_color=None,
1334
+ cpos=None,
1335
+ logger=None,
1336
+ **kwargs):
1337
+ """
1338
+ Displays a 3D image as volume (based on `pyvista`).
1339
+
1340
+ Parameters
1341
+ ----------
1342
+ im : :class:`geone.img.Img`
1343
+ image (3D)
1344
+
1345
+ plotter : :class:`pyvista.Plotter`, optional
1346
+ - if given (not `None`), add element to the plotter, a further call to \
1347
+ `plotter.show()` will be required to show the plot
1348
+ - if not given (`None`, default): a plotter is created and the plot \
1349
+ is shown
1350
+
1351
+ ix0 : int, default: 0
1352
+ index of first slice along x direction, considered for plotting
1353
+
1354
+ ix1 : int, optional
1355
+ 1+index of last slice along x direction (`ix0 < ix1`), considered for
1356
+ plotting; by default: number of cells in x direction (`ix1=im.nx`) is
1357
+ used
1358
+
1359
+ iy0 : int, default: 0
1360
+ index of first slice along y direction, considered for plotting
1361
+
1362
+ iy1 : int, optional
1363
+ 1+index of last slice along y direction (`iy0 < iy1`), considered for
1364
+ plotting; by default: number of cells in x direction (`iy1=im.ny`) is
1365
+ used
1366
+
1367
+ iz0 : int, default: 0
1368
+ index of first slice along z direction, considered for plotting
1369
+
1370
+ iz1 : int, optional
1371
+ 1+index of last slice along z direction (`iz0 < iz1`), considered for
1372
+ plotting; by default: number of cells in z direction (`iz1=im.nz`) is
1373
+ used
1374
+
1375
+ iv : int, default: 0
1376
+ index of the variable to be displayed
1377
+
1378
+ cmap : colormap, default: `geone.customcolors.cmap_def`
1379
+ color map (can be a string, in this case the color map is obtained by
1380
+ `matplotlib.pyplot.get_cmap(cmap)`)
1381
+
1382
+ cmin : float, optional
1383
+ used only if `categ=False`:
1384
+ minimal value to be displayed; by default: minimal value of the displayed
1385
+ variable is used for `cmin`
1386
+
1387
+ cmax : float, optional
1388
+ used only if `categ=False`:
1389
+ maximal value to be displayed; by default: maximal value of the displayed
1390
+ variable is used for `cmax`
1391
+
1392
+ set_out_values_to_nan : bool, default: True
1393
+ indicates if values out of the range `[cmin, cmax]` is set to `numpy.nan`
1394
+ before plotting
1395
+
1396
+ show_scalar_bar : bool, default: True
1397
+ indicates if scalar bar (color bar) is displayed
1398
+
1399
+ show_outline : bool, default: True
1400
+ indicates if outline (around the image) is displayed
1401
+
1402
+ show_bounds : bool, default: False
1403
+ indicates if bounds are displayed (box with graduation)
1404
+
1405
+ show_axes : bool, default: True
1406
+ indicates if axes are displayed
1407
+
1408
+ text : str, optional
1409
+ text (title) to be displayed on the figure
1410
+
1411
+ scalar_bar_annotations : dict, optional
1412
+ annotation (ticks) on the scalar bar (color bar), used if
1413
+ `show_scalar_bar=True`
1414
+
1415
+ scalar_bar_kwargs : dict, optional
1416
+ keyword arguments passed to function `plotter.add_scalar_bar`
1417
+ (can be useful for customization, used if `show_scalar_bar=True`)
1418
+ note: in subplots (multi-sub-window), key 'title' should be distinct for
1419
+ each subplot
1420
+
1421
+ outline_kwargs : dict, optional
1422
+ keyword arguments passed to function `plotter.add_mesh`
1423
+ (can be useful for customization, used if `show_outline=True`)
1424
+
1425
+ bounds_kwargs : dict, optional
1426
+ keyword arguments passed to function `plotter.show_bounds`
1427
+ (can be useful for customization, used if `show_bounds=True`)
1428
+
1429
+ axes_kwargs : dict, optional
1430
+ keyword arguments passed to function `plotter.add_axes`
1431
+ (can be useful for customization, used if `show_axes=True`)
1432
+
1433
+ text_kwargs : dict, optional
1434
+ keyword arguments passed to function `plotter.add_text`
1435
+ (can be useful for customization, used if `text` is not `None`)
1436
+
1437
+ background_color : color, optional
1438
+ background color (3-tuple (RGB code), 4-tuple (RGBA code) or str)
1439
+
1440
+ foreground_color : color, optional
1441
+ foreground color (3-tuple (RGB code), 4-tuple (RGBA code) or str)
1442
+
1443
+ cpos : sequence[sequence[float]], optional
1444
+ camera position (unsused if `plotter=None`);
1445
+ `cpos` = [camera_location, focus_point, viewup_vector], with
1446
+
1447
+ - camera_location: (tuple of length 3) camera location ("eye")
1448
+ - focus_point : (tuple of length 3) focus point
1449
+ - viewup_vector : (tuple of length 3) viewup vector (vector \
1450
+ attached to the "head" and pointed to the "sky")
1451
+
1452
+ note: in principle, (focus_point - camera_location) is orthogonal to
1453
+ viewup_vector
1454
+
1455
+ logger : :class:`logging.Logger`, optional
1456
+ logger (see package `logging`)
1457
+ if specified, messages are written via `logger` (no print)
1458
+
1459
+ kwargs : dict
1460
+ additional keyword arguments passed to `plotter.add_volume`
1461
+ when plotting the variable, such as
1462
+
1463
+ - opacity (float, or str) : \
1464
+ opacity for colors; \
1465
+ default: 'linear', (set 'linear_r' to invert opacity)
1466
+ - show_edges (bool) : \
1467
+ indicates if edges of the grid are displayed
1468
+ - edge_color (color) : \
1469
+ color (3-tuple (RGB code), 4-tuple (RGBA code) or str) for edges \
1470
+ (used if `show_edges=True`)
1471
+ - line_width (float) \
1472
+ line width for edges (used if `show_edges=True`)
1473
+ - etc.
1474
+
1475
+ Notes
1476
+ -----
1477
+ - 'scalar bar', and 'axes' may be not displayed in multiple-plot, bug ?
1478
+ """
1479
+ fname = 'drawImage3D_volume'
1480
+
1481
+ # Check iv
1482
+ if iv < 0:
1483
+ iv = im.nv + iv
1484
+
1485
+ if iv < 0 or iv >= im.nv:
1486
+ err_msg = f'{fname}: invalid `iv` index'
1487
+ if logger: logger.error(err_msg)
1488
+ raise Imgplot3dError(err_msg)
1489
+
1490
+ # Set indices to be plotted
1491
+ if ix1 is None:
1492
+ ix1 = im.nx
1493
+
1494
+ if iy1 is None:
1495
+ iy1 = im.ny
1496
+
1497
+ if iz1 is None:
1498
+ iz1 = im.nz
1499
+
1500
+ if ix0 >= ix1 or ix0 < 0 or ix1 > im.nx:
1501
+ err_msg = f'{fname}: invalid indices along x axis'
1502
+ if logger: logger.error(err_msg)
1503
+ raise Imgplot3dError(err_msg)
1504
+
1505
+ if iy0 >= iy1 or iy0 < 0 or iy1 > im.ny:
1506
+ err_msg = f'{fname}: invalid indices along y axis'
1507
+ if logger: logger.error(err_msg)
1508
+ raise Imgplot3dError(err_msg)
1509
+
1510
+ if iz0 >= iz1 or iz0 < 0 or iz1 > im.nz:
1511
+ err_msg = f'{fname}: invalid indices along z axis'
1512
+ if logger: logger.error(err_msg)
1513
+ raise Imgplot3dError(err_msg)
1514
+
1515
+ # Get the color map
1516
+ if isinstance(cmap, str):
1517
+ try:
1518
+ cmap = plt.get_cmap(cmap)
1519
+ except:
1520
+ err_msg = f'{fname}: invalid `cmap` string'
1521
+ if logger: logger.error(err_msg)
1522
+ raise Imgplot3dError(err_msg)
1523
+
1524
+ # Initialization of dictionary (do not use {} as default argument, it is not re-initialized...)
1525
+ if scalar_bar_annotations is None:
1526
+ scalar_bar_annotations = {}
1527
+
1528
+ if scalar_bar_kwargs is None:
1529
+ scalar_bar_kwargs = {}
1530
+
1531
+ if outline_kwargs is None:
1532
+ outline_kwargs = {}
1533
+
1534
+ if bounds_kwargs is None:
1535
+ bounds_kwargs = {}
1536
+
1537
+ if axes_kwargs is None:
1538
+ axes_kwargs = {}
1539
+
1540
+ if text_kwargs is None:
1541
+ text_kwargs = {}
1542
+
1543
+ # Extract what to be plotted
1544
+ # zz = np.array(im.val[iv][iz0:iz1, iy0:iy1, ix0:ix1]) # np.array() to get a copy
1545
+ zz = im.val[iv][iz0:iz1, iy0:iy1, ix0:ix1].flatten() # .flatten() provides a copy
1546
+
1547
+ # Set cmin and cmax if not specified
1548
+ if cmin is None:
1549
+ cmin = np.nanmin(zz)
1550
+
1551
+ if cmax is None:
1552
+ cmax = np.nanmax(zz)
1553
+
1554
+ if set_out_values_to_nan:
1555
+ np.putmask(zz, np.any((np.isnan(zz), zz < cmin, zz > cmax), axis=0), np.nan)
1556
+
1557
+ # Set pyvista ImageData (previously: UniformGrid)
1558
+ xmin = im.ox + ix0 * im.sx
1559
+ xmax = im.ox + ix1 * im.sx
1560
+ xdim = ix1 - ix0 + 1
1561
+
1562
+ ymin = im.oy + iy0 * im.sy
1563
+ ymay = im.oy + iy1 * im.sy
1564
+ ydim = iy1 - iy0 + 1
1565
+
1566
+ zmin = im.oz + iz0 * im.sz
1567
+ zmaz = im.oz + iz1 * im.sz
1568
+ zdim = iz1 - iz0 + 1
1569
+
1570
+ # pg = pv.UniformGrid(dims=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
1571
+ # pg = pv.UniformGrid(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
1572
+ pg = pv.ImageData(dimensions=(xdim, ydim, zdim), spacing=(im.sx, im.sy, im.sz), origin=(xmin, ymin, zmin))
1573
+
1574
+ pg.cell_data[im.varname[iv]] = zz #.flatten()
1575
+
1576
+ if plotter is not None:
1577
+ pp = plotter
1578
+ else:
1579
+ pp = pv.Plotter()
1580
+
1581
+ # pp.add_volume(pg.ctp(), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=show_scalar_bar, scalar_bar_args=scalar_bar_kwargs)
1582
+ # pp.add_volume(pg.ctp(), cmap=cmap, clim=(cmin, cmax), opacity_unit_distance=0.1, opacity=opacity, annotations=scalar_bar_annotations, show_scalar_bar=False)
1583
+ pp.add_volume(pg.ctp(), cmap=cmap, clim=(cmin, cmax), annotations=scalar_bar_annotations, show_scalar_bar=False, **kwargs)
1584
+
1585
+ if background_color is not None:
1586
+ pp.background_color = background_color
1587
+
1588
+ if foreground_color is not None:
1589
+ for d in [scalar_bar_kwargs, outline_kwargs, bounds_kwargs, axes_kwargs, text_kwargs]:
1590
+ if 'color' not in d.keys():
1591
+ d['color'] = foreground_color
1592
+
1593
+ if show_scalar_bar:
1594
+ # pp.add_mesh(pg, cmap=cmap, clim=(cmin, cmax), opacity=0., nan_opacity=0., annotations=scalar_bar_annotations, show_scalar_bar=False)
1595
+ if 'title' not in scalar_bar_kwargs.keys():
1596
+ scalar_bar_kwargs['title'] = im.varname[iv]
1597
+ pp.add_scalar_bar(**scalar_bar_kwargs)
1598
+
1599
+ if show_outline:
1600
+ pp.add_mesh(pg.outline(), **outline_kwargs)
1601
+
1602
+ if show_bounds:
1603
+ pp.show_bounds(**bounds_kwargs)
1604
+
1605
+ if show_axes:
1606
+ pp.add_axes(**axes_kwargs)
1607
+
1608
+ if text is not None:
1609
+ pp.add_text(text, **text_kwargs)
1610
+
1611
+ if plotter is None:
1612
+ pp.show(cpos=cpos)
1613
+ # ----------------------------------------------------------------------------
1614
+
1615
+ # From: https://docs.pyvista.org/plotting/plotting.html?highlight=add_mesh#pyvista.BasePlotter.add_mesh
1616
+ # add_scalar_bar(title=None, n_labels=5, italic=False, bold=False, title_font_size=None, label_font_size=None, color=None, font_family=None, shadow=False, mapper=None, width=None, height=None, position_x=None, position_y=None, vertical=None, interactive=None, fmt=None, use_opacity=True, outline=False, nan_annotation=False, below_label=None, above_label=None, background_color=None, n_colors=None, fill=False, render=True)
1617
+ #
1618
+ # Create scalar bar using the ranges as set by the last input mesh.
1619
+ #
1620
+ # Parameters
1621
+ #
1622
+ # title (string, optional) – Title of the scalar bar. Default None
1623
+ #
1624
+ # n_labels (int, optional) – Number of labels to use for the scalar bar.
1625
+ #
1626
+ # italic (bool, optional) – Italicises title and bar labels. Default False.
1627
+ #
1628
+ # bold (bool, optional) – Bolds title and bar labels. Default True
1629
+ #
1630
+ # title_font_size (float, optional) – Sets the size of the title font. Defaults to None and is sized automatically.
1631
+ #
1632
+ # label_font_size (float, optional) – Sets the size of the title font. Defaults to None and is sized automatically.
1633
+ #
1634
+ # color (string or 3 item list, optional, defaults to white) –
1635
+ #
1636
+ # Either a string, rgb list, or hex color string. For example:
1637
+ #
1638
+ # color=’white’ color=’w’ color=[1, 1, 1] color=’#FFFFFF’
1639
+ #
1640
+ # font_family (string, optional) – Font family. Must be either courier, times, or arial.
1641
+ #
1642
+ # shadow (bool, optional) – Adds a black shadow to the text. Defaults to False
1643
+ #
1644
+ # width (float, optional) – The percentage (0 to 1) width of the window for the colorbar
1645
+ #
1646
+ # height (float, optional) – The percentage (0 to 1) height of the window for the colorbar
1647
+ #
1648
+ # position_x (float, optional) – The percentage (0 to 1) along the windows’s horizontal direction to place the bottom left corner of the colorbar
1649
+ #
1650
+ # position_y (float, optional) – The percentage (0 to 1) along the windows’s vertical direction to place the bottom left corner of the colorbar
1651
+ #
1652
+ # interactive (bool, optional) – Use a widget to control the size and location of the scalar bar.
1653
+ #
1654
+ # use_opacity (bool, optional) – Optionally display the opacity mapping on the scalar bar
1655
+ #
1656
+ # outline (bool, optional) – Optionally outline the scalar bar to make opacity mappings more obvious.
1657
+ #
1658
+ # nan_annotation (bool, optional) – Annotate the NaN color
1659
+ #
1660
+ # below_label (str, optional) – String annotation for values below the scalars range
1661
+ #
1662
+ # above_label (str, optional) – String annotation for values above the scalars range
1663
+ #
1664
+ # background_color (array, optional) – The color used for the background in RGB format.
1665
+ #
1666
+ # n_colors (int, optional) – The maximum number of color displayed in the scalar bar.
1667
+ #
1668
+ # fill (bool) – Draw a filled box behind the scalar bar with the background_color
1669
+ #
1670
+ # render (bool, optional) – Force a render when True. Default True.
1671
+ #
1672
+ # Notes
1673
+ #
1674
+ # Setting title_font_size, or label_font_size disables automatic font sizing for both the title and label.
1675
+
1676
+ if __name__ == "__main__":
1677
+ print("Module 'geone.imgplot3d' example:")
1678
+
1679
+ # Example with a 3D gaussian random field
1680
+
1681
+ import geone.covModel as gcm
1682
+ from geone import grf, img
1683
+
1684
+ # Define grid
1685
+ nx, ny, nz = 85, 56, 34
1686
+ dx, dy, dz = 1., 1., 1.
1687
+ ox, oy, oz = 0., 0., 0.
1688
+
1689
+ dimension = [nx, ny, nz]
1690
+ spacing = [dx, dy, dy]
1691
+ origin = [ox, oy, oz]
1692
+
1693
+ # Define covariance model
1694
+ cov_model = gcm.CovModel3D(elem=[
1695
+ ('gaussian', {'w':8.9, 'r':[40, 20, 10]}), # elementary contribution
1696
+ ('nugget', {'w':0.1}) # elementary contribution
1697
+ ], alpha=-30, beta=-45, gamma=20, name='')
1698
+
1699
+ # Set seed
1700
+ np.random.seed(123)
1701
+
1702
+ # Generate GRF
1703
+ v = grf.grf3D(cov_model, (nx, ny, nz), (dx, dy, dz), (ox, oy, oz))
1704
+ im = img.Img(nx, ny, nz, dx, dy, dz, ox, oy, oz, nv=1, val=v)
1705
+
1706
+ # ===== Ex1 =====
1707
+ # Simple plot
1708
+ # ------
1709
+ drawImage3D_volume(im, text='Ex1: volume (cont.)')
1710
+
1711
+ # # Equivalent:
1712
+ # pp = pv.Plotter()
1713
+ # drawImage3D_volume(im, text='Ex1: volume (cont.)')
1714
+ # pp.show()
1715
+
1716
+ # # For saving screenshot (png)
1717
+ # # Note: axes will not be displayed in off screen mode
1718
+ # pp = pv.Plotter(off_screen=True)
1719
+ # drawImage3D_volume(im, plotter=pp)
1720
+ # pp.show(screenshot='test.png')
1721
+
1722
+ # ===== Ex2 =====
1723
+ # Multiple plot
1724
+ # ------
1725
+ # Note: scalar bar is not displayed in all plots (even if show_scalar_bar is True) when
1726
+ # same title for scalar bar is used (see Ex4)
1727
+ pp = pv.Plotter(shape=(2,2))
1728
+
1729
+ pp.subplot(0,0)
1730
+ drawImage3D_surface(im, plotter=pp, text='Ex2: surface (cont.)' )
1731
+
1732
+ pp.subplot(0,1)
1733
+ drawImage3D_volume(im, plotter=pp, text='Ex2: volume (cont.)')
1734
+
1735
+ cx, cy, cz = im.ox+0.5*im.nx*im.sx, im.oy+0.5*im.ny*im.sy, im.oz+0.5*im.nz*im.sz # center of image
1736
+ pp.subplot(1,0)
1737
+ drawImage3D_slice(im, plotter=pp,
1738
+ slice_normal_x=cx,
1739
+ slice_normal_y=cy,
1740
+ slice_normal_z=cz,
1741
+ text='Ex2: slice (cont.)')
1742
+
1743
+ pp.subplot(1,1)
1744
+ drawImage3D_slice(im, plotter=pp,
1745
+ slice_normal_custom=[[(1, 1, 0), (cx, cy, cz)], [(1, -1, 0), (cx, cy, cz)]],
1746
+ text='Ex2: slice (cont.)')
1747
+
1748
+ pp.link_views()
1749
+ pp.show(cpos=(1,2,.5))
1750
+
1751
+ # ===== Ex3 =====
1752
+ # Multiple plot
1753
+ # ------
1754
+ # Note: scalar bar is not displayed in all plots (even if show_scalar_bar is True) when
1755
+ # same title for scalar bar is used (see Ex4)
1756
+ pp = pv.Plotter(shape=(1,3))
1757
+
1758
+ pp.subplot(0,0)
1759
+ drawImage3D_volume(im, plotter=pp, cmin=2, cmax=4, text='Ex3: volume - cmin / cmax')
1760
+
1761
+ pp.subplot(0,1)
1762
+ drawImage3D_volume(im, plotter=pp, cmin=2, cmax=4, set_out_values_to_nan=False, text='Ex3: volume - cmin / cmax - set_out_values_to_nan=False')
1763
+
1764
+ pp.subplot(0,2)
1765
+ drawImage3D_surface(im, plotter=pp, cmin=2, cmax=4, text='Ex3: surface - cmin / cmax')
1766
+
1767
+ pp.link_views()
1768
+ pp.show(cpos=(1,2,.5))
1769
+
1770
+ # Categorize image...
1771
+ # -------------------
1772
+ v = im.val.reshape(-1)
1773
+ newv = np.zeros(im.nxyz())
1774
+ for t in [1., 2., 3., 4.]:
1775
+ np.putmask(newv, np.all((np.abs(v) > t, np.abs(v) <= t+1), axis=0), t)
1776
+ np.putmask(newv, np.abs(v) > 5., 10.)
1777
+ # -> newv takes values 0, 1, 3, 4, 10
1778
+ im.set_var(newv, 'categ', 0) # insert variable in image im
1779
+
1780
+ # ===== Ex4 =====
1781
+ pp = pv.Plotter(shape=(2,3))
1782
+
1783
+ pp.subplot(0,0)
1784
+ drawImage3D_volume(im, plotter=pp,
1785
+ scalar_bar_kwargs={'title':''}, # distinct title in each subplot for correct display!
1786
+ text='Ex4: volume (categ. var.)')
1787
+
1788
+ pp.subplot(0,1)
1789
+ drawImage3D_surface(im, plotter=pp, categ=False,
1790
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1791
+ text='Ex4: surface - categ=False')
1792
+
1793
+ cx, cy, cz = im.ox+0.5*im.nx*im.sx, im.oy+0.5*im.ny*im.sy, im.oz+0.5*im.nz*im.sz # center of image
1794
+ pp.subplot(0,2)
1795
+ drawImage3D_slice(im, plotter=pp, categ=False,
1796
+ slice_normal_x=cx,
1797
+ slice_normal_y=cy,
1798
+ slice_normal_z=cz,
1799
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1800
+ text='Ex4: slice - categ=False')
1801
+
1802
+ pp.subplot(1,1)
1803
+ drawImage3D_surface(im, plotter=pp, categ=True,
1804
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1805
+ text='Ex4: surface - categ=True')
1806
+
1807
+ cx, cy, cz = im.ox+0.5*im.nx*im.sx, im.oy+0.5*im.ny*im.sy, im.oz+0.5*im.nz*im.sz # center of image
1808
+ pp.subplot(1,2)
1809
+ drawImage3D_slice(im, plotter=pp, categ=True,
1810
+ slice_normal_x=cx,
1811
+ slice_normal_y=cy,
1812
+ slice_normal_z=cz,
1813
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1814
+ text='Ex4: slice - categ=True')
1815
+
1816
+ pp.link_views()
1817
+ pp.show(cpos=(1,2,.5))
1818
+
1819
+ # Using some options
1820
+ # -------------------
1821
+ cols=['purple', 'blue', 'cyan', 'yellow', 'red', 'pink']
1822
+
1823
+ # ===== Ex5 =====
1824
+ # Multiple plot
1825
+ pp = pv.Plotter(shape=(2,3))
1826
+
1827
+ pp.subplot(0,0)
1828
+ drawImage3D_surface(im, plotter=pp, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols,
1829
+ categActive=[True, False, False, False, False, False],
1830
+ scalar_bar_kwargs={'title':''}, # distinct title in each subplot for correct display!
1831
+ text='Ex5: surface - categ=True\n - active categ "0"')
1832
+
1833
+ pp.subplot(0,1)
1834
+ drawImage3D_surface(im, plotter=pp, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols,
1835
+ categActive=[False, True, False, False, False, False],
1836
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1837
+ text='Ex5: surface - categ=True\n - active categ "1"')
1838
+
1839
+ pp.subplot(0,2)
1840
+ drawImage3D_surface(im, plotter=pp, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols,
1841
+ categActive=[False, False, True, False, False, False],
1842
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1843
+ text='Ex5: surface - categ=True\n - active categ "2"')
1844
+
1845
+ pp.subplot(1,0)
1846
+ drawImage3D_surface(im, plotter=pp, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols,
1847
+ categActive=[False, False, False, True, False, False],
1848
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1849
+ text='Ex5: surface - categ=True\n - active categ "3"')
1850
+
1851
+ pp.subplot(1,1)
1852
+ drawImage3D_surface(im, plotter=pp, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols,
1853
+ categActive=[False, False, False, False, True, False],
1854
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1855
+ text='Ex5: surface - categ=True\n - active categ "4"')
1856
+
1857
+ pp.subplot(1,2)
1858
+ drawImage3D_surface(im, plotter=pp, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols,
1859
+ categActive=[False, False, False, False, False, True],
1860
+ scalar_bar_kwargs={'title':' '}, # distinct title in each subplot for correct display!
1861
+ text='Ex5: surface - categ=True\n - active categ "10"')
1862
+
1863
+ pp.link_views()
1864
+ pp.show(cpos=(1,2,.5))
1865
+
1866
+ # ===== Ex6 =====
1867
+ # activate only some categories
1868
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1869
+ text='Ex6: surface - categ=True - active some categ.')
1870
+
1871
+ # ===== Ex7 =====
1872
+ # do not show outline
1873
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1874
+ show_outline=False, text='Ex7: no outline')
1875
+
1876
+ # ===== Ex8 =====
1877
+ # enlarge outline
1878
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1879
+ show_outline=True, outline_kwargs={'line_width':5}, text='Ex8: thick outline')
1880
+
1881
+ # ===== Ex9 =====
1882
+ # show bounds
1883
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1884
+ show_bounds=True, text='Ex9: show bounds')
1885
+
1886
+ # ===== Ex10 =====
1887
+ # show bounds with grid
1888
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1889
+ show_bounds=True, bounds_kwargs={'grid':True}, text='Ex10: bounds and grid')
1890
+
1891
+ # ===== Ex11 =====
1892
+ # customize scalar bar
1893
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1894
+ scalar_bar_annotations={0.5:'A', 1.5:'B', 2.5:'C', 3.5:'D', 4.5:'E', 5.5:'high'}, scalar_bar_kwargs={'vertical':True, 'title_font_size':24, 'label_font_size':10},
1895
+ text='Ex11: custom scalar bar')
1896
+
1897
+ # ===== Ex12 =====
1898
+ # scalar bar: interactive position...
1899
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1900
+ scalar_bar_kwargs={'interactive':True}, text='Ex12: interactive scalar bar')
1901
+
1902
+ # ===== Ex13 =====
1903
+ # customize title
1904
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1905
+ text='Ex13: custom title', text_kwargs={'font_size':12, 'position':'upper_right'})
1906
+
1907
+ # ===== Ex14 =====
1908
+ # customize axes
1909
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1910
+ axes_kwargs={'x_color':'pink', 'zlabel':'depth'}, text='Ex14: custom axes')
1911
+
1912
+ # ===== Ex15 =====
1913
+ # changing background / foreground colors
1914
+ drawImage3D_surface(im, categ=True, categVal=[0, 1, 2, 3, 4, 10], categCol=cols, categActive=[True, False, False, True, False, True],
1915
+ background_color=(0.9, 0.9, 0.9), foreground_color='k', text='Ex15: background/foreground colors')
1916
+
1917
+ # ===== Ex16 =====
1918
+ drawImage3D_surface(im, cmin=2, cmax=4, text='Ex16: surface (categ. var) - categ=False - cmin / cmax')