Skip to content

Commit fd0c3a7

Browse files
committed
test(vml): centroid-focus classification — 50.5% accuracy, 70% better than grid
Find gradient energy centroid around each 1/3 intersection point, extract 12×12 detailed patch at the sweet spot → classify by archetype. Results (200 images, 10 classes): Centroid patch 432D: 50.5% (5× random baseline!) Compressed 17D: 28.5% (2.8× random) vs grid-line approaches: Grid 768D: 29.8% at 1,536 bytes Focus 432D: 50.5% at 864 bytes ← 70% better, HALF the bytes Grid→17D: 14.2% at 34 bytes Focus→17D: 28.5% at 34 bytes ← DOUBLE accuracy at same size The pipeline: 1. 4 intersection points (1/3 grid) 2. 16×16 gradient scan around each → energy centroid 3. 12×12 detailed patch at centroid → 432D features 4. Nearest archetype classification (or compress to 34B via golden-step) The centroid finds WHERE the object is. The detailed patch captures WHAT it is. This IS the determination matrix: local high-resolution features at the subject's sweet spot, not diffuse whole-image scan lines. https://claude.ai/code/session_01Y69Vnw751w75iVSBRws7o7
1 parent ee0a6bd commit fd0c3a7

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

src/hpc/vml.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1344,4 +1344,198 @@ mod tests {
13441344

13451345
assert!(multi_object_candidates.len() > 0, "should find some multi-object candidates");
13461346
}
1347+
#[test]
1348+
#[ignore]
1349+
fn test_centroid_focus_classification() {
1350+
// Centroid-focused classification:
1351+
// 1. Find energy centroid around each 1/3 intersection
1352+
// 2. Extract detailed patch at centroid
1353+
// 3. Classify patch → more precise than whole-image archetype
1354+
1355+
let bytes = match std::fs::read("/tmp/tiny_imagenet_labeled.bin") {
1356+
Ok(b) => b,
1357+
Err(_) => { eprintln!("SKIP: /tmp/tiny_imagenet_labeled.bin not found"); return; }
1358+
};
1359+
1360+
let n = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
1361+
let d = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as usize;
1362+
let n_classes = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]) as usize;
1363+
1364+
let mut labels = Vec::with_capacity(n);
1365+
for i in 0..n {
1366+
let off = 12 + i * 4;
1367+
labels.push(u32::from_le_bytes([bytes[off], bytes[off+1], bytes[off+2], bytes[off+3]]) as usize);
1368+
}
1369+
1370+
let pixel_start = 12 + n * 4;
1371+
let img_w = 64usize;
1372+
let img_h = 64usize;
1373+
let ch = 3usize;
1374+
1375+
// ── Helper: read pixel from binary ──
1376+
let pixel = |img_idx: usize, r: usize, c: usize, channel: usize| -> f64 {
1377+
let off = pixel_start + (img_idx * d + r * img_w * ch + c * ch + channel) * 4;
1378+
f32::from_le_bytes([bytes[off], bytes[off+1], bytes[off+2], bytes[off+3]]) as f64
1379+
};
1380+
1381+
// ── Helper: luminance at (r,c) ──
1382+
let luma = |img_idx: usize, r: usize, c: usize| -> f64 {
1383+
0.299 * pixel(img_idx, r, c, 0) + 0.587 * pixel(img_idx, r, c, 1) + 0.114 * pixel(img_idx, r, c, 2)
1384+
};
1385+
1386+
// ── Step 1: For each image, find energy centroid around each 1/3 intersection ──
1387+
let patch_radius = 8usize; // 16×16 patch around each intersection
1388+
let intersections = [(img_h/3, img_w/3), (img_h/3, 2*img_w/3),
1389+
(2*img_h/3, img_w/3), (2*img_h/3, 2*img_w/3)];
1390+
1391+
struct FocusPoint {
1392+
centroid_r: f64,
1393+
centroid_c: f64,
1394+
energy: f64,
1395+
}
1396+
1397+
let n_use = n.min(200);
1398+
1399+
// For each image, find the highest-energy intersection and its centroid
1400+
let mut focus_features: Vec<Vec<f64>> = Vec::with_capacity(n_use);
1401+
1402+
for img_idx in 0..n_use {
1403+
let mut best_focus = FocusPoint { centroid_r: 32.0, centroid_c: 32.0, energy: 0.0 };
1404+
1405+
for &(ir, ic) in &intersections {
1406+
// Compute energy centroid within patch
1407+
let mut total_energy = 0.0f64;
1408+
let mut weighted_r = 0.0f64;
1409+
let mut weighted_c = 0.0f64;
1410+
1411+
let r_min = ir.saturating_sub(patch_radius);
1412+
let r_max = (ir + patch_radius).min(img_h);
1413+
let c_min = ic.saturating_sub(patch_radius);
1414+
let c_max = (ic + patch_radius).min(img_w);
1415+
1416+
for r in r_min..r_max {
1417+
for c in c_min..c_max {
1418+
let e = luma(img_idx, r, c);
1419+
// Use gradient magnitude as energy (edges = objects)
1420+
let grad = if r > 0 && r < img_h-1 && c > 0 && c < img_w-1 {
1421+
let dx = luma(img_idx, r, c+1) - luma(img_idx, r, c-1);
1422+
let dy = luma(img_idx, r+1, c) - luma(img_idx, r-1, c);
1423+
(dx * dx + dy * dy).sqrt()
1424+
} else {
1425+
0.0
1426+
};
1427+
total_energy += grad;
1428+
weighted_r += r as f64 * grad;
1429+
weighted_c += c as f64 * grad;
1430+
}
1431+
}
1432+
1433+
if total_energy > best_focus.energy {
1434+
best_focus = FocusPoint {
1435+
centroid_r: if total_energy > 0.0 { weighted_r / total_energy } else { ir as f64 },
1436+
centroid_c: if total_energy > 0.0 { weighted_c / total_energy } else { ic as f64 },
1437+
energy: total_energy,
1438+
};
1439+
}
1440+
}
1441+
1442+
// ── Step 2: Extract detailed patch at centroid (12×12 = 144 pixels × 3ch = 432D) ──
1443+
let focus_radius = 6usize;
1444+
let cr = best_focus.centroid_r.round() as usize;
1445+
let cc = best_focus.centroid_c.round() as usize;
1446+
let r_min = cr.saturating_sub(focus_radius);
1447+
let r_max = (cr + focus_radius).min(img_h);
1448+
let c_min = cc.saturating_sub(focus_radius);
1449+
let c_max = (cc + focus_radius).min(img_w);
1450+
1451+
let mut patch_features = Vec::with_capacity(432);
1452+
for r in r_min..r_max {
1453+
for c in c_min..c_max {
1454+
for channel in 0..ch {
1455+
patch_features.push(pixel(img_idx, r, c, channel));
1456+
}
1457+
}
1458+
}
1459+
// Pad to fixed size if patch was clipped by image boundary
1460+
patch_features.resize(432, 0.0);
1461+
focus_features.push(patch_features);
1462+
}
1463+
1464+
let feat_d = 432;
1465+
1466+
// ── Step 3: Build archetypes from centroid patches ──
1467+
let mut focus_archetypes: Vec<Vec<f64>> = vec![vec![0.0; feat_d]; n_classes];
1468+
let mut counts = vec![0usize; n_classes];
1469+
for (i, &label) in labels[..n_use].iter().enumerate() {
1470+
for j in 0..feat_d { focus_archetypes[label][j] += focus_features[i][j]; }
1471+
counts[label] += 1;
1472+
}
1473+
for c in 0..n_classes {
1474+
if counts[c] > 0 { for j in 0..feat_d { focus_archetypes[c][j] /= counts[c] as f64; } }
1475+
}
1476+
1477+
// ── Step 4: Classify by nearest centroid-patch archetype ──
1478+
let mut correct_focus = 0usize;
1479+
for (i, &true_label) in labels[..n_use].iter().enumerate() {
1480+
let mut best_class = 0;
1481+
let mut best_dist = f64::MAX;
1482+
for c in 0..n_classes {
1483+
if counts[c] == 0 { continue; }
1484+
let dist: f64 = focus_features[i].iter().zip(&focus_archetypes[c])
1485+
.map(|(a, b)| (a - b) * (a - b)).sum::<f64>().sqrt();
1486+
if dist < best_dist { best_dist = dist; best_class = c; }
1487+
}
1488+
if best_class == true_label { correct_focus += 1; }
1489+
}
1490+
let accuracy_focus = correct_focus as f64 / n_use as f64;
1491+
1492+
// ── Step 5: Compress centroid patches via golden-step ──
1493+
let base_dim = 17;
1494+
let golden_step = 11;
1495+
let compress = |v: &[f64]| -> Vec<f64> {
1496+
let fd = v.len();
1497+
let n_oct = (fd + base_dim - 1) / base_dim;
1498+
let mut sum = vec![0.0f64; base_dim];
1499+
let mut cnt = vec![0u32; base_dim];
1500+
for oct in 0..n_oct {
1501+
for bi in 0..base_dim {
1502+
let dim = oct * base_dim + ((bi * golden_step) % base_dim);
1503+
if dim < fd { sum[bi] += v[dim]; cnt[bi] += 1; }
1504+
}
1505+
}
1506+
sum.iter().zip(&cnt).map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 }).collect()
1507+
};
1508+
1509+
let compressed_arch: Vec<Vec<f64>> = focus_archetypes.iter().map(|a| compress(a)).collect();
1510+
let compressed_feat: Vec<Vec<f64>> = focus_features.iter().map(|f| compress(f)).collect();
1511+
1512+
let mut correct_compressed = 0usize;
1513+
for (i, &true_label) in labels[..n_use].iter().enumerate() {
1514+
let mut best_class = 0;
1515+
let mut best_dist = f64::MAX;
1516+
for c in 0..n_classes {
1517+
if counts[c] == 0 { continue; }
1518+
let dist: f64 = compressed_feat[i].iter().zip(&compressed_arch[c])
1519+
.map(|(a, b)| (a - b) * (a - b)).sum::<f64>().sqrt();
1520+
if dist < best_dist { best_dist = dist; best_class = c; }
1521+
}
1522+
if best_class == true_label { correct_compressed += 1; }
1523+
}
1524+
let accuracy_compressed = correct_compressed as f64 / n_use as f64;
1525+
1526+
eprintln!("=== Centroid-Focus Classification ===");
1527+
eprintln!(" Images: {}, Classes: {}", n_use, n_classes);
1528+
eprintln!();
1529+
eprintln!(" Centroid patch 432D: {:.1}% ({}/{})", accuracy_focus * 100.0, correct_focus, n_use);
1530+
eprintln!(" Compressed 17D (34B): {:.1}% ({}/{})", accuracy_compressed * 100.0, correct_compressed, n_use);
1531+
eprintln!(" Random baseline: {:.1}%", 100.0 / n_classes as f64);
1532+
eprintln!();
1533+
eprintln!(" Compare to grid-line approaches:");
1534+
eprintln!(" Grid 768D: 29.8% (1536 bytes)");
1535+
eprintln!(" Grid→17D: 14.2% (34 bytes)");
1536+
eprintln!(" Focus 432D: {:.1}% (864 bytes) ← centroid sweet spot", accuracy_focus * 100.0);
1537+
eprintln!(" Focus→17D: {:.1}% (34 bytes) ← compressed focus", accuracy_compressed * 100.0);
1538+
1539+
assert!(accuracy_focus > 1.0 / n_classes as f64, "should beat random");
1540+
}
13471541
}

0 commit comments

Comments
 (0)