wolfhece 2.2.33__py3-none-any.whl → 2.2.34__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.
- wolfhece/PyConfig.py +26 -3
- wolfhece/PyDraw.py +329 -4
- wolfhece/PyGui.py +35 -1
- wolfhece/PyPictures.py +420 -3
- wolfhece/PyVertexvectors.py +127 -11
- wolfhece/apps/version.py +1 -1
- wolfhece/textpillow.py +1 -1
- wolfhece/wolf_texture.py +81 -13
- wolfhece/wolf_zi_db.py +586 -2
- {wolfhece-2.2.33.dist-info → wolfhece-2.2.34.dist-info}/METADATA +1 -1
- {wolfhece-2.2.33.dist-info → wolfhece-2.2.34.dist-info}/RECORD +14 -14
- {wolfhece-2.2.33.dist-info → wolfhece-2.2.34.dist-info}/WHEEL +0 -0
- {wolfhece-2.2.33.dist-info → wolfhece-2.2.34.dist-info}/entry_points.txt +0 -0
- {wolfhece-2.2.33.dist-info → wolfhece-2.2.34.dist-info}/top_level.txt +0 -0
wolfhece/PyPictures.py
CHANGED
@@ -15,8 +15,17 @@ from exif import Image
|
|
15
15
|
from osgeo import ogr
|
16
16
|
from osgeo import osr
|
17
17
|
import wx
|
18
|
+
from pathlib import Path
|
19
|
+
import logging
|
20
|
+
import PIL.Image
|
21
|
+
from PIL import ExifTags
|
22
|
+
|
23
|
+
from shapely.geometry import Polygon
|
24
|
+
import numpy as np
|
18
25
|
|
19
26
|
from .PyTranslate import _
|
27
|
+
from .PyVertexvectors import Zones, zone, vector, wolfvertex as wv
|
28
|
+
from .Coordinates_operations import transform_coordinates
|
20
29
|
|
21
30
|
"""
|
22
31
|
Ajout des coordonnées GPS d'une photo en Lambert72 si n'existe pas
|
@@ -24,10 +33,418 @@ Ajout des coordonnées GPS d'une photo en Lambert72 si n'existe pas
|
|
24
33
|
!!! A COMPLETER !!!
|
25
34
|
|
26
35
|
"""
|
27
|
-
class Picture(wx.Frame):
|
36
|
+
# class Picture(wx.Frame):
|
37
|
+
|
38
|
+
# def __init__(self, *args, **kw):
|
39
|
+
# super().__init__(*args, **kw)
|
40
|
+
|
41
|
+
def find_gps_in_file(picture: str | Path) -> tuple[float, float, float]:
|
42
|
+
"""
|
43
|
+
Find GPS coordinates in the picture file.
|
44
|
+
Returns a tuple (latitude, longitude, altitude).
|
45
|
+
If not found, returns (None, None, None).
|
46
|
+
"""
|
47
|
+
try:
|
48
|
+
with PIL.Image.open(picture) as img:
|
49
|
+
if 'gps_latitude' in img.info and 'gps_longitude' in img.info:
|
50
|
+
lat = img.info['gps_latitude']
|
51
|
+
lon = img.info['gps_longitude']
|
52
|
+
alt = img.info.get('gps_altitude', 0.0)
|
53
|
+
return lat, lon, alt
|
54
|
+
else:
|
55
|
+
# Try to read EXIF data
|
56
|
+
exif_data = img._getexif()
|
57
|
+
if exif_data is not None:
|
58
|
+
for tag, value in exif_data.items():
|
59
|
+
if ExifTags.TAGS.get(tag) == 'GPSInfo':
|
60
|
+
gps_info = value
|
61
|
+
|
62
|
+
allgps = {ExifTags.GPSTAGS.get(k, k): v for k, v in gps_info.items() if k in ExifTags.GPSTAGS}
|
63
|
+
|
64
|
+
lat = allgps['GPSLatitude']
|
65
|
+
lon = allgps['GPSLongitude']
|
66
|
+
alt = allgps['GPSAltitude']
|
67
|
+
if lat and lon:
|
68
|
+
return lat, lon, alt
|
69
|
+
|
70
|
+
except Exception as e:
|
71
|
+
logging.error(f"Error reading GPS data from {picture}: {e}")
|
72
|
+
return None, None, None
|
73
|
+
|
74
|
+
def find_exif_in_file(picture: str | Path) -> dict:
|
75
|
+
"""
|
76
|
+
Find EXIF data in the picture file.
|
77
|
+
Returns a dictionary of EXIF data.
|
78
|
+
If not found, returns an empty dictionary.
|
79
|
+
"""
|
80
|
+
try:
|
81
|
+
with PIL.Image.open(picture) as img:
|
82
|
+
exif_data = img._getexif()
|
83
|
+
if exif_data is not None:
|
84
|
+
return {ExifTags.TAGS.get(tag, tag): value for tag, value in exif_data.items()}
|
85
|
+
except Exception as e:
|
86
|
+
logging.debug(f"Error reading EXIF data from {picture}: {e}")
|
87
|
+
return {}
|
88
|
+
|
89
|
+
def find_Lambert72_in_file(picture: str | Path) -> tuple[float, float]:
|
90
|
+
"""
|
91
|
+
Find Lambert72 coordinates in the picture file.
|
92
|
+
Returns a tuple (Lambert72X, Lambert72Y).
|
93
|
+
If not found, returns (None, None).
|
94
|
+
"""
|
95
|
+
try:
|
96
|
+
with PIL.Image.open(picture) as img:
|
97
|
+
x = img.get('Lambert72X')
|
98
|
+
y = img.get('Lambert72Y')
|
99
|
+
if x is not None and y is not None:
|
100
|
+
return x, y
|
101
|
+
except Exception as e:
|
102
|
+
logging.debug(f"Error reading Lambert72 data from {picture}: {e}")
|
103
|
+
return None, None
|
104
|
+
|
105
|
+
class PictureCollection(Zones):
|
106
|
+
"""
|
107
|
+
PictureCollection is a collection of pictures, inheriting from Zones.
|
108
|
+
"""
|
109
|
+
|
110
|
+
def __init__(self,
|
111
|
+
filename:str | Path='',
|
112
|
+
ox:float=0.,
|
113
|
+
oy:float=0.,
|
114
|
+
tx:float=0.,
|
115
|
+
ty:float=0.,
|
116
|
+
parent=None,
|
117
|
+
is2D=True,
|
118
|
+
idx: str = '',
|
119
|
+
plotted: bool = True,
|
120
|
+
mapviewer=None,
|
121
|
+
need_for_wx: bool = False,
|
122
|
+
bbox:Polygon = None,
|
123
|
+
find_minmax:bool = True,
|
124
|
+
shared:bool = False,
|
125
|
+
colors:dict = None):
|
126
|
+
|
127
|
+
super().__init__(filename=filename,
|
128
|
+
ox=ox,
|
129
|
+
oy=oy,
|
130
|
+
tx=tx,
|
131
|
+
ty=ty,
|
132
|
+
parent=parent,
|
133
|
+
is2D=is2D,
|
134
|
+
idx=idx,
|
135
|
+
plotted=plotted,
|
136
|
+
mapviewer=mapviewer,
|
137
|
+
need_for_wx=need_for_wx,
|
138
|
+
bbox=bbox,
|
139
|
+
find_minmax=find_minmax,
|
140
|
+
shared=shared,
|
141
|
+
colors=colors)
|
142
|
+
|
143
|
+
self._default_size = 200 # Taille par défaut des photos in meters
|
144
|
+
|
145
|
+
def hide_all_pictures(self):
|
146
|
+
"""
|
147
|
+
Hide all pictures in the collection.
|
148
|
+
"""
|
149
|
+
for zone in self.myzones:
|
150
|
+
for vector in zone.myvectors:
|
151
|
+
vector.myprop.imagevisible = False
|
152
|
+
self.reset_listogl()
|
153
|
+
self.find_minmax(True)
|
154
|
+
|
155
|
+
def show_all_pictures(self):
|
156
|
+
"""
|
157
|
+
Show all pictures in the collection.
|
158
|
+
"""
|
159
|
+
for zone in self.myzones:
|
160
|
+
for vector in zone.myvectors:
|
161
|
+
vector.myprop.imagevisible = True
|
162
|
+
self.reset_listogl()
|
163
|
+
self.find_minmax(True)
|
164
|
+
|
165
|
+
def extract_pictures(self, directory: str | Path):
|
166
|
+
"""
|
167
|
+
Extract all visible pictures from the collection to a directory.
|
168
|
+
"""
|
169
|
+
directory = Path(directory)
|
170
|
+
if not directory.exists():
|
171
|
+
logging.error(f"Directory {directory} does not exist.")
|
172
|
+
return
|
173
|
+
|
174
|
+
import shutil
|
175
|
+
|
176
|
+
for loczone in self.myzones:
|
177
|
+
for vector in loczone.myvectors:
|
178
|
+
if vector.myprop.imagevisible and vector.myprop.attachedimage:
|
179
|
+
picture_path = Path(vector.myprop.attachedimage)
|
180
|
+
if picture_path.exists():
|
181
|
+
new_path = directory / picture_path.name
|
182
|
+
# copy the picture file to the new path
|
183
|
+
shutil.copy(picture_path, new_path)
|
184
|
+
logging.info(f"Extracted {picture_path} to {new_path}")
|
185
|
+
else:
|
186
|
+
logging.error(f"Picture {picture_path} does not exist.")
|
187
|
+
|
188
|
+
extracted = Zones(idx = 'extract')
|
189
|
+
|
190
|
+
for loczone in self.myzones:
|
191
|
+
newzone = zone(name = loczone.myname)
|
192
|
+
|
193
|
+
for vector in loczone.myvectors:
|
194
|
+
if vector.myprop.imagevisible and vector.myprop.attachedimage:
|
195
|
+
|
196
|
+
picture_path = Path(vector.myprop.attachedimage)
|
197
|
+
new_path = directory / picture_path.name
|
198
|
+
newvec = vector.deepcopy()
|
199
|
+
newzone.add_vector(newvec, forceparent=True)
|
200
|
+
newvec.myprop.attachedimage = new_path
|
201
|
+
|
202
|
+
if newzone.nbvectors > 0:
|
203
|
+
extracted.add_zone(newzone, forceparent=True)
|
204
|
+
|
205
|
+
extracted.saveas(directory / 'extracted_pictures.vec')
|
206
|
+
|
207
|
+
def add_picture(self, picture: str | Path, x:float = None, y:float = None, name:str='', keyzone:str = None):
|
208
|
+
"""
|
209
|
+
Add a picture to the collection at coordinates (x, y).
|
210
|
+
"""
|
211
|
+
|
212
|
+
picture = Path(picture)
|
213
|
+
|
214
|
+
if not picture.exists():
|
215
|
+
logging.error(f"Picture {picture} does not exist.")
|
216
|
+
return
|
217
|
+
|
218
|
+
with PIL.Image.open(picture) as img:
|
219
|
+
width, height = img.size
|
220
|
+
scale = width / height
|
221
|
+
if width > height:
|
222
|
+
width = self._default_size
|
223
|
+
height = width / scale
|
224
|
+
else:
|
225
|
+
height = self._default_size
|
226
|
+
width = height * scale
|
227
|
+
|
228
|
+
if x is None or y is None:
|
229
|
+
x, y = self._find_coordinates_in_file(picture)
|
230
|
+
if x is None or y is None:
|
231
|
+
logging.error(f"Could not find coordinates in {picture}. Please provide coordinates.")
|
232
|
+
return
|
233
|
+
|
234
|
+
if keyzone is None:
|
235
|
+
keyzone = _('New gallery')
|
236
|
+
|
237
|
+
if keyzone not in self.mynames:
|
238
|
+
self.add_zone(zone(name= keyzone, parent=self))
|
239
|
+
|
240
|
+
if name == '':
|
241
|
+
name = picture.stem
|
242
|
+
|
243
|
+
vec = vector(name=name)
|
244
|
+
self[keyzone].add_vector(vec, forceparent=True)
|
245
|
+
|
246
|
+
|
247
|
+
vec.add_vertices_from_array(np.asarray([(x - width /2., y - height /2.),
|
248
|
+
(x + width /2., y - height /2.),
|
249
|
+
(x + width /2., y + height /2.),
|
250
|
+
(x - width /2., y + height /2.)]))
|
251
|
+
|
252
|
+
vec.closed = True
|
253
|
+
|
254
|
+
vec.myprop.image_attached_pointx = x
|
255
|
+
vec.myprop.image_attached_pointy = y
|
256
|
+
vec.myprop.attachedimage = picture
|
257
|
+
|
258
|
+
def _find_coordinates_in_file(self, picture: str | Path) -> tuple[float, float]:
|
259
|
+
"""
|
260
|
+
Find coordinates in the picture file.
|
261
|
+
Returns a tuple (x, y).
|
262
|
+
If not found, returns (None, None).
|
263
|
+
"""
|
264
|
+
lat, lon, alt = find_gps_in_file(picture)
|
265
|
+
x, y = find_Lambert72_in_file(picture)
|
266
|
+
|
267
|
+
def _santitize_coordinates(coord):
|
268
|
+
"""
|
269
|
+
Sanitize coordinates to ensure they are valid floats.
|
270
|
+
Convert tuple with degrees, minutes, seconds to float if needed.
|
271
|
+
"""
|
272
|
+
if isinstance(coord, (list, tuple)):
|
273
|
+
try:
|
274
|
+
coord = float(sum(c / (60 ** i) for i, c in enumerate(coord)))
|
275
|
+
return coord
|
276
|
+
except (ValueError, TypeError):
|
277
|
+
logging.error(f"Invalid coordinate format: {coord}. Expected a list or tuple of numbers.")
|
278
|
+
return None
|
279
|
+
|
280
|
+
return float(coord)
|
281
|
+
|
282
|
+
if lat is not None and lon is not None and x is None and y is None:
|
283
|
+
xy = transform_coordinates(np.asarray([[_santitize_coordinates(lon), _santitize_coordinates(lat)]]), inputEPSG='EPSG:4326', outputEPSG='EPSG:31370')
|
284
|
+
return xy[0,0], xy[0,1]
|
285
|
+
elif lat is None and lon is None and x is not None and y is not None:
|
286
|
+
return x, y
|
287
|
+
elif lat is not None and lon is not None and x is not None and y is not None:
|
288
|
+
# If both GPS and Lambert72 coordinates are found, prefer Lambert72
|
289
|
+
xy = transform_coordinates(np.asarray([[_santitize_coordinates(lon), _santitize_coordinates(lat)]]), inputEPSG='EPSG:4326', outputEPSG='EPSG:31370')
|
290
|
+
xtest, ytest = xy[0,0], xy[0,1]
|
291
|
+
if abs(x - xtest) < 1e-3 and abs(y - ytest) < 1e-3:
|
292
|
+
return x, y
|
293
|
+
else:
|
294
|
+
logging.warning(f"GPS coordinates ({lat}, {lon}) do not match Lambert72 coordinates ({x}, {y}). Using GPS coordinates.")
|
295
|
+
return xtest, ytest
|
296
|
+
else:
|
297
|
+
logging.error(f"Could not find coordinates in {picture}.")
|
298
|
+
return None, None
|
299
|
+
|
300
|
+
def load_from_url_zipfile(self, url: str):
|
301
|
+
"""
|
302
|
+
Load pictures from a zip file at a given URL.
|
303
|
+
The zip file should contain images with names that can be used as picture names.
|
304
|
+
"""
|
305
|
+
|
306
|
+
import requests
|
307
|
+
from zipfile import ZipFile
|
308
|
+
from io import BytesIO
|
309
|
+
from .pydownloader import DATADIR
|
310
|
+
|
311
|
+
response = requests.get(url)
|
312
|
+
if response.status_code != 200:
|
313
|
+
logging.error(f"Failed to download zip file from {url}. Status code: {response.status_code}")
|
314
|
+
return
|
315
|
+
|
316
|
+
# Extract images from the zip file and store them ion the DATADIR
|
317
|
+
with ZipFile(BytesIO(response.content)) as zip_file:
|
318
|
+
zip_file.extractall(DATADIR / 'pictures' / url.split('/')[-1].replace('.zip', ''))
|
319
|
+
directory = DATADIR / 'pictures' / url.split('/')[-1].replace('.zip', '')
|
320
|
+
if not directory.is_dir():
|
321
|
+
logging.error(f"Directory {directory} does not exist after extracting zip file.")
|
322
|
+
return
|
323
|
+
|
324
|
+
self.load_from_directory_georef_pictures(directory, keyzone=_('url'))
|
325
|
+
|
326
|
+
def load_from_directory_with_excel(self, excel_file: str | Path, sheet_name: str = 'Pictures'):
|
327
|
+
"""
|
328
|
+
Load pictures from an Excel file.
|
329
|
+
The Excel file should have columns for picture path, x, y, and name.
|
330
|
+
"""
|
331
|
+
import pandas as pd
|
332
|
+
|
333
|
+
df = pd.read_excel(excel_file, sheet_name=sheet_name)
|
334
|
+
|
335
|
+
for _, row in df.iterrows():
|
336
|
+
picture = row['Picture']
|
337
|
+
x = row.get('X', None)
|
338
|
+
y = row.get('Y', None)
|
339
|
+
name = row.get('Name', '')
|
340
|
+
|
341
|
+
self.add_picture(picture, x, y, name)
|
342
|
+
|
343
|
+
def load_from_directory_georef_pictures(self, directory: str | Path, keyzone: str = None):
|
344
|
+
"""
|
345
|
+
Load pictures from a directory.
|
346
|
+
The directory should contain images with names that can be used as picture names.
|
347
|
+
"""
|
348
|
+
|
349
|
+
directory = Path(directory)
|
350
|
+
if not directory.is_dir():
|
351
|
+
logging.error(f"Directory {directory} does not exist.")
|
352
|
+
return
|
353
|
+
|
354
|
+
if keyzone is None:
|
355
|
+
keyzone = _('New gallery')
|
356
|
+
|
357
|
+
if keyzone not in self.mynames:
|
358
|
+
self.add_zone(zone(name=keyzone, parent=self))
|
359
|
+
|
360
|
+
for picture in directory.glob('*'):
|
361
|
+
if picture.is_file() and picture.suffix.lower() in ['.jpg', '.jpeg', '.png', '.gif']:
|
362
|
+
x,y = self._find_coordinates_in_file(picture)
|
363
|
+
if x is None or y is None:
|
364
|
+
logging.error(f"Could not find coordinates in {picture}. Please provide coordinates.")
|
365
|
+
continue
|
366
|
+
self.add_picture(picture, keyzone=keyzone)
|
367
|
+
|
368
|
+
def load_from_directory_with_shapefile(self,
|
369
|
+
directory: str | Path,
|
370
|
+
shapefile: str | Path = None,
|
371
|
+
keyzone: str = None):
|
372
|
+
""" Load pictures from a directory and associate them with a shapefile. """
|
373
|
+
|
374
|
+
directory = Path(directory)
|
375
|
+
if not directory.is_dir():
|
376
|
+
logging.error(f"Directory {directory} does not exist.")
|
377
|
+
return
|
378
|
+
|
379
|
+
if shapefile is None:
|
380
|
+
shapefile = directory / 'SHP'
|
381
|
+
# list all shapefiles in the directory
|
382
|
+
shapefiles = list(shapefile.glob('*.shp'))
|
383
|
+
if not shapefiles:
|
384
|
+
logging.error(f"No shapefiles found in {directory}.")
|
385
|
+
return
|
386
|
+
shapefile = shapefiles[0]
|
387
|
+
logging.info(f"Using shapefile {shapefile}.")
|
388
|
+
|
389
|
+
shapefile = Path(shapefile)
|
390
|
+
if not shapefile.is_file():
|
391
|
+
logging.error(f"Shapefile {shapefile} does not exist.")
|
392
|
+
return
|
393
|
+
|
394
|
+
import geopandas as gpd
|
395
|
+
gdf = gpd.read_file(shapefile)
|
396
|
+
if gdf.empty:
|
397
|
+
logging.error(f"Shapefile {shapefile} is empty.")
|
398
|
+
return
|
399
|
+
|
400
|
+
possible_columns = ['name', 'path']
|
401
|
+
|
402
|
+
for col in possible_columns:
|
403
|
+
for idx, column in enumerate(gdf.columns):
|
404
|
+
if col.lower() == column.lower():
|
405
|
+
name_column = column
|
406
|
+
break
|
407
|
+
|
408
|
+
for _, row in gdf.iterrows():
|
409
|
+
picture_path = row[name_column]
|
410
|
+
if not isinstance(picture_path, str):
|
411
|
+
logging.error(f"Invalid picture path in shapefile: {picture_path}")
|
412
|
+
continue
|
413
|
+
|
414
|
+
picture_path = Path(picture_path).name
|
415
|
+
|
416
|
+
picture_path = directory / picture_path
|
417
|
+
if not picture_path.exists():
|
418
|
+
logging.error(f"Picture {picture_path} does not exist.")
|
419
|
+
continue
|
420
|
+
|
421
|
+
x, y = row.geometry.x, row.geometry.y
|
422
|
+
|
423
|
+
if x < -100_000. or y < -100_000.:
|
424
|
+
logging.warning(f"Invalid coordinates ({x}, {y}) for picture {picture_path}. Skipping.")
|
425
|
+
logging.warning(f"Trying to find coordinates in the picture file {picture_path}.")
|
426
|
+
x, y = self._find_coordinates_in_file(picture_path)
|
427
|
+
if x is None or y is None:
|
428
|
+
continue
|
429
|
+
|
430
|
+
name = picture_path.stem
|
431
|
+
|
432
|
+
self.add_picture(picture_path, x, y, name, keyzone=keyzone)
|
433
|
+
|
434
|
+
def scale_all_pictures(self, scale_factor: float):
|
435
|
+
""" Scale all vectors in the collection by a scale factor. """
|
436
|
+
|
437
|
+
for zone in self.myzones:
|
438
|
+
for vector in zone.myvectors:
|
439
|
+
# Move each point from the centroid to the new position
|
440
|
+
centroid = vector.centroid
|
28
441
|
|
29
|
-
|
30
|
-
|
442
|
+
for vertex in vector:
|
443
|
+
vertex.x = centroid.x + (vertex.x - centroid.x) * scale_factor
|
444
|
+
vertex.y = centroid.y + (vertex.y - centroid.y) * scale_factor
|
445
|
+
vector.reset_linestring()
|
446
|
+
self.reset_listogl()
|
447
|
+
self.find_minmax(True)
|
31
448
|
|
32
449
|
def main():
|
33
450
|
# Spatial Reference System
|