Skip to content

Commit b7b3612

Browse files
authored
Support more jpeg SOF markers for size reading the dimensions correctly (#49)
* Add more markers for reading jpg dimensions efficiently * Add more tests for getting dimensions from various jpeg files * Address feedback
1 parent f3b489c commit b7b3612

File tree

2 files changed

+135
-60
lines changed

2 files changed

+135
-60
lines changed

src/labelformat/utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,25 @@
2020
".webp",
2121
}
2222

23+
# JPEG SOF (Start of Frame) markers that contain image dimensions.
24+
# Excludes 0xC4 (DHT - Define Huffman Table) and 0xC8 (JPG reserved).
25+
# List from https://www.disktuna.com/list-of-jpeg-markers/
26+
JPEG_SOF_MARKERS = {
27+
0xC0, # SOF0 - Baseline DCT
28+
0xC1, # SOF1 - Extended Sequential DCT
29+
0xC2, # SOF2 - Progressive DCT
30+
0xC3, # SOF3 - Lossless (sequential)
31+
0xC5, # SOF5 - Differential sequential DCT
32+
0xC6, # SOF6 - Differential progressive DCT
33+
0xC7, # SOF7 - Differential lossless (sequential)
34+
0xC9, # SOF9 - Extended sequential DCT, Arithmetic coding
35+
0xCA, # SOF10 - Progressive DCT, Arithmetic coding
36+
0xCB, # SOF11 - Lossless (sequential), Arithmetic coding
37+
0xCD, # SOF13 - Differential sequential DCT, Arithmetic coding
38+
0xCE, # SOF14 - Differential progressive DCT, Arithmetic coding
39+
0xCF, # SOF15 - Differential lossless (sequential), Arithmetic coding
40+
}
41+
2342

2443
class ImageDimensionError(Exception):
2544
"""Raised when unable to extract image dimensions using fast methods."""
@@ -59,7 +78,7 @@ def get_jpeg_dimensions(file_path: Path) -> Tuple[int, int]:
5978
if len(marker) < 2:
6079
raise ImageDimensionError("Invalid JPEG format")
6180
# Find SOFn marker
62-
if 0xFF == marker[0] and marker[1] in range(0xC0, 0xCF):
81+
if marker[0] == 0xFF and marker[1] in JPEG_SOF_MARKERS:
6382
# Skip marker length
6483
img_file.seek(3, 1)
6584
h = int.from_bytes(img_file.read(2), "big")

tests/unit/test_utils.py

Lines changed: 115 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pathlib import Path
2+
from typing import Tuple
23

4+
import PIL.Image
35
import pytest
46

57
from labelformat.utils import (
@@ -12,62 +14,116 @@
1214
FIXTURES_DIR = Path(__file__).parent.parent / "fixtures"
1315

1416

15-
class TestImageDimensions:
16-
def test_jpeg_dimensions_valid_file(self) -> None:
17-
image_path = (
18-
FIXTURES_DIR / "instance_segmentation/YOLOv8/images/000000109005.jpg"
19-
)
20-
width, height = get_jpeg_dimensions(image_path)
21-
assert width == 640
22-
assert height == 428
23-
24-
def test_jpeg_dimensions_nonexistent_file(self) -> None:
25-
with pytest.raises(ImageDimensionError):
26-
get_jpeg_dimensions(Path("nonexistent.jpg"))
27-
28-
def test_jpeg_dimensions_invalid_format(self) -> None:
29-
yaml_file = FIXTURES_DIR / "object_detection/YOLOv8/example.yaml"
30-
with pytest.raises(ImageDimensionError):
31-
get_jpeg_dimensions(yaml_file)
32-
33-
def test_png_dimensions_valid_file(self) -> None:
34-
png_path = FIXTURES_DIR / "image_file_loading/0001.png"
35-
width, height = get_png_dimensions(png_path)
36-
assert width == 278
37-
assert height == 181
38-
39-
def test_png_dimensions_nonexistent_file(self) -> None:
40-
with pytest.raises(ImageDimensionError):
41-
get_png_dimensions(Path("nonexistent.png"))
42-
43-
def test_png_dimensions_invalid_format(self) -> None:
44-
yaml_file = FIXTURES_DIR / "object_detection/YOLOv8/example.yaml"
45-
with pytest.raises(ImageDimensionError):
46-
get_png_dimensions(yaml_file)
47-
48-
def test_get_image_dimensions_jpeg_first_file(self) -> None:
49-
jpeg_path = (
50-
FIXTURES_DIR / "instance_segmentation/YOLOv8/images/000000109005.jpg"
51-
)
52-
width, height = get_image_dimensions(jpeg_path)
53-
assert width == 640
54-
assert height == 428
55-
56-
def test_get_image_dimensions_jpeg_second_file(self) -> None:
57-
jpeg_path = (
58-
FIXTURES_DIR / "instance_segmentation/YOLOv8/images/000000036086.jpg"
59-
)
60-
width, height = get_image_dimensions(jpeg_path)
61-
assert width == 482
62-
assert height == 640
63-
64-
def test_get_image_dimensions_png(self) -> None:
65-
png_path = FIXTURES_DIR / "image_file_loading/0001.png"
66-
width, height = get_image_dimensions(png_path)
67-
assert width == 278
68-
assert height == 181
69-
70-
def test_get_image_dimensions_unsupported_format(self) -> None:
71-
yaml_file = FIXTURES_DIR / "object_detection/YOLOv8/example.yaml"
72-
with pytest.raises(Exception):
73-
get_image_dimensions(yaml_file)
17+
def test_get_jpeg_dimensions() -> None:
18+
image_path = FIXTURES_DIR / "instance_segmentation/YOLOv8/images/000000109005.jpg"
19+
width, height = get_jpeg_dimensions(image_path)
20+
assert width == 640
21+
assert height == 428
22+
23+
24+
def test_get_jpeg_dimensions__baseline(tmp_path: Path) -> None:
25+
# Tests SOF0 (0xC0) - Baseline DCT.
26+
jpeg_path = tmp_path / "baseline.jpg"
27+
_create_test_jpeg(path=jpeg_path, size=(800, 600))
28+
29+
width, height = get_jpeg_dimensions(jpeg_path)
30+
assert width == 800
31+
assert height == 600
32+
33+
34+
def test_get_jpeg_dimensions__progressive(tmp_path: Path) -> None:
35+
# Tests SOF2 (0xC2) - Progressive DCT with DHT markers before SOF.
36+
jpeg_path = tmp_path / "progressive.jpg"
37+
_create_test_jpeg(path=jpeg_path, size=(1920, 1440), progressive=True)
38+
39+
width, height = get_jpeg_dimensions(jpeg_path)
40+
assert width == 1920
41+
assert height == 1440
42+
43+
44+
def test_get_jpeg_dimensions__optimized(tmp_path: Path) -> None:
45+
# Tests SOF0 (0xC0) with custom Huffman tables (more DHT markers before SOF).
46+
jpeg_path = tmp_path / "optimized.jpg"
47+
_create_test_jpeg(path=jpeg_path, size=(1024, 768), optimize=True)
48+
49+
width, height = get_jpeg_dimensions(jpeg_path)
50+
assert width == 1024
51+
assert height == 768
52+
53+
54+
def test_get_jpeg_dimensions__progressive_optimized(tmp_path: Path) -> None:
55+
# Tests SOF2 (0xC2) with custom Huffman tables.
56+
jpeg_path = tmp_path / "progressive_optimized.jpg"
57+
_create_test_jpeg(
58+
path=jpeg_path, size=(2048, 1536), progressive=True, optimize=True
59+
)
60+
61+
width, height = get_jpeg_dimensions(jpeg_path)
62+
assert width == 2048
63+
assert height == 1536
64+
65+
66+
def test_get_jpeg_dimensions__nonexistent_file() -> None:
67+
with pytest.raises(ImageDimensionError):
68+
get_jpeg_dimensions(Path("nonexistent.jpg"))
69+
70+
71+
def test_get_jpeg_dimensions__invalid_format() -> None:
72+
yaml_file = FIXTURES_DIR / "object_detection/YOLOv8/example.yaml"
73+
with pytest.raises(ImageDimensionError):
74+
get_jpeg_dimensions(yaml_file)
75+
76+
77+
def test_get_png_dimensions() -> None:
78+
png_path = FIXTURES_DIR / "image_file_loading/0001.png"
79+
width, height = get_png_dimensions(png_path)
80+
assert width == 278
81+
assert height == 181
82+
83+
84+
def test_get_png_dimensions__nonexistent_file() -> None:
85+
with pytest.raises(ImageDimensionError):
86+
get_png_dimensions(Path("nonexistent.png"))
87+
88+
89+
def test_get_png_dimensions__invalid_format() -> None:
90+
yaml_file = FIXTURES_DIR / "object_detection/YOLOv8/example.yaml"
91+
with pytest.raises(ImageDimensionError):
92+
get_png_dimensions(yaml_file)
93+
94+
95+
def test_get_image_dimensions__jpeg() -> None:
96+
jpeg_path = FIXTURES_DIR / "instance_segmentation/YOLOv8/images/000000109005.jpg"
97+
width, height = get_image_dimensions(jpeg_path)
98+
assert width == 640
99+
assert height == 428
100+
101+
102+
def test_get_image_dimensions__jpeg_second_file() -> None:
103+
jpeg_path = FIXTURES_DIR / "instance_segmentation/YOLOv8/images/000000036086.jpg"
104+
width, height = get_image_dimensions(jpeg_path)
105+
assert width == 482
106+
assert height == 640
107+
108+
109+
def test_get_image_dimensions__png() -> None:
110+
png_path = FIXTURES_DIR / "image_file_loading/0001.png"
111+
width, height = get_image_dimensions(png_path)
112+
assert width == 278
113+
assert height == 181
114+
115+
116+
def test_get_image_dimensions__unsupported_format() -> None:
117+
yaml_file = FIXTURES_DIR / "object_detection/YOLOv8/example.yaml"
118+
with pytest.raises(Exception):
119+
get_image_dimensions(yaml_file)
120+
121+
122+
def _create_test_jpeg(
123+
path: Path,
124+
size: Tuple[int, int],
125+
progressive: bool = False,
126+
optimize: bool = False,
127+
) -> None:
128+
img = PIL.Image.new("RGB", size, color="red")
129+
img.save(path, "JPEG", progressive=progressive, optimize=optimize)

0 commit comments

Comments
 (0)