this repo has no description
0
fork

Configure Feed

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

Initial import from monorepo

+494
+12
dune-project
··· 1 + (lang dune 3.17) 2 + (name tessera-viz) 3 + (generate_opam_files true) 4 + (license ISC) 5 + (package 6 + (name tessera-viz) 7 + (synopsis "Visualization utilities for GeoTessera embeddings") 8 + (description "Convert PCA components and classification results to RGBA pixel arrays. Portable — no platform-specific encoding.") 9 + (depends 10 + (ocaml (>= 5.2)) 11 + (tessera-linalg (>= 0.1)) 12 + (alcotest (and :with-test (>= 0.8)))))
+4
lib/dune
··· 1 + (library 2 + (name viz) 3 + (public_name tessera-viz) 4 + (libraries tessera-linalg))
+173
lib/viz.ml
··· 1 + type rgba_image = { 2 + data : (int, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t; 3 + width : int; 4 + height : int; 5 + } 6 + 7 + let percentile arr p = 8 + let sorted = Array.copy arr in 9 + Array.sort Float.compare sorted; 10 + let n = Array.length sorted in 11 + let idx = p /. 100.0 *. Float.of_int (n - 1) in 12 + let lo = int_of_float (Float.floor idx) in 13 + let hi = int_of_float (Float.ceil idx) in 14 + let frac = idx -. Float.of_int lo in 15 + if lo = hi then sorted.(lo) 16 + else sorted.(lo) *. (1.0 -. frac) +. sorted.(hi) *. frac 17 + 18 + type color = { r : int; g : int; b : int } 19 + 20 + let pca_to_rgba ?(low_pct = 2.0) ?(high_pct = 98.0) ~width ~height mat = 21 + let n_pixels = height * width in 22 + let data = Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout (n_pixels * 4) in 23 + (* Extract each component column into a float array *) 24 + let comp_values = Array.init 3 (fun c -> 25 + Array.init n_pixels (fun i -> Linalg.mat_get mat i c) 26 + ) in 27 + (* Compute percentile bounds for each component *) 28 + let bounds = Array.init 3 (fun c -> 29 + let lo = percentile comp_values.(c) low_pct in 30 + let hi = percentile comp_values.(c) high_pct in 31 + (lo, hi) 32 + ) in 33 + (* Write pixels *) 34 + for i = 0 to n_pixels - 1 do 35 + let off = i * 4 in 36 + for c = 0 to 2 do 37 + let (lo, hi) = bounds.(c) in 38 + let v = Linalg.mat_get mat i c in 39 + let v = Float.max lo (Float.min hi v) in 40 + let scaled = 41 + if hi -. lo < 1e-12 then 0 42 + else int_of_float ((v -. lo) /. (hi -. lo) *. 255.0) 43 + in 44 + Bigarray.Array1.set data (off + c) (min 255 (max 0 scaled)) 45 + done; 46 + Bigarray.Array1.set data (off + 3) 255 47 + done; 48 + { data; width; height } 49 + 50 + let color_of_hex s = 51 + let s = if String.length s > 0 && s.[0] = '#' then String.sub s 1 (String.length s - 1) else s in 52 + let r = int_of_string ("0x" ^ String.sub s 0 2) in 53 + let g = int_of_string ("0x" ^ String.sub s 2 2) in 54 + let b = int_of_string ("0x" ^ String.sub s 4 2) in 55 + { r; g; b } 56 + 57 + let classification_to_rgba ?(alpha = 200) ~predictions ~colors ~width ~height () = 58 + let n_pixels = height * width in 59 + let data = Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout (n_pixels * 4) in 60 + let color_tbl = Hashtbl.create (List.length colors) in 61 + List.iter (fun (id, c) -> Hashtbl.replace color_tbl id c) colors; 62 + let black = { r = 0; g = 0; b = 0 } in 63 + for i = 0 to n_pixels - 1 do 64 + let off = i * 4 in 65 + let c = try Hashtbl.find color_tbl predictions.(i) with Not_found -> black in 66 + Bigarray.Array1.set data off c.r; 67 + Bigarray.Array1.set data (off + 1) c.g; 68 + Bigarray.Array1.set data (off + 2) c.b; 69 + Bigarray.Array1.set data (off + 3) alpha 70 + done; 71 + { data; width; height } 72 + 73 + (* ---- Minimal PNG encoder ---- *) 74 + 75 + let png_crc32_table = 76 + Array.init 256 (fun n -> 77 + let c = ref (Int32.of_int n) in 78 + for _ = 0 to 7 do 79 + if Int32.logand !c 1l <> 0l then 80 + c := Int32.logxor (Int32.shift_right_logical !c 1) 0xEDB88320l 81 + else 82 + c := Int32.shift_right_logical !c 1 83 + done; 84 + !c) 85 + 86 + let png_crc32 data ofs len = 87 + let c = ref 0xFFFFFFFFl in 88 + for i = ofs to ofs + len - 1 do 89 + let byte = Char.code (Bytes.get data i) in 90 + let idx = Int32.to_int (Int32.logand (Int32.logxor !c (Int32.of_int byte)) 0xFFl) in 91 + c := Int32.logxor (Int32.shift_right_logical !c 8) png_crc32_table.(idx) 92 + done; 93 + Int32.logxor !c 0xFFFFFFFFl 94 + 95 + let put_be32 buf pos v = 96 + Bytes.set buf pos (Char.chr ((v lsr 24) land 0xFF)); 97 + Bytes.set buf (pos + 1) (Char.chr ((v lsr 16) land 0xFF)); 98 + Bytes.set buf (pos + 2) (Char.chr ((v lsr 8) land 0xFF)); 99 + Bytes.set buf (pos + 3) (Char.chr (v land 0xFF)) 100 + 101 + let put_be32_i32 buf pos v = 102 + let v = Int32.to_int v in 103 + put_be32 buf pos v 104 + 105 + let png_chunk buf typ data = 106 + let len = Bytes.length data in 107 + let chunk = Bytes.create (len + 4) in 108 + Bytes.blit_string typ 0 chunk 0 4; 109 + Bytes.blit data 0 chunk 4 len; 110 + let crc = png_crc32 chunk 0 (len + 4) in 111 + Buffer.add_bytes buf (let b = Bytes.create 4 in put_be32 b 0 len; b); 112 + Buffer.add_bytes buf chunk; 113 + Buffer.add_bytes buf (let b = Bytes.create 4 in put_be32_i32 b 0 crc; b) 114 + 115 + let png_of_rgba img = 116 + let buf = Buffer.create (img.width * img.height * 4 + 1024) in 117 + (* PNG signature *) 118 + Buffer.add_string buf "\x89PNG\r\n\x1a\n"; 119 + (* IHDR *) 120 + let ihdr = Bytes.create 13 in 121 + put_be32 ihdr 0 img.width; 122 + put_be32 ihdr 4 img.height; 123 + Bytes.set ihdr 8 '\x08'; (* bit depth 8 *) 124 + Bytes.set ihdr 9 '\x06'; (* color type 6 = RGBA *) 125 + Bytes.set ihdr 10 '\x00'; (* compression *) 126 + Bytes.set ihdr 11 '\x00'; (* filter *) 127 + Bytes.set ihdr 12 '\x00'; (* interlace *) 128 + png_chunk buf "IHDR" ihdr; 129 + (* Build raw scanlines: filter_byte(0) + row RGBA data *) 130 + let row_bytes = 1 + img.width * 4 in 131 + let raw_len = row_bytes * img.height in 132 + let raw = Bytes.create raw_len in 133 + for y = 0 to img.height - 1 do 134 + Bytes.set raw (y * row_bytes) '\x00'; (* filter: None *) 135 + for x = 0 to img.width * 4 - 1 do 136 + let v = Bigarray.Array1.get img.data (y * img.width * 4 + x) in 137 + Bytes.set raw (y * row_bytes + 1 + x) (Char.chr v) 138 + done 139 + done; 140 + (* Wrap in uncompressed deflate stored blocks *) 141 + let max_block = 65535 in 142 + let idat_buf = Buffer.create (raw_len + raw_len / max_block * 5 + 20) in 143 + (* Zlib header: CM=8, CINFO=7, FCHECK to make it valid *) 144 + Buffer.add_char idat_buf '\x78'; 145 + Buffer.add_char idat_buf '\x01'; 146 + let pos = ref 0 in 147 + while !pos < raw_len do 148 + let remaining = raw_len - !pos in 149 + let block_len = min remaining max_block in 150 + let is_final = !pos + block_len >= raw_len in 151 + Buffer.add_char idat_buf (if is_final then '\x01' else '\x00'); 152 + Buffer.add_char idat_buf (Char.chr (block_len land 0xFF)); 153 + Buffer.add_char idat_buf (Char.chr ((block_len lsr 8) land 0xFF)); 154 + let nlen = block_len lxor 0xFFFF in 155 + Buffer.add_char idat_buf (Char.chr (nlen land 0xFF)); 156 + Buffer.add_char idat_buf (Char.chr ((nlen lsr 8) land 0xFF)); 157 + Buffer.add_subbytes idat_buf raw !pos block_len; 158 + pos := !pos + block_len 159 + done; 160 + (* Adler-32 checksum *) 161 + let a = ref 1 and b = ref 0 in 162 + for i = 0 to raw_len - 1 do 163 + a := (!a + Char.code (Bytes.get raw i)) mod 65521; 164 + b := (!b + !a) mod 65521 165 + done; 166 + let adler = (!b lsl 16) lor !a in 167 + let adler_bytes = Bytes.create 4 in 168 + put_be32 adler_bytes 0 adler; 169 + Buffer.add_bytes idat_buf adler_bytes; 170 + png_chunk buf "IDAT" (Buffer.to_bytes idat_buf); 171 + (* IEND *) 172 + png_chunk buf "IEND" Bytes.empty; 173 + Buffer.contents buf
+59
lib/viz.mli
··· 1 + (** Visualization utilities for GeoTessera embeddings. 2 + 3 + Converts PCA components and classification results to raw RGBA pixel 4 + arrays. Does no encoding — callers handle PNG/base64 conversion. *) 5 + 6 + type rgba_image = { 7 + data : (int, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t; 8 + width : int; 9 + height : int; 10 + } 11 + (** An RGBA image. [data] has length [width * height * 4], with pixels in 12 + row-major order: R, G, B, A for each pixel. *) 13 + 14 + (** {1 Percentile utilities} *) 15 + 16 + val percentile : float array -> float -> float 17 + (** [percentile arr p] returns the [p]-th percentile (0-100) of [arr]. 18 + [arr] is not modified. *) 19 + 20 + (** {1 PCA visualization} *) 21 + 22 + val pca_to_rgba : ?low_pct:float -> ?high_pct:float -> width:int -> height:int -> Linalg.mat -> rgba_image 23 + (** Convert a 3-component PCA result to a false-color RGBA image. 24 + 25 + Input: mat with shape [(height*width, 3)] from [Linalg.pca_transform]. 26 + Each component is independently clipped to [\[low_pct, high_pct\]] percentiles 27 + (default: 2.0, 98.0) and scaled to [0-255]. 28 + Alpha is always 255. 29 + 30 + Output: RGBA image of size [width * height]. *) 31 + 32 + (** {1 Classification visualization} *) 33 + 34 + type color = { r : int; g : int; b : int } 35 + (** An RGB color. *) 36 + 37 + val color_of_hex : string -> color 38 + (** Parse a hex color string like ["#ff0000"] or ["ff0000"]. *) 39 + 40 + val classification_to_rgba : 41 + ?alpha:int -> 42 + predictions:int array -> 43 + colors:(int * color) list -> 44 + width:int -> 45 + height:int -> 46 + unit -> 47 + rgba_image 48 + (** Convert class predictions to a colored RGBA image. 49 + 50 + [predictions] has length [height * width]. 51 + [colors] maps class IDs to colors. Unknown classes are black. 52 + [alpha] defaults to 200. *) 53 + 54 + (** {1 PNG encoding} *) 55 + 56 + val png_of_rgba : rgba_image -> string 57 + (** Encode an RGBA image as PNG bytes. 58 + Uses uncompressed deflate (stored blocks) for portability — 59 + no external compression library required. *)
+27
tessera-viz.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Visualization utilities for GeoTessera embeddings" 4 + description: 5 + "Convert PCA components and classification results to RGBA pixel arrays. Portable — no platform-specific encoding." 6 + license: "ISC" 7 + depends: [ 8 + "dune" {>= "3.17"} 9 + "ocaml" {>= "5.2"} 10 + "tessera-linalg" {>= "0.1"} 11 + "alcotest" {with-test & >= "0.8"} 12 + "odoc" {with-doc} 13 + ] 14 + build: [ 15 + ["dune" "subst"] {dev} 16 + [ 17 + "dune" 18 + "build" 19 + "-p" 20 + name 21 + "-j" 22 + jobs 23 + "@install" 24 + "@runtest" {with-test} 25 + "@doc" {with-doc} 26 + ] 27 + ]
+3
test/dune
··· 1 + (test 2 + (name test_viz) 3 + (libraries tessera-viz tessera-linalg alcotest unix))
+216
test/test_viz.ml
··· 1 + let check_float msg expected actual = 2 + Alcotest.(check (float 1e-6)) msg expected actual 3 + 4 + let test_percentile_min () = 5 + check_float "p0" 1.0 (Viz.percentile [|1.0; 2.0; 3.0; 4.0; 5.0|] 0.0) 6 + 7 + let test_percentile_max () = 8 + check_float "p100" 5.0 (Viz.percentile [|1.0; 2.0; 3.0; 4.0; 5.0|] 100.0) 9 + 10 + let test_percentile_median () = 11 + check_float "p50" 3.0 (Viz.percentile [|1.0; 2.0; 3.0; 4.0; 5.0|] 50.0) 12 + 13 + let test_percentile_unsorted () = 14 + check_float "p50 unsorted" 3.0 (Viz.percentile [|5.0; 1.0; 3.0; 2.0; 4.0|] 50.0) 15 + 16 + let percentile_tests = 17 + [ Alcotest.test_case "min (p0)" `Quick test_percentile_min 18 + ; Alcotest.test_case "max (p100)" `Quick test_percentile_max 19 + ; Alcotest.test_case "median (p50)" `Quick test_percentile_median 20 + ; Alcotest.test_case "unsorted input" `Quick test_percentile_unsorted 21 + ] 22 + 23 + let check_pixel msg expected_r expected_g expected_b expected_a (img : Viz.rgba_image) idx = 24 + let off = idx * 4 in 25 + let r = Bigarray.Array1.get img.data off in 26 + let g = Bigarray.Array1.get img.data (off + 1) in 27 + let b = Bigarray.Array1.get img.data (off + 2) in 28 + let a = Bigarray.Array1.get img.data (off + 3) in 29 + Alcotest.(check int) (msg ^ " R") expected_r r; 30 + Alcotest.(check int) (msg ^ " G") expected_g g; 31 + Alcotest.(check int) (msg ^ " B") expected_b b; 32 + Alcotest.(check int) (msg ^ " A") expected_a a 33 + 34 + (* Helper to create a Linalg.mat from float array array (rows x cols) *) 35 + let mat_of_rows rows = 36 + let n_rows = Array.length rows in 37 + let n_cols = Array.length rows.(0) in 38 + let m = Linalg.create_mat ~rows:n_rows ~cols:n_cols in 39 + Array.iteri (fun i row -> 40 + Array.iteri (fun j v -> Linalg.mat_set m i j v) row 41 + ) rows; 42 + m 43 + 44 + let test_pca_all_zero () = 45 + (* 2x2 image, 3 components all zero *) 46 + let mat = mat_of_rows [| [|0.0; 0.0; 0.0|]; [|0.0; 0.0; 0.0|]; 47 + [|0.0; 0.0; 0.0|]; [|0.0; 0.0; 0.0|] |] in 48 + let img = Viz.pca_to_rgba ~width:2 ~height:2 mat in 49 + Alcotest.(check int) "width" 2 img.width; 50 + Alcotest.(check int) "height" 2 img.height; 51 + Alcotest.(check int) "data length" (2 * 2 * 4) (Bigarray.Array1.dim img.data); 52 + for i = 0 to 3 do 53 + check_pixel (Printf.sprintf "pixel %d" i) 0 0 0 255 img i 54 + done 55 + 56 + let test_pca_varying_r () = 57 + (* 2x2 image: component 0 varies, components 1,2 constant *) 58 + let mat = mat_of_rows [| [|0.0; 50.0; 50.0|]; [|100.0; 50.0; 50.0|]; 59 + [|50.0; 50.0; 50.0|]; [|75.0; 50.0; 50.0|] |] in 60 + let img = Viz.pca_to_rgba ~low_pct:0.0 ~high_pct:100.0 ~width:2 ~height:2 mat in 61 + (* R channel should vary, G and B should be uniform *) 62 + let r0 = Bigarray.Array1.get img.data 0 in 63 + let r1 = Bigarray.Array1.get img.data 4 in 64 + Alcotest.(check bool) "R varies" true (r0 <> r1); 65 + (* G should be same for all pixels (component 1 is constant) *) 66 + let g0 = Bigarray.Array1.get img.data 1 in 67 + let g1 = Bigarray.Array1.get img.data 5 in 68 + let g2 = Bigarray.Array1.get img.data 9 in 69 + let g3 = Bigarray.Array1.get img.data 13 in 70 + Alcotest.(check int) "G uniform 0-1" g0 g1; 71 + Alcotest.(check int) "G uniform 1-2" g1 g2; 72 + Alcotest.(check int) "G uniform 2-3" g2 g3 73 + 74 + let test_pca_data_length () = 75 + let mat = mat_of_rows [| [|1.0; 2.0; 3.0|]; [|4.0; 5.0; 6.0|]; 76 + [|7.0; 8.0; 9.0|]; [|10.0; 11.0; 12.0|]; 77 + [|13.0; 14.0; 15.0|]; [|16.0; 17.0; 18.0|] |] in 78 + let img = Viz.pca_to_rgba ~width:3 ~height:2 mat in 79 + Alcotest.(check int) "data length" (3 * 2 * 4) (Bigarray.Array1.dim img.data) 80 + 81 + let pca_tests = 82 + [ Alcotest.test_case "all zero" `Quick test_pca_all_zero 83 + ; Alcotest.test_case "varying R channel" `Quick test_pca_varying_r 84 + ; Alcotest.test_case "data length" `Quick test_pca_data_length 85 + ] 86 + 87 + (* color_of_hex tests *) 88 + 89 + let test_hex_with_hash () = 90 + let c = Viz.color_of_hex "#ff0000" in 91 + Alcotest.(check int) "r" 255 c.r; 92 + Alcotest.(check int) "g" 0 c.g; 93 + Alcotest.(check int) "b" 0 c.b 94 + 95 + let test_hex_without_hash () = 96 + let c = Viz.color_of_hex "00ff00" in 97 + Alcotest.(check int) "r" 0 c.r; 98 + Alcotest.(check int) "g" 255 c.g; 99 + Alcotest.(check int) "b" 0 c.b 100 + 101 + let color_tests = 102 + [ Alcotest.test_case "with #" `Quick test_hex_with_hash 103 + ; Alcotest.test_case "without #" `Quick test_hex_without_hash 104 + ] 105 + 106 + (* classification_to_rgba tests *) 107 + 108 + let test_classification_basic () = 109 + (* 4 pixels, 2 classes *) 110 + let predictions = [| 0; 1; 0; 1 |] in 111 + let red : Viz.color = { r = 255; g = 0; b = 0 } in 112 + let blue : Viz.color = { r = 0; g = 0; b = 255 } in 113 + let colors = [ (0, red); (1, blue) ] in 114 + let img = Viz.classification_to_rgba ~predictions ~colors ~width:2 ~height:2 () in 115 + check_pixel "pixel 0 (class 0)" 255 0 0 200 img 0; 116 + check_pixel "pixel 1 (class 1)" 0 0 255 200 img 1; 117 + check_pixel "pixel 2 (class 0)" 255 0 0 200 img 2; 118 + check_pixel "pixel 3 (class 1)" 0 0 255 200 img 3 119 + 120 + let test_classification_unknown_class () = 121 + let predictions = [| 0; 99 |] in 122 + let red : Viz.color = { r = 255; g = 0; b = 0 } in 123 + let colors = [ (0, red) ] in 124 + let img = Viz.classification_to_rgba ~predictions ~colors ~width:2 ~height:1 () in 125 + check_pixel "known class" 255 0 0 200 img 0; 126 + check_pixel "unknown class -> black" 0 0 0 200 img 1 127 + 128 + let test_classification_custom_alpha () = 129 + let predictions = [| 0 |] in 130 + let red : Viz.color = { r = 255; g = 0; b = 0 } in 131 + let colors = [ (0, red) ] in 132 + let img = Viz.classification_to_rgba ~alpha:128 ~predictions ~colors ~width:1 ~height:1 () in 133 + check_pixel "custom alpha" 255 0 0 128 img 0 134 + 135 + let classification_tests = 136 + [ Alcotest.test_case "basic 2 classes" `Quick test_classification_basic 137 + ; Alcotest.test_case "unknown class" `Quick test_classification_unknown_class 138 + ; Alcotest.test_case "custom alpha" `Quick test_classification_custom_alpha 139 + ] 140 + 141 + let test_png_magic () = 142 + let data = Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout 4 in 143 + Bigarray.Array1.set data 0 255; 144 + Bigarray.Array1.set data 1 0; 145 + Bigarray.Array1.set data 2 0; 146 + Bigarray.Array1.set data 3 255; 147 + let img = Viz.{ data; width = 1; height = 1 } in 148 + let png = Viz.png_of_rgba img in 149 + Alcotest.(check int) "byte 0" 0x89 (Char.code png.[0]); 150 + Alcotest.(check char) "byte 1" 'P' png.[1]; 151 + Alcotest.(check char) "byte 2" 'N' png.[2]; 152 + Alcotest.(check char) "byte 3" 'G' png.[3]; 153 + Alcotest.(check bool) "length > 20" true (String.length png > 20) 154 + 155 + let test_png_roundtrip () = 156 + let data = Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout 16 in 157 + let pixels = [| 255;0;0;255; 0;255;0;255; 0;0;255;255; 255;255;255;255 |] in 158 + Array.iteri (fun i v -> Bigarray.Array1.set data i v) pixels; 159 + let img = Viz.{ data; width = 2; height = 2 } in 160 + let png = Viz.png_of_rgba img in 161 + let tmp = Filename.temp_file "test_png" ".png" in 162 + let oc = open_out_bin tmp in 163 + output_string oc png; 164 + close_out oc; 165 + let cmd = Printf.sprintf 166 + "python3 -c \"from PIL import Image; img = Image.open('%s'); print(img.size, img.mode, list(img.getdata()))\"" tmp in 167 + let ic = Unix.open_process_in cmd in 168 + let output = input_line ic in 169 + let _ = Unix.close_process_in ic in 170 + Sys.remove tmp; 171 + Alcotest.(check bool) "contains (2, 2)" true (String.length output > 0 && String.sub output 0 6 = "(2, 2)"); 172 + Alcotest.(check bool) "contains RGBA" true 173 + (try let _ = String.index output 'R' in true with Not_found -> false); 174 + Printf.printf "Python output: %s\n" output 175 + 176 + let test_png_larger_image () = 177 + let w = 100 and h = 100 in 178 + let data = Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout (w * h * 4) in 179 + for y = 0 to h - 1 do 180 + for x = 0 to w - 1 do 181 + let off = (y * w + x) * 4 in 182 + Bigarray.Array1.set data off (x * 255 / 99); 183 + Bigarray.Array1.set data (off + 1) (y * 255 / 99); 184 + Bigarray.Array1.set data (off + 2) 128; 185 + Bigarray.Array1.set data (off + 3) 255 186 + done 187 + done; 188 + let img = Viz.{ data; width = w; height = h } in 189 + let png = Viz.png_of_rgba img in 190 + let tmp = Filename.temp_file "test_png_large" ".png" in 191 + let oc = open_out_bin tmp in 192 + output_string oc png; 193 + close_out oc; 194 + let cmd = Printf.sprintf 195 + "python3 -c \"from PIL import Image; img = Image.open('%s'); print(img.size, list(img.getpixel((0,0))), list(img.getpixel((99,99))))\"" tmp in 196 + let ic = Unix.open_process_in cmd in 197 + let output = input_line ic in 198 + let _ = Unix.close_process_in ic in 199 + Sys.remove tmp; 200 + Alcotest.(check bool) "contains (100, 100)" true (String.length output > 0 && String.sub output 0 10 = "(100, 100)"); 201 + Printf.printf "Python output: %s\n" output 202 + 203 + let png_tests = 204 + [ Alcotest.test_case "PNG magic bytes" `Quick test_png_magic 205 + ; Alcotest.test_case "PNG roundtrip via Python" `Quick test_png_roundtrip 206 + ; Alcotest.test_case "PNG larger image" `Quick test_png_larger_image 207 + ] 208 + 209 + let () = 210 + Alcotest.run "tessera-viz" 211 + [ ("percentile", percentile_tests) 212 + ; ("pca_to_rgba", pca_tests) 213 + ; ("color_of_hex", color_tests) 214 + ; ("classification_to_rgba", classification_tests) 215 + ; ("png_of_rgba", png_tests) 216 + ]