Skip to content

Commit 7418218

Browse files
committed
body: semantic audit — eyes solid (not blue), nervous label, QA gate script
Addresses the ChatGPT visual audit: - Eyes no longer blue: the iris and choroid (eyeball vascular layers) were tagged 'vessel' → material 3 (blue). Reclassified to organ (the brain's "choroid plexus" is excluded). Sclera/cornea/retina/vitreous/lens were already solid. - UI label "nerve" → "nervous" (the compartment is the nervous system; today it's brain-only, hierarchy to follow). - New crates/osint-bake/tools/audit_body_semantics.py — a QA gate: * QA-1 organ-scale vessels (blob in a vessel material — the heart/liver/iris class) * QA-2 floating/misplaced geometry (non-bilateral split; caught "right fibular vein" with a chunk in the thigh far from its calf body) * QA-3 per-organ compartment smoke tests (liver/eyeball→organ, brain→nervous, femur→skeleton, biceps→muscle, aorta/vena cava→vessel) — exit non-zero on fail. All QA-3 smoke tests pass after the liver + eye fixes. x-ray/solid already only affects transparency (uGlobalAlpha/depthWrite), never compartment visibility (uEnabled) — verified, no change needed. Wire + LOD blocks rebaked + reuploaded.
1 parent d6329e8 commit 7418218

4 files changed

Lines changed: 205 additions & 8 deletions

File tree

cockpit/src/BodyV3.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const LAYERS: { id: number; name: string; color: string }[] = [
2929
{ id: 3, name: 'organ', color: '#cc9484' },
3030
{ id: 4, name: 'skeleton', color: '#ebe0c7' },
3131
{ id: 5, name: 'vessel', color: '#cc3838' },
32-
{ id: 6, name: 'nerve', color: '#ebd152' },
32+
{ id: 6, name: 'nervous', color: '#ebd152' },
3333
{ id: 7, name: 'connective', color: '#e0dbcc' },
3434
{ id: 8, name: 'other', color: '#9696a0' },
3535
];
0 Bytes
Binary file not shown.
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env python3
2+
"""Semantic-layer audit for the baked FMA body (QA gate).
3+
4+
Runs over a baked `<soa_dir>` (body.concepts.json + columns) and checks the
5+
ontology / classification rules the /body viewer relies on:
6+
7+
QA-1 organ-scale vessel — a concept in a vessel material (0..3) whose mesh is
8+
a low-aspect BLOB (not a thin tube), single connected component, organ-sized.
9+
Catches the heart / liver / iris / choroid class of "whole organ rendered as
10+
a blue vessel" bug.
11+
QA-2 orphan / missing class — a concept with no name, or no is_a parent and no
12+
part_of placement (parent_row == -1 and part_of all zero).
13+
QA-3 smoke tests — assert representative structures land in the right
14+
compartment: liver/eyeball→organ, brain→nervous, femur→skeleton,
15+
biceps→muscle, aorta/vena cava→vessel.
16+
17+
Exit code is non-zero if any QA-3 smoke test fails, so it can gate CI / a rebake.
18+
Usage: python3 audit_body_semantics.py <soa_dir> (default: soa)
19+
"""
20+
import json
21+
import os
22+
import re
23+
import struct
24+
import sys
25+
from collections import defaultdict
26+
27+
VESSEL_MATERIALS = {0, 1, 2, 3}
28+
29+
# mirror of soabake's layer_of(tissue) → compartment id, and the UI layer names.
30+
LAYER_OF = {
31+
"skin": 1, "flesh": 1, "muscle": 2,
32+
"heart": 3, "lung": 3, "liver": 3, "kidney": 3, "gi": 3, "gland": 3, "viscus": 3,
33+
"bone": 4, "cartilage": 4, "artery": 5, "vein": 5, "vessel": 5, "nerve": 6,
34+
}
35+
LAYER_NAME = {1: "skin", 2: "muscle", 3: "organ", 4: "skeleton", 5: "vessel", 6: "nervous", 7: "connective", 8: "other"}
36+
37+
38+
def layer_of(tissue):
39+
return LAYER_OF.get(tissue, 8)
40+
41+
42+
def n_components(pts, cell=0.05):
43+
grid = defaultdict(list)
44+
for i, p in enumerate(pts):
45+
grid[(int(p[0] // cell), int(p[1] // cell), int(p[2] // cell))].append(i)
46+
seen, n = set(), 0
47+
for s in list(grid):
48+
if s in seen:
49+
continue
50+
n += 1
51+
st = [s]
52+
seen.add(s)
53+
while st:
54+
c = st.pop()
55+
for dx in (-1, 0, 1):
56+
for dy in (-1, 0, 1):
57+
for dz in (-1, 0, 1):
58+
nb = (c[0] + dx, c[1] + dy, c[2] + dz)
59+
if nb in grid and nb not in seen:
60+
seen.add(nb)
61+
st.append(nb)
62+
return n
63+
64+
65+
def main(d):
66+
doc = json.load(open(os.path.join(d, "body.concepts.json")))
67+
labels = json.load(open(os.path.join(d, "body.labels.json")))
68+
concepts = doc["concepts"]
69+
nV = doc["verts"]
70+
pos = struct.unpack(f"<{nV * 3}f", open(os.path.join(d, "body.pos"), "rb").read()[:nV * 12])
71+
row = struct.unpack(f"<{nV}I", open(os.path.join(d, "body.row"), "rb").read()[:nV * 4])
72+
by_row = defaultdict(list)
73+
for i in range(nV):
74+
by_row[row[i]].append(i)
75+
76+
def name(c):
77+
return labels[c["name_idx"]] if 0 <= c["name_idx"] < len(labels) else ""
78+
79+
# ── QA-1: organ-scale vessel ───────────────────────────────────────────────
80+
print("── QA-1 organ-scale vessels (blob in a vessel material) ──")
81+
q1 = []
82+
for c in concepts:
83+
if c["material"] not in VESSEL_MATERIALS:
84+
continue
85+
idx = by_row.get(c["row"], [])
86+
if len(idx) < 400:
87+
continue
88+
xs = [pos[i * 3] for i in idx]
89+
ys = [pos[i * 3 + 1] for i in idx]
90+
zs = [pos[i * 3 + 2] for i in idx]
91+
dims = sorted([max(xs) - min(xs), max(ys) - min(ys), max(zs) - min(zs)])
92+
if dims[0] < 1e-6:
93+
continue
94+
aspect = dims[2] / dims[0]
95+
# organ blob: low aspect AND a fat minor axis AND a single solid component
96+
if aspect < 1.6 and dims[0] > 0.05 and n_components([(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]) for i in idx]) == 1:
97+
q1.append((dims[0], aspect, name(c), c["material"], len(idx)))
98+
for mind, asp, nm, mat, n in sorted(q1, reverse=True):
99+
print(f" ⚠ min={mind:.2f} aspect={asp:.2f} mat={mat} v={n:>6} {nm}")
100+
if not q1:
101+
print(" ✓ none")
102+
103+
# ── QA-2: floating / misplaced geometry ────────────────────────────────────
104+
# A concept whose mesh splits into regions FAR apart that are NOT a left/right
105+
# mirror pair = geometry attached to the wrong place (the vessel-bridge / "organ
106+
# fragment floating elsewhere" class). Bilateral pairs (hands, ears) are expected.
107+
print("── QA-2 floating / misplaced geometry (non-bilateral split) ──")
108+
q2 = 0
109+
for c in concepts:
110+
if not name(c):
111+
q2 += 1
112+
print(f" ⚠ row {c['row']:>4} unnamed concept")
113+
continue
114+
idx = by_row.get(c["row"], [])
115+
if len(idx) < 300:
116+
continue
117+
pts = [(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]) for i in idx]
118+
# cluster into far-apart regions (coarse cell)
119+
grid = defaultdict(list)
120+
for p in pts:
121+
grid[(round(p[0] / 0.12), round(p[1] / 0.12), round(p[2] / 0.12))].append(p)
122+
seen, comps = set(), []
123+
for s in list(grid):
124+
if s in seen:
125+
continue
126+
stack, cells = [s], []
127+
seen.add(s)
128+
while stack:
129+
cc = stack.pop()
130+
cells.append(cc)
131+
for dx in (-1, 0, 1):
132+
for dy in (-1, 0, 1):
133+
for dz in (-1, 0, 1):
134+
nb = (cc[0] + dx, cc[1] + dy, cc[2] + dz)
135+
if nb in grid and nb not in seen:
136+
seen.add(nb)
137+
stack.append(nb)
138+
comps.append([q for cell in cells for q in grid[cell]])
139+
if len(comps) < 2:
140+
continue
141+
comps.sort(key=len, reverse=True)
142+
cen = [[sum(p[k] for p in comp) / len(comp) for k in range(3)] for comp in comps[:2]]
143+
sep = sum((cen[0][k] - cen[1][k]) ** 2 for k in range(3)) ** 0.5
144+
# bilateral if the two regions are x-mirror images (x flips, y/z stay)
145+
bilateral = abs(cen[0][0] + cen[1][0]) < 0.06 and abs(cen[0][1] - cen[1][1]) < 0.08 and abs(cen[0][2] - cen[1][2]) < 0.08
146+
if sep > 0.18 and not bilateral:
147+
q2 += 1
148+
print(f" ⚠ {name(c)} sep={sep:.2f} cen0=({cen[0][0]:.2f},{cen[0][1]:.2f},{cen[0][2]:.2f}) cen1=({cen[1][0]:.2f},{cen[1][1]:.2f},{cen[1][2]:.2f})")
149+
print(f" {'⚠' if q2 else '✓'} {q2} concept(s) with non-bilateral split geometry")
150+
151+
# ── QA-3: smoke tests ──────────────────────────────────────────────────────
152+
print("── QA-3 per-organ compartment smoke tests ──")
153+
fails = 0
154+
155+
def assert_layer(label_pat, want_layers, exclude=None):
156+
nonlocal fails
157+
pat = re.compile(label_pat, re.I)
158+
exc = re.compile(exclude, re.I) if exclude else None
159+
hits = [c for c in concepts if pat.search(name(c)) and not (exc and exc.search(name(c)))]
160+
bad = [c for c in hits if layer_of(c["tissue"]) not in want_layers]
161+
want = "/".join(LAYER_NAME[w] for w in want_layers)
162+
if not hits:
163+
print(f" ? {label_pat!r}: no concepts matched (skipped)")
164+
return
165+
if bad:
166+
fails += 1
167+
print(f" ✗ {label_pat!r} should be {want}: {len(bad)}/{len(hits)} in wrong compartment, e.g.:")
168+
for c in bad[:4]:
169+
print(f" {name(c)}{LAYER_NAME[layer_of(c['tissue'])]} (tissue={c['tissue']}, mat={c['material']})")
170+
else:
171+
print(f" ✓ {label_pat!r}: all {len(hits)} in {want}")
172+
173+
# liver parenchyma → organ (exclude the true hepatic/portal vessels)
174+
assert_layer(r"hepatovenous segment|caudate lobe of liver", {3})
175+
# eyeball structures → organ (skin-layer flesh ok too); must NOT be vessel
176+
assert_layer(r"sclera|cornea|retina|vitreous|^.*\biris\b|choroid(?! plexus)|eyeball", {1, 3}, exclude=r"plexus")
177+
# brain → nervous
178+
assert_layer(r"\bbrain\b|cerebral cortex|cerebellum", {6}, exclude=r"artery|vein|vessel")
179+
# femur → skeleton
180+
assert_layer(r"\bfemur\b", {4})
181+
# biceps → muscle
182+
assert_layer(r"biceps", {2})
183+
# aorta / vena cava trunks → vessel (exclude organ-supply *branches* of the aorta,
184+
# which correctly carry their target organ's tissue)
185+
assert_layer(r"\baorta\b|vena cava", {5}, exclude=r"branch|oesophageal|bronchial")
186+
187+
print(f"\nsummary: QA-1 flagged {len(q1)} · QA-2 flagged {q2} · QA-3 {'FAILED ' + str(fails) if fails else 'passed'}")
188+
sys.exit(1 if fails else 0)
189+
190+
191+
if __name__ == "__main__":
192+
main(sys.argv[1] if len(sys.argv) > 1 else "soa")

crates/osint-bake/tools/bake_body_soa.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,19 @@ def label_index(s):
158158
r = row_of[c]
159159
nm = canon.get(c, name.get(c, c))
160160
tissue = tissue_of(c, parent_isa, name, canon, tcache)
161-
# Liver parenchyma is modelled as Couinaud "hepatovenous segment N" — named for
162-
# its venous drainage, so tissue_of tags it 'vein'. That coloured the whole liver
163-
# blue and slicer-filled it as a tube (it read as a blue vessel blob above the
164-
# colon). It is solid liver tissue; only the true hepatic/portal *veins* (which
165-
# all carry 'vein' in their name) stay vessels. Reclassify to liver → solid
166-
# organ colour + the organ compartment.
167-
if "hepatovenous segment" in nm.lower():
161+
# ── semantic-class corrections: structures named for their vasculature that
162+
# tissue_of mis-tags as vessels, turning whole ORGANS blue + slicer-filled ──
163+
nm_l = nm.lower()
164+
# Liver parenchyma = Couinaud "hepatovenous segment N" (named for venous
165+
# drainage) → was tagged 'vein'. It's solid liver; only the true hepatic/portal
166+
# *veins* (all carry 'vein' in their name) stay vessels.
167+
if "hepatovenous segment" in nm_l:
168168
tissue = "liver"
169+
# Eyeball vascular layers — iris + choroid — were tagged 'vessel', so the eyes
170+
# rendered blue. They're part of a sense ORGAN → solid/organ. NB the brain's
171+
# "choroid plexus" is NOT the eye, so it is excluded.
172+
elif "iris" in nm_l or ("choroid" in nm_l and "plexus" not in nm_l):
173+
tissue = "viscus"
169174
material = TISSUE_MATERIAL.get(tissue, 4)
170175
v_start = len(px)
171176
for fj in meshes_of[c]:

0 commit comments

Comments
 (0)