@@ -222,3 +222,155 @@ def test_read_header_info(self, tmp_path):
222222 assert header .numresolutions == 4
223223 assert header .irreversible is False
224224 grok_core .grk_object_unref (codec2 )
225+
226+
227+ class TestFastMct :
228+ """Tests for the --fast-mct (fast_16bit_mct) option: 9/7 MCT decompress via int16 DWT."""
229+
230+ def _compress_irrev_rgb (self , path , width , height , prec ):
231+ """Compress a synthetic RGB image with irreversible 9/7 + MCT."""
232+ params = grok_core .grk_cparameters ()
233+ grok_core .grk_compress_set_default_params (params )
234+ params .cod_format = grok_core .GRK_FMT_J2K
235+ params .irreversible = True
236+ params .mct = 1
237+
238+ image = grok_core .grk_image_new_uniform (
239+ 3 , width , height , 1 , 1 , prec , False , grok_core .GRK_CLRSPC_SRGB
240+ )
241+ if image is None :
242+ return False
243+
244+ max_val = (1 << prec ) - 1
245+ for c in range (3 ):
246+ comp = image .comps [c ]
247+ data_ptr = ctypes .cast (
248+ int (comp .data ),
249+ ctypes .POINTER (ctypes .c_int32 * (comp .h * comp .stride )),
250+ )
251+ arr = data_ptr .contents
252+ for y in range (comp .h ):
253+ for x in range (comp .w ):
254+ arr [y * comp .stride + x ] = ((x * 13 + y * 7 + c * 41 ) * 37 ) % (max_val + 1 )
255+
256+ stream = grok_core .grk_stream_params ()
257+ stream .file = path
258+ codec = grok_core .grk_compress_init (stream , params , image )
259+ if codec is None :
260+ grok_core .grk_object_unref (image .obj )
261+ return False
262+
263+ length = grok_core .grk_compress (codec , None )
264+ grok_core .grk_object_unref (codec )
265+ grok_core .grk_object_unref (image .obj )
266+ return length > 0
267+
268+ def _decompress_with_fast_mct (self , path , fast_mct ):
269+ """Decompress with fast_16bit_mct flag set or unset."""
270+ stream = grok_core .grk_stream_params ()
271+ stream .file = path
272+
273+ params = grok_core .grk_decompress_parameters ()
274+ params .core .fast_16bit_mct = fast_mct
275+ codec = grok_core .grk_decompress_init (stream , params )
276+ if codec is None :
277+ return None , None
278+
279+ header = grok_core .grk_header_info ()
280+ if not grok_core .grk_decompress_read_header (codec , header ):
281+ grok_core .grk_object_unref (codec )
282+ return None , None
283+
284+ image = grok_core .grk_decompress_get_image (codec )
285+ if image is None :
286+ grok_core .grk_object_unref (codec )
287+ return None , None
288+
289+ if not grok_core .grk_decompress (codec , None ):
290+ grok_core .grk_object_unref (codec )
291+ return None , None
292+
293+ return image , codec
294+
295+ def test_fast_mct_12bit_produces_int16 (self , tmp_path ):
296+ """12-bit RGB 9/7 with fast_mct should use GRK_INT_16 data type."""
297+ j2k = str (tmp_path / "rgb12_irrev.j2k" )
298+ assert self ._compress_irrev_rgb (j2k , 64 , 64 , 12 )
299+
300+ image , codec = self ._decompress_with_fast_mct (j2k , True )
301+ assert image is not None
302+ for c in range (3 ):
303+ assert image .comps [c ].data_type == grok_core .GRK_INT_16
304+ grok_core .grk_object_unref (codec )
305+
306+ def test_no_fast_mct_12bit_produces_int32 (self , tmp_path ):
307+ """12-bit RGB 9/7 without fast_mct should use GRK_INT_32 (conformant path)."""
308+ j2k = str (tmp_path / "rgb12_irrev.j2k" )
309+ assert self ._compress_irrev_rgb (j2k , 64 , 64 , 12 )
310+
311+ image , codec = self ._decompress_with_fast_mct (j2k , False )
312+ assert image is not None
313+ for c in range (3 ):
314+ assert image .comps [c ].data_type == grok_core .GRK_INT_32
315+ grok_core .grk_object_unref (codec )
316+
317+ def test_fast_mct_pixel_values_close (self , tmp_path ):
318+ """fast_mct output should be within ±2 of conformant path for 12-bit 9/7."""
319+ j2k = str (tmp_path / "rgb12_irrev.j2k" )
320+ w , h , prec = 64 , 64 , 12
321+ assert self ._compress_irrev_rgb (j2k , w , h , prec )
322+
323+ # Decompress with conformant path
324+ img_ref , codec_ref = self ._decompress_with_fast_mct (j2k , False )
325+ assert img_ref is not None
326+
327+ # Decompress with fast_mct path
328+ img_fast , codec_fast = self ._decompress_with_fast_mct (j2k , True )
329+ assert img_fast is not None
330+
331+ max_diff = 0
332+ for c in range (3 ):
333+ comp_ref = img_ref .comps [c ]
334+ comp_fast = img_fast .comps [c ]
335+
336+ ref_ptr = ctypes .cast (
337+ int (comp_ref .data ),
338+ ctypes .POINTER (ctypes .c_int32 * (comp_ref .h * comp_ref .stride )),
339+ )
340+ fast_ptr = ctypes .cast (
341+ int (comp_fast .data ),
342+ ctypes .POINTER (ctypes .c_int16 * (comp_fast .h * comp_fast .stride )),
343+ )
344+ ref_arr = ref_ptr .contents
345+ fast_arr = fast_ptr .contents
346+
347+ for y in range (h ):
348+ for x in range (w ):
349+ ref_val = ref_arr [y * comp_ref .stride + x ]
350+ fast_val = fast_arr [y * comp_fast .stride + x ]
351+ diff = abs (ref_val - fast_val )
352+ if diff > max_diff :
353+ max_diff = diff
354+
355+ # Fast path uses Q15 fixed-point ICT and int16 DWT coefficients,
356+ # which introduces quantization error vs the float path.
357+ # For 12-bit content, max diff ~40 is typical (< 1% of range).
358+ assert max_diff <= 40 , f"Max pixel difference { max_diff } exceeds tolerance of 40"
359+
360+ grok_core .grk_object_unref (codec_ref )
361+ grok_core .grk_object_unref (codec_fast )
362+
363+ def test_grk_get_data_type_fast_mct (self ):
364+ """grk_get_data_type with fast_mct=True returns INT_16 for 12-bit 9/7 MCT."""
365+ # 12-bit, MCT, 9/7, fast_mct=True → INT_16
366+ assert (
367+ grok_core .grk_get_data_type (False , 12 , True , 0 , True ) == grok_core .GRK_INT_16
368+ )
369+ # 12-bit, MCT, 9/7, fast_mct=False → INT_32 (conformant)
370+ assert (
371+ grok_core .grk_get_data_type (False , 12 , True , 0 , False ) == grok_core .GRK_INT_32
372+ )
373+ # 12-bit, no MCT, 9/7 → INT_16 regardless of fast_mct
374+ assert (
375+ grok_core .grk_get_data_type (False , 12 , False , 0 , False ) == grok_core .GRK_INT_16
376+ )
0 commit comments