geone 1.3.1__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.
geone/tools.py ADDED
@@ -0,0 +1,1025 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # -------------------------------------------------------------------------
5
+ # Python module: 'tools.py'
6
+ # author: Julien Straubhaar
7
+ # date: nov-2023
8
+ # -------------------------------------------------------------------------
9
+
10
+ """
11
+ Module for miscellaneous tools.
12
+ """
13
+
14
+ import sys
15
+ import multiprocessing
16
+
17
+ import numpy as np
18
+ import matplotlib.pyplot as plt
19
+ import scipy.ndimage
20
+
21
+ from matplotlib.backend_bases import MouseButton
22
+
23
+ from geone import img
24
+
25
+ # -----------------------------------------------------------------------------
26
+ def add_path_by_drawing(
27
+ path_list,
28
+ close=False,
29
+ show_instructions=True,
30
+ last_point_marker='o',
31
+ last_point_color='red',
32
+ **kwargs):
33
+ """
34
+ Add paths in a list, by interatively drawing on a plot.
35
+
36
+ The first argument of the function is a list that is updated when the
37
+ function is called by appending one (or more) path(s) (see notes below).
38
+ A path is a 2D array of floats with two columns, each row is (the x and y
39
+ coordinates of) a point. A path is interactively determined on the plot on
40
+ the current axis (get with `matplotlib.pyplot.gca()`), with the following
41
+ rules:
42
+
43
+ - left click: add the next point (or first one)
44
+ - right click: remove the last point
45
+
46
+ When pressing a key:
47
+
48
+ - key n/N: terminate the current path, and start a new path
49
+ - key ENTER (or other): terminate the current path and exits
50
+
51
+ Parameters
52
+ ----------
53
+ path_list : list
54
+ list of paths, that will be updated by appending the path interactively
55
+ drawn in the current axis (one can start with `path_list = []`)
56
+
57
+ close : bool, default: False
58
+ if `True`: when a path is terminated, the first points of the path is
59
+ replicated at the end of the path to form a close line / path
60
+
61
+ show_instructions : bool, default: True
62
+ if `True`: instructions are printed in the standard output
63
+
64
+ last_point_marker : "matplotlib marker", default: 'o'
65
+ marker used for highlighting the last clicked point
66
+
67
+ last_point_color : "matplotlib color", default: 'red'
68
+ color used for highlighting the last clicked point
69
+
70
+ kwargs : dict
71
+ keyword arguments passed to `matplotlib.pyplot.plot` to plot the path(s)
72
+
73
+ Notes
74
+ -----
75
+ * The function does not return anything. The first argument, `path_list` is \
76
+ updated, with the path(s) drawn interactively, for example `path_list[-1]` \
77
+ is the last path, a 2D array, where `path_list[-1][i]` is the i-th point \
78
+ (1D array of two floats) of that path
79
+ * An interactive maplotlib backend must be used, so that this function works \
80
+ properly
81
+ """
82
+ # fname = 'add_path_by_drawing'
83
+
84
+ ax = plt.gca()
85
+ obj_drawn = []
86
+ x, y = [], []
87
+
88
+ set_default_color = False
89
+ if 'color' not in kwargs.keys() and 'c' not in kwargs.keys():
90
+ set_default_color = True
91
+ col = plt.rcParams['axes.prop_cycle'].by_key()['color']
92
+ ncol = len(col)
93
+ col_ind = [0]
94
+ kwargs['color'] = col[col_ind[0]] # default color for lines (segments)
95
+ # if 'color' not in kwargs.keys() and 'c' not in kwargs.keys():
96
+ # kwargs['color'] = 'tab:blue' # default color for lines (segments)
97
+
98
+ if show_instructions:
99
+ instruct0 = '\n'.join([' Left click: add next point', ' Right click: remove last point', ' Key n/N: select a new line', ' key ENTER (or other): finish (quit)'])
100
+ print('\n'.join(['Draw path', instruct0]))
101
+ sys.stdout.flush()
102
+
103
+ def on_click(event):
104
+ if not event.inaxes:
105
+ return
106
+ if event.button is MouseButton.LEFT:
107
+ if len(x):
108
+ # remove last point from obj_drawn
109
+ ax.lines[-1].remove()
110
+ # add clicked point
111
+ x.append(event.xdata)
112
+ y.append(event.ydata)
113
+ if len(x) > 1:
114
+ # add line (segment) to obj_drawn
115
+ obj_drawn.append(plt.plot(x[-2:], y[-2:], **kwargs))
116
+ # add point to obj_drawn
117
+ obj_drawn.append(plt.plot(x[-1], y[-1], marker=last_point_marker, color=last_point_color))
118
+ if event.button is MouseButton.RIGHT:
119
+ if len(x):
120
+ # remove last clicked point
121
+ del x[-1], y[-1]
122
+ # remove last point from obj_drawn
123
+ ax.lines[-1].remove()
124
+ if len(x):
125
+ # point(s) are still in the line
126
+ # remove last line (segment) from obj_drawn
127
+ ax.lines[-1].remove()
128
+ # add last point to obj_drawn
129
+ obj_drawn.append(plt.plot(x[-1], y[-1], marker=last_point_marker, color=last_point_color))
130
+
131
+ def on_key(event):
132
+ if len(x):
133
+ # remove last point from obj_drawn
134
+ ax.lines[-1].remove()
135
+ if close:
136
+ # close the line
137
+ if len(x):
138
+ x.append(x[0])
139
+ y.append(y[0])
140
+ # add line (closing segment) to obj_drawn
141
+ obj_drawn.append(plt.plot(x[-2:], y[-2:], **kwargs))
142
+ # add path to path_list
143
+ if len(x):
144
+ path_list.append(np.array((x, y)).T)
145
+ if event.key.lower() == 'n':
146
+ if show_instructions:
147
+ print('\n'.join(['Draw next path', instruct0]))
148
+ sys.stdout.flush()
149
+ x.clear() # Do not use: x = [], because x is global for this function!
150
+ y.clear()
151
+ if set_default_color:
152
+ # Set color for next line
153
+ col_ind[0] = (col_ind[0]+1)%ncol
154
+ kwargs['color'] = col[col_ind[0]] # default color for lines (segments)
155
+ return
156
+
157
+ plt.disconnect(cid_click)
158
+ plt.disconnect(cid_key)
159
+ return
160
+ # if
161
+ # path_list.append(np.array((x, y)).T)
162
+ # cid_click = plt.connect('button_press_event', on_click)
163
+
164
+
165
+ cid_click = plt.connect('button_press_event', on_click)
166
+ cid_key = plt.connect('key_press_event', on_key)
167
+ # -----------------------------------------------------------------------------
168
+
169
+ # -----------------------------------------------------------------------------
170
+ def is_in_polygon(x, vertices, wrap=None, return_sum_of_angles=False, **kwargs):
171
+ """
172
+ Checks if point(s) is (are) in a polygon given by its vertices forming a close line.
173
+
174
+ To check if a point is in the polygon, the method consists in computing
175
+ the vectors from the given point to the vertices of the polygon and
176
+ the sum of signed angles between two successives vectors (and the last
177
+ and first one). Then, the point is in the polygon if and only if this sum
178
+ is equal to +/- 2 pi.
179
+
180
+ Note that if the sum of angles is +2 pi (resp. -2 pi), then the vertices form
181
+ a close line counterclockwise (resp. clockwise); this can be checked by
182
+ specifying a point `x` in the polygon and `return_sum_of_angles=True`.
183
+
184
+ Parameters
185
+ ----------
186
+ x : 2D array-like or 1D array-like
187
+ point(s) coordinates, each row `x[i]` (if 2D array-like) (or `x` if
188
+ 1D array-like) contains the two coordinates of one point
189
+
190
+ vertices : 2D array
191
+ vertices of a polygon in 2D, each row of `vertices` contains the two
192
+ coordinates of one vertex; the segments of the polygon are obtained by
193
+ linking two successive vertices (as well as the last one with the first
194
+ one, if `wrap=True` (see below)), so that the vertices form a close line
195
+ (clockwise or counterclockwise)
196
+
197
+ wrap : bool, optional
198
+ - if `True`: last and first vertices has to be linked to form a close line
199
+ - if `False`: last and first vertices should be the same ones (i.e. the \
200
+ vertices form a close line);
201
+
202
+ by default (`None`): `wrap` is automatically computed
203
+
204
+ return_sum_of_angles : bool, default: False
205
+ if `True`, the sum of angles (computed by the method) is returned
206
+
207
+ kwargs :
208
+ keyword arguments passed to function `numpy.isclose`
209
+
210
+ Returns
211
+ -------
212
+ out : 1D array of bools, or bool
213
+ indicates for each point in `x` if it is inside (True) or outside (False)
214
+ the polygon;
215
+ note: if `x` is of shape (m, 2), then `out` is a 1D array of shape (m, ),
216
+ and if `x` is of shape (2, ) (one point), `out` is bool
217
+
218
+ sum_of_angles : 1D array of floats, or float
219
+ returned if `return_sum_of_angles=True`; for each point in `x`, the sum
220
+ of angles computed by the method is returned (`nan` if the sum is not
221
+ computed);
222
+ note: `sum_of_angles` is an array of same shape as `out` or a float
223
+ """
224
+ # fname = 'is_in_polygon'
225
+
226
+ # Set wrap (and adjust vertices) if needed
227
+ if wrap is None:
228
+ wrap = ~np.isclose(np.sqrt(((vertices[-1] - vertices[0])**2).sum()), 0.0)
229
+ if not wrap:
230
+ # remove last vertice (should be equal to the first one)
231
+ vertices = np.delete(vertices, -1, axis=0)
232
+
233
+ # Array of shifted indices (on vertices)
234
+ ind = np.hstack((np.arange(1, vertices.shape[0]), [0]))
235
+
236
+ # Initialization
237
+ xx = np.atleast_2d(x)
238
+ res = np.zeros(xx.shape[0], dtype='bool')
239
+ if return_sum_of_angles:
240
+ res_sum = np.full((xx.shape[0],), np.nan)
241
+
242
+ xmin, ymin = vertices.min(axis=0)
243
+ xmax, ymax = vertices.max(axis=0)
244
+
245
+ for j, xj in enumerate(xx):
246
+ if xj[0] < xmin or xj[0] > xmax or xj[1] < ymin or xj[1] > ymax:
247
+ continue
248
+
249
+ # Set a, b, c: edge of the triangles to compute angle between a and b
250
+ a = xj - vertices
251
+
252
+ # a_norm2: square norm of a
253
+ a_norm2 = (a**2).sum(axis=1)
254
+
255
+ b = a[ind]
256
+ b_norm2 = a_norm2[ind]
257
+ ab_norm = np.sqrt(a_norm2*b_norm2)
258
+
259
+ if np.any(np.isclose(ab_norm, 0, **kwargs)):
260
+ # xj on the border of the polygon
261
+ continue
262
+
263
+ # Compute the sum of angles using the theorem of cosine
264
+ c = b - a
265
+ c_norm2 = (c**2).sum(axis=1)
266
+ sign = 2*(a[:,0]*b[:,1] - a[:,1]*b[:,0] > 0) - 1
267
+ sum_angles = np.sum(sign*np.arccos(np.minimum(1.0, np.maximum(-1.0, (a_norm2 + b_norm2 - c_norm2)/(2.0*ab_norm)))))
268
+
269
+ res[j] = np.isclose(np.abs(sum_angles), 2*np.pi)
270
+ if return_sum_of_angles:
271
+ res_sum[j] = sum_angles
272
+
273
+ if np.asarray(x).ndim == 1:
274
+ res = res[0]
275
+ if return_sum_of_angles:
276
+ res_sum = res_sum[0]
277
+
278
+ if return_sum_of_angles:
279
+ return res, res_sum
280
+
281
+ return res
282
+ # -----------------------------------------------------------------------------
283
+
284
+ # -----------------------------------------------------------------------------
285
+ def is_in_polygon_mp(x, vertices, wrap=None, return_sum_of_angles=False, nproc=-1, **kwargs):
286
+ """
287
+ Computes the same as the function :func:`is_in_polygon`, using multiprocessing.
288
+
289
+ All the parameters except `nproc` are the same as those of the function
290
+ :func:`is_in_polygon`.
291
+
292
+ The number of processes used (in parallel) is n, and determined by the
293
+ parameter `nproc` (int, optional) as follows:
294
+
295
+ - if `nproc > 0`: n = `nproc`,
296
+ - if `nproc <= 0`: n = max(nmax+`nproc`, 1), where nmax is the total \
297
+ number of cpu(s) of the system (retrieved by `multiprocessing.cpu_count()`), \
298
+ i.e. all cpus except `-nproc` is used (but at least one)
299
+
300
+ See function :func:`is_in_polygon`.
301
+ """
302
+ # fname = 'is_in_polygon_mp'
303
+
304
+ # Set wrap (and adjust vertices) if needed
305
+ if wrap is None:
306
+ wrap = ~np.isclose(np.sqrt(((vertices[-1] - vertices[0])**2).sum()), 0.0)
307
+ if not wrap:
308
+ # remove last vertice (should be equal to the first one)
309
+ vertices = np.delete(vertices, -1, axis=0)
310
+
311
+ # Set wrap key in keywords arguments
312
+ kwargs['wrap'] = True
313
+
314
+ # Set return_sum_of_angles key in keywords arguments
315
+ kwargs['return_sum_of_angles'] = return_sum_of_angles
316
+
317
+ # Initialization
318
+ xx = np.atleast_2d(x)
319
+
320
+ # Set number of processes (n)
321
+ if nproc > 0:
322
+ n = nproc
323
+ else:
324
+ n = min(multiprocessing.cpu_count()+nproc, 1)
325
+
326
+ # Set index for distributing tasks
327
+ q, r = np.divmod(xx.shape[0], n)
328
+ ids_proc = [i*q + min(i, r) for i in range(n+1)]
329
+
330
+ # Set pool of n workers
331
+ pool = multiprocessing.Pool(n)
332
+ out_pool = []
333
+ for i in range(n):
334
+ # Set i-th process
335
+ out_pool.append(pool.apply_async(is_in_polygon, args=(xx[ids_proc[i]:ids_proc[i+1]], vertices), kwds=kwargs))
336
+
337
+ # Properly end working process
338
+ pool.close() # Prevents any more tasks from being submitted to the pool,
339
+ pool.join() # then, wait for the worker processes to exit.
340
+
341
+ # Get result from each process
342
+ if return_sum_of_angles:
343
+ res = []
344
+ res_sum = []
345
+ for w in out_pool:
346
+ r, s = w.get()
347
+ res.append(r)
348
+ res_sum.append(s)
349
+ res = np.hstack(res)
350
+ res_sum = np.hstack(res_sum)
351
+ else:
352
+ res = np.hstack([w.get() for w in out_pool])
353
+
354
+ if np.asarray(x).ndim == 1:
355
+ res = res[0]
356
+ if return_sum_of_angles:
357
+ res_sum = res_sum[0]
358
+
359
+ if return_sum_of_angles:
360
+ return res, res_sum
361
+
362
+ return res
363
+ # -----------------------------------------------------------------------------
364
+
365
+ # -----------------------------------------------------------------------------
366
+ def rasterize_polygon_2d(
367
+ vertices,
368
+ nx=None, ny=None,
369
+ sx=None, sy=None,
370
+ ox=None, oy=None,
371
+ xmin_ext=0.0, xmax_ext=0.0,
372
+ ymin_ext=0.0, ymax_ext=0.0,
373
+ wrap=None,
374
+ logger=None,
375
+ **kwargs):
376
+ """
377
+ Rasterizes a polygon (close line) in a 2D grid.
378
+
379
+ This function returns an image with one variable indicating for each cell
380
+ if it is inside (1) or outside (0) the polygon defined by the given
381
+ vertices.
382
+
383
+ The grid geometry of the output image is set by the given parameters or
384
+ computed from the vertices, as in function :func:`geone.img.imageFromPoints`,
385
+ i.e. for the x axis (similar for y):
386
+
387
+ - `ox` (origin), `nx` (number of cells) and `sx` (resolution, cell size)
388
+ - or only `nx`: `ox` and `sx` automatically computed
389
+ - or only `sx`: `ox` and `nx` automatically computed
390
+
391
+ In the two last cases, the parameters `xmin_ext`, `xmax_ext`, are used and
392
+ the approximate limit of the grid along x axis is set to x0, x1, where
393
+
394
+ - x0: min x coordinate of the vertices minus `xmin_ext`
395
+ - x1: max x coordinate of the vertices plus `xmax_ext`
396
+
397
+ Parameters
398
+ ----------
399
+ vertices : 2D array
400
+ vertices of a polygon in 2D, each row of `vertices` contains the two
401
+ coordinates of one vertex; the segments of the polygon are obtained by
402
+ linking two successive vertices (as well as the last one with the first
403
+ one, if `wrap=True` (see below)), so that the vertices form a close line
404
+ (clockwise or counterclockwise)
405
+
406
+ nx : int, optional
407
+ number of grid cells along x axis; see above for possible inputs
408
+
409
+ ny : int, optional
410
+ number of grid cells along y axis; see above for possible inputs
411
+
412
+ sx : float, optional
413
+ cell size along x axis; see above for possible inputs
414
+
415
+ sy : float, optional
416
+ cell size along y axis; see above for possible inputs
417
+
418
+ ox : float, optional
419
+ origin of the grid along x axis (x coordinate of cell border);
420
+ see above for possible
421
+
422
+ oy : float, optional
423
+ origin of the grid along y axis (y coordinate of cell border);
424
+ see above for possible
425
+
426
+ Note: `(ox, oy)` is the "lower-left" corner of the grid
427
+
428
+ xmin_ext : float, default: 0.0
429
+ extension beyond the min x coordinate of the vertices (see above)
430
+
431
+ xmax_ext : float, default: 0.0
432
+ extension beyond the max x coordinate of the vertices (see above)
433
+
434
+ ymin_ext : float, default: 0.0
435
+ extension beyond the min y coordinate of the vertices (see above)
436
+
437
+ ymax_ext : float, default: 0.0
438
+ extension beyond the max y coordinate of the vertices (see above)
439
+
440
+ wrap : bool, optional
441
+ - if `True`: last and first vertices has to be linked to form a close line
442
+ - if `False`: last and first vertices should be the same ones (i.e. the \
443
+ vertices form a close line);
444
+
445
+ by default (`None`): `wrap` is automatically computed
446
+
447
+ logger : :class:`logging.Logger`, optional
448
+ logger (see package `logging`)
449
+ if specified, messages are written via `logger` (no print)
450
+
451
+ kwargs : dict
452
+ keyword arguments passed to function :func:`is_in_polygon`
453
+
454
+ Returns
455
+ -------
456
+ im : :class:`geone.img.Img`
457
+ output image (see above);
458
+ note: the image grid is defined in 3D with `nz=1`, `sz=1.0`, `oz=-0.5`
459
+ """
460
+ # fname = 'rasterize_polygon_2d'
461
+
462
+ # Define grid geometry (image with no variable)
463
+ im = img.imageFromPoints(vertices,
464
+ nx=nx, ny=ny, sx=sx, sy=sy, ox=ox, oy=oy,
465
+ xmin_ext=xmin_ext, xmax_ext=xmax_ext,
466
+ ymin_ext=ymin_ext, ymax_ext=ymax_ext,
467
+ logger=logger)
468
+
469
+ # Rasterize: for each cell, check if its center is within the grid
470
+ v = np.asarray(is_in_polygon(np.array((im.xx().reshape(-1), im.yy().reshape(-1))).T, vertices, wrap=wrap, **kwargs)).astype('float')
471
+ im.append_var(v, varname='in', logger=logger)
472
+
473
+ return im
474
+ # -----------------------------------------------------------------------------
475
+
476
+ # -----------------------------------------------------------------------------
477
+ def rasterize_polygon_2d_mp(
478
+ vertices,
479
+ nx=None, ny=None,
480
+ sx=None, sy=None,
481
+ ox=None, oy=None,
482
+ xmin_ext=0.0, xmax_ext=0.0,
483
+ ymin_ext=0.0, ymax_ext=0.0,
484
+ wrap=None,
485
+ nproc=-1,
486
+ logger=None,
487
+ **kwargs):
488
+ """
489
+ Computes the same as the function :func:`rasterize_polygon_2d`, using multiprocessing.
490
+
491
+ All the parameters except `nproc` are the same as those of the function
492
+ :func:`rasterize_polygon_2d`.
493
+
494
+ The number of processes used (in parallel) is n, and determined by the
495
+ parameter `nproc` (int, optional) as follows:
496
+
497
+ - if `nproc > 0`: n = `nproc`,
498
+ - if `nproc <= 0`: n = max(nmax+`nproc`, 1), where nmax is the total \
499
+ number of cpu(s) of the system (retrieved by `multiprocessing.cpu_count()`), \
500
+ i.e. all cpus except `-nproc` is used (but at least one).
501
+
502
+ See function :func:`rasterize_polygon_2d`.
503
+ """
504
+ # fname = 'rasterize_polygon_2d_mp'
505
+
506
+ # Define grid geometry (image with no variable)
507
+ im = img.imageFromPoints(vertices,
508
+ nx=nx, ny=ny, sx=sx, sy=sy, ox=ox, oy=oy,
509
+ xmin_ext=xmin_ext, xmax_ext=xmax_ext,
510
+ ymin_ext=ymin_ext, ymax_ext=ymax_ext,
511
+ logger=logger)
512
+
513
+ # Rasterize: for each cell, check if its center is within the grid
514
+ v = np.asarray(is_in_polygon_mp(np.array((im.xx().reshape(-1), im.yy().reshape(-1))).T, vertices, wrap=wrap, nproc=nproc, **kwargs)).astype('float')
515
+ im.append_var(v, varname='in', logger=logger)
516
+
517
+ return im
518
+ # -----------------------------------------------------------------------------
519
+
520
+ # -----------------------------------------------------------------------------
521
+ def rasterize_polygon_2d_with_PIL(
522
+ vertices,
523
+ nx=None, ny=None,
524
+ sx=None, sy=None,
525
+ ox=None, oy=None,
526
+ xmin_ext=0.0, xmax_ext=0.0,
527
+ ymin_ext=0.0, ymax_ext=0.0,
528
+ update_result=True,
529
+ wrap=None,
530
+ use_multiprocessing=False,
531
+ nproc=-1,
532
+ logger=None,
533
+ **kwargs):
534
+ """
535
+ Rasterizes a polygon (close line) in a 2D grid.
536
+
537
+ This function returns an image with one variable indicating for each cell
538
+ if it is inside (1) or outside (0) the polygon defined by the given
539
+ vertices.
540
+
541
+ The grid geometry of the output image is set by the given parameters or
542
+ computed from the vertices, as in function :func:`geone.img.imageFromPoints`,
543
+ i.e. for the x axis (similar for y):
544
+
545
+ - `ox` (origin), `nx` (number of cells) and `sx` (resolution, cell size)
546
+ - or only `nx`: `ox` and `sx` automatically computed
547
+ - or only `sx`: `ox` and `nx` automatically computed
548
+
549
+ In the two last cases, the parameters `xmin_ext`, `xmax_ext`, are used and
550
+ the approximate limit of the grid along x axis is set to x0, x1, where
551
+
552
+ - x0: min x coordinate of the vertices minus `xmin_ext`
553
+ - x1: max x coordinate of the vertices plus `xmax_ext`
554
+
555
+ Note: this function uses the package `PIL` (Pillow) to rasterize the polygon;
556
+ it is much more efficient than the function :func:`rasterize_polygon_2d`;
557
+ the result may slightly differ (on the border of the polygon) due to
558
+ differences in the rasterization algorithm: but the result may be updated
559
+ by checking if the pixels (cell centers) on the border are inside the polygon
560
+ or not, to get more precise result (as with the function :func:`rasterize_polygon_2d`).
561
+
562
+ Parameters
563
+ ----------
564
+ vertices : 2D array
565
+ vertices of a polygon in 2D, each row of `vertices` contains the two
566
+ coordinates of one vertex; the segments of the polygon are obtained by
567
+ linking two successive vertices (as well as the last one with the first
568
+ one, if not close), (clockwise or counterclockwise)
569
+
570
+ nx : int, optional
571
+ number of grid cells along x axis; see above for possible inputs
572
+
573
+ ny : int, optional
574
+ number of grid cells along y axis; see above for possible inputs
575
+
576
+ sx : float, optional
577
+ cell size along x axis; see above for possible inputs
578
+
579
+ sy : float, optional
580
+ cell size along y axis; see above for possible inputs
581
+
582
+ ox : float, optional
583
+ origin of the grid along x axis (x coordinate of cell border);
584
+ see above for possible
585
+
586
+ oy : float, optional
587
+ origin of the grid along y axis (y coordinate of cell border);
588
+ see above for possible
589
+
590
+ Note: `(ox, oy)` is the "lower-left" corner of the grid
591
+
592
+ xmin_ext : float, default: 0.0
593
+ extension beyond the min x coordinate of the vertices (see above)
594
+
595
+ xmax_ext : float, default: 0.0
596
+ extension beyond the max x coordinate of the vertices (see above)
597
+
598
+ ymin_ext : float, default: 0.0
599
+ extension beyond the min y coordinate of the vertices (see above)
600
+
601
+ ymax_ext : float, default: 0.0
602
+ extension beyond the max y coordinate of the vertices (see above)
603
+
604
+ update_result: bool, default: True
605
+ - `True`: the result obtained with PIL is updated by checking if \
606
+ the pixels (cell centers) on the border are inside the polygon or not \
607
+ with the function :func:`is_in_polygon[_mp]` (more precise)
608
+ - `False`: the result obtained with PIL is kept (less precise, a bit more \
609
+ pixels inside the polygon (near the border) than the result obtained with \
610
+ with the function :func:`rasterize_polygon_2d[_mp]`).
611
+
612
+ wrap : bool, optional
613
+ used only if `update_result=True`: parameter passed to the function
614
+ :func: `is_in_polygon_2d[_mp]`
615
+
616
+ use_multiprocessing : bool, default: False
617
+ used only if `update_result=True`: indicates if multiprocessing is used in
618
+ for updating result with function:
619
+
620
+ - :func: `is_in_polygon_2d_mp` if `use_multiprocessing=True`
621
+ - :func: `is_in_polygon_2d` if `use_multiprocessing=False`
622
+
623
+ nproc : int, default: -1
624
+ used only if `update_result=True` and `use_multiprocessing=True`:
625
+ parameter passed to the function :func: `is_in_polygon_2d_mp`
626
+
627
+ logger : :class:`logging.Logger`, optional
628
+ logger (see package `logging`)
629
+ if specified, messages are written via `logger` (no print)
630
+
631
+ kwargs: dict
632
+ used only if `update_result=True`: keyword arguments passed to function
633
+ :func:`is_in_polygon[_mp]`
634
+
635
+ Returns
636
+ -------
637
+ im : :class:`geone.img.Img`
638
+ output image (see above);
639
+ note: the image grid is defined in 3D with `nz=1`, `sz=1.0`, `oz=-0.5`
640
+ """
641
+ # fname = 'rasterize_polygon_2d_with_PIL'
642
+ from PIL import Image, ImageDraw
643
+
644
+ # Define grid geometry (image with no variable)
645
+ im = img.imageFromPoints(vertices,
646
+ nx=nx, ny=ny, sx=sx, sy=sy, ox=ox, oy=oy,
647
+ xmin_ext=xmin_ext, xmax_ext=xmax_ext,
648
+ ymin_ext=ymin_ext, ymax_ext=ymax_ext,
649
+ logger=logger)
650
+
651
+ # Rasterize polygon with PIL (integer coordinates)
652
+ p = (vertices - np.array([im.ox, im.oy])) / np.array([im.sx, im.sy])
653
+ p = [tuple([int(pi[0]), int(pi[1])]) for pi in p]
654
+ im_pil = Image.new("1", (im.nx, im.ny), 0) # "1" for boolean image
655
+ im_pil_draw = ImageDraw.Draw(im_pil)
656
+ im_pil_draw.polygon(p, fill=1)
657
+ v = np.array(im_pil, dtype=float)
658
+
659
+ im.append_var(v, varname='in', logger=logger)
660
+
661
+ if update_result:
662
+ # Identify points on the border of the polygon
663
+ arr = scipy.ndimage.convolve(im.val[0, 0], np.ones((3, 3)), mode='constant', cval=0)
664
+ # arr = np.all((arr*im.val[0, 0] > 0, arr < 9), axis=0).reshape(-1).astype(float)
665
+ arr = np.all((arr > 0, arr < 9), axis=0).reshape(-1).astype(float)
666
+ ig = np.where(arr)[0]
667
+ if len(ig):
668
+ # points to be checked
669
+ p = np.array((im.xx().reshape(-1)[ig], im.yy().reshape(-1)[ig])).T
670
+ if use_multiprocessing:
671
+ v_border = is_in_polygon_mp(p, vertices, wrap=wrap, return_sum_of_angles=False, nproc=nproc, **kwargs)
672
+ else:
673
+ v_border = is_in_polygon(p, vertices, wrap=wrap, return_sum_of_angles=False, **kwargs)
674
+
675
+ v = im.val[0, 0].reshape(-1)
676
+ v[ig] = v_border.astype(float)
677
+ im.val[0, 0] = v.reshape(im.ny, im.nx)
678
+
679
+ return im
680
+ # -----------------------------------------------------------------------------
681
+
682
+ # -----------------------------------------------------------------------------
683
+ def curv_coord_2d_from_center_line(
684
+ x, cl_position, im_cl_dist,
685
+ cl_u=None,
686
+ gradx=None,
687
+ grady=None,
688
+ dg=None,
689
+ gradtol=1.e-5,
690
+ path_len_max=10000,
691
+ return_path=False,
692
+ verbose=1,
693
+ logger=None):
694
+ """
695
+ Computes curvilinear coordinates in 2D from a center line, for points given in standard coordinates.
696
+
697
+ This functions allows to change coordinates system in 2D. For a point in 2D,
698
+ let the coordinates
699
+
700
+ - u = (u1, u2) (in 2D) in curvilinear system
701
+ - x = (x1, x2) (in 2D) in standard system
702
+
703
+ The curvilinear coordinates system (u) is defined according to a center line
704
+ in a 2D grid as follows:
705
+
706
+ - considering the distance map (geone image `im_cl_dist`) of L2 distance \
707
+ to the center line (`cl_position`)
708
+ - the path from x to the point I on the center line is computed, descending \
709
+ the gradient (`gradx`, `grady`) of the distance map
710
+ - u = (u1, u2) is defined as:
711
+ - u1: the distance along the center line to the point I,
712
+ - u2: +/-the value of the distance map at x, with
713
+ * sign + for point "at left" of the center line and,
714
+ * sign - for point "at right" of the center line.
715
+
716
+ Parameters
717
+ ----------
718
+ x : 2D array-like or 1D array-like
719
+ points coordinates in standard system (should be in the grid of the image
720
+ `im_cl_dist`, see below), each row `x[i]` (if 2D array-like) (or `x` if
721
+ 1D array-like) contains the two coordinates of one point
722
+
723
+ cl_position : 2D array of shape (n, 2)
724
+ position of the points of the center line (in standard system);
725
+ note: the distance between two successive points of the center line gives
726
+ the resolution of the u1 coordinate
727
+
728
+ im_cl_dist : :class:`geone.img.Img`
729
+ image of the distance to the center line:
730
+
731
+ - its grid is the "support" of standard coordinate system and it \
732
+ should contain all the points `x`
733
+ - the center line (`cl_position`) should "separate" the image grid \
734
+ in two (disconnected) regions
735
+ - this image can be computed using the function \
736
+ `geone.geosclassicinterface.imgDistanceImage`
737
+
738
+ cl_u : 1D array-like of length n, optional
739
+ distance along the center line (automatically computed if not given
740
+ (`None`)), used for determining u1 coordinate
741
+
742
+ gradx, grady : 2D array-like, optional
743
+ gradient of the distance to the centerline, array of shape
744
+ `(im_cl_dist.ny, im_cl_dist.nx)` (automatically computed if not given
745
+ (`None`));
746
+ `gradx[iy, ix], grady[iy, ix]`: gradient in grid cell of index `iy`, `ix`
747
+ along x and y axes respectively
748
+
749
+ dg : float, optional
750
+ step (length) used for descending the gradient map; by default (`None`):
751
+ `dg` is set to the minimal distance between two successive points in
752
+ `cl_position`, (i.e. minimal difference between two succesive values in
753
+ `cl_u`)
754
+
755
+ gradtol : float, default: 1.e-5
756
+ tolerance for the gradient magnitude (if the magintude of the gradient
757
+ is below `gradtol`, it is considered as zero vector)
758
+
759
+ path_len_max : int, default: 10000
760
+ maximal length of the path from the initial point(s) (`x`) to the center
761
+ line
762
+
763
+ return_path : bool, default: False
764
+ indicates if the path(s) from the initial point(s) (`x`) to the point(s)
765
+ I of the centerline (descending the gradient of the distance map) is
766
+ (are) retrieved
767
+
768
+ verbose : int, default: 1
769
+ verbose mode, integer >=0, higher implies more display
770
+
771
+ logger : :class:`logging.Logger`, optional
772
+ logger (see package `logging`)
773
+ if specified, messages are written via `logger` (no print)
774
+
775
+ Returns
776
+ -------
777
+ u : 2D array or 1D array (same shape as `x`)
778
+ coordinates in curvilinear system of the input point(s) `x` (see above)
779
+
780
+ x_path : list of 2D arrays (or 2D array), optional
781
+ path(s) from the initial point(s) to the point(s) I of the centerline
782
+ (descending the gradient of the distance map):
783
+
784
+ - `x_path[i]` : 2D array of floats with 2 columns
785
+ * `xpath[i][j]` is the coordinates (in standard system) of the \
786
+ j-th point of the path from `x[i]` to the center line
787
+
788
+ note: if `x` is reduced to one point and given as 1D array-like, then
789
+ `x_path` is a 2D array of floats with 2 columns containing the path from
790
+ `x` to the center line;
791
+ returned if `returned_path=True`
792
+ """
793
+ fname = 'curv_coord_2d_from_center_line'
794
+
795
+ if cl_u is None:
796
+ cl_u = np.insert(np.cumsum(np.sqrt(((cl_position[1:,:] - cl_position[:-1,:])**2).sum(axis=1))), 0, 0.0)
797
+
798
+ if gradx is None or grady is None:
799
+ # Gradient of distance map
800
+ grady, gradx = np.gradient(im_cl_dist.val[0,0])
801
+ gradx = gradx / im_cl_dist.sx
802
+ grady = grady / im_cl_dist.sy
803
+
804
+ if dg is None:
805
+ dg = np.diff(cl_u).min()
806
+
807
+ x = np.asarray(x)
808
+ x_ndim = x.ndim
809
+ x = np.atleast_2d(x)
810
+ u = np.zeros_like(x)
811
+
812
+ if return_path:
813
+ x_path = []
814
+
815
+ for i in range(x.shape[0]):
816
+ # Treat the i-th point
817
+ x_cur = x[i]
818
+ if return_path:
819
+ xi_path = [x_cur]
820
+
821
+ u2 = 0.0
822
+ d_prev = np.inf
823
+
824
+ for j in range(path_len_max):
825
+ # index in the grid (and interpolation factor) of the "current" point
826
+ tx = max(0, min((x_cur[0] - im_cl_dist.ox)/im_cl_dist.sx - 0.5, im_cl_dist.nx - 1.00001))
827
+ ix = int(tx)
828
+ tx = tx - int(tx)
829
+
830
+ ty = max(0, min((x_cur[1] - im_cl_dist.oy)/im_cl_dist.sy - 0.5, im_cl_dist.ny - 1.00001))
831
+ iy = int(ty)
832
+ ty = ty - int(ty)
833
+
834
+ # "current" distance to the center line
835
+ d_cur = (1.-ty)*((1.-tx)*im_cl_dist.val[0, 0, iy, ix] + tx*im_cl_dist.val[0, 0, iy, ix+1]) + ty*((1.-tx)*im_cl_dist.val[0, 0, iy+1, ix] + tx*im_cl_dist.val[0, 0, iy+1, ix+1])
836
+
837
+ # if np.abs(d_cur) < dg or d_cur*d_prev < 0:
838
+ if d_cur < dg or d_cur > d_prev:
839
+ break
840
+
841
+ # "current" gradient
842
+ gradx_cur = (1.-ty)*((1.-tx)*gradx[iy, ix] + tx*gradx[iy, ix+1]) + ty*((1.-tx)*gradx[iy+1, ix] + tx*gradx[iy+1, ix+1])
843
+ grady_cur = (1.-ty)*((1.-tx)*grady[iy, ix] + tx*grady[iy, ix+1]) + ty*((1.-tx)*grady[iy+1, ix] + tx*grady[iy+1, ix+1])
844
+
845
+ gradl = np.sqrt(gradx_cur**2 + grady_cur**2)
846
+ if gradl < gradtol:
847
+ break
848
+
849
+ # compute next point descending the gradient
850
+ # x_cur = x_cur - np.sign(d_cur) * dg*np.array([gradx_cur, grady_cur])/gradl
851
+ x_cur = x_cur - dg*np.array([gradx_cur, grady_cur])/gradl
852
+ if return_path:
853
+ xi_path.append(x_cur)
854
+
855
+ u2 = u2 + dg
856
+ d_prev = d_cur
857
+
858
+ # Finalize the computation of coordinate (u1, u2)
859
+ d_cur = ((cl_position[:-1,:] - x_cur)**2).sum(axis=1)
860
+ try:
861
+ k = np.where(d_cur == d_cur.min())[0][0]
862
+ except:
863
+ # k = len(cl_position[-2])
864
+ k = len(cl_position) - 2
865
+ if verbose > 0:
866
+ if logger:
867
+ logger.warning(f'{fname}: closest point on center line not found (last segment selected)')
868
+ else:
869
+ print(f'{fname}: WARNING: closest point on center line not found (last segment selected)')
870
+
871
+ u1 = cl_u[k]
872
+
873
+ s = np.sign(np.linalg.det(np.vstack((cl_position[k+1] - cl_position[k], x[i] - x_cur))))
874
+ u2 = s*u2
875
+
876
+ u[i] = np.array([u1, u2])
877
+
878
+ if return_path:
879
+ x_path.append(np.asarray(xi_path))
880
+
881
+ if return_path:
882
+ if x_ndim == 1:
883
+ u = u[0]
884
+ x_path = x_path[0]
885
+ return u, x_path
886
+ else:
887
+ if x_ndim == 1:
888
+ u = u[0]
889
+ return u
890
+ # -----------------------------------------------------------------------------
891
+
892
+ # -----------------------------------------------------------------------------
893
+ def curv_coord_2d_from_center_line_mp(
894
+ x, cl_position, im_cl_dist,
895
+ cl_u=None,
896
+ gradx=None,
897
+ grady=None,
898
+ dg=None,
899
+ gradtol=1.e-5,
900
+ path_len_max=10000,
901
+ return_path=False,
902
+ nproc=-1,
903
+ logger=None):
904
+ """
905
+ Computes the same as the function :func:`curv_coord_2d_from_center_line`, using multiprocessing.
906
+
907
+ All the parameters except `nproc` are the same as those of the function
908
+ :func:`curv_coord_2d_from_center_line`.
909
+
910
+ The number of processes used (in parallel) is n, and determined by the
911
+ parameter `nproc` (int, optional) as follows:
912
+
913
+ - if `nproc > 0`: n = `nproc`,
914
+ - if `nproc <= 0`: n = max(nmax+`nproc`, 1), where nmax is the total \
915
+ number of cpu(s) of the system (retrieved by `multiprocessing.cpu_count()`), \
916
+ i.e. all cpus except `-nproc` is used (but at least one).
917
+
918
+ See function :func:`curv_coord_2d_from_center_line`.
919
+ """
920
+ # fname = 'curv_coord_2d_from_center_line_mp'
921
+
922
+ # Initialization
923
+ xx = np.atleast_2d(x)
924
+
925
+ # Set number of processes (n)
926
+ if nproc > 0:
927
+ n = nproc
928
+ else:
929
+ n = min(multiprocessing.cpu_count()+nproc, 1)
930
+
931
+ # Set index for distributing tasks
932
+ q, r = np.divmod(xx.shape[0], n)
933
+ ids_proc = [i*q + min(i, r) for i in range(n+1)]
934
+
935
+ kwargs = dict(cl_u=cl_u, gradx=gradx, grady=grady, dg=dg, gradtol=gradtol, path_len_max=path_len_max, return_path=return_path, verbose=0, logger=logger)
936
+ # Set pool of n workers
937
+ pool = multiprocessing.Pool(n)
938
+ out_pool = []
939
+ for i in range(n):
940
+ # Set i-th process
941
+ out_pool.append(pool.apply_async(curv_coord_2d_from_center_line, args=(xx[ids_proc[i]:ids_proc[i+1]], cl_position, im_cl_dist), kwds=kwargs))
942
+
943
+ # Properly end working process
944
+ pool.close() # Prevents any more tasks from being submitted to the pool,
945
+ pool.join() # then, wait for the worker processes to exit.
946
+
947
+ # Get result from each process
948
+ if return_path:
949
+ u = []
950
+ x_path = []
951
+ for p in out_pool:
952
+ u_p, x_path_p = p.get()
953
+ u.extend(u_p)
954
+ x_path.extend(x_path_p)
955
+ if np.asarray(x).ndim == 1:
956
+ u = u[0]
957
+ x_path = x_path[0]
958
+ return u, x_path
959
+ else:
960
+ u = np.vstack([p.get() for p in out_pool])
961
+ if np.asarray(x).ndim == 1:
962
+ u = u[0]
963
+ return u
964
+ # -----------------------------------------------------------------------------
965
+
966
+ ##### OLD BELOW #####
967
+ # # -----------------------------------------------------------------------------
968
+ # def sector_angle(x, line, **kwargs):
969
+ # """
970
+ # Checks if point(s) (`x`) is (are) in a polygon given by its vertices
971
+ # `vertices` forming a close line.
972
+ #
973
+ # To check if a point is in the polygon, the method consists in computing
974
+ # the vectors from the given point to the vertices of the polygon and
975
+ # the sum of signed angles between two successives vectors (and the last
976
+ # and first one). The point is in the polygon if and only if this sum is
977
+ # equal to +/- 2 pi.
978
+ #
979
+ # :param x: (1d-array of 2 floats, or 2d-array of shape (m, 2))
980
+ # one or several points in 2D
981
+ # :param vertices:(2d-array of shape (n,2)) vertices of a polygon in 2D,
982
+ # the segments of the polygon are obtained by linking
983
+ # two successive vertices (and the last one with the
984
+ # first one, if `wrap` is True (see below) or automatically
985
+ # checked if it is needed), so that the vertices form a
986
+ # close line (clockwise or counterclockwise)
987
+ # :param wrap: (bool)
988
+ # - if True, last and first vertices has to be linked
989
+ # to form a close line,
990
+ # - if False, last and first vertices should be the same
991
+ # ones (i.e. the vertices form a close line)
992
+ # - if None, automatically computed
993
+ # :param kwargs: keyword arguments passed to `numpy.isclose` (see below)
994
+ #
995
+ # :return: (bool or 1d-array of bool of shape (m,)) True / False
996
+ # value(s) indicating if the point(s) `x` is (are)
997
+ # inside / outside the polygon
998
+ # """
999
+ #
1000
+ # # Initialization
1001
+ # xx = np.atleast_2d(x)
1002
+ # res = np.zeros(xx.shape[0], dtype='bool')
1003
+ #
1004
+ # for j, xj in enumerate(xx):
1005
+ # # Set a, b, c: edge of the triangles to compute angle between a and b
1006
+ # v = xj - vertices
1007
+ #
1008
+ # # v_norm2: square norm of v
1009
+ # v_norm2 = (a**2).sum(axis=1)
1010
+ #
1011
+ # a = v[:-1]
1012
+ # b = v[1:]
1013
+ # # b_norm2 = v_norm2[1:]
1014
+ # ab_norm = np.sqrt(v_norm2[:-1]*v_norm2[1:])
1015
+ #
1016
+ # ind = np.isclose(ab_norm, 0, **kwargs)
1017
+ # # Compute the sum of angles using the theorem of cosine
1018
+ # c = b - a
1019
+ # c_norm2 = (c**2).sum(axis=1)
1020
+ # sign = 2*(a[:,0]*b[:,1] - a[:,1]*b[:,0] > 0) - 1
1021
+ # sum_angles = np.sum(sign[ind]*np.arccos(np.minimum(1.0, np.maximum(-1.0, (a_norm2[ind] + b_norm2[ind] - c_norm2[ind])/(2.0*ab_norm[ind])))))
1022
+ # res[i] = sum_angles > 0
1023
+ #
1024
+ # return res
1025
+ # # -----------------------------------------------------------------------------