From 201d4c98f16d1d332912d92d71057310bd5eca8d Mon Sep 17 00:00:00 2001 From: Scott Chase Waggener Date: Mon, 16 Feb 2026 13:41:45 -0600 Subject: [PATCH] Fix lossless restart prediction boundaries Reset prediction at restart intervals based on pixel index and add a restart-marker regression fixture to guard decoder behavior. --- src/decoder/lossless.rs | 65 +++++++++--------- tests/reftest/images/lossless/3/README.md | 17 +++++ .../images/lossless/3/restart_psv1_dri3.jpg | Bin 0 -> 3112 bytes .../images/lossless/3/restart_psv1_dri3.png | Bin 0 -> 1717 bytes 4 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 tests/reftest/images/lossless/3/README.md create mode 100644 tests/reftest/images/lossless/3/restart_psv1_dri3.jpg create mode 100644 tests/reftest/images/lossless/3/restart_psv1_dri3.png diff --git a/src/decoder/lossless.rs b/src/decoder/lossless.rs index cd00476f..d9cc9896 100644 --- a/src/decoder/lossless.rs +++ b/src/decoder/lossless.rs @@ -45,6 +45,16 @@ impl Decoder { let width = frame.image_size.width as usize; let height = frame.image_size.height as usize; + let point_transform = scan.point_transform; + let restart_interval = self.restart_interval as usize; + let prediction_baseline = if frame.precision > 1 + point_transform { + 1i32 << (frame.precision - point_transform - 1) + } else { + 0 + }; + let is_restart_boundary = |pixel_index: usize| { + restart_interval > 0 && pixel_index > 0 && pixel_index % restart_interval == 0 + }; let mut differences = vec![Vec::with_capacity(npixel); ncomp]; for _mcu_y in 0..height { @@ -107,32 +117,22 @@ impl Decoder { if scan.predictor_selection == Predictor::Ra { for (i, _component) in components.iter().enumerate() { - // calculate the top left pixel - let diff = differences[i][0]; - let prediction = 1 << (frame.precision - scan.point_transform - 1) as i32; - let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 - let result = result << scan.point_transform; - results[i][0] = result; + for mcu_y in 0..height { + for mcu_x in 0..width { + let pixel_index = mcu_y * width + mcu_x; + let diff = differences[i][pixel_index]; + let restart = is_restart_boundary(pixel_index); - // calculate leftmost column, using top pixel as predictor - let mut previous = result; - for mcu_y in 1..height { - let diff = differences[i][mcu_y * width]; - let prediction = previous as i32; - let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 - let result = result << scan.point_transform; - results[i][mcu_y * width] = result; - previous = result; - } + let prediction = if (mcu_x == 0 && mcu_y == 0) || restart { + prediction_baseline + } else if mcu_x == 0 { + results[i][pixel_index - width] as i32 + } else { + results[i][pixel_index - 1] as i32 + }; - // calculate rows, using left pixel as predictor - for mcu_y in 0..height { - for mcu_x in 1..width { - let diff = differences[i][mcu_y * width + mcu_x]; - let prediction = results[i][mcu_y * width + mcu_x - 1] as i32; let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 - let result = result << scan.point_transform; - results[i][mcu_y * width + mcu_x] = result; + results[i][pixel_index] = result << point_transform; } } } @@ -140,21 +140,21 @@ impl Decoder { for mcu_y in 0..height { for mcu_x in 0..width { for (i, _component) in components.iter().enumerate() { - let diff = differences[i][mcu_y * width + mcu_x]; + let pixel_index = mcu_y * width + mcu_x; + let diff = differences[i][pixel_index]; // The following lines could be further optimized, e.g. moving the checks // and updates of the previous values into the prediction function or // iterating such that diagonals with mcu_x + mcu_y = const are computed at // the same time to exploit independent predictions in this case if mcu_x > 0 { - ra[i] = results[i][mcu_y * frame.image_size.width as usize + mcu_x - 1]; + ra[i] = results[i][pixel_index - 1]; } if mcu_y > 0 { - rb[i] = - results[i][(mcu_y - 1) * frame.image_size.width as usize + mcu_x]; + let top_index = pixel_index - width; + rb[i] = results[i][top_index]; if mcu_x > 0 { - rc[i] = results[i] - [(mcu_y - 1) * frame.image_size.width as usize + (mcu_x - 1)]; + rc[i] = results[i][top_index - 1]; } } let prediction = predict( @@ -162,15 +162,14 @@ impl Decoder { rb[i] as i32, rc[i] as i32, scan.predictor_selection, - scan.point_transform, + point_transform, frame.precision, mcu_x, mcu_y, - self.restart_interval > 0 - && mcus_left_until_restart == self.restart_interval - 1, + is_restart_boundary(pixel_index), ); let result = ((prediction + diff) & 0xFFFF) as u16; // modulo 2^16 - results[i][mcu_y * width + mcu_x] = result << scan.point_transform; + results[i][pixel_index] = result << point_transform; } } } diff --git a/tests/reftest/images/lossless/3/README.md b/tests/reftest/images/lossless/3/README.md new file mode 100644 index 00000000..87ecbf69 --- /dev/null +++ b/tests/reftest/images/lossless/3/README.md @@ -0,0 +1,17 @@ +Synthetic JPEG Lossless restart-interval fixture. + +- Dimensions: `64 x 32`, grayscale 16-bit. +- Encoding: JPEG Lossless (PSV=1, Pt=0), restart interval every 3 rows. +- Files: + - `restart_psv1_dri3.jpg` (test input) + - `restart_psv1_dri3.png` (reference output) + +Generation command used: + +```bash +cjpeg -lossless 1,0 -precision 16 -restart 3 -outfile restart_psv1_dri3.jpg restart_psv1_dri3.pgm +``` + +The source pixel values are deterministic from this formula: + +`v(x, y) = ((y*257 + x*73 + (x*y)%31) * 13) % 65535` diff --git a/tests/reftest/images/lossless/3/restart_psv1_dri3.jpg b/tests/reftest/images/lossless/3/restart_psv1_dri3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..10d93eee5f87dc8b13de90455aa4d587e38eb122 GIT binary patch literal 3112 zcmb`Je>~K89LK*GQA8BONvj^@qA@u%Mt*PTa5^PR9-HD2Wnrp^H8pBC^cYG=?g&R@ zatcd%sPQPjjyvR{^~)UVT91wFq3OHN=X*b$i+VhEo*s|)=k@FUJ@h{M2+{KP^zuY- zI0V6gKLovvsB0nX5N8}t8$s_Ncwk{4MEp@xU$Ig{QwtqIl#v#chb#kKQ2bXAa=AY7 zN-={NE3c^&I)raqPzQMaemt zBsryONxL+#h`QEsCCYyGtS9b;V|&B&+NZd69#>CpsFy4BPa-}Tp*~jY)|ND(NmRI3zP}# zU6fzDs4eq%J+b^1R8J%WL!!Dkv0Z`c$AWRfx+3a&kTvDOI8eFL5wR*6Nn`Ug*@-7B z%}%n6=o2)$K$U7=%ye+!X)-Gn8vVM!V<&j{CxQlvpy+ai&U#7-r z@DI8rc)1-ltn@Zyj|BKXOq<9`o3x|k*fE+eF`6~{O*Hti6$F{k!&7RDB<9TU4Bfpq znBzB)wS#(Bou;*&&T-#KbT8ge3o+5w&Cn*CuA=_n?hz);nVTtyV!3%yI0;yda;8lQ zB_6uT9(cm#2}rL;C>h{MGn=6i$-B6aV%kl}S)ITs2(`F|iFM>^SZ97Mlg+avAKBjv zIQnCUg$VB6-SLp(zdA5Fi>LI?g@|C@xXK4KdyM9an6TX31Q89*-Fo0g9rUcPmC|HW zO_L$trnmY4S(Ymq(O*f4kN!eu4kf|&=Mai=`AP9fC9R_hxTi=2$zRv%ee@pKruFF_ z36R5Jo+1udnz`1zJ0_!_hcH-Kp_3A_L;rY_LS7(BE%*Ur5-BORpiK z88DVEnK@34G1u=h59%-|4TFRct00@h&!+EB@2iE>=I^uuoU`_B+tL;p1PSVbC4Jcv zY{->Bql>|9+z05)&)uAt9Wa``C2G&raFWkC!1lWpU*FBMvDj_Bv9x2OI3*v%Qi_6L zZVVlG;!11_KhS-?vn!8dkXL6PW^Eg>Jv_W~JiT+M&?>LMy3E=-JYF1G%%3v>`q$Dp zBsx(olBJSDvi**YUrOXDIhPeUQ#F?4$KdEYfGKn~m9gT-vJi*hT!sO@C{Lg)o@3s^*aQ^Zxw2liS=N+f(N6~6loo3!;(6fyiSdA zDsYH6Ah=jWDy+Mf*yC5$J@mjKqMQS?!aB}4(7K1lC!w}rh%F9H&cGsEs%F%y#%A<3 z*?ErpC-O%OOX6eCzD?kdm}z2(j6S*mRzyNVteg-J7bzd2?3a2IghzYSQw~`yZQ+tz z8uXT6FB=^wU@r}iHA1^VPl^1Q(6&%1&V;ry3^PlxHVj8ewi@uX7>x?`%((7P+P99a zd3xb+=0dNy{`J!2nK`daxmUOSB?q^tJ--h>C^#G&)75z0R(x_BWnW?=!2*l5OJdNN zP}Fa1$~}(u-f;9CabqTEGPw4q{3nbSJZXhZxr=2)Bf1Ve{5$^Y9(uoD=%MJ)e-WVk A8vp2Bwb+Yw8H;-n zi^6tGgaon|5oSs##G;TE6h>GA5fnlo0}(#Q~Cy=Gc#{M2(qL(!-nQ5nrv-i4zwdc-4L_+PYlfeoA^ zssbWRJ3|yGs6as{RiKj+J_UB!6SQhp1qv)vpg@5gy`V)-QC{t96(~^fPSUoowMT(g zZh zU%lO0wY3_eCStF-4)yh+>hM0TuJq~Ko;$LVW3+RG%R^UqNSLs9g)mvdR$grp))m5p z=_uzc%p>ng#T=;#Yl<-c3=Zhpz7-uB9i^Ri+I6-0>|M3WOuJQP+O0A-RUOBvYQ3(~ zjw|2E-Ad9;w{9{~nHTBSO}g1)S2yXVRkO@ha@}m|CSh9CsOR`S-L9-xD*A**D!QBb zihiY?nl3kBG4Hm;iPw%<;<`F{*pa(64((=#RX6)~`J6beA7vL~+id3@gU4?lNq{-a{MKYqDx% zg>^l&j5lDz3~P#E%`vaM4f9L)u=*qyzO2crK{KW&HpZCd8PjUL-fzIS zq_=JNHOi#6ZBi#$R3{m2+lqd{q&^E;HEWaFW?8G2_qJ`a)BPm$XQeoV%pa?R8q+@>SSF#6+M2m4(;yRE%|m! zKDXq`t4ieKQ;GJ^HAao~M@9Rx8d0kV?d{neMeJ{iC}MZYA4RxBgge!9^%=XP2zL~z z&(~*&l=Ie9AKo`)#@9MyU+c_K$2aAUuQ*PJ-hG$*oY#4~uOp9~KdR=qd^Ov=M(6E* zNB@(u$aZTA{4J$!{$0X$2j?SnB$78ybu%!Bq((I8? z!a6jk`34P61gWyMpbr8il zJE1OOO46sajgr<3X{FU>E2OnSS_6#O3AK_|iFrlrW2H%Jhw?vA*6Erq>N-^&XF_Ry z9aOaDNgvcWXQyio3^8pJOzS$+YG+#0^w??bvZqO=G|RN+?KIO$uvy#UfYvnA$}*#A ze#}pTMjf$>2es#%iw0dT79?F?Fyu!4r>CC#uU$4+bSuHS+wd>Bf&EJmeB_qhs>{1g zSGxp_+W)uzpWx5IVo=fk0qq~q)wf}cBz?*-sthw^l%Y+XoTJA+&@LHJE3?XwVOgtW zv`MD&8>;%}%J5rdg2Q^RL8r(!=yC(Zv_O#|Eil1?7RYLm1=h8}0s~sWXG#mqv7luZ z$ZM0G+7|EYL$=(mh7c24p!=<~b9|{BKjZJ*)dO-o)pK&3q)j=_(W4wCM#w40EIH*^ zW>tA|?C??{0{(O=$6v^CR}c6HPxYMFoKzc~`&v299Ma|6@XP-KDM02zW8>D#00000 LNkvXXu0mjfy1HMP literal 0 HcmV?d00001