geopic-tag-reader 1.1.5__tar.gz → 1.3.0__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.
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/.gitlab-ci.yml +14 -14
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/CHANGELOG.md +29 -1
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/Makefile +1 -1
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/PKG-INFO +6 -6
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/README.md +5 -5
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/docs/index.md +6 -6
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/docs/tech/api_reference.md +4 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/__init__.py +1 -1
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/main.py +5 -1
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/reader.py +100 -70
- geopic_tag_reader-1.3.0/geopic_tag_reader/sequence.py +342 -0
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/de/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/de/LC_MESSAGES/geopic_tag_reader.po +169 -0
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.po +42 -30
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/es/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- geopic_tag_reader-1.1.5/geopic_tag_reader/translations/geopic_tag_reader.pot → geopic_tag_reader-1.3.0/geopic_tag_reader/translations/es/LC_MESSAGES/geopic_tag_reader.po +4 -5
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/fi/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/fi/LC_MESSAGES/geopic_tag_reader.po +146 -0
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/fr/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/translations/fr/LC_MESSAGES/geopic_tag_reader.po +5 -1
- geopic_tag_reader-1.3.0/geopic_tag_reader/translations/geopic_tag_reader.pot +159 -0
- geopic_tag_reader-1.1.5/geopic_tag_reader/translations/en/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- geopic_tag_reader-1.1.5/geopic_tag_reader/translations/fr/LC_MESSAGES/geopic_tag_reader.mo +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/.gitignore +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/.pre-commit-config.yaml +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/CODE_OF_CONDUCT.md +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/LICENSE +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/docs/develop.md +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/docs/install.md +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/docs/tech/cli.md +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/camera.py +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/i18n.py +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/model.py +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/py.typed +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/geopic_tag_reader/writer.py +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/mkdocs.yml +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/pyproject.toml +0 -0
- {geopic_tag_reader-1.1.5 → geopic_tag_reader-1.3.0}/pytest.ini +0 -0
|
@@ -38,20 +38,20 @@ tests-writer:
|
|
|
38
38
|
- make i18n-po2code
|
|
39
39
|
- pytest -s -vv
|
|
40
40
|
|
|
41
|
-
tests-deploy_pypi:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
# tests-deploy_pypi:
|
|
42
|
+
# stage: deploy
|
|
43
|
+
# image: python:3.8
|
|
44
|
+
# variables:
|
|
45
|
+
# FLIT_INDEX_URL: https://test.pypi.org/legacy/
|
|
46
|
+
# FLIT_USERNAME: $TEST_FLIT_USERNAME
|
|
47
|
+
# FLIT_PASSWORD: $TEST_FLIT_PASSWORD
|
|
48
|
+
# script:
|
|
49
|
+
# - apt update && apt install -y gcc git gettext
|
|
50
|
+
# - pip install .[build]
|
|
51
|
+
# - make i18n-po2code
|
|
52
|
+
# - flit publish
|
|
53
|
+
# only:
|
|
54
|
+
# - develop
|
|
55
55
|
|
|
56
56
|
deploy_pypi:
|
|
57
57
|
stage: deploy
|
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.3.0] - 2024-09-30
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Reader offers a new property `ts_by_source` to distinguish read datetime from GPS and camera.
|
|
15
|
+
- Sequence sorting uses same timestamp source through all pictures (GPS if available, camera else, fallback with other value if two timestamps are identical).
|
|
16
|
+
- Sequence splits uses whenever possible same timestamp source.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Documentation links were not up-to-date in README file.
|
|
21
|
+
- Sub-seconds values and time offset are applied only if part of the same EXIF group.
|
|
22
|
+
|
|
23
|
+
## [1.2.0] - 2024-07-30
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- A new module `sequence` is available through Python to dispatch a set of pictures (based on their metadata) into several sequences, based on de-duplicate and split parameters. This is based on existing code previously stored in [command-line client](https://gitlab.com/panoramax/clients/cli), moved here to be shared between API and CLI.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Reader offers a `yaw` value (360° sphere correction), distinct from `heading` (GPS direction). EXIF tags are read a bit differently to reflect this : `yaw` comes from `Xmp.Camera.Yaw & Xmp.GPano.PoseHeadingDegrees`, `heading` from `Exif.GPSInfo.GPSImgDirection & MAPCompassHeading`.
|
|
32
|
+
|
|
33
|
+
### Removed
|
|
34
|
+
- Previously used values `Exif.GPSInfo.GPS(Pitch|Roll)` and `Xmp.GPano.InitialView(Pitch|Roll)Degrees` are dropped, first one for not being standard, second one for not being correct to use for pitch/roll (prefer `Xmp.GPano.Pose(Pitch|Roll)Degrees`).
|
|
35
|
+
|
|
10
36
|
## [1.1.5] - 2024-07-10
|
|
11
37
|
|
|
12
38
|
### Fixed
|
|
@@ -199,7 +225,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
199
225
|
|
|
200
226
|
- EXIF tag reading methods extracted from [GeoVisio API](https://gitlab.com/panoramax/server/api)
|
|
201
227
|
|
|
202
|
-
[Unreleased]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.
|
|
228
|
+
[Unreleased]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.3.0...main
|
|
229
|
+
[1.3.0]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.2.0...1.3.0
|
|
230
|
+
[1.2.0]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.1.5...1.2.0
|
|
203
231
|
[1.1.5]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.1.4...1.1.5
|
|
204
232
|
[1.1.4]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.1.3...1.1.4
|
|
205
233
|
[1.1.3]: https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/compare/1.1.2...1.1.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: geopic-tag-reader
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: GeoPicTagReader
|
|
5
5
|
Author-email: Adrien PAVIE <panieravide@riseup.net>
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -31,7 +31,7 @@ Provides-Extra: write-exif
|
|
|
31
31
|
|
|
32
32
|
#  Panoramax
|
|
33
33
|
|
|
34
|
-
__Panoramax__ is a digital resource for sharing and
|
|
34
|
+
__Panoramax__ is a digital resource for sharing and using 📍📷 field photos. Anyone can take photographs of places visible from the public streets and contribute them to the Panoramax database. This data is then freely accessible and reusable by all. More information available at [gitlab.com/panoramax](https://gitlab.com/panoramax) and [panoramax.fr](https://panoramax.fr/).
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# 📷 GeoPic Tag Reader
|
|
@@ -56,7 +56,7 @@ pip install geopic_tag_reader
|
|
|
56
56
|
geopic-tag-reader --help
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
To know more about install and other options, see [install documentation](./docs/
|
|
59
|
+
To know more about install and other options, see [install documentation](./docs/install.md).
|
|
60
60
|
|
|
61
61
|
If at some point you're lost or need help, you can contact us through [issues](https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/issues) or by [email](mailto:panieravide@riseup.net).
|
|
62
62
|
|
|
@@ -88,7 +88,7 @@ geopic-tag-reader write \
|
|
|
88
88
|
--output /path/to/edited_image.jpg
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
[Full documentation is also available here](./docs/
|
|
91
|
+
[Full documentation is also available here](./docs/index.md).
|
|
92
92
|
|
|
93
93
|
### As Python library
|
|
94
94
|
|
|
@@ -119,14 +119,14 @@ editedImg.write(editedImgBytes)
|
|
|
119
119
|
editedImg.close()
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
[Full documentation is also available here](./docs/
|
|
122
|
+
[Full documentation is also available here](./docs/tech/api_reference.md).
|
|
123
123
|
|
|
124
124
|
|
|
125
125
|
## Contributing
|
|
126
126
|
|
|
127
127
|
Pull requests are welcome. For major changes, please open an [issue](https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/issues) first to discuss what you would like to change.
|
|
128
128
|
|
|
129
|
-
More information about developing is available in [documentation](./docs/
|
|
129
|
+
More information about developing is available in [documentation](./docs/develop.md).
|
|
130
130
|
|
|
131
131
|
|
|
132
132
|
## ⚖️ License
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#  Panoramax
|
|
2
2
|
|
|
3
|
-
__Panoramax__ is a digital resource for sharing and
|
|
3
|
+
__Panoramax__ is a digital resource for sharing and using 📍📷 field photos. Anyone can take photographs of places visible from the public streets and contribute them to the Panoramax database. This data is then freely accessible and reusable by all. More information available at [gitlab.com/panoramax](https://gitlab.com/panoramax) and [panoramax.fr](https://panoramax.fr/).
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
# 📷 GeoPic Tag Reader
|
|
@@ -25,7 +25,7 @@ pip install geopic_tag_reader
|
|
|
25
25
|
geopic-tag-reader --help
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
To know more about install and other options, see [install documentation](./docs/
|
|
28
|
+
To know more about install and other options, see [install documentation](./docs/install.md).
|
|
29
29
|
|
|
30
30
|
If at some point you're lost or need help, you can contact us through [issues](https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/issues) or by [email](mailto:panieravide@riseup.net).
|
|
31
31
|
|
|
@@ -57,7 +57,7 @@ geopic-tag-reader write \
|
|
|
57
57
|
--output /path/to/edited_image.jpg
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
[Full documentation is also available here](./docs/
|
|
60
|
+
[Full documentation is also available here](./docs/index.md).
|
|
61
61
|
|
|
62
62
|
### As Python library
|
|
63
63
|
|
|
@@ -88,14 +88,14 @@ editedImg.write(editedImgBytes)
|
|
|
88
88
|
editedImg.close()
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
[Full documentation is also available here](./docs/
|
|
91
|
+
[Full documentation is also available here](./docs/tech/api_reference.md).
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
## Contributing
|
|
95
95
|
|
|
96
96
|
Pull requests are welcome. For major changes, please open an [issue](https://gitlab.com/panoramax/server/geo-picture-tag-reader/-/issues) first to discuss what you would like to change.
|
|
97
97
|
|
|
98
|
-
More information about developing is available in [documentation](./docs/
|
|
98
|
+
More information about developing is available in [documentation](./docs/develop.md).
|
|
99
99
|
|
|
100
100
|
|
|
101
101
|
## ⚖️ License
|
|
@@ -50,7 +50,6 @@ The following EXIF tags are recognized and used if defined, but are **optional**
|
|
|
50
50
|
Image orientation is read from
|
|
51
51
|
|
|
52
52
|
- `GPSImgDirection`
|
|
53
|
-
- `GPano:PoseHeadingDegrees`
|
|
54
53
|
- or in [Mapillary](https://www.mapillary.com/) tags: `MAPCompassHeading`
|
|
55
54
|
|
|
56
55
|
#### :material-timer: Milliseconds in date
|
|
@@ -80,21 +79,22 @@ Camera focal length (to get precise field of view) is read from:
|
|
|
80
79
|
- `Exif.Image.FocalLength`
|
|
81
80
|
- `Exif.Photo.FocalLength`
|
|
82
81
|
|
|
83
|
-
#### :octicons-horizontal-rule-16: Pitch and
|
|
82
|
+
#### :octicons-horizontal-rule-16: Yaw, Pitch and Roll
|
|
83
|
+
|
|
84
|
+
Yaw value is read from:
|
|
85
|
+
|
|
86
|
+
- `Xmp.Camera.Yaw`
|
|
87
|
+
- `Xmp.GPano.PoseHeadingDegrees`
|
|
84
88
|
|
|
85
89
|
Pitch value is read from:
|
|
86
90
|
|
|
87
91
|
- `Xmp.Camera.Pitch`
|
|
88
|
-
- `Exif.GPSInfo.GPSPitch`
|
|
89
92
|
- `Xmp.GPano.PosePitchDegrees`
|
|
90
|
-
- `Xmp.GPano.InitialViewPitchDegrees`
|
|
91
93
|
|
|
92
94
|
Roll value is read from:
|
|
93
95
|
|
|
94
96
|
- `Xmp.Camera.Roll`
|
|
95
|
-
- `Exif.GPSInfo.GPSRoll`
|
|
96
97
|
- `Xmp.GPano.PoseRollDegrees`
|
|
97
|
-
- `Xmp.GPano.InitialViewRollDegrees`
|
|
98
98
|
|
|
99
99
|
#### ⛰️ Altitude
|
|
100
100
|
|
|
@@ -25,7 +25,10 @@ def read(
|
|
|
25
25
|
_ = i18n_init(lang)
|
|
26
26
|
print(_("Latitude:"), metadata.lat)
|
|
27
27
|
print(_("Longitude:"), metadata.lon)
|
|
28
|
-
print(_("Timestamp:"), metadata.ts
|
|
28
|
+
print(_("Timestamp:"), metadata.ts)
|
|
29
|
+
if metadata.ts_by_source is not None:
|
|
30
|
+
print(" -", (metadata.ts_by_source.gps or _("not set")), _("(GPS)"))
|
|
31
|
+
print(" -", (metadata.ts_by_source.camera or _("not set")), _("(Camera)"))
|
|
29
32
|
print(_("Heading:"), metadata.heading)
|
|
30
33
|
print(_("Type:"), metadata.type)
|
|
31
34
|
print(_("Make:"), metadata.make)
|
|
@@ -34,6 +37,7 @@ def read(
|
|
|
34
37
|
print(_("Crop parameters:"), metadata.crop)
|
|
35
38
|
print(_("Pitch:"), metadata.pitch)
|
|
36
39
|
print(_("Roll:"), metadata.roll)
|
|
40
|
+
print(_("Yaw:"), metadata.yaw)
|
|
37
41
|
|
|
38
42
|
if len(metadata.tagreader_warnings) > 0:
|
|
39
43
|
print(_("Warnings raised by reader:"))
|
|
@@ -39,6 +39,32 @@ class CropValues:
|
|
|
39
39
|
top: int
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@dataclass
|
|
43
|
+
class TimeBySource:
|
|
44
|
+
"""All datetimes read from available sources
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
gps (datetime): Time read from GPS clock
|
|
48
|
+
camera (datetime): Time read from camera clock (DateTimeOriginal)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
gps: Optional[datetime.datetime] = None
|
|
52
|
+
camera: Optional[datetime.datetime] = None
|
|
53
|
+
|
|
54
|
+
def getBest(self) -> Optional[datetime.datetime]:
|
|
55
|
+
"""Get the best available datetime to use"""
|
|
56
|
+
if self.gps is not None and self.camera is None:
|
|
57
|
+
return self.gps
|
|
58
|
+
elif self.gps is None and self.camera is not None:
|
|
59
|
+
return self.camera
|
|
60
|
+
elif self.gps is None and self.camera is None:
|
|
61
|
+
return None
|
|
62
|
+
elif self.camera.microsecond > 0 and self.gps.microsecond == 0: # type: ignore
|
|
63
|
+
return self.camera
|
|
64
|
+
else:
|
|
65
|
+
return self.gps
|
|
66
|
+
|
|
67
|
+
|
|
42
68
|
@dataclass
|
|
43
69
|
class GeoPicTags:
|
|
44
70
|
"""Tags associated to a geolocated picture
|
|
@@ -47,7 +73,7 @@ class GeoPicTags:
|
|
|
47
73
|
lat (float): GPS Latitude (in WGS84)
|
|
48
74
|
lon (float): GPS Longitude (in WGS84)
|
|
49
75
|
ts (datetime): The capture date (date & time with timezone)
|
|
50
|
-
heading (int): Picture heading
|
|
76
|
+
heading (int): Picture GPS heading (in degrees, North = 0°, East = 90°, South = 180°, West = 270°). Value is computed based on image center (if yaw=0°)
|
|
51
77
|
type (str): The kind of picture (flat, equirectangular)
|
|
52
78
|
make (str): The camera manufacturer name
|
|
53
79
|
model (str): The camera model name
|
|
@@ -58,6 +84,8 @@ class GeoPicTags:
|
|
|
58
84
|
altitude (float): altitude (in m) (optional)
|
|
59
85
|
pitch (float): Picture pitch angle, compared to horizon (in degrees, bottom = -90°, horizon = 0°, top = 90°)
|
|
60
86
|
roll (float): Picture roll angle, on a right/left axis (in degrees, left-arm down = -90°, flat = 0°, right-arm down = 90°)
|
|
87
|
+
yaw (float): Picture yaw angle, on a vertical axis (in degrees, front = 0°, right = 90°, rear = 180°, left = 270°). This offsets the center image from GPS direction for a correct 360° sphere correction
|
|
88
|
+
ts_by_source (TimeBySource): all read timestamps from image, for finer processing.
|
|
61
89
|
|
|
62
90
|
|
|
63
91
|
Implementation note: this needs to be sync with the PartialGeoPicTags structure
|
|
@@ -77,6 +105,8 @@ class GeoPicTags:
|
|
|
77
105
|
altitude: Optional[float] = None
|
|
78
106
|
pitch: Optional[float] = None
|
|
79
107
|
roll: Optional[float] = None
|
|
108
|
+
yaw: Optional[float] = None
|
|
109
|
+
ts_by_source: Optional[TimeBySource] = None
|
|
80
110
|
|
|
81
111
|
|
|
82
112
|
class InvalidExifException(Exception):
|
|
@@ -111,6 +141,8 @@ class PartialGeoPicTags:
|
|
|
111
141
|
altitude: Optional[float] = None
|
|
112
142
|
pitch: Optional[float] = None
|
|
113
143
|
roll: Optional[float] = None
|
|
144
|
+
yaw: Optional[float] = None
|
|
145
|
+
ts_by_source: Optional[TimeBySource] = None
|
|
114
146
|
|
|
115
147
|
|
|
116
148
|
class PartialExifException(Exception):
|
|
@@ -184,35 +216,21 @@ def readPictureMetadata(picture: bytes, lang_code: str = "en") -> GeoPicTags:
|
|
|
184
216
|
if lon is not None and (lon < -180 or lon > 180):
|
|
185
217
|
raise InvalidExifException(_("Read longitude is out of WGS84 bounds (should be in [-180, 180])"))
|
|
186
218
|
|
|
187
|
-
# Parse date/time
|
|
188
|
-
|
|
219
|
+
# Parse GPS date/time
|
|
220
|
+
gpsTs, llw = decodeGPSDateTime(data, "Exif.GPSInfo", _, lat, lon)
|
|
189
221
|
|
|
190
222
|
if len(llw) > 0:
|
|
191
223
|
warnings.extend(llw)
|
|
192
224
|
|
|
193
|
-
if
|
|
194
|
-
|
|
225
|
+
if gpsTs is None:
|
|
226
|
+
gpsTs, llw = decodeGPSDateTime(data, "Xmp.exif", _, lat, lon)
|
|
195
227
|
if len(llw) > 0:
|
|
196
228
|
warnings.extend(llw)
|
|
197
229
|
|
|
198
|
-
|
|
199
|
-
"Exif.Image.DateTimeOriginal",
|
|
200
|
-
"Exif.Photo.DateTimeOriginal",
|
|
201
|
-
"Exif.Image.DateTime",
|
|
202
|
-
"Xmp.GPano.SourceImageCreateTime",
|
|
203
|
-
]:
|
|
204
|
-
if d is None:
|
|
205
|
-
d, llw = decodeDateTimeOriginal(data, exifField, _, lat, lon)
|
|
206
|
-
if len(llw) > 0:
|
|
207
|
-
warnings.extend(llw)
|
|
208
|
-
|
|
209
|
-
if d is not None:
|
|
210
|
-
break
|
|
211
|
-
|
|
212
|
-
if d is None and isExifTagUsable(data, "MAPGpsTime"):
|
|
230
|
+
if gpsTs is None and isExifTagUsable(data, "MAPGpsTime"):
|
|
213
231
|
try:
|
|
214
232
|
year, month, day, hour, minutes, seconds, milliseconds = [int(dp) for dp in data["MAPGpsTime"].split("_")]
|
|
215
|
-
|
|
233
|
+
gpsTs = datetime.datetime(
|
|
216
234
|
year,
|
|
217
235
|
month,
|
|
218
236
|
day,
|
|
@@ -226,57 +244,59 @@ def readPictureMetadata(picture: bytes, lang_code: str = "en") -> GeoPicTags:
|
|
|
226
244
|
except Exception as e:
|
|
227
245
|
warnings.append(_("Skipping Mapillary date/time as it was not recognized: {v}").format(v=data["MAPGpsTime"]))
|
|
228
246
|
|
|
229
|
-
#
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
# Parse camera date/time
|
|
248
|
+
cameraTs = None
|
|
249
|
+
for exifGroup, dtField, subsecField in [
|
|
250
|
+
("Exif.Photo", "DateTimeOriginal", "SubSecTimeOriginal"),
|
|
251
|
+
("Exif.Image", "DateTimeOriginal", "SubSecTimeOriginal"),
|
|
252
|
+
("Exif.Image", "DateTime", "SubSecTimeOriginal"),
|
|
253
|
+
("Xmp.GPano", "SourceImageCreateTime", "SubSecTimeOriginal"),
|
|
254
|
+
("Xmp.exif", "DateTimeOriginal", "SubsecTimeOriginal"), # Case matters
|
|
255
|
+
]:
|
|
256
|
+
if cameraTs is None:
|
|
257
|
+
cameraTs, llw = decodeDateTimeOriginal(data, exifGroup, dtField, subsecField, _, lat, lon)
|
|
258
|
+
if len(llw) > 0:
|
|
259
|
+
warnings.extend(llw)
|
|
242
260
|
|
|
243
|
-
|
|
244
|
-
|
|
261
|
+
if cameraTs is not None:
|
|
262
|
+
break
|
|
263
|
+
tsSources = TimeBySource(gps=gpsTs, camera=cameraTs) if gpsTs or cameraTs else None
|
|
264
|
+
d = tsSources.getBest() if tsSources is not None else None
|
|
245
265
|
|
|
246
|
-
|
|
266
|
+
# GPS Heading
|
|
267
|
+
heading = None
|
|
268
|
+
if isExifTagUsable(data, "Exif.GPSInfo.GPSImgDirection", Fraction):
|
|
247
269
|
heading = int(round(float(Fraction(data["Exif.GPSInfo.GPSImgDirection"]))))
|
|
248
270
|
|
|
249
271
|
elif "MAPCompassHeading" in data and isExifTagUsable(data["MAPCompassHeading"], "TrueHeading", float):
|
|
250
272
|
heading = int(round(float(data["MAPCompassHeading"]["TrueHeading"])))
|
|
251
273
|
|
|
252
|
-
# Pitch
|
|
274
|
+
# Yaw / Pitch / roll
|
|
275
|
+
yaw = None
|
|
253
276
|
pitch = None
|
|
254
277
|
roll = None
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
278
|
+
exifYPRFields = {
|
|
279
|
+
"yaw": ["Xmp.Camera.Yaw", "Xmp.GPano.PoseHeadingDegrees"],
|
|
280
|
+
"pitch": ["Xmp.Camera.Pitch", "Xmp.GPano.PosePitchDegrees"],
|
|
281
|
+
"roll": ["Xmp.Camera.Roll", "Xmp.GPano.PoseRollDegrees"],
|
|
282
|
+
}
|
|
283
|
+
for ypr in exifYPRFields:
|
|
284
|
+
for exifTag in exifYPRFields[ypr]:
|
|
261
285
|
foundValue = None
|
|
262
286
|
# Look for float or fraction
|
|
263
|
-
if isExifTagUsable(data,
|
|
264
|
-
foundValue = float(data[
|
|
265
|
-
elif isExifTagUsable(data,
|
|
266
|
-
foundValue = float(Fraction(data[
|
|
287
|
+
if isExifTagUsable(data, exifTag, float):
|
|
288
|
+
foundValue = float(data[exifTag])
|
|
289
|
+
elif isExifTagUsable(data, exifTag, Fraction):
|
|
290
|
+
foundValue = float(Fraction(data[exifTag]))
|
|
267
291
|
|
|
268
|
-
# Save
|
|
292
|
+
# Save found value
|
|
269
293
|
if foundValue is not None:
|
|
270
|
-
if
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if roll is None:
|
|
277
|
-
roll = foundValue
|
|
278
|
-
else:
|
|
279
|
-
continue
|
|
294
|
+
if ypr == "yaw" and yaw is None:
|
|
295
|
+
yaw = foundValue
|
|
296
|
+
elif ypr == "pitch" and pitch is None:
|
|
297
|
+
pitch = foundValue
|
|
298
|
+
elif ypr == "roll" and roll is None:
|
|
299
|
+
roll = foundValue
|
|
280
300
|
|
|
281
301
|
# Make and model
|
|
282
302
|
make = data.get("Exif.Image.Make") or data.get("MAPDeviceMake")
|
|
@@ -383,6 +403,8 @@ def readPictureMetadata(picture: bytes, lang_code: str = "en") -> GeoPicTags:
|
|
|
383
403
|
altitude=altitude,
|
|
384
404
|
pitch=pitch,
|
|
385
405
|
roll=roll,
|
|
406
|
+
yaw=yaw,
|
|
407
|
+
ts_by_source=tsSources,
|
|
386
408
|
),
|
|
387
409
|
)
|
|
388
410
|
|
|
@@ -402,6 +424,8 @@ def readPictureMetadata(picture: bytes, lang_code: str = "en") -> GeoPicTags:
|
|
|
402
424
|
altitude=altitude,
|
|
403
425
|
pitch=pitch,
|
|
404
426
|
roll=roll,
|
|
427
|
+
yaw=yaw,
|
|
428
|
+
ts_by_source=tsSources,
|
|
405
429
|
)
|
|
406
430
|
|
|
407
431
|
|
|
@@ -492,20 +516,28 @@ def decodeLatLon(data: dict, group: str, _: Callable[[str], str]) -> Tuple[Optio
|
|
|
492
516
|
|
|
493
517
|
|
|
494
518
|
def decodeDateTimeOriginal(
|
|
495
|
-
data: dict,
|
|
519
|
+
data: dict,
|
|
520
|
+
exifGroup: str,
|
|
521
|
+
datetimeField: str,
|
|
522
|
+
subsecField: str,
|
|
523
|
+
_: Callable[[str], str],
|
|
524
|
+
lat: Optional[float] = None,
|
|
525
|
+
lon: Optional[float] = None,
|
|
496
526
|
) -> Tuple[Optional[datetime.datetime], List[str]]:
|
|
497
527
|
d = None
|
|
498
528
|
warnings = []
|
|
529
|
+
dtField = f"{exifGroup}.{datetimeField}"
|
|
530
|
+
ssField = f"{exifGroup}.{subsecField}"
|
|
499
531
|
|
|
500
|
-
if d is None and isExifTagUsable(data,
|
|
532
|
+
if d is None and isExifTagUsable(data, dtField):
|
|
501
533
|
try:
|
|
502
|
-
dateRaw = data[
|
|
503
|
-
timeRaw = data[
|
|
534
|
+
dateRaw = data[dtField][:10].replace(":", "-")
|
|
535
|
+
timeRaw = data[dtField][11:].split(":")
|
|
504
536
|
hourRaw = int(timeRaw[0])
|
|
505
537
|
minutesRaw = int(timeRaw[1])
|
|
506
538
|
secondsRaw, microsecondsRaw, msw = decodeSecondsAndMicroSeconds(
|
|
507
|
-
timeRaw[2],
|
|
508
|
-
data[
|
|
539
|
+
timeRaw[2] if len(timeRaw) >= 3 else "0",
|
|
540
|
+
data[ssField] if isExifTagUsable(data, ssField, float) else "0",
|
|
509
541
|
_,
|
|
510
542
|
)
|
|
511
543
|
warnings += msw
|
|
@@ -522,7 +554,7 @@ def decodeDateTimeOriginal(
|
|
|
522
554
|
|
|
523
555
|
# Timezone handling
|
|
524
556
|
# Try to read from EXIF
|
|
525
|
-
tz = decodeTimeOffset(data, f"
|
|
557
|
+
tz = decodeTimeOffset(data, f"{exifGroup}.OffsetTime{'Original' if 'DateTimeOriginal' in dtField else ''}")
|
|
526
558
|
if tz is not None:
|
|
527
559
|
d = d.replace(tzinfo=tz)
|
|
528
560
|
|
|
@@ -543,9 +575,7 @@ def decodeDateTimeOriginal(
|
|
|
543
575
|
|
|
544
576
|
except ValueError as e:
|
|
545
577
|
warnings.append(
|
|
546
|
-
_("Skipping original date/time (from {datefield}) as it was not recognized: {v}").format(
|
|
547
|
-
datefield=datetimeField, v=data[datetimeField]
|
|
548
|
-
)
|
|
578
|
+
_("Skipping original date/time (from {datefield}) as it was not recognized: {v}").format(datefield=dtField, v=data[dtField])
|
|
549
579
|
)
|
|
550
580
|
|
|
551
581
|
return (d, warnings)
|
|
@@ -583,7 +613,7 @@ def decodeGPSDateTime(
|
|
|
583
613
|
if timeRaw:
|
|
584
614
|
seconds, microseconds, msw = decodeSecondsAndMicroSeconds(
|
|
585
615
|
str(float(timeRaw[2])),
|
|
586
|
-
|
|
616
|
+
"0", # No SubSecTimeOriginal, it's only for DateTimeOriginal
|
|
587
617
|
_,
|
|
588
618
|
)
|
|
589
619
|
|