geopic-tag-reader 1.7.0__py3-none-any.whl → 1.8.0__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.
- geopic_tag_reader/__init__.py +1 -1
- geopic_tag_reader/sequence.py +87 -33
- geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.po +2 -2
- geopic_tag_reader/translations/geopic_tag_reader.pot +1 -1
- {geopic_tag_reader-1.7.0.dist-info → geopic_tag_reader-1.8.0.dist-info}/METADATA +2 -1
- {geopic_tag_reader-1.7.0.dist-info → geopic_tag_reader-1.8.0.dist-info}/RECORD +10 -10
- {geopic_tag_reader-1.7.0.dist-info → geopic_tag_reader-1.8.0.dist-info}/WHEEL +0 -0
- {geopic_tag_reader-1.7.0.dist-info → geopic_tag_reader-1.8.0.dist-info}/entry_points.txt +0 -0
- {geopic_tag_reader-1.7.0.dist-info → geopic_tag_reader-1.8.0.dist-info}/licenses/LICENSE +0 -0
geopic_tag_reader/__init__.py
CHANGED
geopic_tag_reader/sequence.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Optional, List, Tuple
|
|
4
4
|
from pathlib import PurePath
|
|
5
5
|
from geopic_tag_reader.reader import GeoPicTags
|
|
6
6
|
import datetime
|
|
7
|
+
from rtree import index
|
|
7
8
|
import math
|
|
8
9
|
|
|
9
10
|
|
|
@@ -37,6 +38,7 @@ class SplitParams:
|
|
|
37
38
|
class Picture:
|
|
38
39
|
filename: str
|
|
39
40
|
metadata: GeoPicTags
|
|
41
|
+
heading_computed: bool = False
|
|
40
42
|
|
|
41
43
|
def distance_to(self, other) -> float:
|
|
42
44
|
"""Computes distance in meters based on Haversine formula"""
|
|
@@ -50,7 +52,7 @@ class Picture:
|
|
|
50
52
|
distance = R * c
|
|
51
53
|
return distance
|
|
52
54
|
|
|
53
|
-
def rotation_angle(self, other) -> Optional[
|
|
55
|
+
def rotation_angle(self, other) -> Optional[int]:
|
|
54
56
|
return rotation_angle(self.metadata.heading, other.metadata.heading)
|
|
55
57
|
|
|
56
58
|
|
|
@@ -116,11 +118,19 @@ class Sequence:
|
|
|
116
118
|
return (otherSeq.from_ts() - self.to_ts(), otherSeq.pictures[0].distance_to(self.pictures[-1])) # type: ignore
|
|
117
119
|
|
|
118
120
|
|
|
121
|
+
@dataclass
|
|
122
|
+
class Duplicate:
|
|
123
|
+
picture: Picture
|
|
124
|
+
duplicate_of: Picture
|
|
125
|
+
distance: float
|
|
126
|
+
angle: Optional[int]
|
|
127
|
+
|
|
128
|
+
|
|
119
129
|
@dataclass
|
|
120
130
|
class DispatchReport:
|
|
121
131
|
sequences: List[Sequence]
|
|
122
|
-
duplicate_pictures:
|
|
123
|
-
sequences_splits:
|
|
132
|
+
duplicate_pictures: List[Duplicate] = field(default_factory=list)
|
|
133
|
+
sequences_splits: List[Split] = field(default_factory=list)
|
|
124
134
|
|
|
125
135
|
|
|
126
136
|
def sort_pictures(pictures: List[Picture], method: Optional[SortMethod] = SortMethod.time_asc) -> List[Picture]:
|
|
@@ -202,7 +212,38 @@ def sort_pictures(pictures: List[Picture], method: Optional[SortMethod] = SortMe
|
|
|
202
212
|
return pictures
|
|
203
213
|
|
|
204
214
|
|
|
205
|
-
def
|
|
215
|
+
def are_duplicates(a: Picture, b: Picture, params: MergeParams) -> Optional[Tuple[float, Optional[int]]]:
|
|
216
|
+
"""
|
|
217
|
+
Check if 2 pictures are too similar and should be considered duplicates
|
|
218
|
+
|
|
219
|
+
They are duplicates if they are close to each other, and for non 360 pictures, if they are roughly in the same direction.
|
|
220
|
+
|
|
221
|
+
Note that we only consider the direction (also called heading) if it is provided by the camera (and not computed with the sequences geometries)
|
|
222
|
+
since GPS can drift a bit resulting in erratic direction when waiting at a traffic light cf https://gitlab.com/panoramax/server/api/-/issues/231#note_2329723526
|
|
223
|
+
|
|
224
|
+
Return None if not duplicates, or the distance/angle if they are
|
|
225
|
+
"""
|
|
226
|
+
dist = a.distance_to(b)
|
|
227
|
+
|
|
228
|
+
if params.maxDistance is None or dist > params.maxDistance:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
# Compare angle (if available on both images)
|
|
232
|
+
angle = a.rotation_angle(b)
|
|
233
|
+
# if one of the heading has been computed, we cannot rely on this angle being correct, so we don't consider it for the deduplication
|
|
234
|
+
# it's especially important when stopped and the GPS drift a bit, cf https://gitlab.com/panoramax/server/api/-/issues/231#note_2329723526
|
|
235
|
+
angle_computed = b.heading_computed or a.heading_computed
|
|
236
|
+
if angle is None or angle_computed or params.maxRotationAngle is None:
|
|
237
|
+
return (dist, None)
|
|
238
|
+
if angle <= params.maxRotationAngle:
|
|
239
|
+
return (dist, angle)
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
APPROX_DEGREE_TO_METER = 0.00001 # this is roughly 1m
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def find_duplicates(pictures: List[Picture], params: Optional[MergeParams] = None) -> Tuple[List[Picture], List[Duplicate]]:
|
|
206
247
|
"""
|
|
207
248
|
Finds too similar pictures.
|
|
208
249
|
Note that input list should be properly sorted.
|
|
@@ -217,42 +258,51 @@ def find_duplicates(pictures: List[Picture], params: Optional[MergeParams] = Non
|
|
|
217
258
|
(Non-duplicates pictures, Duplicates pictures)
|
|
218
259
|
"""
|
|
219
260
|
|
|
220
|
-
if params is None or not params.is_merge_needed():
|
|
261
|
+
if params is None or not params.is_merge_needed() or not pictures:
|
|
221
262
|
return (pictures, [])
|
|
263
|
+
assert params.maxDistance
|
|
222
264
|
|
|
223
265
|
nonDups: List[Picture] = []
|
|
224
|
-
|
|
225
|
-
|
|
266
|
+
duplicates = []
|
|
267
|
+
duplicates_idx = set()
|
|
226
268
|
|
|
227
|
-
for i,
|
|
228
|
-
if i == 0:
|
|
229
|
-
nonDups.append(currentPic)
|
|
230
|
-
continue
|
|
269
|
+
rtree_index = index.Index(((i, (p.metadata.lon, p.metadata.lat, p.metadata.lon, p.metadata.lat), None) for i, p in enumerate(pictures)))
|
|
231
270
|
|
|
232
|
-
|
|
271
|
+
# the rtree will give us all the neighbors in an approximated bounding box,
|
|
272
|
+
# and will check, for all those pictures if some pictures are really closed, using a real haversine distance
|
|
273
|
+
# we do a rough conversion between the maxDistance (in m) to degree, since it's only for the initial bounding box
|
|
274
|
+
# and we use a bbox bigger than necessary (could be half by direction) to not miss duplicates due to the degree to meter approximation
|
|
233
275
|
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
bounding_box_tolerance_approx = params.maxDistance * APPROX_DEGREE_TO_METER
|
|
277
|
+
for i, currentPic in enumerate(pictures):
|
|
278
|
+
if i in duplicates_idx:
|
|
279
|
+
# the picture has already been flagged as duplicate by one of its neighbor, we can skip it
|
|
236
280
|
continue
|
|
237
281
|
|
|
238
|
-
|
|
282
|
+
bounding_box = (
|
|
283
|
+
currentPic.metadata.lon - bounding_box_tolerance_approx,
|
|
284
|
+
currentPic.metadata.lat - bounding_box_tolerance_approx,
|
|
285
|
+
currentPic.metadata.lon + bounding_box_tolerance_approx,
|
|
286
|
+
currentPic.metadata.lat + bounding_box_tolerance_approx,
|
|
287
|
+
)
|
|
239
288
|
|
|
240
|
-
|
|
241
|
-
dist = prevPic.distance_to(currentPic)
|
|
289
|
+
near_pics_idx = rtree_index.nearest(bounding_box, num_results=100, objects=False)
|
|
242
290
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if
|
|
247
|
-
|
|
291
|
+
for neighbor_idx in near_pics_idx:
|
|
292
|
+
if neighbor_idx == i:
|
|
293
|
+
continue
|
|
294
|
+
if neighbor_idx in duplicates_idx:
|
|
295
|
+
continue
|
|
296
|
+
neighbor = pictures[neighbor_idx]
|
|
297
|
+
duplicate_details = are_duplicates(currentPic, neighbor, params)
|
|
298
|
+
if duplicate_details:
|
|
299
|
+
distance, angle = duplicate_details
|
|
300
|
+
duplicates_idx.add(neighbor_idx)
|
|
301
|
+
duplicates.append(Duplicate(picture=neighbor, duplicate_of=currentPic, distance=round(distance, 2), angle=angle))
|
|
248
302
|
|
|
249
|
-
|
|
250
|
-
dups.append(currentPic)
|
|
251
|
-
else:
|
|
252
|
-
lastNonDuplicatedPicId = i
|
|
253
|
-
nonDups.append(currentPic)
|
|
303
|
+
nonDups.append(currentPic)
|
|
254
304
|
|
|
255
|
-
return (nonDups,
|
|
305
|
+
return (nonDups, duplicates)
|
|
256
306
|
|
|
257
307
|
|
|
258
308
|
def split_in_sequences(pictures: List[Picture], splitParams: Optional[SplitParams] = SplitParams()) -> Tuple[List[Sequence], List[Split]]:
|
|
@@ -354,10 +404,14 @@ def dispatch_pictures(
|
|
|
354
404
|
# Sort
|
|
355
405
|
myPics = sort_pictures(pictures, sortMethod)
|
|
356
406
|
|
|
357
|
-
# De-duplicate
|
|
358
|
-
(myPics, dupsPics) = find_duplicates(myPics, mergeParams)
|
|
359
|
-
|
|
360
407
|
# Split in sequences
|
|
361
408
|
(mySeqs, splits) = split_in_sequences(myPics, splitParams)
|
|
362
409
|
|
|
363
|
-
|
|
410
|
+
# De-duplicate inside each sequences
|
|
411
|
+
dups_pics = []
|
|
412
|
+
for s in mySeqs:
|
|
413
|
+
(myPics, dups) = find_duplicates(s.pictures, mergeParams)
|
|
414
|
+
s.pictures = myPics
|
|
415
|
+
dups_pics.extend(dups)
|
|
416
|
+
|
|
417
|
+
return DispatchReport(sequences=mySeqs, duplicate_pictures=dups_pics, sequences_splits=splits)
|
|
Binary file
|
|
@@ -7,8 +7,8 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: PACKAGE VERSION\n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date: 2025-08-25 15:
|
|
11
|
-
"PO-Revision-Date: 2025-08-25 15:
|
|
10
|
+
"POT-Creation-Date: 2025-08-25 15:35+0200\n"
|
|
11
|
+
"PO-Revision-Date: 2025-08-25 15:35+0200\n"
|
|
12
12
|
"Last-Translator: Automatically generated\n"
|
|
13
13
|
"Language-Team: none\n"
|
|
14
14
|
"Language: en\n"
|
|
@@ -8,7 +8,7 @@ msgid ""
|
|
|
8
8
|
msgstr ""
|
|
9
9
|
"Project-Id-Version: PACKAGE VERSION\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: \n"
|
|
11
|
-
"POT-Creation-Date: 2025-08-25 15:
|
|
11
|
+
"POT-Creation-Date: 2025-08-25 15:35+0200\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: geopic-tag-reader
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.0
|
|
4
4
|
Summary: GeoPicTagReader
|
|
5
5
|
Author-email: Adrien PAVIE <panieravide@riseup.net>
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -14,6 +14,7 @@ Requires-Dist: timezonefinder == 6.5.9
|
|
|
14
14
|
Requires-Dist: pytz ~= 2025.2
|
|
15
15
|
Requires-Dist: types-pytz ~= 2025.2.0
|
|
16
16
|
Requires-Dist: types-python-dateutil ~= 2.9.0
|
|
17
|
+
Requires-Dist: rtree ~= 1.4.0
|
|
17
18
|
Requires-Dist: flit ~= 3.8.0 ; extra == "build"
|
|
18
19
|
Requires-Dist: black ~= 25.1.0 ; extra == "dev"
|
|
19
20
|
Requires-Dist: mypy ~= 1.15.0 ; extra == "dev"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
geopic_tag_reader/__init__.py,sha256=
|
|
1
|
+
geopic_tag_reader/__init__.py,sha256=R1g8ksEjS4-95HpvS59-gaKksq0XeBHj281o0I285k0,47
|
|
2
2
|
geopic_tag_reader/camera.py,sha256=ucuAVsLq_luKe8-wFXd7ruPVyhVK43p9niyoCXkAysQ,4131
|
|
3
3
|
geopic_tag_reader/cameras.csv,sha256=mAV0sJvVwNGNnvUHnzBrac53VfhCmEV1Z5hvgm4ErRY,120476
|
|
4
4
|
geopic_tag_reader/i18n.py,sha256=LOLBj7eB_hpHTc5XdMP97EoWdD2kgmkP_uvJJDKEVsU,342
|
|
@@ -6,9 +6,9 @@ geopic_tag_reader/main.py,sha256=xeEXMq-fFu0CtUiAgAeS9mb872D65OAQup3UeF6w050,403
|
|
|
6
6
|
geopic_tag_reader/model.py,sha256=rsWVE3T1kpNsKXX8iv6xb_3PCVY6Ea7iU9WOqUgXklU,129
|
|
7
7
|
geopic_tag_reader/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
8
|
geopic_tag_reader/reader.py,sha256=xaRJwN9iftCMRyNN1BvsTZDVMlG_G3EaIvomz73PryM,33400
|
|
9
|
-
geopic_tag_reader/sequence.py,sha256=
|
|
9
|
+
geopic_tag_reader/sequence.py,sha256=hVSYmhVFlzrH7Ger7f19aB3G79EWWNeabHGj_kzmFjs,14443
|
|
10
10
|
geopic_tag_reader/writer.py,sha256=HdZenoY_5Qv1Kq0jedCJhVFDYsv0iQaCzB6necU_LrY,8793
|
|
11
|
-
geopic_tag_reader/translations/geopic_tag_reader.pot,sha256=
|
|
11
|
+
geopic_tag_reader/translations/geopic_tag_reader.pot,sha256=4zEdLOiQhD6lk8bMYEa2Z1CrafSwQpUuUWUpUWMyaMk,4622
|
|
12
12
|
geopic_tag_reader/translations/ar/LC_MESSAGES/geopic_tag_reader.mo,sha256=irAVZnI56p6_8zyput5y1r4zsQ7Pv-EFLQTOK2Gt2ug,321
|
|
13
13
|
geopic_tag_reader/translations/ar/LC_MESSAGES/geopic_tag_reader.po,sha256=rR1cRk9rtGmFfWQktCZKLAijHy0I-ndtkpI1KyYEjx4,4595
|
|
14
14
|
geopic_tag_reader/translations/br/LC_MESSAGES/geopic_tag_reader.mo,sha256=o9Rr5Polmq4nr_XMwsDuVq7SvsTFFvVMg9y1opxeMM4,321
|
|
@@ -17,8 +17,8 @@ geopic_tag_reader/translations/da/LC_MESSAGES/geopic_tag_reader.mo,sha256=vJACTG
|
|
|
17
17
|
geopic_tag_reader/translations/da/LC_MESSAGES/geopic_tag_reader.po,sha256=VY2ktiabJsSmDu7plGjtIzP_m8acOJSftnANqFtv1MM,6635
|
|
18
18
|
geopic_tag_reader/translations/de/LC_MESSAGES/geopic_tag_reader.mo,sha256=Y7nUp-_OhdZS3s0WjYn-Dyxu6MRU-NHaUOyazpRdlic,5137
|
|
19
19
|
geopic_tag_reader/translations/de/LC_MESSAGES/geopic_tag_reader.po,sha256=Ppn4A7n5m_bBePZuQ1Hq3jtf7ReS2C4VDRJLD-o9IRo,6954
|
|
20
|
-
geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.mo,sha256=
|
|
21
|
-
geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.po,sha256=
|
|
20
|
+
geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.mo,sha256=oQ7_-DVyqjedIguy3lHJeNTP-rz0psx05AL3cmMyGSI,4504
|
|
21
|
+
geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.po,sha256=bQbuqpy4TQvJg3PLJrh3QIuqFqWGvdNZktmV_Djyo5U,6289
|
|
22
22
|
geopic_tag_reader/translations/eo/LC_MESSAGES/geopic_tag_reader.mo,sha256=5cpIaSHwwcSwPNfJG_bLErZs8S-8dD4kYNHpSVWMong,4860
|
|
23
23
|
geopic_tag_reader/translations/eo/LC_MESSAGES/geopic_tag_reader.po,sha256=1KrHLPpTJCCb5cPC6Ss3n2PDZ2S_f-2EkLVv1XvZ9LI,6634
|
|
24
24
|
geopic_tag_reader/translations/es/LC_MESSAGES/geopic_tag_reader.mo,sha256=IizWkn7zrx5GC5jZNlpGQzOdJPAiMB9dCXoH2NzkXKY,3827
|
|
@@ -45,8 +45,8 @@ geopic_tag_reader/translations/ti/LC_MESSAGES/geopic_tag_reader.mo,sha256=arpJUs
|
|
|
45
45
|
geopic_tag_reader/translations/ti/LC_MESSAGES/geopic_tag_reader.po,sha256=moCvnE_DIZVfqIHAhuu8L4K8UOb-2RBrjirlWHwc0ko,4595
|
|
46
46
|
geopic_tag_reader/translations/zh_Hant/LC_MESSAGES/geopic_tag_reader.mo,sha256=6bKHZnihlDOQQ5IQMKIgWViL5BorECqJ2ERFkE4LC6s,326
|
|
47
47
|
geopic_tag_reader/translations/zh_Hant/LC_MESSAGES/geopic_tag_reader.po,sha256=QIiHRmrEHny4njQPBsj07fqtK2QFTgrAFc-E3s7ddJU,4600
|
|
48
|
-
geopic_tag_reader-1.
|
|
49
|
-
geopic_tag_reader-1.
|
|
50
|
-
geopic_tag_reader-1.
|
|
51
|
-
geopic_tag_reader-1.
|
|
52
|
-
geopic_tag_reader-1.
|
|
48
|
+
geopic_tag_reader-1.8.0.dist-info/entry_points.txt,sha256=c9YwjCNhxveDf-61_aSRlzcpoutvM6KQCerlzaVt_JU,64
|
|
49
|
+
geopic_tag_reader-1.8.0.dist-info/licenses/LICENSE,sha256=OCZiFd7ok-n5jly2LwP7hEjuUukkvSt5iMkK_cY_00o,1076
|
|
50
|
+
geopic_tag_reader-1.8.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
51
|
+
geopic_tag_reader-1.8.0.dist-info/METADATA,sha256=J5t4EMbkxZi6HwtyLkkK8dEFvsR0_V2uUWHqiMGKwB0,4669
|
|
52
|
+
geopic_tag_reader-1.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|