Skip to content

Commit 58e66e9

Browse files
authored
improve: cut sequences by max 6 gigapixel of images (mapillary#616)
* improve: cut sequences by max 6 gigapixel of images * fix types * add tests * fix tests
1 parent bae983e commit 58e66e9

10 files changed

+184
-20
lines changed

mapillary_tools/constants.py

+2
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@
4444
MAX_SEQUENCE_LENGTH = int(os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_LENGTH", 500))
4545
# Max file size per sequence (sum of image filesizes in the sequence)
4646
MAX_SEQUENCE_FILESIZE: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_FILESIZE", "2G")
47+
# Max number of pixels per sequence (sum of image pixels in the sequence)
48+
MAX_SEQUENCE_PIXELS: str = os.getenv(_ENV_PREFIX + "MAX_SEQUENCE_PIXELS", "6G")

mapillary_tools/exif_read.py

+16-3
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ class ExifRead:
110110
EXIF class for reading exif from an image
111111
"""
112112

113-
path_or_stream: T.Union[Path, T.BinaryIO]
114-
115113
def __init__(
116114
self, path_or_stream: T.Union[Path, T.BinaryIO], details: bool = False
117115
) -> None:
@@ -125,7 +123,6 @@ def __init__(
125123
self.tags = exifread.process_file(
126124
path_or_stream, details=details, debug=True
127125
)
128-
self.path_or_stream = path_or_stream
129126

130127
def extract_altitude(self) -> T.Optional[float]:
131128
"""
@@ -285,6 +282,22 @@ def extract_model(self) -> T.Optional[str]:
285282
"""
286283
return self._extract_alternative_fields(["EXIF LensModel", "Image Model"], str)
287284

285+
def extract_width(self) -> T.Optional[int]:
286+
"""
287+
Extract image width in pixels
288+
"""
289+
return self._extract_alternative_fields(
290+
["Image ImageWidth", "EXIF ExifImageWidth"], int
291+
)
292+
293+
def extract_height(self) -> T.Optional[int]:
294+
"""
295+
Extract image height in pixels
296+
"""
297+
return self._extract_alternative_fields(
298+
["Image ImageLength", "EXIF ExifImageLength"], int
299+
)
300+
288301
def extract_orientation(self) -> int:
289302
"""
290303
Extract image orientation

mapillary_tools/geotag/geotag_from_exif.py

+2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ def to_description(self) -> T.List[types.ImageMetadataOrError]:
7575
alt=exif.extract_altitude(),
7676
angle=exif.extract_direction(),
7777
time=geo.as_unix_time(timestamp),
78+
width=exif.extract_width(),
79+
height=exif.extract_height(),
7880
MAPOrientation=exif.extract_orientation(),
7981
MAPDeviceMake=exif.extract_make(),
8082
MAPDeviceModel=exif.extract_model(),

mapillary_tools/geotag/geotag_from_gpx.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,29 @@ def __init__(
3131
self.use_gpx_start_time = use_gpx_start_time
3232
self.use_image_start_time = use_image_start_time
3333
self.offset_time = offset_time
34+
self._exifs: T.Dict[Path, ExifRead] = {}
3435

3536
def read_image_time(self, image_path: Path) -> T.Optional[float]:
36-
image_time = ExifRead(image_path).extract_capture_time()
37+
exif = self._exifs.get(image_path)
38+
if exif is None:
39+
exif = ExifRead(image_path)
40+
self._exifs[image_path] = exif
41+
image_time = exif.extract_capture_time()
3742
if image_time is None:
3843
return None
3944
return geo.as_unix_time(image_time)
4045

46+
def read_image_size(
47+
self, image_path: Path
48+
) -> T.Tuple[T.Optional[int], T.Optional[int]]:
49+
exif = self._exifs.get(image_path)
50+
if exif is None:
51+
exif = ExifRead(image_path)
52+
self._exifs[image_path] = exif
53+
width = exif.extract_width()
54+
height = exif.extract_height()
55+
return width, height
56+
4157
def to_description(self) -> T.List[types.ImageMetadataOrError]:
4258
metadatas: T.List[types.ImageMetadataOrError] = []
4359

@@ -165,13 +181,16 @@ def to_description(self) -> T.List[types.ImageMetadataOrError]:
165181
continue
166182

167183
interpolated = geo.interpolate(sorted_points, image_time)
184+
width, height = self.read_image_size(image_path)
168185
image_metadata = types.ImageMetadata(
169186
filename=image_path,
170187
lat=interpolated.lat,
171188
lon=interpolated.lon,
172189
alt=interpolated.alt,
173190
angle=interpolated.angle,
174191
time=interpolated.time,
192+
width=width,
193+
height=height,
175194
)
176195
metadatas.append(image_metadata)
177196

mapillary_tools/process_sequence_properties.py

+69-8
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22
import itertools
33
import math
44
import typing as T
5+
import logging
56

67
from . import constants, geo, types
78
from .exceptions import MapillaryDuplicationError, MapillaryBadParameterError
89

10+
LOG = logging.getLogger(__name__)
11+
912

1013
Point = T.TypeVar("Point", bound=geo.Point)
1114
PointSequence = T.List[Point]
@@ -95,26 +98,63 @@ def cut_sequence(
9598
sequence: T.List[types.ImageMetadata],
9699
max_images: int,
97100
max_sequence_filesize: int,
101+
max_sequence_pixels: int,
98102
) -> T.List[T.List[types.ImageMetadata]]:
99103
"""
100104
Cut a sequence into multiple sequences by max_images or max filesize
101105
"""
102106
sequences: T.List[T.List[types.ImageMetadata]] = []
103107
last_sequence_file_size = 0
108+
last_sequence_pixels = 0
109+
104110
for image in sequence:
111+
# decent default values if width/height not available
112+
width = 1024 if image.width is None else image.width
113+
height = 1024 if image.height is None else image.height
114+
105115
filesize = os.path.getsize(image.filename)
106-
if len(sequences) == 0 or (
107-
sequences[-1]
108-
and (
109-
max_images < len(sequences[-1])
110-
or max_sequence_filesize < last_sequence_file_size + filesize
111-
)
112-
):
116+
117+
if len(sequences) == 0:
118+
start_new_sequence = True
119+
else:
120+
if sequences[-1]:
121+
if max_images < len(sequences[-1]):
122+
LOG.debug(
123+
"Cut the sequence because the current sequence (%s) reaches the max number of images (%s)",
124+
len(sequences[-1]),
125+
max_images,
126+
)
127+
start_new_sequence = True
128+
elif max_sequence_filesize < last_sequence_file_size + filesize:
129+
LOG.debug(
130+
"Cut the sequence because the current sequence (%s) reaches the max filesize (%s)",
131+
last_sequence_file_size + filesize,
132+
max_sequence_filesize,
133+
)
134+
start_new_sequence = True
135+
elif max_sequence_pixels < last_sequence_pixels + width * height:
136+
LOG.debug(
137+
"Cut the sequence because the current sequence (%s) reaches the max pixels (%s)",
138+
last_sequence_pixels + width * height,
139+
max_sequence_pixels,
140+
)
141+
start_new_sequence = True
142+
else:
143+
start_new_sequence = False
144+
else:
145+
start_new_sequence = False
146+
147+
if start_new_sequence:
113148
sequences.append([])
114149
last_sequence_file_size = 0
150+
last_sequence_pixels = 0
151+
115152
sequences[-1].append(image)
116153
last_sequence_file_size += filesize
154+
last_sequence_pixels += width * height
155+
117156
assert sum(len(s) for s in sequences) == len(sequence)
157+
118158
return sequences
119159

120160

@@ -192,8 +232,21 @@ def _parse_filesize_in_bytes(filesize_str: str) -> int:
192232
return int(filesize_str)
193233

194234

235+
def _parse_pixels(pixels_str: str) -> int:
236+
pixels_str = pixels_str.strip().upper()
237+
238+
if pixels_str.endswith("K"):
239+
return int(pixels_str[:-1]) * 1000
240+
elif pixels_str.endswith("M"):
241+
return int(pixels_str[:-1]) * 1000 * 1000
242+
elif pixels_str.endswith("G"):
243+
return int(pixels_str[:-1]) * 1000 * 1000 * 1000
244+
else:
245+
return int(pixels_str)
246+
247+
195248
def process_sequence_properties(
196-
metadatas: T.List[types.MetadataOrError],
249+
metadatas: T.Sequence[types.MetadataOrError],
197250
cutoff_distance=constants.CUTOFF_DISTANCE,
198251
cutoff_time=constants.CUTOFF_TIME,
199252
interpolate_directions=False,
@@ -209,6 +262,13 @@ def process_sequence_properties(
209262
f"Expect the envvar {constants._ENV_PREFIX}MAX_SEQUENCE_FILESIZE to be a valid filesize that ends with B, K, M, or G, but got {constants.MAX_SEQUENCE_FILESIZE}"
210263
)
211264

265+
try:
266+
max_sequence_pixels = _parse_pixels(constants.MAX_SEQUENCE_PIXELS)
267+
except ValueError:
268+
raise MapillaryBadParameterError(
269+
f"Expect the envvar {constants._ENV_PREFIX}MAX_SEQUENCE_PIXELS to be a valid number of pixels that ends with K, M, or G, but got {constants.MAX_SEQUENCE_PIXELS}"
270+
)
271+
212272
error_metadatas: T.List[types.ErrorMetadata] = []
213273
image_metadatas: T.List[types.ImageMetadata] = []
214274
video_metadatas: T.List[types.VideoMetadata] = []
@@ -262,6 +322,7 @@ def process_sequence_properties(
262322
dedups,
263323
constants.MAX_SEQUENCE_LENGTH,
264324
max_sequence_filesize_in_bytes,
325+
max_sequence_pixels,
265326
)
266327

267328
for c in cut:

mapillary_tools/types.py

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ class FileType(enum.Enum):
4444
class ImageMetadata(geo.Point):
4545
filename: Path
4646
# filetype is always FileType.IMAGE
47+
width: T.Optional[int]
48+
height: T.Optional[int]
4749
MAPSequenceUUID: T.Optional[str] = None
4850
MAPDeviceMake: T.Optional[str] = None
4951
MAPDeviceModel: T.Optional[str] = None
@@ -518,6 +520,8 @@ def _from_image_desc(desc) -> ImageMetadata:
518520
alt=desc.get("MAPAltitude"),
519521
time=geo.as_unix_time(map_capture_time_to_datetime(desc["MAPCaptureTime"])),
520522
angle=desc.get("MAPCompassHeading", {}).get("TrueHeading"),
523+
width=None,
524+
height=None,
521525
**kwargs,
522526
)
523527

tests/cli/exif_read.py

+3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def main():
1313
parsed_args = parser.parse_args()
1414
for image_path in utils.find_images([Path(p) for p in parsed_args.path]):
1515
exif = ExifRead(image_path, details=True)
16+
pprint.pprint(exif.tags)
1617
pprint.pprint(
1718
{
1819
"filename": image_path,
@@ -24,6 +25,8 @@ def main():
2425
"lon_lat": exif.extract_lon_lat(),
2526
"make": exif.extract_make(),
2627
"model": exif.extract_model(),
28+
"width": exif.extract_width(),
29+
"height": exif.extract_height(),
2730
}
2831
)
2932

tests/integration/test_process.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,12 @@ def test_process_images_with_overwrite_all_EXIF_tags(setup_data: py.path.local):
163163
assert x.returncode == 0, x.stderr
164164
expected_descs = [
165165
{
166-
**_DEFAULT_EXPECTED_DESCS["DSC00001.JPG"],
166+
**_DEFAULT_EXPECTED_DESCS["DSC00001.JPG"], # type: ignore
167167
"filename": str(Path(setup_data, "images", "DSC00001.JPG")),
168168
"MAPCaptureTime": "2018_06_08_20_24_13_500",
169169
},
170170
{
171-
**_DEFAULT_EXPECTED_DESCS["DSC00497.JPG"],
171+
**_DEFAULT_EXPECTED_DESCS["DSC00497.JPG"], # type: ignore
172172
"filename": str(Path(setup_data, "images", "DSC00497.JPG")),
173173
"MAPCaptureTime": "2018_06_08_20_32_30_500",
174174
},

0 commit comments

Comments
 (0)