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/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
- def __init__(self, *args, **kw):
30
- super().__init__(*args, **kw)
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