Skip to content

Commit 67a1259

Browse files
authored
Merge pull request #1811 from wkentaro/perf/large-image-loading
perf: fix large image loading performance
2 parents ca316d7 + 24104be commit 67a1259

File tree

3 files changed

+98
-26
lines changed

3 files changed

+98
-26
lines changed

labelme/_label_file.py

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io
55
import json
66
import os.path as osp
7+
import time
78
from pathlib import PureWindowsPath
89
from typing import TypedDict
910

@@ -139,20 +140,26 @@ def __init__(self, filename=None):
139140

140141
@staticmethod
141142
def load_image_file(filename):
143+
t0 = time.time()
142144
image_pil = PIL.Image.open(filename)
143145

144-
# apply orientation to image according to exif
145-
image_pil = utils.apply_exif_orientation(image_pil)
146-
147-
with io.BytesIO() as f:
148-
ext = osp.splitext(filename)[1].lower()
149-
if ext in [".jpg", ".jpeg"]:
150-
format = "JPEG"
151-
else:
152-
format = "PNG"
153-
image_pil.save(f, format=format)
154-
f.seek(0)
155-
return f.read()
146+
oriented: PIL.Image.Image = utils.apply_exif_orientation(image_pil)
147+
ext = osp.splitext(filename)[1].lower()
148+
if oriented is image_pil and ext in (".jpg", ".jpeg", ".png"):
149+
# no encoding needed
150+
with builtins.open(filename, "rb") as f:
151+
imageData = f.read()
152+
else:
153+
with io.BytesIO() as f:
154+
format = "PNG" if "A" in oriented.mode else "JPEG"
155+
oriented.save(f, format=format, quality=95)
156+
f.seek(0)
157+
imageData = f.read()
158+
159+
logger.debug(
160+
"Loaded image file: {!r} in {:.0f}ms", filename, (time.time() - t0) * 1000
161+
)
162+
return imageData
156163

157164
def load(self, filename):
158165
keys = [
@@ -180,7 +187,7 @@ def load(self, filename):
180187
)
181188
flags = data.get("flags") or {}
182189
self._check_image_height_and_width(
183-
base64.b64encode(imageData).decode("utf-8"),
190+
imageData,
184191
data.get("imageHeight"),
185192
data.get("imageWidth"),
186193
)
@@ -205,19 +212,20 @@ def load(self, filename):
205212

206213
@staticmethod
207214
def _check_image_height_and_width(imageData, imageHeight, imageWidth):
208-
img_arr = utils.img_b64_to_arr(imageData)
209-
if imageHeight is not None and img_arr.shape[0] != imageHeight:
215+
img_pil = utils.img_data_to_pil(imageData)
216+
actual_w, actual_h = img_pil.size
217+
if imageHeight is not None and actual_h != imageHeight:
210218
logger.error(
211219
"imageHeight does not match with imageData or imagePath, "
212220
"so getting imageHeight from actual image."
213221
)
214-
imageHeight = img_arr.shape[0]
215-
if imageWidth is not None and img_arr.shape[1] != imageWidth:
222+
imageHeight = actual_h
223+
if imageWidth is not None and actual_w != imageWidth:
216224
logger.error(
217225
"imageWidth does not match with imageData or imagePath, "
218226
"so getting imageWidth from actual image."
219227
)
220-
imageWidth = img_arr.shape[1]
228+
imageWidth = actual_w
221229
return imageHeight, imageWidth
222230

223231
def save(
@@ -232,10 +240,10 @@ def save(
232240
flags=None,
233241
):
234242
if imageData is not None:
235-
imageData = base64.b64encode(imageData).decode("utf-8")
236243
imageHeight, imageWidth = self._check_image_height_and_width(
237244
imageData, imageHeight, imageWidth
238245
)
246+
imageData = base64.b64encode(imageData).decode("utf-8")
239247
if otherData is None:
240248
otherData = {}
241249
if flags is None:

labelme/app.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import platform
1010
import re
1111
import subprocess
12+
import time
1213
import types
1314
import webbrowser
1415
from pathlib import Path
@@ -1718,12 +1719,6 @@ def brightnessContrast(self, value: bool, is_initial_load: bool = False):
17181719
logger.warning("filename is None, cannot set brightness/contrast")
17191720
return
17201721

1721-
dialog = BrightnessContrastDialog(
1722-
utils.img_data_to_pil(self.imageData).convert("RGB"),
1723-
self.onNewBrightnessContrast,
1724-
parent=self,
1725-
)
1726-
17271722
brightness: int | None
17281723
contrast: int | None
17291724
brightness, contrast = self._brightness_contrast_values.get(
@@ -1735,6 +1730,20 @@ def brightnessContrast(self, value: bool, is_initial_load: bool = False):
17351730
brightness, contrast = self._brightness_contrast_values.get(
17361731
prev_filename, (None, None)
17371732
)
1733+
if brightness is None and contrast is None:
1734+
return
1735+
1736+
logger.debug(
1737+
"Opening brightness/contrast dialog with brightness={}, contrast={}",
1738+
brightness,
1739+
contrast,
1740+
)
1741+
dialog = BrightnessContrastDialog(
1742+
utils.img_data_to_pil(self.imageData).convert("RGB"),
1743+
self.onNewBrightnessContrast,
1744+
parent=self,
1745+
)
1746+
17381747
if brightness is not None:
17391748
dialog.slider_brightness.setValue(brightness)
17401749
if contrast is not None:
@@ -1748,6 +1757,12 @@ def brightnessContrast(self, value: bool, is_initial_load: bool = False):
17481757
contrast = dialog.slider_contrast.value()
17491758

17501759
self._brightness_contrast_values[self.filename] = (brightness, contrast)
1760+
logger.debug(
1761+
"Updated states for {}: brightness={}, contrast={}",
1762+
self.filename,
1763+
brightness,
1764+
contrast,
1765+
)
17511766

17521767
def togglePolygons(self, value):
17531768
flag = value
@@ -1786,6 +1801,7 @@ def _load_file(self, filename=None):
17861801
return False
17871802
# assumes same name, but json extension
17881803
self.show_status_message(self.tr("Loading %s...") % osp.basename(str(filename)))
1804+
t0_load_file = time.time()
17891805
label_file = f"{osp.splitext(filename)[0]}.json"
17901806
if self.output_dir:
17911807
label_file_without_path = osp.basename(label_file)
@@ -1830,7 +1846,9 @@ def _load_file(self, filename=None):
18301846
self.imagePath = filename
18311847
self.labelFile = None
18321848
assert self.imageData is not None
1849+
t0 = time.time()
18331850
image = QtGui.QImage.fromData(self.imageData)
1851+
logger.debug("Created QImage in {:.0f}ms", (time.time() - t0) * 1000)
18341852

18351853
if image.isNull():
18361854
formats = [
@@ -1848,7 +1866,9 @@ def _load_file(self, filename=None):
18481866
return False
18491867
self.image = image
18501868
self.filename = filename
1869+
t0 = time.time()
18511870
self.canvas.loadPixmap(QtGui.QPixmap.fromImage(image))
1871+
logger.debug("Loaded pixmap in {:.0f}ms", (time.time() - t0) * 1000)
18521872
flags = {k: False for k in self._config["flags"] or []}
18531873
if self.labelFile:
18541874
self._load_shape_dicts(shape_dicts=self.labelFile.shapes)
@@ -1881,7 +1901,11 @@ def _load_file(self, filename=None):
18811901
self.toggleActions(True)
18821902
self.canvas.setFocus()
18831903
self.show_status_message(self.tr("Loaded %s") % osp.basename(filename))
1884-
logger.debug("loaded file: {!r}", filename)
1904+
logger.info(
1905+
"Loaded file: {!r} in {:.0f}ms",
1906+
filename,
1907+
(time.time() - t0_load_file) * 1000,
1908+
)
18851909
return True
18861910

18871911
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:

tests/unit/load_image_file_test.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
import numpy as np
6+
import PIL.Image
7+
8+
from labelme._label_file import LabelFile
9+
10+
11+
def _make_image(tmp_path: Path, filename: str, mode: str = "RGB", size=(100, 100)):
12+
channels = 4 if mode == "RGBA" else 3
13+
arr = np.random.randint(0, 255, (size[1], size[0], channels), dtype=np.uint8)
14+
path = tmp_path / filename
15+
PIL.Image.fromarray(arr, mode=mode).save(str(path))
16+
return path
17+
18+
19+
def test_tiff_without_alpha_encoded_as_jpeg(tmp_path):
20+
path = _make_image(tmp_path, "test.tiff")
21+
data = LabelFile.load_image_file(str(path))
22+
assert data[:2] == b"\xff\xd8"
23+
24+
25+
def test_tiff_with_alpha_encoded_as_png(tmp_path):
26+
path = _make_image(tmp_path, "test.tiff", mode="RGBA")
27+
data = LabelFile.load_image_file(str(path))
28+
assert data[:4] == b"\x89PNG"
29+
30+
31+
def test_jpeg_returns_raw_bytes(tmp_path):
32+
path = _make_image(tmp_path, "test.jpg")
33+
data = LabelFile.load_image_file(str(path))
34+
assert data == path.read_bytes()
35+
36+
37+
def test_png_returns_raw_bytes(tmp_path):
38+
path = _make_image(tmp_path, "test.png")
39+
data = LabelFile.load_image_file(str(path))
40+
assert data == path.read_bytes()

0 commit comments

Comments
 (0)