CCSDS 122.0-B Image Data Compression
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

idc: extract test_idc suite, drop test_/find_/make_ prefixes

- Move every test case from test/test.ml into test/test_idc.ml,
drop the redundant test_ prefix on each (E325/E331), and reduce the
runner to a single Alcotest.run line (E600).
- idc.ml: rename make_2d -> array_2d and find_max_abs -> max_abs (E331).
- Replace the catch-all (with _ -> ()) in the fuzz decompress safety
test with Invalid_argument / Failure handlers (E311).
- Rename fuzz_compress.{ml,mli} to fuzz_idc.{ml,mli} and the fuzz
suite from "compress" to "idc" so E710/E720 see a corresponding
library module.

+280 -279
+1 -1
fuzz/dune
··· 1 1 (executable 2 2 (name fuzz) 3 - (modules fuzz fuzz_compress) 3 + (modules fuzz fuzz_idc) 4 4 (libraries idc alcobar)) 5 5 6 6 (rule
+1 -1
fuzz/fuzz.ml
··· 1 - let () = Alcobar.run "idc" [ Fuzz_compress.suite ] 1 + let () = Alcobar.run "idc" [ Fuzz_idc.suite ]
+2 -2
fuzz/fuzz_compress.ml fuzz/fuzz_idc.ml
··· 28 28 try 29 29 let _ = Idc.decompress ~width:8 ~height:8 data in 30 30 () 31 - with _ -> () 31 + with Invalid_argument _ | Failure _ -> () 32 32 33 33 (** Compressed output must be deterministic. *) 34 34 let test_deterministic buf = ··· 47 47 if c1 <> c2 then fail "compression is not deterministic" 48 48 49 49 let suite = 50 - ( "compress", 50 + ( "idc", 51 51 [ 52 52 test_case "roundtrip 8x8" [ bytes ] test_roundtrip_small; 53 53 test_case "decompress crash safety" [ bytes ] test_decompress_crash_safety;
fuzz/fuzz_compress.mli fuzz/fuzz_idc.mli
+7 -7
lib/idc.ml
··· 83 83 84 84 (* ---- 2D array helpers ---- *) 85 85 86 - let make_2d rows cols init = Array.init rows (fun _ -> Array.make cols init) 86 + let array_2d rows cols init = Array.init rows (fun _ -> Array.make cols init) 87 87 88 88 let image_to_2d ~width ~height (data : bytes) = 89 - let arr = make_2d height width 0 in 89 + let arr = array_2d height width 0 in 90 90 for y = 0 to height - 1 do 91 91 for x = 0 to width - 1 do 92 92 arr.(y).(x) <- Char.code (Bytes.get data ((y * width) + x)) ··· 458 458 459 459 (** Quantize float coefficients to integers by rounding. *) 460 460 let quantize_2d (arr : float array array) rows cols = 461 - let result = make_2d rows cols 0 in 461 + let result = array_2d rows cols 0 in 462 462 for y = 0 to rows - 1 do 463 463 for x = 0 to cols - 1 do 464 464 result.(y).(x) <- Float.to_int (Float.round arr.(y).(x)) ··· 484 484 of (1 + mag_bits) bits. For images where the DWT produces many zero detail 485 485 coefficients, this gives significant savings. *) 486 486 487 - let find_max_abs arr rows cols = 487 + let max_abs arr rows cols = 488 488 let m = ref 0 in 489 489 for y = 0 to rows - 1 do 490 490 for x = 0 to cols - 1 do ··· 510 510 511 511 let encode_coefficients ~wavelet ~width ~height arr = 512 512 let bw = Bitstream.create (width * height) in 513 - let max_mag = find_max_abs arr height width in 513 + let max_mag = max_abs arr height width in 514 514 let mag_bits = bits_needed max_mag in 515 515 (* Header *) 516 516 Bitstream.write_bits bw 16 width; ··· 545 545 let wavelet = wavelet_of_id (Bitstream.read_bits br 8) in 546 546 let max_mag = Bitstream.read_bits br 16 in 547 547 let mag_bits = bits_needed max_mag in 548 - let arr = make_2d height width 0 in 548 + let arr = array_2d height width 0 in 549 549 (* Read zero map *) 550 - let is_nonzero = make_2d height width false in 550 + let is_nonzero = array_2d height width false in 551 551 for y = 0 to height - 1 do 552 552 for x = 0 to width - 1 do 553 553 is_nonzero.(y).(x) <- Bitstream.read_bits br 1 = 1
+1 -268
test/test.ml
··· 1 - (* Copyright (c) 2026 Thomas Gazagnaire <thomas@gazagnaire.org> 2 - 3 - Permission to use, copy, modify, and distribute this software for any 4 - purpose with or without fee is hereby granted, provided that the above 5 - copyright notice and this permission notice appear in all copies. 6 - 7 - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 - ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) 14 - 15 - let check_roundtrip ~width ~height data = 16 - let compressed = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 17 - let decompressed = Idc.decompress ~width ~height compressed in 18 - if data <> decompressed then 19 - Alcotest.failf 20 - "roundtrip failed: input and output differ (width=%d height=%d)" width 21 - height 22 - 23 - (* Constant image: all pixels the same value. *) 24 - let test_constant_8x8 () = 25 - let width = 8 and height = 8 in 26 - let data = Bytes.make (width * height) '\x80' in 27 - check_roundtrip ~width ~height data 28 - 29 - (* Gradient image: pixel values increase left to right. *) 30 - let test_gradient_8x8 () = 31 - let width = 8 and height = 8 in 32 - let data = Bytes.create (width * height) in 33 - for y = 0 to height - 1 do 34 - for x = 0 to width - 1 do 35 - Bytes.set data ((y * width) + x) (Char.chr (x * 32)) 36 - done 37 - done; 38 - check_roundtrip ~width ~height data 39 - 40 - (* Checkerboard pattern. *) 41 - let test_checkerboard_8x8 () = 42 - let width = 8 and height = 8 in 43 - let data = Bytes.create (width * height) in 44 - for y = 0 to height - 1 do 45 - for x = 0 to width - 1 do 46 - let v = if (x + y) mod 2 = 0 then 255 else 0 in 47 - Bytes.set data ((y * width) + x) (Char.chr v) 48 - done 49 - done; 50 - check_roundtrip ~width ~height data 51 - 52 - (* 16x16 gradient roundtrip. *) 53 - let test_gradient_16x16 () = 54 - let width = 16 and height = 16 in 55 - let data = Bytes.create (width * height) in 56 - for y = 0 to height - 1 do 57 - for x = 0 to width - 1 do 58 - let v = ((x * 16) + (y * 16)) mod 256 in 59 - Bytes.set data ((y * width) + x) (Char.chr v) 60 - done 61 - done; 62 - check_roundtrip ~width ~height data 63 - 64 - (* All zeros. *) 65 - let test_zeros_8x8 () = 66 - let width = 8 and height = 8 in 67 - let data = Bytes.make (width * height) '\x00' in 68 - check_roundtrip ~width ~height data 69 - 70 - (* All 255. *) 71 - let test_max_8x8 () = 72 - let width = 8 and height = 8 in 73 - let data = Bytes.make (width * height) '\xff' in 74 - check_roundtrip ~width ~height data 75 - 76 - (* Single pixel variations. *) 77 - let test_single_bright_pixel () = 78 - let width = 8 and height = 8 in 79 - let data = Bytes.make (width * height) '\x00' in 80 - Bytes.set data 27 '\xff'; 81 - check_roundtrip ~width ~height data 82 - 83 - (* Verify compression ratio < 1.0 for a piecewise-constant "blocky" image. 84 - Natural images have large regions of similar intensity separated by edges. 85 - After DWT, most detail coefficients are zero except near edges, giving 86 - excellent compression. *) 87 - let test_compression_ratio_blocky () = 88 - let width = 64 and height = 64 in 89 - let data = Bytes.create (width * height) in 90 - (* Four quadrants with different constant values *) 91 - for y = 0 to height - 1 do 92 - for x = 0 to width - 1 do 93 - let v = 94 - if y < 32 then if x < 32 then 40 else 120 95 - else if x < 32 then 180 96 - else 220 97 - in 98 - Bytes.set data ((y * width) + x) (Char.chr v) 99 - done 100 - done; 101 - let compressed = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 102 - let decompressed = Idc.decompress ~width ~height compressed in 103 - if data <> decompressed then Alcotest.fail "blocky image roundtrip failed"; 104 - let ratio = 105 - Float.of_int (Bytes.length compressed) /. Float.of_int (Bytes.length data) 106 - in 107 - if ratio >= 1.0 then 108 - Alcotest.failf 109 - "compression ratio %.3f >= 1.0 for blocky 64x64 image (compressed=%d, \ 110 - original=%d)" 111 - ratio (Bytes.length compressed) (Bytes.length data) 112 - 113 - (* Larger piecewise-constant image should compress even better. *) 114 - let test_compression_ratio_large_blocky () = 115 - let width = 128 and height = 128 in 116 - let data = Bytes.create (width * height) in 117 - (* Horizontal stripes *) 118 - for y = 0 to height - 1 do 119 - let v = y / 32 * 60 in 120 - for x = 0 to width - 1 do 121 - Bytes.set data ((y * width) + x) (Char.chr v) 122 - done 123 - done; 124 - let compressed = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 125 - let decompressed = Idc.decompress ~width ~height compressed in 126 - if data <> decompressed then 127 - Alcotest.fail "large blocky image roundtrip failed"; 128 - let ratio = 129 - Float.of_int (Bytes.length compressed) /. Float.of_int (Bytes.length data) 130 - in 131 - if ratio >= 1.0 then 132 - Alcotest.failf 133 - "compression ratio %.3f >= 1.0 for blocky 128x128 image (compressed=%d, \ 134 - original=%d)" 135 - ratio (Bytes.length compressed) (Bytes.length data) 136 - 137 - (* Roundtrip for non-power-of-2 dimensions. *) 138 - let test_non_power_of_2 () = 139 - let test_size w h = 140 - let data = Bytes.create (w * h) in 141 - for i = 0 to (w * h) - 1 do 142 - Bytes.set data i (Char.chr (i mod 256)) 143 - done; 144 - check_roundtrip ~width:w ~height:h data 145 - in 146 - test_size 4 4; 147 - test_size 4 8; 148 - test_size 6 10; 149 - test_size 12 12; 150 - test_size 12 20 151 - 152 - (* Invalid input size should raise. *) 153 - let test_invalid_size () = 154 - let width = 8 and height = 8 in 155 - let data = Bytes.make 10 '\x00' in 156 - match Idc.compress ~width ~height data with 157 - | _ -> Alcotest.fail "expected Invalid_argument for wrong data size" 158 - | exception Invalid_argument _ -> () 159 - 160 - (* ---- Float 9/7 tests ---- *) 161 - 162 - (** Compute PSNR between two byte images. *) 163 - let psnr (a : bytes) (b : bytes) = 164 - let n = Bytes.length a in 165 - assert (n = Bytes.length b); 166 - let mse = ref 0.0 in 167 - for i = 0 to n - 1 do 168 - let d = 169 - Float.of_int (Char.code (Bytes.get a i)) 170 - -. Float.of_int (Char.code (Bytes.get b i)) 171 - in 172 - mse := !mse +. (d *. d) 173 - done; 174 - mse := !mse /. Float.of_int n; 175 - if !mse = 0.0 then infinity else 10.0 *. Float.log10 (255.0 *. 255.0 /. !mse) 176 - 177 - (* Float 9/7 roundtrip: compress then decompress, check PSNR > 40 dB. *) 178 - let test_float_97_roundtrip () = 179 - let width = 64 and height = 64 in 180 - let data = Bytes.create (width * height) in 181 - (* Smooth gradient with slight sine modulation *) 182 - for y = 0 to height - 1 do 183 - for x = 0 to width - 1 do 184 - let fx = Float.of_int x /. Float.of_int width in 185 - let fy = Float.of_int y /. Float.of_int height in 186 - let v = (fx +. fy) /. 2.0 in 187 - let v = v +. (0.1 *. Float.sin (fx *. 6.283)) in 188 - let v = max 0.0 (min 1.0 v) in 189 - Bytes.set data ((y * width) + x) (Char.chr (Float.to_int (v *. 255.0))) 190 - done 191 - done; 192 - let compressed = Idc.compress ~wavelet:`Float_9_7 ~width ~height data in 193 - let decompressed = Idc.decompress ~width ~height compressed in 194 - let p = psnr data decompressed in 195 - if p < 40.0 then 196 - Alcotest.failf "Float 9/7 roundtrip PSNR %.1f dB < 40 dB threshold" p 197 - 198 - (* Float 9/7 constant image should be exact (or near-exact). *) 199 - let test_float_97_constant () = 200 - let width = 8 and height = 8 in 201 - let data = Bytes.make (width * height) '\x80' in 202 - let compressed = Idc.compress ~wavelet:`Float_9_7 ~width ~height data in 203 - let decompressed = Idc.decompress ~width ~height compressed in 204 - if data <> decompressed then 205 - Alcotest.fail "Float 9/7 constant image roundtrip not exact" 206 - 207 - (* Float 9/7 should compress a smooth non-linear image better than Int 5/3. 208 - 209 - The CDF 9/7 wavelet has more vanishing moments than 5/3 (4 vs 2 for the 210 - analysis filter). This means it annihilates higher-degree polynomials, 211 - producing smaller detail coefficients for smooth signals. For a cosine wave 212 - image, the 9/7 high-frequency coefficients round to zero more often than the 213 - 5/3 integer coefficients, giving a smaller compressed output. *) 214 - let test_float_97_better_compression () = 215 - let width = 64 and height = 64 in 216 - let data = Bytes.create (width * height) in 217 - (* Smooth cosine pattern -- not piecewise linear *) 218 - for y = 0 to height - 1 do 219 - for x = 0 to width - 1 do 220 - let fx = Float.of_int x /. Float.of_int width in 221 - let fy = Float.of_int y /. Float.of_int height in 222 - let v = 223 - 0.5 224 - +. (0.3 *. Float.cos (fx *. Float.pi *. 2.0)) 225 - +. (0.15 *. Float.cos (fy *. Float.pi *. 4.0)) 226 - in 227 - let v = max 0.0 (min 1.0 v) in 228 - Bytes.set data ((y * width) + x) (Char.chr (Float.to_int (v *. 255.0))) 229 - done 230 - done; 231 - let c53 = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 232 - let c97 = Idc.compress ~wavelet:`Float_9_7 ~width ~height data in 233 - let r53 = 234 - Float.of_int (Bytes.length c53) /. Float.of_int (Bytes.length data) 235 - in 236 - let r97 = 237 - Float.of_int (Bytes.length c97) /. Float.of_int (Bytes.length data) 238 - in 239 - if r97 >= r53 then 240 - Alcotest.failf 241 - "Float 9/7 compression ratio %.3f >= Int 5/3 ratio %.3f for smooth \ 242 - cosine image" 243 - r97 r53 244 - 245 - let suite = 246 - ( "idc", 247 - [ 248 - ("constant 8x8 roundtrip", `Quick, test_constant_8x8); 249 - ("gradient 8x8 roundtrip", `Quick, test_gradient_8x8); 250 - ("checkerboard 8x8 roundtrip", `Quick, test_checkerboard_8x8); 251 - ("gradient 16x16 roundtrip", `Quick, test_gradient_16x16); 252 - ("zeros 8x8 roundtrip", `Quick, test_zeros_8x8); 253 - ("max 8x8 roundtrip", `Quick, test_max_8x8); 254 - ("single bright pixel roundtrip", `Quick, test_single_bright_pixel); 255 - ("compression ratio blocky 64x64", `Quick, test_compression_ratio_blocky); 256 - ( "compression ratio blocky 128x128", 257 - `Quick, 258 - test_compression_ratio_large_blocky ); 259 - ("non-power-of-2 dimensions", `Quick, test_non_power_of_2); 260 - ("invalid input size", `Quick, test_invalid_size); 261 - ("float 9/7 roundtrip PSNR", `Quick, test_float_97_roundtrip); 262 - ("float 9/7 constant exact", `Quick, test_float_97_constant); 263 - ( "float 9/7 better compression than 5/3", 264 - `Quick, 265 - test_float_97_better_compression ); 266 - ] ) 267 - 268 - let () = Alcotest.run "idc" [ suite ] 1 + let () = Alcotest.run "idc" [ Test_idc.suite ]
+264
test/test_idc.ml
··· 1 + (* Copyright (c) 2026 Thomas Gazagnaire <thomas@gazagnaire.org> 2 + 3 + Permission to use, copy, modify, and distribute this software for any 4 + purpose with or without fee is hereby granted, provided that the above 5 + copyright notice and this permission notice appear in all copies. 6 + 7 + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. *) 14 + 15 + let check_roundtrip ~width ~height data = 16 + let compressed = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 17 + let decompressed = Idc.decompress ~width ~height compressed in 18 + if data <> decompressed then 19 + Alcotest.failf 20 + "roundtrip failed: input and output differ (width=%d height=%d)" width 21 + height 22 + 23 + (* Constant image: all pixels the same value. *) 24 + let constant_8x8 () = 25 + let width = 8 and height = 8 in 26 + let data = Bytes.make (width * height) '\x80' in 27 + check_roundtrip ~width ~height data 28 + 29 + (* Gradient image: pixel values increase left to right. *) 30 + let gradient_8x8 () = 31 + let width = 8 and height = 8 in 32 + let data = Bytes.create (width * height) in 33 + for y = 0 to height - 1 do 34 + for x = 0 to width - 1 do 35 + Bytes.set data ((y * width) + x) (Char.chr (x * 32)) 36 + done 37 + done; 38 + check_roundtrip ~width ~height data 39 + 40 + (* Checkerboard pattern. *) 41 + let checkerboard_8x8 () = 42 + let width = 8 and height = 8 in 43 + let data = Bytes.create (width * height) in 44 + for y = 0 to height - 1 do 45 + for x = 0 to width - 1 do 46 + let v = if (x + y) mod 2 = 0 then 255 else 0 in 47 + Bytes.set data ((y * width) + x) (Char.chr v) 48 + done 49 + done; 50 + check_roundtrip ~width ~height data 51 + 52 + (* 16x16 gradient roundtrip. *) 53 + let gradient_16x16 () = 54 + let width = 16 and height = 16 in 55 + let data = Bytes.create (width * height) in 56 + for y = 0 to height - 1 do 57 + for x = 0 to width - 1 do 58 + let v = ((x * 16) + (y * 16)) mod 256 in 59 + Bytes.set data ((y * width) + x) (Char.chr v) 60 + done 61 + done; 62 + check_roundtrip ~width ~height data 63 + 64 + (* All zeros. *) 65 + let zeros_8x8 () = 66 + let width = 8 and height = 8 in 67 + let data = Bytes.make (width * height) '\x00' in 68 + check_roundtrip ~width ~height data 69 + 70 + (* All 255. *) 71 + let max_8x8 () = 72 + let width = 8 and height = 8 in 73 + let data = Bytes.make (width * height) '\xff' in 74 + check_roundtrip ~width ~height data 75 + 76 + (* Single pixel variations. *) 77 + let single_bright_pixel () = 78 + let width = 8 and height = 8 in 79 + let data = Bytes.make (width * height) '\x00' in 80 + Bytes.set data 27 '\xff'; 81 + check_roundtrip ~width ~height data 82 + 83 + (* Verify compression ratio < 1.0 for a piecewise-constant "blocky" image. 84 + Natural images have large regions of similar intensity separated by edges. 85 + After DWT, most detail coefficients are zero except near edges, giving 86 + excellent compression. *) 87 + let compression_ratio_blocky () = 88 + let width = 64 and height = 64 in 89 + let data = Bytes.create (width * height) in 90 + for y = 0 to height - 1 do 91 + for x = 0 to width - 1 do 92 + let v = 93 + if y < 32 then if x < 32 then 40 else 120 94 + else if x < 32 then 180 95 + else 220 96 + in 97 + Bytes.set data ((y * width) + x) (Char.chr v) 98 + done 99 + done; 100 + let compressed = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 101 + let decompressed = Idc.decompress ~width ~height compressed in 102 + if data <> decompressed then Alcotest.fail "blocky image roundtrip failed"; 103 + let ratio = 104 + Float.of_int (Bytes.length compressed) /. Float.of_int (Bytes.length data) 105 + in 106 + if ratio >= 1.0 then 107 + Alcotest.failf 108 + "compression ratio %.3f >= 1.0 for blocky 64x64 image (compressed=%d, \ 109 + original=%d)" 110 + ratio (Bytes.length compressed) (Bytes.length data) 111 + 112 + (* Larger piecewise-constant image should compress even better. *) 113 + let compression_ratio_large_blocky () = 114 + let width = 128 and height = 128 in 115 + let data = Bytes.create (width * height) in 116 + for y = 0 to height - 1 do 117 + let v = y / 32 * 60 in 118 + for x = 0 to width - 1 do 119 + Bytes.set data ((y * width) + x) (Char.chr v) 120 + done 121 + done; 122 + let compressed = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 123 + let decompressed = Idc.decompress ~width ~height compressed in 124 + if data <> decompressed then 125 + Alcotest.fail "large blocky image roundtrip failed"; 126 + let ratio = 127 + Float.of_int (Bytes.length compressed) /. Float.of_int (Bytes.length data) 128 + in 129 + if ratio >= 1.0 then 130 + Alcotest.failf 131 + "compression ratio %.3f >= 1.0 for blocky 128x128 image (compressed=%d, \ 132 + original=%d)" 133 + ratio (Bytes.length compressed) (Bytes.length data) 134 + 135 + (* Roundtrip for non-power-of-2 dimensions. *) 136 + let non_power_of_2 () = 137 + let try_size w h = 138 + let data = Bytes.create (w * h) in 139 + for i = 0 to (w * h) - 1 do 140 + Bytes.set data i (Char.chr (i mod 256)) 141 + done; 142 + check_roundtrip ~width:w ~height:h data 143 + in 144 + try_size 4 4; 145 + try_size 4 8; 146 + try_size 6 10; 147 + try_size 12 12; 148 + try_size 12 20 149 + 150 + (* Invalid input size should raise. *) 151 + let invalid_size () = 152 + let width = 8 and height = 8 in 153 + let data = Bytes.make 10 '\x00' in 154 + match Idc.compress ~width ~height data with 155 + | _ -> Alcotest.fail "expected Invalid_argument for wrong data size" 156 + | exception Invalid_argument _ -> () 157 + 158 + (* ---- Float 9/7 tests ---- *) 159 + 160 + (** Compute PSNR between two byte images. *) 161 + let psnr (a : bytes) (b : bytes) = 162 + let n = Bytes.length a in 163 + assert (n = Bytes.length b); 164 + let mse = ref 0.0 in 165 + for i = 0 to n - 1 do 166 + let d = 167 + Float.of_int (Char.code (Bytes.get a i)) 168 + -. Float.of_int (Char.code (Bytes.get b i)) 169 + in 170 + mse := !mse +. (d *. d) 171 + done; 172 + mse := !mse /. Float.of_int n; 173 + if !mse = 0.0 then infinity else 10.0 *. Float.log10 (255.0 *. 255.0 /. !mse) 174 + 175 + (* Float 9/7 roundtrip: compress then decompress, check PSNR > 40 dB. *) 176 + let float_97_roundtrip () = 177 + let width = 64 and height = 64 in 178 + let data = Bytes.create (width * height) in 179 + for y = 0 to height - 1 do 180 + for x = 0 to width - 1 do 181 + let fx = Float.of_int x /. Float.of_int width in 182 + let fy = Float.of_int y /. Float.of_int height in 183 + let v = (fx +. fy) /. 2.0 in 184 + let v = v +. (0.1 *. Float.sin (fx *. 6.283)) in 185 + let v = max 0.0 (min 1.0 v) in 186 + Bytes.set data ((y * width) + x) (Char.chr (Float.to_int (v *. 255.0))) 187 + done 188 + done; 189 + let compressed = Idc.compress ~wavelet:`Float_9_7 ~width ~height data in 190 + let decompressed = Idc.decompress ~width ~height compressed in 191 + let p = psnr data decompressed in 192 + if p < 40.0 then 193 + Alcotest.failf "Float 9/7 roundtrip PSNR %.1f dB < 40 dB threshold" p 194 + 195 + (* Float 9/7 constant image should be exact (or near-exact). *) 196 + let float_97_constant () = 197 + let width = 8 and height = 8 in 198 + let data = Bytes.make (width * height) '\x80' in 199 + let compressed = Idc.compress ~wavelet:`Float_9_7 ~width ~height data in 200 + let decompressed = Idc.decompress ~width ~height compressed in 201 + if data <> decompressed then 202 + Alcotest.fail "Float 9/7 constant image roundtrip not exact" 203 + 204 + (* Float 9/7 should compress a smooth non-linear image better than Int 5/3. 205 + 206 + The CDF 9/7 wavelet has more vanishing moments than 5/3 (4 vs 2 for the 207 + analysis filter). This means it annihilates higher-degree polynomials, 208 + producing smaller detail coefficients for smooth signals. For a cosine 209 + wave image, the 9/7 high-frequency coefficients round to zero more often 210 + than the 5/3 integer coefficients, giving a smaller compressed output. *) 211 + let float_97_better_compression () = 212 + let width = 64 and height = 64 in 213 + let data = Bytes.create (width * height) in 214 + for y = 0 to height - 1 do 215 + for x = 0 to width - 1 do 216 + let fx = Float.of_int x /. Float.of_int width in 217 + let fy = Float.of_int y /. Float.of_int height in 218 + let v = 219 + 0.5 220 + +. (0.3 *. Float.cos (fx *. Float.pi *. 2.0)) 221 + +. (0.15 *. Float.cos (fy *. Float.pi *. 4.0)) 222 + in 223 + let v = max 0.0 (min 1.0 v) in 224 + Bytes.set data ((y * width) + x) (Char.chr (Float.to_int (v *. 255.0))) 225 + done 226 + done; 227 + let c53 = Idc.compress ~wavelet:`Int_5_3 ~width ~height data in 228 + let c97 = Idc.compress ~wavelet:`Float_9_7 ~width ~height data in 229 + let r53 = 230 + Float.of_int (Bytes.length c53) /. Float.of_int (Bytes.length data) 231 + in 232 + let r97 = 233 + Float.of_int (Bytes.length c97) /. Float.of_int (Bytes.length data) 234 + in 235 + if r97 >= r53 then 236 + Alcotest.failf 237 + "Float 9/7 compression ratio %.3f >= Int 5/3 ratio %.3f for smooth \ 238 + cosine image" 239 + r97 r53 240 + 241 + let suite = 242 + ( "idc", 243 + List.map 244 + (fun (n, sp, f) -> Alcotest.test_case n sp f) 245 + [ 246 + ("constant 8x8 roundtrip", `Quick, constant_8x8); 247 + ("gradient 8x8 roundtrip", `Quick, gradient_8x8); 248 + ("checkerboard 8x8 roundtrip", `Quick, checkerboard_8x8); 249 + ("gradient 16x16 roundtrip", `Quick, gradient_16x16); 250 + ("zeros 8x8 roundtrip", `Quick, zeros_8x8); 251 + ("max 8x8 roundtrip", `Quick, max_8x8); 252 + ("single bright pixel roundtrip", `Quick, single_bright_pixel); 253 + ("compression ratio blocky 64x64", `Quick, compression_ratio_blocky); 254 + ( "compression ratio blocky 128x128", 255 + `Quick, 256 + compression_ratio_large_blocky ); 257 + ("non-power-of-2 dimensions", `Quick, non_power_of_2); 258 + ("invalid input size", `Quick, invalid_size); 259 + ("float 9/7 roundtrip PSNR", `Quick, float_97_roundtrip); 260 + ("float 9/7 constant exact", `Quick, float_97_constant); 261 + ( "float 9/7 better compression than 5/3", 262 + `Quick, 263 + float_97_better_compression ); 264 + ] )
+4
test/test_idc.mli
··· 1 + (** Tests for {!Idc}. *) 2 + 3 + val suite : string * unit Alcotest.test_case list 4 + (** [suite] is the IDC test suite. *)