pylineament 0.1.0__tar.gz → 0.1.1__tar.gz
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.
Potentially problematic release.
This version of pylineament might be problematic. Click here for more details.
- {pylineament-0.1.0 → pylineament-0.1.1}/PKG-INFO +1 -1
- pylineament-0.1.1/pylineament/__init__.py +30 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament/pylineament.py +376 -77
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament/pylineament_ui.py +69 -20
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament.egg-info/PKG-INFO +1 -1
- pylineament-0.1.1/pylineament.egg-info/entry_points.txt +2 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/pyproject.toml +2 -2
- pylineament-0.1.0/pylineament/__init__.py +0 -0
- pylineament-0.1.0/pylineament.egg-info/entry_points.txt +0 -2
- {pylineament-0.1.0 → pylineament-0.1.1}/README.md +0 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament.egg-info/SOURCES.txt +0 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament.egg-info/dependency_links.txt +0 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament.egg-info/requires.txt +0 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/pylineament.egg-info/top_level.txt +0 -0
- {pylineament-0.1.0 → pylineament-0.1.1}/setup.cfg +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .pylineament import (
|
|
2
|
+
read_raster,
|
|
3
|
+
reduce_lines,
|
|
4
|
+
extract_lineament_points,
|
|
5
|
+
convert_points_to_line,
|
|
6
|
+
hillshade,
|
|
7
|
+
image_splitting,
|
|
8
|
+
merge_lines_csv_to_shp,
|
|
9
|
+
merge_single_csv_to_shp,
|
|
10
|
+
raster_resize,
|
|
11
|
+
dem_to_line,
|
|
12
|
+
dem_to_shp,
|
|
13
|
+
dem_to_shp_small
|
|
14
|
+
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"read_raster",
|
|
19
|
+
"reduce_lines",
|
|
20
|
+
"extract_lineament_points",
|
|
21
|
+
"convert_points_to_line",
|
|
22
|
+
"hillshade",
|
|
23
|
+
"image_splitting",
|
|
24
|
+
"merge_lines_csv_to_shp",
|
|
25
|
+
"merge_single_csv_to_shp",
|
|
26
|
+
"raster_resize",
|
|
27
|
+
"dem_to_line",
|
|
28
|
+
"dem_to_shp",
|
|
29
|
+
"dem_to_shp_small"
|
|
30
|
+
]
|
|
@@ -9,7 +9,27 @@ import rasterio
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def read_raster(im_path, split_size=500):
|
|
12
|
-
|
|
12
|
+
"""
|
|
13
|
+
Read and process a raster image (e.g., DEM).
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
im_path : str
|
|
18
|
+
Path to the input raster file.
|
|
19
|
+
split_size : int, optional
|
|
20
|
+
Block size for splitting large rasters during processing (default is 500).
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
imageSplitting : pandas.DataFrame
|
|
25
|
+
image subset coordinate and array slicing.
|
|
26
|
+
dem : numpy.ndarray
|
|
27
|
+
Digital Elevation Model array.
|
|
28
|
+
extent : tuple
|
|
29
|
+
Spatial extent of the raster (left, bottom, right, yop).
|
|
30
|
+
crs_espg : int
|
|
31
|
+
EPSG code of the raster’s coordinate reference system.
|
|
32
|
+
"""
|
|
13
33
|
img_format = os.path.split(im_path)[-1].split('.')[-1]
|
|
14
34
|
|
|
15
35
|
im_path = os.path.abspath(im_path)
|
|
@@ -64,21 +84,48 @@ def read_raster(im_path, split_size=500):
|
|
|
64
84
|
resYs = [resY]*len(grids)
|
|
65
85
|
szs = [sz]*len(grids)
|
|
66
86
|
|
|
67
|
-
|
|
87
|
+
imageSplitting = pd.DataFrame(zip(ns, lefts, tops, resXs, resYs, szs), columns=['ns', 'lefts', 'tops', 'resXs', 'resYs', 'szs'])
|
|
68
88
|
|
|
69
|
-
|
|
89
|
+
imageSplitting[['L', 'R', 'B', 'T']] = grids
|
|
70
90
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
91
|
+
imageSplitting['left_bound'] = imageSplitting['lefts'] + imageSplitting['L']*imageSplitting['resXs']
|
|
92
|
+
imageSplitting['bottom_bound'] = imageSplitting['tops'] - imageSplitting['T']*imageSplitting['resYs']
|
|
93
|
+
imageSplitting['right_bound'] = imageSplitting['left_bound'] + abs(imageSplitting['L'] - imageSplitting['R'])*imageSplitting['resXs']
|
|
94
|
+
imageSplitting['top_bound'] = imageSplitting['bottom_bound'] + abs(imageSplitting['T'] - imageSplitting['B'])*imageSplitting['resYs']
|
|
75
95
|
|
|
76
96
|
dem = im
|
|
77
97
|
|
|
78
|
-
return
|
|
98
|
+
return imageSplitting, dem, extent,crs_espg
|
|
79
99
|
|
|
80
100
|
|
|
81
101
|
def reduce_lines(lines, extent, dem_shape, min_dist=10, seg_len=10):
|
|
102
|
+
|
|
103
|
+
"""
|
|
104
|
+
Simplify and merge extracted line segments to reduce redundancy.
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
lines : dataframes
|
|
108
|
+
Extracted line segments.
|
|
109
|
+
extent : tuple
|
|
110
|
+
Raster extent used for spatial reference L B R T.
|
|
111
|
+
dem_shape : tuple
|
|
112
|
+
Shape of the DEM (rows, columns).
|
|
113
|
+
min_dist : float, optional
|
|
114
|
+
Minimum allowed distance between lines before merging (default is 10).
|
|
115
|
+
seg_len : float, optional
|
|
116
|
+
Minimum segment length to retain (default is 10).
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
initial_lines : list
|
|
121
|
+
Original extracted lines.
|
|
122
|
+
broken_lines_ : list
|
|
123
|
+
Split or fragmented lines before reduction.
|
|
124
|
+
reduced_lines : list
|
|
125
|
+
Final reduced set of merged lineaments.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
82
129
|
# frag lines
|
|
83
130
|
|
|
84
131
|
d = np.sqrt((lines['max_x'] - lines['min_x'])**2 + (lines['max_y'] - lines['min_y'])**2)
|
|
@@ -112,21 +159,21 @@ def reduce_lines(lines, extent, dem_shape, min_dist=10, seg_len=10):
|
|
|
112
159
|
df_[['quad', 'group', 'min_x', 'max_x', 'min_y', 'max_y', 'dr_dx', 'coef', 'intercept']] = azi, clust, xmin, xmax, ymin, ymax, dr_dx, coef, intercept
|
|
113
160
|
container.append(df_)
|
|
114
161
|
|
|
115
|
-
|
|
162
|
+
initial_lines = pd.concat(container)
|
|
116
163
|
|
|
117
|
-
if len(
|
|
164
|
+
if len(initial_lines)>10:
|
|
118
165
|
|
|
119
166
|
from sklearn.neighbors import KDTree
|
|
120
167
|
|
|
121
|
-
tree= KDTree(
|
|
122
|
-
dist, idxs = tree.query(
|
|
168
|
+
tree= KDTree(initial_lines[['xs_a', 'xs_b', 'ys_a', 'ys_b']])
|
|
169
|
+
dist, idxs = tree.query(initial_lines[['xs_a', 'xs_b', 'ys_a', 'ys_b']], k=10, return_distance=True)
|
|
123
170
|
idxs = np.where(dist>min_dist,dist.shape[0]+1,idxs)
|
|
124
171
|
|
|
125
172
|
keepit = np.unique(np.sort(idxs, axis=1)[:,0])
|
|
126
|
-
broken_lines_ =
|
|
173
|
+
broken_lines_ = initial_lines.iloc[keepit]
|
|
127
174
|
# broken_lines_ = broken_lines.iloc[keepit].drop_duplicates(['azi', 'clust'])
|
|
128
175
|
else:
|
|
129
|
-
broken_lines_ =
|
|
176
|
+
broken_lines_ = initial_lines
|
|
130
177
|
|
|
131
178
|
|
|
132
179
|
container = []
|
|
@@ -145,28 +192,52 @@ def reduce_lines(lines, extent, dem_shape, min_dist=10, seg_len=10):
|
|
|
145
192
|
deg = -np.degrees(np.arctan((max_y_ - min_y_) / (max_x_ - min_x_)))+90
|
|
146
193
|
container.append([quad, group, min_x_, max_x_, min_y_, max_y_, L, deg])
|
|
147
194
|
|
|
148
|
-
|
|
195
|
+
reduced_lines = pd.DataFrame(container, columns=['quad', 'group', 'min_x', 'max_x', 'min_y', 'max_y', 'L', 'deg'])
|
|
149
196
|
|
|
150
197
|
left, right, top, bot = extent
|
|
151
198
|
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
reduced_lines['min_x_'] = reduced_lines['min_x']
|
|
200
|
+
reduced_lines['max_x_'] = reduced_lines['max_x']
|
|
154
201
|
|
|
155
|
-
|
|
156
|
-
|
|
202
|
+
reduced_lines['min_y_'] = reduced_lines['min_y']
|
|
203
|
+
reduced_lines['max_y_'] = reduced_lines['max_y']
|
|
157
204
|
|
|
158
205
|
|
|
159
|
-
|
|
160
|
-
|
|
206
|
+
reduced_lines['min_x'] = (reduced_lines['min_x']/dem_shape[1])*(right - left) + left
|
|
207
|
+
reduced_lines['max_x'] = (reduced_lines['max_x']/dem_shape[1])*(right - left) + left
|
|
161
208
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
209
|
+
reduced_lines['min_y'] = -(reduced_lines['min_y']/dem_shape[0])*(bot - top) + bot
|
|
210
|
+
reduced_lines['max_y'] = -(reduced_lines['max_y']/dem_shape[0])*(bot - top) + bot
|
|
211
|
+
reduced_lines['length'] = np.sqrt((reduced_lines['max_x'] - reduced_lines['min_x'])**2 + (reduced_lines['max_y'] - reduced_lines['min_y'])**2)
|
|
165
212
|
|
|
166
|
-
return
|
|
213
|
+
return initial_lines, broken_lines_, reduced_lines
|
|
167
214
|
|
|
168
215
|
|
|
169
216
|
def extract_lineament_points(dem, eps=1.2, thresh=40,z_multip=1):
|
|
217
|
+
"""
|
|
218
|
+
Detect edge points representing potential lineaments from a DEM.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
dem : numpy.ndarray
|
|
223
|
+
Input Digital Elevation Model.
|
|
224
|
+
eps : float, optional
|
|
225
|
+
Edge detection sensitivity parameter (default is 1.2).
|
|
226
|
+
thresh : float, optional
|
|
227
|
+
Threshold for edge classification (default is 40).
|
|
228
|
+
z_multip : float, optional
|
|
229
|
+
Vertical exaggeration factor for slope computation (default is 1).
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
container : list
|
|
234
|
+
Container of extracted lineament points.
|
|
235
|
+
im_prewitt : numpy.ndarray
|
|
236
|
+
Image after applying Prewitt edge filter.
|
|
237
|
+
im_prewitt_clip : numpy.ndarray
|
|
238
|
+
Thresholded edge image highlighting lineament points.
|
|
239
|
+
"""
|
|
240
|
+
|
|
170
241
|
from sklearn.cluster import DBSCAN
|
|
171
242
|
# import rasterio
|
|
172
243
|
from skimage.filters import prewitt
|
|
@@ -227,6 +298,21 @@ def extract_lineament_points(dem, eps=1.2, thresh=40,z_multip=1):
|
|
|
227
298
|
|
|
228
299
|
|
|
229
300
|
def convert_points_to_line(container):
|
|
301
|
+
|
|
302
|
+
"""
|
|
303
|
+
Convert sets of edge points into connected line segments.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
container : list
|
|
308
|
+
Extracted lineament points grouped by proximity.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
lines : dataFrame
|
|
313
|
+
Line geometries derived from point clusters.
|
|
314
|
+
"""
|
|
315
|
+
|
|
230
316
|
from sklearn.linear_model import LinearRegression
|
|
231
317
|
|
|
232
318
|
if len(container)>0:
|
|
@@ -264,6 +350,26 @@ def convert_points_to_line(container):
|
|
|
264
350
|
|
|
265
351
|
|
|
266
352
|
def hillshade(array, azimuth=315, angle_altitude=45, z_multip=1):
|
|
353
|
+
"""
|
|
354
|
+
Generate a hillshade image from a DEM for enhanced visualization.
|
|
355
|
+
|
|
356
|
+
Parameters
|
|
357
|
+
----------
|
|
358
|
+
array : numpy.ndarray
|
|
359
|
+
DEM array.
|
|
360
|
+
azimuth : float, optional
|
|
361
|
+
Illumination azimuth in degrees (default is 315).
|
|
362
|
+
angle_altitude : float, optional
|
|
363
|
+
Illumination altitude angle in degrees (default is 45).
|
|
364
|
+
z_multip : float, optional
|
|
365
|
+
Vertical exaggeration multiplier (default is 1).
|
|
366
|
+
|
|
367
|
+
Returns
|
|
368
|
+
-------
|
|
369
|
+
hs : numpy.ndarray
|
|
370
|
+
Hillshaded representation of the DEM.
|
|
371
|
+
"""
|
|
372
|
+
|
|
267
373
|
array = array*z_multip
|
|
268
374
|
azimuth = 360.0 - azimuth # Convert azimuth to geographic convention
|
|
269
375
|
x, y = np.gradient(array)
|
|
@@ -273,11 +379,33 @@ def hillshade(array, azimuth=315, angle_altitude=45, z_multip=1):
|
|
|
273
379
|
alt_rad = angle_altitude * np.pi / 180. # Convert altitude to radians
|
|
274
380
|
|
|
275
381
|
shaded = np.sin(alt_rad) * np.sin(slope) + np.cos(alt_rad) * np.cos(slope) * np.cos((azm_rad - np.pi / 2.) - aspect)
|
|
382
|
+
hs = 255 * (shaded + 1) / 2 # Scale to 0-255
|
|
383
|
+
|
|
384
|
+
|
|
276
385
|
|
|
277
|
-
return
|
|
386
|
+
return hs
|
|
278
387
|
|
|
279
388
|
|
|
280
389
|
def image_splitting(im_path='srtm/srtm_java.tif', split_size=500, tempfolder='temp'):
|
|
390
|
+
|
|
391
|
+
"""
|
|
392
|
+
Split a large image or DEM into smaller tiles for efficient processing.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
im_path : str, optional
|
|
397
|
+
Path to the input raster file (default is 'srtm/srtm_java.tif').
|
|
398
|
+
split_size : int, optional
|
|
399
|
+
Tile size in pixels (default is 500).
|
|
400
|
+
tempfolder : str, optional
|
|
401
|
+
Temporary folder for saving image tiles (default is 'temp').
|
|
402
|
+
|
|
403
|
+
Returns
|
|
404
|
+
-------
|
|
405
|
+
imageBoundaries : dataFrame
|
|
406
|
+
List of boundaries or filenames for generated image tiles.
|
|
407
|
+
"""
|
|
408
|
+
|
|
281
409
|
import pandas as pd
|
|
282
410
|
import numpy as np
|
|
283
411
|
import rasterio
|
|
@@ -332,14 +460,14 @@ def image_splitting(im_path='srtm/srtm_java.tif', split_size=500, tempfolder='te
|
|
|
332
460
|
resYs = [resY]*len(grids)
|
|
333
461
|
szs = [sz]*len(grids)
|
|
334
462
|
|
|
335
|
-
|
|
463
|
+
imageBoundaries = pd.DataFrame(zip(ns, target_folders, lefts, tops, resXs, resYs, szs), columns=['ns', 'target_folders', 'lefts', 'tops', 'resXs', 'resYs', 'szs'])
|
|
336
464
|
|
|
337
|
-
|
|
465
|
+
imageBoundaries[['L', 'R', 'B', 'T']] = grids
|
|
338
466
|
|
|
339
|
-
for n, tempfolder, left, top, resX, resY, sz, L, R, B, T in
|
|
467
|
+
for n, tempfolder, left, top, resX, resY, sz, L, R, B, T in imageBoundaries.values:
|
|
340
468
|
l = left + L*resX
|
|
341
469
|
# b = top - T*resY - sz*resY
|
|
342
|
-
b =
|
|
470
|
+
b = top - T*resY
|
|
343
471
|
|
|
344
472
|
transform = rasterio.Affine.translation(l - resX / 2, b - resY / 2) * rasterio.Affine.scale(resX, resY)
|
|
345
473
|
|
|
@@ -360,26 +488,43 @@ def image_splitting(im_path='srtm/srtm_java.tif', split_size=500, tempfolder='te
|
|
|
360
488
|
new_dataset.write(Z, 1)
|
|
361
489
|
new_dataset.close()
|
|
362
490
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
491
|
+
imageBoundaries['left_bound'] = imageBoundaries['lefts'] + imageBoundaries['L']*imageBoundaries['resXs']
|
|
492
|
+
imageBoundaries['bottom_bound'] = imageBoundaries['tops'] - imageBoundaries['T']*imageBoundaries['resYs']
|
|
493
|
+
imageBoundaries['right_bound'] = imageBoundaries['left_bound'] + abs(imageBoundaries['L'] - imageBoundaries['R'])*imageBoundaries['resXs']
|
|
494
|
+
imageBoundaries['top_bound'] = imageBoundaries['bottom_bound'] + abs(imageBoundaries['T'] - imageBoundaries['B'])*imageBoundaries['resYs']
|
|
367
495
|
|
|
368
|
-
return
|
|
496
|
+
return imageBoundaries
|
|
369
497
|
|
|
370
498
|
|
|
371
499
|
def merge_lines_csv_to_shp(tempfolder, shppath, save_to_file):
|
|
372
|
-
|
|
500
|
+
"""
|
|
501
|
+
Merge multiple lineament CSV outputs into a single shapefile.
|
|
502
|
+
|
|
503
|
+
Parameters
|
|
504
|
+
----------
|
|
505
|
+
tempfolder : str
|
|
506
|
+
Directory containing intermediate CSV line data.
|
|
507
|
+
shppath : str
|
|
508
|
+
Path to the base shapefile used for spatial reference.
|
|
509
|
+
save_to_file : str
|
|
510
|
+
Output shapefile path.
|
|
511
|
+
|
|
512
|
+
Returns
|
|
513
|
+
-------
|
|
514
|
+
lines : Dataframe
|
|
515
|
+
Combined lineament data as a Dataframe.
|
|
516
|
+
"""
|
|
517
|
+
|
|
373
518
|
flist = glob.glob(f'{tempfolder}/*.csv')
|
|
374
519
|
|
|
375
|
-
|
|
520
|
+
all_lines = [pd.read_csv(f) for f in flist]
|
|
376
521
|
|
|
377
|
-
|
|
378
|
-
|
|
522
|
+
lines = pd.concat(all_lines).reset_index(drop=True)
|
|
523
|
+
lines = lines[['quad', 'group', 'min_x', 'max_x', 'min_y', 'max_y', 'L', 'deg', 'length', 'crs']]
|
|
379
524
|
|
|
380
525
|
if save_to_file==True:
|
|
381
526
|
|
|
382
|
-
crs_espg =
|
|
527
|
+
crs_espg = lines['crs'].values[0]
|
|
383
528
|
|
|
384
529
|
if type(crs_espg) != int:
|
|
385
530
|
crs_espg = 4326
|
|
@@ -410,7 +555,7 @@ def merge_lines_csv_to_shp(tempfolder, shppath, save_to_file):
|
|
|
410
555
|
lineShp = fiona.open(shppath, mode='w', driver='ESRI Shapefile',
|
|
411
556
|
schema = schema, crs = crs_espg)
|
|
412
557
|
|
|
413
|
-
for quad, group,min_x, max_x, min_y, max_y, L_px, deg, L_crs, crs in
|
|
558
|
+
for quad, group,min_x, max_x, min_y, max_y, L_px, deg, L_crs, crs in lines.values:
|
|
414
559
|
g = ([(min_x, min_y), (max_x, max_y)])
|
|
415
560
|
rowDict = {
|
|
416
561
|
'geometry' : {'type':'LineString',
|
|
@@ -427,15 +572,34 @@ def merge_lines_csv_to_shp(tempfolder, shppath, save_to_file):
|
|
|
427
572
|
|
|
428
573
|
lineShp.close()
|
|
429
574
|
|
|
430
|
-
return
|
|
575
|
+
return lines
|
|
431
576
|
|
|
432
577
|
|
|
433
578
|
def merge_single_csv_to_shp(fname, shppath, save_to_file):
|
|
434
|
-
|
|
579
|
+
"""
|
|
580
|
+
Merge a single CSV lineament file into a shapefile.
|
|
581
|
+
|
|
582
|
+
Parameters
|
|
583
|
+
----------
|
|
584
|
+
fname : str
|
|
585
|
+
Path to the CSV file containing lineament data.
|
|
586
|
+
shppath : str
|
|
587
|
+
Path to reference shapefile for coordinate system.
|
|
588
|
+
save_to_file : str
|
|
589
|
+
Output shapefile path.
|
|
435
590
|
|
|
436
|
-
|
|
591
|
+
Returns
|
|
592
|
+
-------
|
|
593
|
+
lines : dataFrame
|
|
594
|
+
Converted lineament data as shapefile.
|
|
595
|
+
"""
|
|
437
596
|
|
|
438
|
-
|
|
597
|
+
|
|
598
|
+
lines = pd.read_csv(fname)
|
|
599
|
+
|
|
600
|
+
lines = lines[['quad', 'group', 'min_x', 'max_x', 'min_y', 'max_y', 'L', 'deg', 'length', 'crs']]
|
|
601
|
+
|
|
602
|
+
crs_espg = lines['crs'].values[0]
|
|
439
603
|
|
|
440
604
|
if save_to_file == True:
|
|
441
605
|
|
|
@@ -454,7 +618,7 @@ def merge_single_csv_to_shp(fname, shppath, save_to_file):
|
|
|
454
618
|
lineShp = fiona.open(shppath, mode='w', driver='ESRI Shapefile',
|
|
455
619
|
schema = schema, crs = f"EPSG:{crs_espg}")
|
|
456
620
|
|
|
457
|
-
for quad, group,min_x, max_x, min_y, max_y, L_px, deg, L_crs, crs in
|
|
621
|
+
for quad, group,min_x, max_x, min_y, max_y, L_px, deg, L_crs, crs in lines.values:
|
|
458
622
|
g = ([(min_x, min_y), (max_x, max_y)])
|
|
459
623
|
rowDict = {
|
|
460
624
|
'geometry' : {'type':'LineString',
|
|
@@ -472,10 +636,25 @@ def merge_single_csv_to_shp(fname, shppath, save_to_file):
|
|
|
472
636
|
|
|
473
637
|
lineShp.close()
|
|
474
638
|
|
|
475
|
-
return
|
|
639
|
+
return lines
|
|
476
640
|
|
|
477
641
|
|
|
478
642
|
def raster_resize (dem, factor=1):
|
|
643
|
+
"""
|
|
644
|
+
Resize or downscale a DEM array.
|
|
645
|
+
|
|
646
|
+
Parameters
|
|
647
|
+
----------
|
|
648
|
+
dem : numpy.ndarray
|
|
649
|
+
Input DEM array.
|
|
650
|
+
factor : float, optional
|
|
651
|
+
Rescaling factor (e.g., 0.5 reduces size by half, default is 1).
|
|
652
|
+
|
|
653
|
+
Returns
|
|
654
|
+
-------
|
|
655
|
+
dem : numpy.ndarray
|
|
656
|
+
Rescaled DEM array.
|
|
657
|
+
"""
|
|
479
658
|
|
|
480
659
|
from skimage.transform import rescale
|
|
481
660
|
|
|
@@ -504,7 +683,48 @@ def dem_to_line(path,
|
|
|
504
683
|
min_dist=10,
|
|
505
684
|
seg_len=10,
|
|
506
685
|
z_multip=1.0,
|
|
507
|
-
downscale = 1.0
|
|
686
|
+
downscale = 1.0,
|
|
687
|
+
save_csv = False):
|
|
688
|
+
"""
|
|
689
|
+
Extract lineaments directly from a DEM and return line features.
|
|
690
|
+
|
|
691
|
+
Parameters
|
|
692
|
+
----------
|
|
693
|
+
path : str
|
|
694
|
+
Path to input DEM file.
|
|
695
|
+
tempfolder : str, optional
|
|
696
|
+
Folder for temporary outputs (default is 'temp').
|
|
697
|
+
eps : float, optional
|
|
698
|
+
Edge detection sensitivity (default is 1.2).
|
|
699
|
+
thresh : float, optional
|
|
700
|
+
Edge threshold for filtering (default is 40).
|
|
701
|
+
min_dist : float, optional
|
|
702
|
+
Minimum distance between line segments for merging (default is 10).
|
|
703
|
+
seg_len : float, optional
|
|
704
|
+
Minimum segment length (default is 10).
|
|
705
|
+
z_multip : float, optional
|
|
706
|
+
Vertical exaggeration multiplier (default is 1.0).
|
|
707
|
+
downscale : float, optional
|
|
708
|
+
DEM downscaling factor (default is 1.0).
|
|
709
|
+
save_csv : bool, optional
|
|
710
|
+
Save intermediate line data to CSV (default is False).
|
|
711
|
+
|
|
712
|
+
Returns
|
|
713
|
+
-------
|
|
714
|
+
dem : numpy.ndarray
|
|
715
|
+
Processed DEM array.
|
|
716
|
+
extent : tuple
|
|
717
|
+
Raster extent (Left, Bottom, Right, Top).
|
|
718
|
+
lines : dataFrame
|
|
719
|
+
Extracted lineament features.
|
|
720
|
+
im_prewitt : numpy.ndarray
|
|
721
|
+
Prewitt-filtered edge image.
|
|
722
|
+
im_prewitt_clip : numpy.ndarray
|
|
723
|
+
Thresholded edge map.
|
|
724
|
+
container : list
|
|
725
|
+
Intermediate lineament point container.
|
|
726
|
+
"""
|
|
727
|
+
|
|
508
728
|
|
|
509
729
|
path = os.path.normpath(path)
|
|
510
730
|
|
|
@@ -522,10 +742,10 @@ def dem_to_line(path,
|
|
|
522
742
|
if len(lines) > 0:
|
|
523
743
|
_,_,lines = reduce_lines(lines, extent=extent, dem_shape=dem.shape, min_dist=min_dist, seg_len=seg_len)
|
|
524
744
|
fname = path.split('\\')[-1].split('.')[0]
|
|
525
|
-
|
|
526
745
|
# fname = path.replace('.tiff', '.csv')
|
|
527
746
|
lines['crs'] = crs_espg
|
|
528
|
-
|
|
747
|
+
if save_csv:
|
|
748
|
+
lines.to_csv(f'{tempfolder}\\{fname}.csv', index=False)
|
|
529
749
|
return dem, extent, lines, im_prewitt, im_prewitt_clip, container
|
|
530
750
|
|
|
531
751
|
else:
|
|
@@ -533,18 +753,55 @@ def dem_to_line(path,
|
|
|
533
753
|
return dem, extent, lines, im_prewitt, im_prewitt_clip, container
|
|
534
754
|
|
|
535
755
|
|
|
536
|
-
def
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
756
|
+
def dem_to_shp(im_path,
|
|
757
|
+
tempfolder='temp',
|
|
758
|
+
eps=1.2,
|
|
759
|
+
thresh=40,
|
|
760
|
+
min_dist=10,
|
|
761
|
+
seg_len=10,
|
|
762
|
+
split_size=500,
|
|
763
|
+
z_multip=1,
|
|
764
|
+
downscale = 1,
|
|
765
|
+
shp_name = None,
|
|
766
|
+
save_to_file=True,
|
|
767
|
+
keep_intermediate_file = False,
|
|
768
|
+
n_job = -1):
|
|
769
|
+
"""
|
|
770
|
+
Extract lineaments from a (large) DEM and export results as a shapefile.
|
|
771
|
+
This will involve image splitting, and the use of parallel computing to achieve calculation efficiency.
|
|
772
|
+
|
|
773
|
+
Parameters
|
|
774
|
+
----------
|
|
775
|
+
im_path : str
|
|
776
|
+
Path to input DEM.
|
|
777
|
+
tempfolder : str, optional
|
|
778
|
+
Folder for temporary files (default is 'temp').
|
|
779
|
+
eps : float, optional
|
|
780
|
+
Edge detection sensitivity (default is 1.2).
|
|
781
|
+
thresh : float, optional
|
|
782
|
+
Edge threshold value (default is 40).
|
|
783
|
+
min_dist : float, optional
|
|
784
|
+
Minimum distance between lines (default is 10).
|
|
785
|
+
seg_len : float, optional
|
|
786
|
+
Minimum line segment length (default is 10).
|
|
787
|
+
split_size : int, optional
|
|
788
|
+
DEM tile size for splitting (default is 500).
|
|
789
|
+
z_multip : float, optional
|
|
790
|
+
Vertical exaggeration factor (default is 1).
|
|
791
|
+
downscale : float, optional
|
|
792
|
+
Downscaling factor for DEM (default is 1).
|
|
793
|
+
shp_name : str, optional
|
|
794
|
+
Output shapefile name.
|
|
795
|
+
save_to_file : bool, optional
|
|
796
|
+
Whether to save shapefile to disk (default is True).
|
|
797
|
+
keep_intermediate_file : bool, optional
|
|
798
|
+
Retain temporary files (default is False).
|
|
799
|
+
|
|
800
|
+
Returns
|
|
801
|
+
-------
|
|
802
|
+
lines : dataFrame
|
|
803
|
+
Extracted lineament shapefile.
|
|
804
|
+
"""
|
|
548
805
|
|
|
549
806
|
im_path = os.path.normpath(im_path)
|
|
550
807
|
|
|
@@ -568,13 +825,20 @@ def dem_to_line_runner(im_path,
|
|
|
568
825
|
[min_dist]*len(flist),
|
|
569
826
|
[seg_len]*len(flist),
|
|
570
827
|
[z_multip]*len(flist),
|
|
571
|
-
[downscale]*len(flist)
|
|
828
|
+
[downscale]*len(flist),
|
|
829
|
+
[True]*len(flist))).values
|
|
572
830
|
|
|
573
831
|
from joblib import Parallel, delayed
|
|
574
|
-
Parallel(n_jobs
|
|
832
|
+
Parallel(n_jobs=n_job)(delayed(dem_to_line)(*c) for c in cases)
|
|
833
|
+
|
|
834
|
+
fname = im_path.split('\\')[-1].split('.')[0]
|
|
835
|
+
csv_name = f'{tempfolder}\\{fname}.csv'
|
|
836
|
+
|
|
837
|
+
if shp_name == None:
|
|
838
|
+
shp_name = f'{fname}'
|
|
575
839
|
|
|
576
840
|
df = merge_lines_csv_to_shp(tempfolder=tempfolder,
|
|
577
|
-
shppath=
|
|
841
|
+
shppath=shp_name,
|
|
578
842
|
save_to_file=save_to_file)
|
|
579
843
|
|
|
580
844
|
|
|
@@ -590,17 +854,51 @@ def dem_to_line_runner(im_path,
|
|
|
590
854
|
return df
|
|
591
855
|
|
|
592
856
|
|
|
593
|
-
def
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
857
|
+
def dem_to_shp_small(im_path,
|
|
858
|
+
tempfolder='temp',
|
|
859
|
+
eps=1.2,
|
|
860
|
+
thresh=40,
|
|
861
|
+
min_dist=10,
|
|
862
|
+
seg_len=10,
|
|
863
|
+
z_multip=1,
|
|
864
|
+
downscale=1,
|
|
865
|
+
shp_name=None,
|
|
866
|
+
save_to_file=True):
|
|
603
867
|
|
|
868
|
+
"""
|
|
869
|
+
Extract lineaments from a (small) DEM and export results as a shapefile.
|
|
870
|
+
The image will not splited, and be run as single chunk. This process only use single core,
|
|
871
|
+
therefore calculation for large dataset will took very long time.
|
|
872
|
+
|
|
873
|
+
Parameters
|
|
874
|
+
----------
|
|
875
|
+
im_path : str
|
|
876
|
+
Path to the input DEM file.
|
|
877
|
+
tempfolder : str, optional
|
|
878
|
+
Folder for temporary outputs (default is 'temp').
|
|
879
|
+
eps : float, optional
|
|
880
|
+
Edge detection sensitivity (default is 1.2).
|
|
881
|
+
thresh : float, optional
|
|
882
|
+
Edge threshold value (default is 40).
|
|
883
|
+
min_dist : float, optional
|
|
884
|
+
Minimum distance between line segments (default is 10).
|
|
885
|
+
seg_len : float, optional
|
|
886
|
+
Minimum segment length (default is 10).
|
|
887
|
+
z_multip : float, optional
|
|
888
|
+
Vertical exaggeration multiplier (default is 1).
|
|
889
|
+
downscale : float, optional
|
|
890
|
+
DEM downscaling factor (default is 1).
|
|
891
|
+
shp_name : str, optional
|
|
892
|
+
Output shapefile name.
|
|
893
|
+
save_to_file : bool, optional
|
|
894
|
+
Whether to save shapefile to disk (default is True).
|
|
895
|
+
|
|
896
|
+
Returns
|
|
897
|
+
-------
|
|
898
|
+
lines : dataFrame
|
|
899
|
+
Extracted lineament features.
|
|
900
|
+
"""
|
|
901
|
+
|
|
604
902
|
im_path = os.path.normpath(im_path)
|
|
605
903
|
if os.path.isdir(tempfolder):
|
|
606
904
|
'delete content'
|
|
@@ -618,7 +916,8 @@ def dem_to_line_runner_small(im_path,
|
|
|
618
916
|
min_dist=min_dist,
|
|
619
917
|
seg_len=seg_len,
|
|
620
918
|
z_multip=z_multip,
|
|
621
|
-
downscale=downscale
|
|
919
|
+
downscale=downscale,
|
|
920
|
+
save_csv=True)
|
|
622
921
|
|
|
623
922
|
fname = im_path.split('\\')[-1].split('.')[0]
|
|
624
923
|
csv_name = f'{tempfolder}\\{fname}.csv'
|
|
@@ -637,5 +936,5 @@ def dem_to_line_runner_small(im_path,
|
|
|
637
936
|
os.removedirs(tempfolder)
|
|
638
937
|
|
|
639
938
|
|
|
640
|
-
return
|
|
939
|
+
return lines
|
|
641
940
|
|
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import numpy as np
|
|
3
3
|
import pyqtgraph as pg
|
|
4
|
-
from PyQt5.QtWidgets import (QApplication,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
from PyQt5.QtWidgets import (QApplication,
|
|
5
|
+
QMainWindow,
|
|
6
|
+
QWidget,
|
|
7
|
+
QVBoxLayout,
|
|
8
|
+
QPushButton,
|
|
9
|
+
QFileDialog,
|
|
10
|
+
QTabWidget,
|
|
11
|
+
QHBoxLayout,
|
|
12
|
+
QSlider,
|
|
13
|
+
QLabel,
|
|
14
|
+
QComboBox,
|
|
15
|
+
QCheckBox,
|
|
16
|
+
QSplitter,
|
|
17
|
+
QMessageBox )
|
|
18
|
+
|
|
19
|
+
from PyQt5.QtCore import QThreadPool, QRunnable
|
|
12
20
|
|
|
13
21
|
|
|
14
22
|
from skimage.transform import rescale
|
|
23
|
+
|
|
15
24
|
from PyQt5.QtCore import Qt
|
|
25
|
+
|
|
16
26
|
import numpy as np
|
|
17
27
|
import pandas as pd
|
|
18
|
-
from pylineament import (dem_to_line,
|
|
28
|
+
from pylineament import (dem_to_line,
|
|
29
|
+
read_raster,
|
|
19
30
|
extract_lineament_points,
|
|
20
31
|
convert_points_to_line,
|
|
21
|
-
|
|
22
|
-
dem_to_line_runner_small,
|
|
23
|
-
reduce_lines,
|
|
24
|
-
image_splitting,
|
|
32
|
+
reduce_lines,
|
|
25
33
|
merge_lines_csv_to_shp,
|
|
26
|
-
merge_single_csv_to_shp,
|
|
27
34
|
hillshade)
|
|
28
35
|
import os
|
|
29
36
|
|
|
30
37
|
|
|
31
|
-
|
|
32
38
|
class ImageSplitterParallel(QRunnable):
|
|
33
39
|
def __init__(self, i, crs, im, transform ,temp_folder):
|
|
34
40
|
super().__init__()
|
|
@@ -64,10 +70,10 @@ class ImageSplitterParallel(QRunnable):
|
|
|
64
70
|
|
|
65
71
|
new_dataset.write(Z, 1)
|
|
66
72
|
new_dataset.close()
|
|
67
|
-
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
class MainWindow(QMainWindow):
|
|
76
|
+
|
|
71
77
|
def __init__(self):
|
|
72
78
|
super().__init__()
|
|
73
79
|
self.threadpool = QThreadPool.globalInstance()
|
|
@@ -94,6 +100,8 @@ class MainWindow(QMainWindow):
|
|
|
94
100
|
self.previewRegen.clicked.connect(self.previewRegenActionAndSmallCalc)
|
|
95
101
|
self.extractButon.clicked.connect(self.extractLineamentAction)
|
|
96
102
|
|
|
103
|
+
self.aboutButton.clicked.connect(self.aboutAction)
|
|
104
|
+
|
|
97
105
|
self.previewImageOverlaySelector.currentIndexChanged.connect(self.previewRegenAction)
|
|
98
106
|
self.previewRegionSelector.currentIndexChanged.connect(self.previewRegenAction)
|
|
99
107
|
self.hillshadeAngleSelector.currentIndexChanged.connect(self.previewRegenAction)
|
|
@@ -143,6 +151,9 @@ class MainWindow(QMainWindow):
|
|
|
143
151
|
|
|
144
152
|
self.resetButton = QPushButton('Reset to Default')
|
|
145
153
|
self.previewRegen = QPushButton('Regenerate Preview')
|
|
154
|
+
self.aboutButton = QPushButton('?')
|
|
155
|
+
self.aboutButton.setMaximumWidth(20)
|
|
156
|
+
|
|
146
157
|
self.extractButon = QPushButton('Extract Lineament')
|
|
147
158
|
|
|
148
159
|
self.subsetSideSlider, subsetSideLabel, subsetSideLayout = self.add_slider("Image Subset size", 500, 100, 2000, 50)
|
|
@@ -250,6 +261,7 @@ class MainWindow(QMainWindow):
|
|
|
250
261
|
leftLayout.addWidget(self.resetButton)
|
|
251
262
|
# leftLayout.addWidget(self.previewRegen)
|
|
252
263
|
leftLayout.addStretch()
|
|
264
|
+
leftLayout.addWidget(self.aboutButton)
|
|
253
265
|
leftLayout.addWidget(self.extractButon)
|
|
254
266
|
|
|
255
267
|
|
|
@@ -266,8 +278,19 @@ class MainWindow(QMainWindow):
|
|
|
266
278
|
def loadImageAndPreview(self):
|
|
267
279
|
sz = self.subsetSideSlider.value() * self.subsetSideSlider.step
|
|
268
280
|
|
|
281
|
+
|
|
269
282
|
self.regions, self.dem, self.extent, self.crs_espg = read_raster(self.file_name, split_size=sz)
|
|
270
283
|
|
|
284
|
+
imgFormat = self.file_name.split('.')[-1]
|
|
285
|
+
if (imgFormat == 'jpg') or (imgFormat == 'jpeg'):
|
|
286
|
+
from PIL import Image
|
|
287
|
+
i = Image.open( r"C:\Users\user\Downloads\WhatsApp Image 2025-09-23 at 16.34.01.jpeg" )
|
|
288
|
+
self.orgImg = np.transpose(np.array(i),( 1,0,2))[:,::-1]
|
|
289
|
+
|
|
290
|
+
else:
|
|
291
|
+
|
|
292
|
+
self.orgImg = self.dem[::-1].T
|
|
293
|
+
|
|
271
294
|
L,R,B,T = self.extent
|
|
272
295
|
|
|
273
296
|
if (L == 0) & (T==0):
|
|
@@ -275,7 +298,9 @@ class MainWindow(QMainWindow):
|
|
|
275
298
|
|
|
276
299
|
|
|
277
300
|
W, H = abs(L-R), abs(B-T)
|
|
278
|
-
img = pg.ImageItem(self.dem[::-1].T)
|
|
301
|
+
# img = pg.ImageItem(self.dem[::-1].T)
|
|
302
|
+
img = pg.ImageItem(self.orgImg)
|
|
303
|
+
|
|
279
304
|
img.setRect(L,B,W,H)
|
|
280
305
|
|
|
281
306
|
self.previewBigImg.clear()
|
|
@@ -425,7 +450,6 @@ class MainWindow(QMainWindow):
|
|
|
425
450
|
hs = hs.replace(' ', '').split('-')
|
|
426
451
|
avgAng = (float(hs[0]) + float(hs[1])) /2
|
|
427
452
|
|
|
428
|
-
|
|
429
453
|
if overlay == 'original image':
|
|
430
454
|
img = self.demSubset
|
|
431
455
|
|
|
@@ -757,10 +781,35 @@ class MainWindow(QMainWindow):
|
|
|
757
781
|
os.removedirs(tempfolder)
|
|
758
782
|
|
|
759
783
|
|
|
760
|
-
|
|
761
|
-
|
|
784
|
+
def aboutAction(self):
|
|
785
|
+
msg = QMessageBox(self)
|
|
786
|
+
msg.setWindowTitle("About PyLineament")
|
|
787
|
+
msg.setTextFormat(Qt.RichText) # enable HTML formatting
|
|
788
|
+
msg.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse)
|
|
789
|
+
msg.setStandardButtons(QMessageBox.Ok)
|
|
790
|
+
msg.setText(
|
|
791
|
+
"""<b>PyLineament</b><br><br>
|
|
792
|
+
PyLineament is an open-source Python toolkit for
|
|
793
|
+
<b>automatic and regional-scale lineament extraction</b>
|
|
794
|
+
from DEMs and remote sensing imagery.<br><br>
|
|
795
|
+
Version: 1.0.0<br>
|
|
796
|
+
Author: Epo Prasetya Kusumah - Universitas Pertamina<br>
|
|
797
|
+
License: MIT<br>
|
|
798
|
+
GitHub: <a href='https://github.com/epokus/pylineament'>github.com/epokus/pylineament</a><br><br>
|
|
799
|
+
<b>How to cite:</b><br>
|
|
800
|
+
Kusumah, E. P., (2025). <i>PyLineament: A Python Toolkit for Regional-Scale Lineament Extraction</i>.
|
|
801
|
+
Version 1.0. <a href='https://github.com/epokus/pylineament'>https://github.com/epokus/pylineament</a><br><br>
|
|
802
|
+
© 2025 PyLineament Developers
|
|
803
|
+
"""
|
|
804
|
+
)
|
|
805
|
+
msg.exec_()
|
|
806
|
+
|
|
807
|
+
def main():
|
|
762
808
|
app = QApplication(sys.argv)
|
|
763
809
|
window = MainWindow()
|
|
764
810
|
window.show()
|
|
765
811
|
sys.exit(app.exec_())
|
|
766
812
|
|
|
813
|
+
|
|
814
|
+
if __name__ == "__main__":
|
|
815
|
+
main()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pylineament"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "Python Geological Lineament extraction package"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name="Epo Kusumah", email="epo.pk@universiaspertamina.ac.id" }]
|
|
@@ -47,6 +47,6 @@ dependencies = [
|
|
|
47
47
|
Homepage = "https://github.com/epokus/pylineament"
|
|
48
48
|
|
|
49
49
|
[project.scripts]
|
|
50
|
-
|
|
50
|
+
pylineament = "pylineament.pylineament_ui:main"
|
|
51
51
|
|
|
52
52
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|