Skip to content

Commit 64cbc17

Browse files
committed
feat: add optional PLT and TLM markers during encoding
These can significantly improve decoding performance when pulling a small area out of a very large file, where the decoder supports it. OpenJPEG supports using the TLM marker as of uclouvain/openjpeg#1538
1 parent d9ca6b2 commit 64cbc17

File tree

4 files changed

+174
-1
lines changed

4 files changed

+174
-1
lines changed

lib/interface/encode.c

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ extern int EncodeArray(
3737
int use_mct,
3838
PyObject *compression_ratios,
3939
PyObject *signal_noise_ratios,
40-
int codec_format
40+
int codec_format,
41+
int add_tlm,
42+
int add_plt
4143
)
4244
{
4345
/* Encode a numpy ndarray using JPEG 2000.
@@ -64,6 +66,10 @@ extern int EncodeArray(
6466
The format of the encoded JPEG 2000 data, one of:
6567
* ``0`` - OPJ_CODEC_J2K : JPEG-2000 codestream
6668
* ``1`` - OPJ_CODEC_JP2 : JP2 file format
69+
add_tlm : int
70+
Add tile-part data length markers (TLM). Supported values 0-1.
71+
add_plt : int
72+
Add packet length tile-part header markers (PLT). Supported values 0-1.
6773
6874
Returns
6975
-------
@@ -411,6 +417,24 @@ extern int EncodeArray(
411417
goto failure;
412418
}
413419

420+
const char* extra_options[3] = { NULL, NULL, NULL };
421+
int extra_option_index = 0;
422+
if (add_plt) {
423+
extra_options[extra_option_index] = "PLT=YES";
424+
extra_option_index += 1;
425+
}
426+
if (add_tlm) {
427+
extra_options[extra_option_index] = "TLM=YES";
428+
extra_option_index += 1;
429+
}
430+
if (extra_option_index > 0) {
431+
if (! opj_encoder_set_extra_options(codec, extra_options)) {
432+
py_error("Failed to set extra options on the encoder");
433+
return_code = 28;
434+
goto failure;
435+
}
436+
}
437+
414438
/* Send info, warning, error message to Python logging */
415439
opj_set_info_handler(codec, info_callback, NULL);
416440
opj_set_warning_handler(codec, warning_callback, NULL);

openjpeg/_openjpeg.pyx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ cdef extern int EncodeArray(
3232
PyObject* compression_ratios,
3333
PyObject* signal_noise_ratios,
3434
int codec_format,
35+
bint add_tlm,
36+
bint add_plt,
3537
)
3638
cdef extern int EncodeBuffer(
3739
PyObject* src,
@@ -213,6 +215,8 @@ def encode_array(
213215
List[float] compression_ratios,
214216
List[float] signal_noise_ratios,
215217
int codec_format,
218+
bint add_tlm,
219+
bint add_plt,
216220
) -> Tuple[int, bytes]:
217221
"""Return the JPEG 2000 compressed `arr`.
218222

@@ -239,6 +243,10 @@ def encode_array(
239243

240244
* ``0``: JPEG 2000 codestream only (default) (J2K/J2C format)
241245
* ``1``: A boxed JPEG 2000 codestream (JP2 format)
246+
add_tlm : bool
247+
If ``True`` then add tile-part length markers (TLM) to the codestream.
248+
add_plt : bool
249+
If ``True`` then add packet length tile-part header markers (PLT) to the codestream.
242250

243251
Returns
244252
-------
@@ -318,6 +326,8 @@ def encode_array(
318326
<PyObject *> compression_ratios,
319327
<PyObject *> signal_noise_ratios,
320328
codec_format,
329+
add_tlm,
330+
add_plt,
321331
)
322332
return return_code, dst.getvalue()
323333

openjpeg/tests/test_encode.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,57 @@ def parse_j2k(buffer):
7070

7171
return param
7272

73+
def parse_codestream_markers(buffer):
74+
offset = 0
75+
markers = []
76+
while offset < len(buffer):
77+
symbol_code_bytes = buffer[offset: offset + 2]
78+
marker = unpack(">H", symbol_code_bytes)[0]
79+
offset += 2
80+
if marker == 0xFF4F:
81+
markers.append("SOC")
82+
elif marker == 0xFF51:
83+
markers.append("SIZ")
84+
length_bytes = buffer[offset: offset + 2]
85+
length = unpack(">H", length_bytes)[0]
86+
offset += length
87+
elif marker == 0xFF52:
88+
markers.append("COD")
89+
length_bytes = buffer[offset: offset + 2]
90+
length = unpack(">H", length_bytes)[0]
91+
offset += length
92+
elif marker == 0xFF55:
93+
markers.append("TLM")
94+
length_bytes = buffer[offset: offset + 2]
95+
length = unpack(">H", length_bytes)[0]
96+
offset += length
97+
elif marker == 0xFF58:
98+
markers.append("PLT")
99+
length_bytes = buffer[offset: offset + 2]
100+
length = unpack(">H", length_bytes)[0]
101+
offset += length
102+
elif marker == 0xFF5C:
103+
markers.append("QCD")
104+
length_bytes = buffer[offset: offset + 2]
105+
length = unpack(">H", length_bytes)[0]
106+
offset += length
107+
elif marker == 0xFF64:
108+
markers.append("COM")
109+
length_bytes = buffer[offset: offset + 2]
110+
length = unpack(">H", length_bytes)[0]
111+
offset += length
112+
elif marker == 0xFF90:
113+
markers.append("SOT")
114+
length_bytes = buffer[offset: offset + 2]
115+
length = unpack(">H", length_bytes)[0]
116+
offset += length
117+
elif marker == 0xFF93:
118+
markers.append("SOD")
119+
# If we get to here, we have the marker info we need
120+
break
121+
else:
122+
raise Exception(f"unexpected marker: 0x{marker:04X}")
123+
return markers
73124

74125
class TestEncode:
75126
"""Tests for encode_array()"""
@@ -700,7 +751,84 @@ def test_jp2(self):
700751

701752
buffer = encode_array(arr, codec_format=1)
702753
assert buffer.startswith(b"\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a")
754+
755+
def test_no_tlm_or_plt_explicit(self):
756+
"""Test encoding with no TLM or PLT, explicitly disabled"""
757+
rows = 123
758+
cols = 234
759+
bit_depth = 8
760+
maximum = 2**bit_depth - 1
761+
dtype = f"u{math.ceil(bit_depth / 8)}"
762+
arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype)
763+
buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_tlm=False, add_plt=False)
764+
out = decode(buffer)
765+
markers = parse_codestream_markers(buffer)
766+
assert "TLM" not in markers
767+
assert "PLT" not in markers
768+
assert np.allclose(arr, out, atol=5)
703769

770+
def test_no_tlm_or_plt_default(self):
771+
"""Test encoding with no TLM or PLT, default options"""
772+
rows = 123
773+
cols = 234
774+
bit_depth = 8
775+
maximum = 2**bit_depth - 1
776+
dtype = f"u{math.ceil(bit_depth / 8)}"
777+
arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype)
778+
buffer = encode_array(arr, compression_ratios=[4, 2, 1])
779+
out = decode(buffer)
780+
markers = parse_codestream_markers(buffer)
781+
assert "TLM" not in markers
782+
assert "PLT" not in markers
783+
assert np.allclose(arr, out, atol=5)
784+
785+
786+
def test_tlm(self):
787+
"""Test encoding with TLM"""
788+
rows = 123
789+
cols = 234
790+
bit_depth = 8
791+
maximum = 2**bit_depth - 1
792+
dtype = f"u{math.ceil(bit_depth / 8)}"
793+
arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype)
794+
buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_tlm=True)
795+
out = decode(buffer)
796+
markers = parse_codestream_markers(buffer)
797+
assert "TLM" in markers
798+
assert "PLT" not in markers
799+
assert np.allclose(arr, out, atol=5)
800+
801+
def test_plt(self):
802+
"""Test encoding with PLT"""
803+
rows = 123
804+
cols = 234
805+
bit_depth = 8
806+
maximum = 2**bit_depth - 1
807+
dtype = f"u{math.ceil(bit_depth / 8)}"
808+
arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype)
809+
buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_plt=True)
810+
out = decode(buffer)
811+
markers = parse_codestream_markers(buffer)
812+
assert "TLM" not in markers
813+
assert "PLT" in markers
814+
assert np.allclose(arr, out, atol=5)
815+
816+
def test_tlm_and_plt(self):
817+
"""Test encoding with both TLM and PLT"""
818+
rows = 123
819+
cols = 234
820+
bit_depth = 8
821+
maximum = 2**bit_depth - 1
822+
dtype = f"u{math.ceil(bit_depth / 8)}"
823+
arr = np.random.randint(0, high=maximum + 1, size=(rows, cols), dtype=dtype)
824+
buffer = encode_array(arr, compression_ratios=[4, 2, 1], add_plt=True, add_tlm=True)
825+
out = decode(buffer)
826+
param = parse_j2k(buffer)
827+
assert param["precision"] == bit_depth
828+
markers = parse_codestream_markers(buffer)
829+
assert "TLM" in markers
830+
assert "PLT" in markers
831+
assert np.allclose(arr, out, atol=5)
704832

705833
class TestEncodeBuffer:
706834
"""Tests for _openjpeg.encode_buffer"""

openjpeg/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ def encode_array(
434434
compression_ratios: Union[List[float], None] = None,
435435
signal_noise_ratios: Union[List[float], None] = None,
436436
codec_format: int = 0,
437+
add_tlm: bool = False,
438+
add_plt: bool = False,
437439
**kwargs: Any,
438440
) -> bytes:
439441
"""Return the JPEG 2000 compressed `arr`.
@@ -523,6 +525,13 @@ def encode_array(
523525
524526
* ``0``: JPEG 2000 codestream only (default) (J2K/J2C format)
525527
* ``1``: A boxed JPEG 2000 codestream (JP2 format)
528+
add_tlm : bool, optional
529+
Add tile-part length markers (TLM) to the codestream. This can help
530+
to speed up decoding of parts of very large images for some decoders.
531+
add_plt : bool, optional
532+
Add packet length, tile-part length markers (PLT) to the codestream. This
533+
can help to speed up decoding of parts of very large images for some
534+
decoders.
526535
527536
Returns
528537
-------
@@ -553,6 +562,8 @@ def encode_array(
553562
compression_ratios,
554563
signal_noise_ratios,
555564
codec_format,
565+
add_tlm,
566+
add_plt,
556567
)
557568

558569
if return_code != 0:

0 commit comments

Comments
 (0)