- modules/Capture_File/segment_chunks.jai: a front "segs" chunk (text: engine, date, the image read and its size, the class list's hash, and each segment's id, score, area, box and class) and a back "mask" chunk (a segment id per pixel, run-length encoded with LEB128 lengths, longest side at most 1024), keyed by model. No new file version: chunks came in version 3. - mcap_store takes back chunks too, so a model's summary and mask are stored in one rewrite. - Tests: the text round trip, the runs' edge cases (one segment over everything, none, runs across rows, multi-byte lengths, id 255), bad masks refused, and storing, replacing and erasing both chunks in a file with other chunks kept. - segment-probe takes .mcap files (the image each is shown as; videos skipped) and with -store stores its segments in them. On 27 images a mask is 0.8 to 6.4 KB. - The format README's "Segmentation chunks" section; the plan's phase 3 results.
198 lines
8.4 KiB
Plaintext
198 lines
8.4 KiB
Plaintext
// The chunks a segmentation model adds to a capture (no new file version: chunks came in version 3),
|
|
// each keyed by the model's name:
|
|
//
|
|
// "segs", in front: what is in the image, as text in the style of the Ex4 text formats
|
|
//
|
|
// @version 1
|
|
// engine: "onnxruntime 1.30.0, CUDA"
|
|
// date: 2026-10-12T09:30:00Z
|
|
// image: rendered 4032 3024
|
|
// classes: 5c1e9a0d77b2f413
|
|
// segment: 1 0.981 0.2140 1210 380 900 2210 "person"
|
|
//
|
|
// image: which image was read (rendered or original) and its size, which the boxes are in. classes:
|
|
// a hash of the model's class list, so segments from another list can be told apart. segment: its
|
|
// id in the mask, its score (0 to 1), its area (a fraction of the image), its box (x y width
|
|
// height), and its class, quoted with "" for a quote; largest first. A line that cannot be read is
|
|
// skipped, as are unknown keys.
|
|
//
|
|
// "mask", after the images: the segment id of every pixel, run-length encoded:
|
|
//
|
|
// u8 format 1
|
|
// u16 width the map's: the image's shape, its longest side at most 1024 (or so the model says)
|
|
// u16 height
|
|
// u8 id bits 8
|
|
// then runs, row by row, wrapping from one row to the next: a u8 id (0: no segment) and a length,
|
|
// LEB128 (7 bits a byte, low first, the top bit set on all but the last). The runs cover every
|
|
// pixel exactly.
|
|
|
|
MCAP_SEGMENTS_TAG :: "segs";
|
|
MCAP_MASK_TAG :: "mask";
|
|
MCAP_SEGMENTS_TEXT_VERSION :: 1;
|
|
MCAP_MASK_RLE :: 1;
|
|
|
|
Mcap_Segment :: struct {
|
|
id: u8; // its pixels' id in the mask
|
|
score: float; // the model's confidence in its class, 0 to 1
|
|
area: float; // a fraction of the image
|
|
x, y, width, height: s32; // its box, in the image's pixels
|
|
class: string; // as people see and search it: "person", "wall"
|
|
}
|
|
|
|
Mcap_Segments :: struct {
|
|
engine: string; // what ran it, such as "onnxruntime 1.30.0, CUDA"
|
|
date: string; // when, UTC: 2026-10-12T09:30:00Z
|
|
image: string; // which image was read: "rendered" or "original"
|
|
image_width, image_height: s32;
|
|
classes: string; // the class list's hash
|
|
segments: [..] Mcap_Segment; // largest first
|
|
}
|
|
|
|
Mcap_Mask :: struct {
|
|
width, height: s32;
|
|
map: [] u8; // a segment id per pixel, row by row; 0: none
|
|
}
|
|
|
|
// A segs chunk's text. Temp.
|
|
mcap_segments_text :: (s: Mcap_Segments) -> string {
|
|
builder: String_Builder;
|
|
builder.allocator = temp;
|
|
print_to_builder(*builder, "@version %\n", MCAP_SEGMENTS_TEXT_VERSION);
|
|
print_to_builder(*builder, "engine: %\n", mcap_quote(s.engine));
|
|
print_to_builder(*builder, "date: %\n", s.date);
|
|
print_to_builder(*builder, "image: % % %\n", s.image, s.image_width, s.image_height);
|
|
print_to_builder(*builder, "classes: %\n", s.classes);
|
|
for s.segments {
|
|
print_to_builder(*builder, "segment: % % % % % % % %\n", it.id, formatFloat(it.score, trailing_width = 3, zero_removal = .NO),
|
|
formatFloat(it.area, trailing_width = 4, zero_removal = .NO), it.x, it.y, it.width, it.height, mcap_quote(it.class));
|
|
}
|
|
return builder_to_string(*builder,, temp);
|
|
}
|
|
|
|
// A segs chunk's segments; strings and arrays from the context allocator.
|
|
mcap_segments_parse :: (text: string) -> Mcap_Segments, ok: bool, error: string {
|
|
result: Mcap_Segments;
|
|
version_seen := false;
|
|
for raw: split(text, "\n",, temp) {
|
|
line := trim(raw);
|
|
if !line.count || line[0] == #char "#" continue;
|
|
if begins_with(line, "@version") {
|
|
rest := trim(advance(line, 8));
|
|
version, ok := string_to_int(rest);
|
|
if !ok || version < 1 || version > MCAP_SEGMENTS_TEXT_VERSION return result, false, tprint("its segments are version %, but this build reads up to %", rest, MCAP_SEGMENTS_TEXT_VERSION);
|
|
version_seen = true;
|
|
continue;
|
|
}
|
|
if !version_seen return result, false, "its segments do not start with @version";
|
|
colon := find_index_from_left(line, #char ":");
|
|
if colon < 0 continue;
|
|
key := trim(slice(line, 0, colon));
|
|
value := trim(advance(line, colon + 1));
|
|
if key == {
|
|
case "engine";
|
|
quoted, _, ok := mcap_read_quoted(value);
|
|
if ok result.engine = copy_string(quoted);
|
|
case "date"; result.date = copy_string(value);
|
|
case "classes"; result.classes = copy_string(value);
|
|
case "image";
|
|
parts := split(value, " ",, temp);
|
|
if parts.count == 3 {
|
|
w, w_ok := string_to_int(parts[1], T = s32);
|
|
h, h_ok := string_to_int(parts[2], T = s32);
|
|
if w_ok && h_ok { result.image = copy_string(parts[0]); result.image_width, result.image_height = w, h; }
|
|
}
|
|
case "segment";
|
|
// Seven numbers, then the quoted class.
|
|
quote := find_index_from_left(value, #char "\"");
|
|
if quote < 0 continue;
|
|
numbers := split(trim(slice(value, 0, quote)), " ",, temp);
|
|
if numbers.count != 7 continue;
|
|
id, id_ok := string_to_int(numbers[0]);
|
|
score, score_ok := string_to_float(numbers[1]);
|
|
area, area_ok := string_to_float(numbers[2]);
|
|
box: [4] s32;
|
|
box_ok := true;
|
|
for 0..3 {
|
|
n, ok := string_to_int(numbers[3 + it], T = s32);
|
|
box[it] = n;
|
|
box_ok &&= ok;
|
|
}
|
|
class, after, class_ok := mcap_read_quoted(advance(value, quote));
|
|
if !id_ok || id < 1 || id > 255 || !score_ok || !area_ok || !box_ok || !class_ok || trim(after).count continue;
|
|
array_add(*result.segments, .{cast(u8) id, score, area, box[0], box[1], box[2], box[3], copy_string(class)});
|
|
}
|
|
}
|
|
return result, true, "";
|
|
}
|
|
|
|
// A mask chunk's data for a map of width x height segment ids. Temp.
|
|
mcap_mask_bytes :: (width: s32, height: s32, map: [] u8) -> [] u8 {
|
|
assert(map.count == cast(s64) width * height && width > 0 && height > 0 && width <= 0xFFFF && height <= 0xFFFF);
|
|
out: [..] u8;
|
|
out.allocator = temp;
|
|
array_reserve(*out, 64 + map.count / 16);
|
|
array_add(*out, MCAP_MASK_RLE);
|
|
array_add(*out, cast(u8) (width & 0xFF), cast(u8) (width >> 8), cast(u8) (height & 0xFF), cast(u8) (height >> 8));
|
|
array_add(*out, 8);
|
|
at := 0;
|
|
while at < map.count {
|
|
id := map[at];
|
|
run := 1;
|
|
while at + run < map.count && map[at + run] == id run += 1;
|
|
array_add(*out, id);
|
|
length := cast(u64) run;
|
|
while true { // LEB128
|
|
byte := cast(u8) (length & 0x7F);
|
|
length >>= 7;
|
|
if length { array_add(*out, byte | 0x80); }
|
|
else { array_add(*out, byte); break; }
|
|
}
|
|
at += run;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// A mask chunk's map; the map from the context allocator.
|
|
mcap_mask_parse :: (data: [] u8) -> Mcap_Mask, ok: bool, error: string {
|
|
mask: Mcap_Mask;
|
|
if data.count < 6 return mask, false, "its mask is cut short";
|
|
if data[0] != MCAP_MASK_RLE return mask, false, tprint("its mask is in format %, which this build does not read", data[0]);
|
|
mask.width = (cast(s32) data[1]) | ((cast(s32) data[2]) << 8);
|
|
mask.height = (cast(s32) data[3]) | ((cast(s32) data[4]) << 8);
|
|
if data[5] != 8 return mask, false, tprint("its mask has %-bit ids, which this build does not read", data[5]);
|
|
if !mask.width || !mask.height return mask, false, "its mask has no size";
|
|
total := cast(s64) mask.width * mask.height;
|
|
mask.map = NewArray(total, u8, initialized = false);
|
|
filled := 0;
|
|
at := 6;
|
|
while at < data.count {
|
|
id := data[at];
|
|
at += 1;
|
|
length: u64;
|
|
shift: u32;
|
|
while true {
|
|
if at >= data.count || shift > 35 { array_free(mask.map); mask.map = .[]; return mask, false, "its mask's runs are cut short"; }
|
|
byte := data[at];
|
|
at += 1;
|
|
length |= (cast(u64) (byte & 0x7F)) << shift;
|
|
shift += 7;
|
|
if !(byte & 0x80) break;
|
|
}
|
|
if !length || cast(s64) length > total - filled { array_free(mask.map); mask.map = .[]; return mask, false, "its mask's runs do not fit its size"; }
|
|
memset(mask.map.data + filled, id, cast(s64) length);
|
|
filled += cast(s64) length;
|
|
}
|
|
if filled != total { array_free(mask.map); mask.map = .[]; return mask, false, "its mask's runs do not cover it"; }
|
|
return mask, true, "";
|
|
}
|
|
|
|
// The chunks for segments by model, ready to store together with mcap_store (segs in front, mask in
|
|
// back). Their data is temp.
|
|
mcap_segments_chunk :: (model: string, segments: Mcap_Segments) -> Mcap_Chunk {
|
|
return .{tag = mcap_tag(MCAP_SEGMENTS_TAG), key = model, data = cast([] u8) mcap_segments_text(segments)};
|
|
}
|
|
|
|
mcap_mask_chunk :: (model: string, width: s32, height: s32, map: [] u8) -> Mcap_Chunk {
|
|
return .{tag = mcap_tag(MCAP_MASK_TAG), key = model, data = mcap_mask_bytes(width, height, map)};
|
|
}
|