samuellimabraz Cursor commited on
Commit
da962e7
·
unverified ·
1 Parent(s): be27253

feat: kmeans viz, new media via LFS, classification layout fix

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ *.jpg filter=lfs diff=lfs merge=lfs -text
38
+ *.jpeg filter=lfs diff=lfs merge=lfs -text
39
+ *.png filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -5,3 +5,4 @@ out
5
  *.log
6
  .env*
7
  .vercel
 
 
5
  *.log
6
  .env*
7
  .vercel
8
+ *.tsbuildinfo
components/viz/ClassificationDemo.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import { useEffect, useState } from "react";
5
+ import { COLORS } from "./common";
6
+ import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
7
+
8
+ type Sample = {
9
+ src: string;
10
+ alt: string;
11
+ // First entry is the predicted label (highest confidence). Probabilities
12
+ // must sum to (approximately) 1.
13
+ probs: { label: string; p: number }[];
14
+ };
15
+
16
+ const SAMPLES: Sample[] = [
17
+ {
18
+ src: "/team/01.jpg",
19
+ alt: "team member 1",
20
+ probs: [
21
+ { label: "que mulher maravilhosa (UAU)", p: 0.94 },
22
+ { label: "readstone", p: 0.030 },
23
+ { label: "modelo de comercial de shampoo", p: 0.012 },
24
+ { label: "sereia", p: 0.011 },
25
+ { label: "engenheira da computação", p: 0.007 },
26
+ ],
27
+ },
28
+ {
29
+ src: "/team/02.jpg",
30
+ alt: "team member 2",
31
+ probs: [
32
+ { label: "frutinha++", p: 0.91 },
33
+ { label: "morango maduro", p: 0.040 },
34
+ { label: "compota artesanal", p: 0.025 },
35
+ { label: "smoothie de açaí", p: 0.015 },
36
+ { label: "fruta exótica", p: 0.010 },
37
+ ],
38
+ },
39
+ {
40
+ src: "/team/03.jpg",
41
+ alt: "team member 3",
42
+ probs: [
43
+ { label: "homens extremamente atraentes", p: 0.96 },
44
+ { label: "testosterona", p: 0.018 },
45
+ { label: "hardware", p: 0.012 },
46
+ { label: "modelos", p: 0.006 },
47
+ { label: "gambiarra", p: 0.004 },
48
+ ],
49
+ },
50
+ {
51
+ src: "/team/04.jpg",
52
+ alt: "team member 4",
53
+ probs: [
54
+ { label: "Jorge", p: 0.97 },
55
+ { label: "homem em flagrante", p: 0.015 },
56
+ { label: "professor disfarçado", p: 0.008 },
57
+ { label: "George Clooney brasileiro", p: 0.005 },
58
+ { label: "vendedor de seguros", p: 0.002 },
59
+ ],
60
+ },
61
+ {
62
+ src: "/team/05.jpg",
63
+ alt: "team member 5",
64
+ probs: [
65
+ { label: "boiadeira", p: 0.62 },
66
+ { label: "princesa da roça", p: 0.31 },
67
+ { label: "agro pop", p: 0.040 },
68
+ { label: "festa junina", p: 0.020 },
69
+ { label: "cowgirl", p: 0.010 },
70
+ ],
71
+ },
72
+ ];
73
+
74
+ export function ClassificationDemo({
75
+ intervalMs = 5500,
76
+ maxImageHeight = 360,
77
+ }: {
78
+ intervalMs?: number;
79
+ maxImageHeight?: number;
80
+ }) {
81
+ const [idx, setIdx] = useState(0);
82
+ const [paused, setPaused] = useState(false);
83
+ const reduce = useReducedMotion();
84
+
85
+ useEffect(() => {
86
+ if (paused || reduce) return;
87
+ const id = setInterval(
88
+ () => setIdx((i) => (i + 1) % SAMPLES.length),
89
+ intervalMs,
90
+ );
91
+ return () => clearInterval(id);
92
+ }, [paused, reduce, intervalMs]);
93
+
94
+ const sample = SAMPLES[idx];
95
+
96
+ return (
97
+ <figure className="mx-auto flex w-full max-w-[920px] flex-col items-center">
98
+ <div className="w-full overflow-hidden rounded-md border border-stroke bg-surface">
99
+ <div className="grid grid-cols-5 items-center gap-5 p-5">
100
+ {/* Image — left 2 cols, fixed max-height to keep portrait and
101
+ landscape photos at the same vertical footprint. */}
102
+ <div className="col-span-2 flex items-center justify-center">
103
+ <motion.img
104
+ key={sample.src}
105
+ src={sample.src}
106
+ alt={sample.alt}
107
+ initial={{ opacity: 0 }}
108
+ animate={{ opacity: 1 }}
109
+ transition={{ duration: 0.4 }}
110
+ className="rounded-md border border-stroke object-contain"
111
+ style={{ maxHeight: maxImageHeight, maxWidth: "100%" }}
112
+ />
113
+ </div>
114
+
115
+ {/* Probability list — right 3 cols, auto-sized */}
116
+ <div className="col-span-3 flex flex-col gap-2.5">
117
+ <div className="mb-1 font-mono text-[11px] uppercase tracking-[0.14em] text-muted">
118
+ predicted distribution
119
+ </div>
120
+ {sample.probs.map((row, i) => {
121
+ const isTop = i === 0;
122
+ const pct = row.p * 100;
123
+ return (
124
+ <div key={`${idx}-${i}`} className="space-y-1">
125
+ <div className="flex items-baseline justify-between gap-3">
126
+ <span
127
+ className={
128
+ "truncate text-[13px] " +
129
+ (isTop ? "text-ink" : "text-muted")
130
+ }
131
+ >
132
+ {row.label}
133
+ </span>
134
+ <span className="font-mono text-[11px] tabular-nums text-ink">
135
+ {pct < 10 ? pct.toFixed(1) : pct.toFixed(0)}%
136
+ </span>
137
+ </div>
138
+ <div className="h-1.5 w-full overflow-hidden rounded-full bg-stroke">
139
+ <motion.div
140
+ key={`${idx}-${i}-bar`}
141
+ initial={{ width: 0 }}
142
+ animate={{ width: `${pct}%` }}
143
+ transition={{
144
+ duration: 0.7,
145
+ delay: 0.15 + i * 0.05,
146
+ ease: [0.22, 0.61, 0.36, 1],
147
+ }}
148
+ className="h-full"
149
+ style={{
150
+ background: isTop ? COLORS.accent : COLORS.honey,
151
+ }}
152
+ />
153
+ </div>
154
+ </div>
155
+ );
156
+ })}
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <figcaption className="mt-3 text-center font-mono text-[11px] uppercase tracking-[0.12em] text-muted">
162
+ softmax over a class vocabulary — argmax wins
163
+ </figcaption>
164
+
165
+ {/* Thumbnail strip + pause */}
166
+ <div className="mt-3 flex items-center gap-3">
167
+ <div className="flex gap-1.5">
168
+ {SAMPLES.map((_, i) => (
169
+ <button
170
+ key={i}
171
+ type="button"
172
+ aria-label={`Sample ${i + 1}`}
173
+ onClick={() => setIdx(i)}
174
+ data-active={i === idx}
175
+ className="h-1.5 w-1.5 rounded-full bg-stroke transition-all hover:bg-muted data-[active=true]:w-6 data-[active=true]:bg-ink"
176
+ />
177
+ ))}
178
+ </div>
179
+ <button
180
+ type="button"
181
+ onClick={() => setPaused((p) => !p)}
182
+ className="rounded-md border border-stroke bg-surface px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.12em] text-muted transition hover:border-ink hover:text-ink"
183
+ >
184
+ {paused ? "play" : "pause"}
185
+ </button>
186
+ </div>
187
+ </figure>
188
+ );
189
+ }
components/viz/KMeans.tsx CHANGED
@@ -1,9 +1,18 @@
1
  "use client";
2
 
3
- import { useMemo, useState } from "react";
 
4
  import { COLORS, VizFrame } from "./common";
 
5
 
6
- function rng(seed = 7) {
 
 
 
 
 
 
 
7
  let s = seed;
8
  return () => {
9
  s = (s * 1664525 + 1013904223) % 4294967296;
@@ -11,132 +20,289 @@ function rng(seed = 7) {
11
  };
12
  }
13
 
14
- function gen(K = 3, perCluster = 25) {
 
 
 
 
 
 
 
 
 
 
15
  const r = rng(11);
16
- const centers = Array.from({ length: K }, () => [r() * 8 + 1, r() * 6 + 1] as [number, number]);
17
- const pts: { x: number; y: number; trueK: number }[] = [];
18
- centers.forEach((c, k) => {
19
  for (let i = 0; i < perCluster; i++) {
20
  pts.push({
21
- x: c[0] + (r() - 0.5) * 1.4,
22
- y: c[1] + (r() - 0.5) * 1.4,
23
- trueK: k,
24
  });
25
  }
26
  });
27
- return { points: pts, centers };
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
- function step(
31
- points: { x: number; y: number }[],
32
- centroids: [number, number][],
33
- ) {
34
- const assign = points.map((p) =>
35
- centroids.reduce((best, c, k) => {
36
- const d = (p.x - c[0]) ** 2 + (p.y - c[1]) ** 2;
37
- return d < best.d ? { d, k } : best;
38
- }, { d: Infinity, k: 0 }).k,
39
  );
40
- const next: [number, number][] = centroids.map((_, k) => {
 
 
 
41
  const cls = points.filter((_, i) => assign[i] === k);
42
- if (!cls.length) return centroids[k];
43
- const mx = cls.reduce((a, p) => a + p.x, 0) / cls.length;
44
- const my = cls.reduce((a, p) => a + p.y, 0) / cls.length;
45
- return [mx, my];
 
46
  });
47
- return { assign, next };
48
  }
49
 
50
- const PALETTE = [COLORS.accent, COLORS.honey, COLORS.green, COLORS.red];
 
 
 
 
 
 
 
 
 
51
 
52
  export function KMeans({
53
  width = 720,
54
- height = 460,
55
- K = 3,
56
  }: {
57
  width?: number;
58
  height?: number;
59
  K?: number;
60
  }) {
61
  const padX = 50;
62
- const padY = 40;
63
- const { points } = useMemo(() => gen(K, 25), [K]);
64
- const initial: [number, number][] = useMemo(() => {
65
- const r = rng(99);
66
- return Array.from({ length: K }, () => [r() * 8 + 1, r() * 6 + 1] as [number, number]);
67
- }, [K]);
68
- const [centroids, setCentroids] = useState<[number, number][]>(initial);
69
- const [iter, setIter] = useState(0);
70
-
71
- const sx = (x: number) => padX + (x / 10) * (width - padX * 2);
72
- const sy = (y: number) => height - padY - (y / 8) * (height - padY * 2);
73
 
74
- const { assign } = step(points, centroids);
 
 
 
 
75
 
76
- const advance = () => {
77
- const { next } = step(points, centroids);
78
- setCentroids(next);
79
- setIter((i) => i + 1);
80
- };
81
- const reset = () => {
82
  setCentroids(initial);
83
  setIter(0);
84
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
  return (
87
  <div className="flex w-full max-w-full flex-col items-center">
88
- <VizFrame width={width} height={height}>
 
 
 
 
 
 
 
 
89
  <svg viewBox={`0 0 ${width} ${height}`} className="h-full w-full">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  {points.map((p, i) => (
91
  <circle
92
- key={i}
93
  cx={sx(p.x)}
94
  cy={sy(p.y)}
95
  r={3}
96
  fill={PALETTE[assign[i] % PALETTE.length]}
97
- fillOpacity={0.6}
98
  />
99
  ))}
 
100
  {centroids.map((c, k) => (
101
- <g key={k}>
102
- <circle
103
- cx={sx(c[0])}
104
- cy={sy(c[1])}
105
- r={9}
 
 
 
 
106
  fill={PALETTE[k % PALETTE.length]}
107
  stroke={COLORS.ink}
108
  strokeWidth={1.5}
109
  />
110
- <text
111
- x={sx(c[0])}
112
- y={sy(c[1]) + 4}
113
  textAnchor="middle"
114
  fontSize={11}
115
  fill={COLORS.surface}
116
  fontFamily="JetBrains Mono, monospace"
117
  >
118
- {k + 1}
119
- </text>
120
- </g>
121
  ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </svg>
123
  </VizFrame>
124
- <div className="mt-4 flex items-center gap-3 font-mono text-[11px] uppercase tracking-[0.12em] text-muted">
125
- <button
126
- type="button"
127
- onClick={advance}
128
- className="rounded-md border border-stroke bg-surface px-3 py-1.5 transition hover:border-ink hover:text-ink"
129
- >
130
- Step
131
- </button>
132
- <button
133
- type="button"
134
- onClick={reset}
135
- className="rounded-md border border-stroke bg-surface px-3 py-1.5 transition hover:border-ink hover:text-ink"
136
- >
137
- Reset
138
- </button>
139
- <span>iter = {String(iter).padStart(2, "0")}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  </div>
141
  </div>
142
  );
 
1
  "use client";
2
 
3
+ import { motion } from "framer-motion";
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
  import { COLORS, VizFrame } from "./common";
6
+ import { useReducedMotion } from "@/lib/hooks/useReducedMotion";
7
 
8
+ type Pt = { x: number; y: number };
9
+
10
+ const X_MIN = 0.5;
11
+ const X_MAX = 9.5;
12
+ const Y_MIN = 0.5;
13
+ const Y_MAX = 7.5;
14
+
15
+ function rng(seed: number) {
16
  let s = seed;
17
  return () => {
18
  s = (s * 1664525 + 1013904223) % 4294967296;
 
20
  };
21
  }
22
 
23
+ // Well-separated cluster centres — pick K from a fixed pool placed at the
24
+ // corners and middles of the plot area so the groups stay visually distinct.
25
+ const CENTRE_POOL: Pt[] = [
26
+ { x: 1.8, y: 1.6 },
27
+ { x: 8.2, y: 1.6 },
28
+ { x: 8.4, y: 6.3 },
29
+ { x: 1.6, y: 6.2 },
30
+ { x: 5.0, y: 4.0 },
31
+ ];
32
+
33
+ function genPoints(K: number, perCluster: number) {
34
  const r = rng(11);
35
+ const centres = CENTRE_POOL.slice(0, K);
36
+ const pts: Pt[] = [];
37
+ centres.forEach((c) => {
38
  for (let i = 0; i < perCluster; i++) {
39
  pts.push({
40
+ x: c.x + (r() - 0.5) * 1.6,
41
+ y: c.y + (r() - 0.5) * 1.6,
 
42
  });
43
  }
44
  });
45
+ return pts;
46
+ }
47
+
48
+ // Random initial centroids spread across the *full* plot — not just where the
49
+ // data lives — so the first few iterations clearly show centroids moving.
50
+ function genInitial(K: number, seed: number): Pt[] {
51
+ const r = rng(seed);
52
+ return Array.from({ length: K }, () => ({
53
+ x: X_MIN + r() * (X_MAX - X_MIN),
54
+ y: Y_MIN + r() * (Y_MAX - Y_MIN),
55
+ }));
56
  }
57
 
58
+ function assignStep(points: Pt[], centroids: Pt[]): number[] {
59
+ return points.map((p) =>
60
+ centroids.reduce(
61
+ (best, c, k) => {
62
+ const d = (p.x - c.x) ** 2 + (p.y - c.y) ** 2;
63
+ return d < best.d ? { d, k } : best;
64
+ },
65
+ { d: Infinity, k: 0 },
66
+ ).k,
67
  );
68
+ }
69
+
70
+ function updateStep(points: Pt[], assign: number[], K: number, prev: Pt[]): Pt[] {
71
+ return Array.from({ length: K }, (_, k) => {
72
  const cls = points.filter((_, i) => assign[i] === k);
73
+ if (!cls.length) return prev[k];
74
+ return {
75
+ x: cls.reduce((a, p) => a + p.x, 0) / cls.length,
76
+ y: cls.reduce((a, p) => a + p.y, 0) / cls.length,
77
+ };
78
  });
 
79
  }
80
 
81
+ function inertia(points: Pt[], assign: number[], centroids: Pt[]): number {
82
+ return points.reduce((acc, p, i) => {
83
+ const c = centroids[assign[i]];
84
+ return acc + (p.x - c.x) ** 2 + (p.y - c.y) ** 2;
85
+ }, 0);
86
+ }
87
+
88
+ const PALETTE = [COLORS.accent, COLORS.honey, COLORS.green, COLORS.red, "#7E57C2"];
89
+
90
+ type Phase = "assign" | "update";
91
 
92
  export function KMeans({
93
  width = 720,
94
+ height = 500,
95
+ K = 4,
96
  }: {
97
  width?: number;
98
  height?: number;
99
  K?: number;
100
  }) {
101
  const padX = 50;
102
+ const padY = 50;
103
+ const points = useMemo(() => genPoints(K, 28), [K]);
104
+ // Bumped on every Reset; drives a fresh random centroid initialisation.
105
+ const [seedTick, setSeedTick] = useState(0);
106
+ const initial = useMemo(
107
+ () => genInitial(K, 1 + seedTick * 7919),
108
+ [K, seedTick],
109
+ );
 
 
 
110
 
111
+ const [centroids, setCentroids] = useState<Pt[]>(initial);
112
+ const [iter, setIter] = useState(0);
113
+ const [phase, setPhase] = useState<Phase>("assign");
114
+ const [playing, setPlaying] = useState(false);
115
+ const reduce = useReducedMotion();
116
 
117
+ // Whenever the seed bumps, reset state to the new initial centroids.
118
+ useEffect(() => {
 
 
 
 
119
  setCentroids(initial);
120
  setIter(0);
121
+ setPhase("assign");
122
+ setPlaying(false);
123
+ }, [initial]);
124
+
125
+ const sx = useCallback(
126
+ (x: number) => padX + (x / 10) * (width - padX * 2),
127
+ [width],
128
+ );
129
+ const sy = useCallback(
130
+ (y: number) => height - padY - (y / 8) * (height - padY * 2),
131
+ [height],
132
+ );
133
+
134
+ const assign = useMemo(
135
+ () => assignStep(points, centroids),
136
+ [points, centroids],
137
+ );
138
+ const J = useMemo(
139
+ () => inertia(points, assign, centroids),
140
+ [points, assign, centroids],
141
+ );
142
+
143
+ // Run the next half-step. assign → update → next iter.
144
+ const advance = useCallback(() => {
145
+ if (phase === "assign") {
146
+ setPhase("update");
147
+ } else {
148
+ const next = updateStep(points, assign, K, centroids);
149
+ const moved = next.some(
150
+ (c, k) =>
151
+ Math.abs(c.x - centroids[k].x) > 1e-4 ||
152
+ Math.abs(c.y - centroids[k].y) > 1e-4,
153
+ );
154
+ setCentroids(next);
155
+ setIter((i) => i + 1);
156
+ setPhase("assign");
157
+ if (!moved) setPlaying(false);
158
+ }
159
+ }, [phase, points, assign, K, centroids]);
160
+
161
+ const advanceRef = useRef(advance);
162
+ advanceRef.current = advance;
163
+
164
+ useEffect(() => {
165
+ if (!playing || reduce) return;
166
+ const id = setInterval(() => advanceRef.current(), 900);
167
+ return () => clearInterval(id);
168
+ }, [playing, reduce]);
169
+
170
+ // Reset draws a *new* random initial centroid placement each time.
171
+ const reset = () => setSeedTick((t) => t + 1);
172
+
173
+ // For the assign phase, draw a thin line from each point to its winning
174
+ // centroid. This is the "compute distance, pick the smallest" step made
175
+ // visible.
176
+ const showLinks = phase === "assign";
177
 
178
  return (
179
  <div className="flex w-full max-w-full flex-col items-center">
180
+ <VizFrame
181
+ width={width}
182
+ height={height}
183
+ caption={
184
+ phase === "assign"
185
+ ? "step 1 — assign each point to the nearest centroid"
186
+ : "step 2 — move each centroid to the mean of its cluster"
187
+ }
188
+ >
189
  <svg viewBox={`0 0 ${width} ${height}`} className="h-full w-full">
190
+ {/* Faint outline at the initial centroid spot — anchors the eye. */}
191
+ {initial.map((c, k) => (
192
+ <circle
193
+ key={`init-${k}`}
194
+ cx={sx(c.x)}
195
+ cy={sy(c.y)}
196
+ r={10}
197
+ fill="none"
198
+ stroke={PALETTE[k % PALETTE.length]}
199
+ strokeOpacity={0.35}
200
+ strokeDasharray="3 3"
201
+ strokeWidth={1}
202
+ />
203
+ ))}
204
+
205
+ {showLinks
206
+ ? points.map((p, i) => {
207
+ const c = centroids[assign[i]];
208
+ return (
209
+ <line
210
+ key={`l-${i}`}
211
+ x1={sx(p.x)}
212
+ y1={sy(p.y)}
213
+ x2={sx(c.x)}
214
+ y2={sy(c.y)}
215
+ stroke={PALETTE[assign[i] % PALETTE.length]}
216
+ strokeOpacity={0.18}
217
+ strokeWidth={1}
218
+ />
219
+ );
220
+ })
221
+ : null}
222
+
223
  {points.map((p, i) => (
224
  <circle
225
+ key={`p-${i}`}
226
  cx={sx(p.x)}
227
  cy={sy(p.y)}
228
  r={3}
229
  fill={PALETTE[assign[i] % PALETTE.length]}
230
+ fillOpacity={0.85}
231
  />
232
  ))}
233
+
234
  {centroids.map((c, k) => (
235
+ <motion.g
236
+ key={`c-${k}`}
237
+ animate={{ cx: sx(c.x), cy: sy(c.y) }}
238
+ transition={{ duration: 0.5, ease: [0.22, 0.61, 0.36, 1] }}
239
+ >
240
+ <motion.circle
241
+ animate={{ cx: sx(c.x), cy: sy(c.y) }}
242
+ transition={{ duration: 0.5, ease: [0.22, 0.61, 0.36, 1] }}
243
+ r={10}
244
  fill={PALETTE[k % PALETTE.length]}
245
  stroke={COLORS.ink}
246
  strokeWidth={1.5}
247
  />
248
+ <motion.text
249
+ animate={{ x: sx(c.x), y: sy(c.y) + 4 }}
250
+ transition={{ duration: 0.5, ease: [0.22, 0.61, 0.36, 1] }}
251
  textAnchor="middle"
252
  fontSize={11}
253
  fill={COLORS.surface}
254
  fontFamily="JetBrains Mono, monospace"
255
  >
256
+ μ{k + 1}
257
+ </motion.text>
258
+ </motion.g>
259
  ))}
260
+
261
+ {/* Step badge top-left of plot area */}
262
+ <g transform={`translate(${padX}, ${padY - 22})`}>
263
+ <text
264
+ fontSize={11}
265
+ fontFamily="JetBrains Mono, monospace"
266
+ fill={phase === "assign" ? COLORS.accent : COLORS.honey}
267
+ style={{ textTransform: "uppercase", letterSpacing: "0.16em" }}
268
+ >
269
+ {phase === "assign"
270
+ ? `iter ${iter} · assignment`
271
+ : `iter ${iter} · update`}
272
+ </text>
273
+ </g>
274
  </svg>
275
  </VizFrame>
276
+
277
+ <div className="mt-4 flex w-full max-w-[640px] flex-wrap items-center justify-between gap-3 font-mono text-[11px] uppercase tracking-[0.12em]">
278
+ <div className="flex items-center gap-2">
279
+ <button
280
+ type="button"
281
+ onClick={advance}
282
+ className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink"
283
+ >
284
+ Step
285
+ </button>
286
+ <button
287
+ type="button"
288
+ onClick={() => setPlaying((p) => !p)}
289
+ className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink"
290
+ >
291
+ {playing ? "Pause" : "Play"}
292
+ </button>
293
+ <button
294
+ type="button"
295
+ onClick={reset}
296
+ className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink"
297
+ >
298
+ Reset
299
+ </button>
300
+ </div>
301
+ <div className="text-muted">
302
+ iter <span className="text-ink">{String(iter).padStart(2, "0")}</span>{" "}
303
+ · J ={" "}
304
+ <span className="text-ink">{J.toFixed(2)}</span>
305
+ </div>
306
  </div>
307
  </div>
308
  );
components/viz/VideoDemo.tsx ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { COLORS, VizFrame } from "./common";
5
+
6
+ export type VideoClip = {
7
+ src: string;
8
+ label: string;
9
+ };
10
+
11
+ export function VideoDemo({
12
+ clips,
13
+ caption,
14
+ width = 880,
15
+ height = 500,
16
+ }: {
17
+ clips: VideoClip[];
18
+ caption?: string;
19
+ width?: number;
20
+ height?: number;
21
+ }) {
22
+ const [idx, setIdx] = useState(0);
23
+ const clip = clips[idx];
24
+
25
+ return (
26
+ <div className="flex w-full max-w-full flex-col items-center">
27
+ <VizFrame width={width} height={height} caption={caption}>
28
+ <div className="flex h-full w-full items-center justify-center bg-bone p-3">
29
+ <video
30
+ key={clip.src}
31
+ src={clip.src}
32
+ autoPlay
33
+ muted
34
+ loop
35
+ playsInline
36
+ controls
37
+ className="max-h-full max-w-full rounded-md border border-stroke bg-black"
38
+ />
39
+ </div>
40
+ </VizFrame>
41
+
42
+ {clips.length > 1 ? (
43
+ <div className="mt-4 flex flex-wrap items-center gap-2 font-mono text-[11px] uppercase tracking-[0.12em]">
44
+ {clips.map((c, i) => (
45
+ <button
46
+ key={c.src}
47
+ type="button"
48
+ onClick={() => setIdx(i)}
49
+ data-active={i === idx}
50
+ className="rounded-md border border-stroke bg-surface px-3 py-1.5 text-muted transition hover:border-ink hover:text-ink data-[active=true]:border-ink data-[active=true]:text-ink"
51
+ style={i === idx ? { color: COLORS.ink } : undefined}
52
+ >
53
+ {c.label}
54
+ </button>
55
+ ))}
56
+ </div>
57
+ ) : null}
58
+ </div>
59
+ );
60
+ }
content/slides/ch04-classical-ml.tsx CHANGED
@@ -167,22 +167,107 @@ export const ch04: Chapter = {
167
  },
168
  {
169
  id: "ch04-07",
170
- title: "k-means clustering",
171
  eyebrow: "Unsupervised baseline",
172
- layout: "split",
173
  content: (
174
  <div className="space-y-4">
175
  <p>
176
- Initialise <M>k</M> centroids, alternate assignment and update.
177
- Centroids converge to local minima of the within-cluster sum of
178
- squares:
 
 
179
  </p>
180
- <MBlock>{"\\sum_{k} \\sum_{x \\in C_k} \\|x - \\mu_k\\|^2"}</MBlock>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
  <p className="text-muted">
182
- Sensitive to initialisation; we typically run several seeds and
183
- keep the best. <strong>k-means++</strong> picks initial centroids
184
- more carefully and almost always converges faster.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  </div>
187
  ),
188
  viz: <KMeans />,
 
167
  },
168
  {
169
  id: "ch04-07",
170
+ title: "k-means — the algorithm",
171
  eyebrow: "Unsupervised baseline",
172
+ layout: "prose",
173
  content: (
174
  <div className="space-y-4">
175
  <p>
176
+ Given <M>N</M> points <M>{"\\{x_i\\} \\subset \\mathbb{R}^d"}</M>{" "}
177
+ and a chosen number of clusters <M>K</M>, find an assignment{" "}
178
+ <M>{"r_{ik} \\in \\{0, 1\\}"}</M> and centroids{" "}
179
+ <M>{"\\mu_k \\in \\mathbb{R}^d"}</M> that minimise the{" "}
180
+ <em>within-cluster sum of squares</em> (also called inertia):
181
  </p>
182
+ <MBlock>
183
+ {"J = \\sum_{i=1}^{N}\\sum_{k=1}^{K} r_{ik}\\,\\|x_i - \\mu_k\\|^2"}
184
+ </MBlock>
185
+ <p>
186
+ Joint minimisation over <M>r</M> and{" "}
187
+ <M>{"\\mu"}</M> is NP-hard. Lloyd&apos;s algorithm (1957) is the
188
+ workhorse heuristic — alternate two closed-form steps until
189
+ nothing changes:
190
+ </p>
191
+ <div className="space-y-3 rounded-md border border-stroke bg-surface px-5 py-4 text-[14px]">
192
+ <div>
193
+ <div className="mb-1 font-mono text-[11px] uppercase tracking-[0.12em] text-accent">
194
+ step 1 · assignment
195
+ </div>
196
+ <p className="mb-2">
197
+ Fix the centroids. Send each point to the closest one.
198
+ </p>
199
+ <MBlock>
200
+ {"r_{ik} = \\begin{cases} 1 & k = \\arg\\min_j \\|x_i - \\mu_j\\|^2 \\\\ 0 & \\text{otherwise} \\end{cases}"}
201
+ </MBlock>
202
+ </div>
203
+ <div>
204
+ <div className="mb-1 font-mono text-[11px] uppercase tracking-[0.12em] text-honey">
205
+ step 2 · update
206
+ </div>
207
+ <p className="mb-2">
208
+ Fix the assignment. Move each centroid to the mean of its
209
+ points (this is the value that minimises <M>J</M> for fixed{" "}
210
+ <M>r</M>):
211
+ </p>
212
+ <MBlock>
213
+ {"\\mu_k = \\frac{\\sum_i r_{ik}\\,x_i}{\\sum_i r_{ik}}"}
214
+ </MBlock>
215
+ </div>
216
+ </div>
217
  <p className="text-muted">
218
+ Both steps strictly decrease <M>J</M> (or leave it unchanged), so
219
+ the algorithm <em>always converges</em> typically in fewer than
220
+ 20 iterations. Cost per iteration:{" "}
221
+ <M>{"O(N \\cdot K \\cdot d)"}</M>.
222
+ </p>
223
+ </div>
224
+ ),
225
+ },
226
+ {
227
+ id: "ch04-07b",
228
+ title: "k-means in action",
229
+ eyebrow: "Watch J fall",
230
+ layout: "split",
231
+ content: (
232
+ <div className="space-y-4">
233
+ <p>
234
+ <span className="font-mono text-[12px] text-accent">Step</span>{" "}
235
+ advances one half-iteration at a time, alternating between the
236
+ two phases. <span className="font-mono text-[12px] text-accent">Play</span>{" "}
237
+ runs to convergence. <span className="font-mono text-[12px]">J</span>{" "}
238
+ is the inertia; watch it monotonically decrease.
239
  </p>
240
+ <ul className="space-y-2 text-[14px] text-ink/85">
241
+ <li>
242
+ · Thin lines in the assignment phase show each point pulled to
243
+ its nearest centroid (the <M>{"\\arg\\min"}</M>).
244
+ </li>
245
+ <li>
246
+ · In the update phase, the centroids translate to the cluster
247
+ mean — usually the largest <M>J</M> drop happens here.
248
+ </li>
249
+ <li>
250
+ · The animation stops automatically when no centroid moves.
251
+ </li>
252
+ </ul>
253
+ <Callout label="Gotchas" tone="warm">
254
+ <ul className="space-y-1">
255
+ <li>
256
+ · <strong>Initialisation matters.</strong> Plain k-means is
257
+ sensitive to the starting centroids;{" "}
258
+ <strong>k-means++</strong> seeds them apart on purpose and
259
+ almost always converges to a better minimum.
260
+ </li>
261
+ <li>
262
+ · <strong>You must pick K.</strong> Use the elbow method on{" "}
263
+ <M>J(K)</M> or the silhouette score.
264
+ </li>
265
+ <li>
266
+ · <strong>Assumes spherical, equal-size clusters.</strong>{" "}
267
+ For arbitrary shapes use DBSCAN or a Gaussian mixture model.
268
+ </li>
269
+ </ul>
270
+ </Callout>
271
  </div>
272
  ),
273
  viz: <KMeans />,
content/slides/ch05-deep-learning.tsx CHANGED
@@ -9,8 +9,18 @@ import { RegularizationViz } from "@/components/viz/RegularizationViz";
9
  import { Callout } from "@/components/ui/Callout";
10
  import { M, MBlock } from "@/components/math/Math";
11
 
12
- // Replace with your own playground URL.
13
- const PLAYGROUND_URL = "https://playground.tensorflow.org/";
 
 
 
 
 
 
 
 
 
 
14
 
15
  export const ch05: Chapter = {
16
  id: "ch05",
@@ -131,30 +141,31 @@ export const ch05: Chapter = {
131
  interact is to build a tiny network in a browser and watch it
132
  train.
133
  </p>
134
- <a
135
- href={PLAYGROUND_URL}
136
- target="_blank"
137
- rel="noreferrer"
138
- className="inline-flex items-center gap-3 rounded-md border border-ink/40 bg-surface px-5 py-3 transition hover:border-ink hover:bg-bone"
139
- >
140
- <span className="font-mono text-[11px] uppercase tracking-[0.16em] text-muted">
141
- Open
142
- </span>
143
- <span className="font-serif text-lg text-ink">
144
- Neural Network Playground
145
- </span>
146
- <span className="font-mono text-[11px] text-muted"></span>
147
- </a>
 
 
 
 
 
 
148
  <p className="text-muted">
149
  Things to look for: how the spiral dataset needs ≥ 2 hidden layers;
150
  how ReLU vs sigmoid affects convergence; how a too-large learning
151
  rate explodes; how regularisation smooths the boundary.
152
  </p>
153
- <p className="font-mono text-[11px] uppercase tracking-[0.12em] text-muted">
154
- <span className="text-honey">tip</span> · the playground URL is a
155
- constant in <code className="font-mono normal-case">ch05-deep-learning.tsx</code>; replace it with
156
- your own demo.
157
- </p>
158
  </div>
159
  ),
160
  },
 
9
  import { Callout } from "@/components/ui/Callout";
10
  import { M, MBlock } from "@/components/math/Math";
11
 
12
+ const PLAYGROUNDS = [
13
+ {
14
+ name: "Neural Network Playground",
15
+ url: "https://playground.tensorflow.org/",
16
+ note: "TensorFlow · the original interactive playground",
17
+ },
18
+ {
19
+ name: "Samuel's NN Playground",
20
+ url: "https://samuellimabraz.github.io/#nn-playground",
21
+ note: "custom build · same idea, our notation",
22
+ },
23
+ ];
24
 
25
  export const ch05: Chapter = {
26
  id: "ch05",
 
141
  interact is to build a tiny network in a browser and watch it
142
  train.
143
  </p>
144
+ <div className="flex flex-col gap-3">
145
+ {PLAYGROUNDS.map((p) => (
146
+ <a
147
+ key={p.url}
148
+ href={p.url}
149
+ target="_blank"
150
+ rel="noreferrer"
151
+ className="inline-flex items-center gap-3 rounded-md border border-ink/40 bg-surface px-5 py-3 transition hover:border-ink hover:bg-bone"
152
+ >
153
+ <span className="font-mono text-[11px] uppercase tracking-[0.16em] text-muted">
154
+ Open
155
+ </span>
156
+ <span className="font-serif text-lg text-ink">{p.name}</span>
157
+ <span className="ml-auto font-mono text-[11px] text-muted">
158
+ {p.note}
159
+ </span>
160
+ <span className="font-mono text-[11px] text-muted">↗</span>
161
+ </a>
162
+ ))}
163
+ </div>
164
  <p className="text-muted">
165
  Things to look for: how the spiral dataset needs ≥ 2 hidden layers;
166
  how ReLU vs sigmoid affects convergence; how a too-large learning
167
  rate explodes; how regularisation smooths the boundary.
168
  </p>
 
 
 
 
 
169
  </div>
170
  ),
171
  },
content/slides/ch07-cv-tasks.tsx CHANGED
@@ -1,6 +1,8 @@
1
  import type { Chapter } from "@/components/slide/types";
2
  import { MaskOverlay } from "@/components/viz/Scene";
3
  import { BboxFormats } from "@/components/viz/BboxFormats";
 
 
4
  import { Callout } from "@/components/ui/Callout";
5
  import { M, MBlock } from "@/components/math/Math";
6
 
@@ -19,17 +21,24 @@ export const ch07: Chapter = {
19
  content: (
20
  <div className="space-y-4">
21
  <p>
22
- A single label per image. Output is a probability distribution over
23
- classes through softmax:
24
  </p>
25
  <MBlock>{"\\hat p_k = \\frac{e^{z_k}}{\\sum_j e^{z_j}}"}</MBlock>
26
- <p className="text-muted">
27
- Loss is cross-entropy. Top-1 and top-5 accuracy are the standard metrics.
28
- ImageNet was the long-standing benchmark.
 
 
29
  </p>
 
 
 
 
 
30
  </div>
31
  ),
32
- viz: <MaskOverlay mode="classification" />,
33
  },
34
  {
35
  id: "ch07-01",
@@ -39,16 +48,26 @@ export const ch07: Chapter = {
39
  content: (
40
  <div className="space-y-4">
41
  <p>
42
- Predict a set of bounding boxes plus a class label per box. Each prediction
43
- carries a confidence score.
 
44
  </p>
45
- <p className="text-muted">
46
- Most Black Bee perception lives here: gates, posts, drones. Chapter 8 is
47
- entirely about this task.
 
48
  </p>
49
  </div>
50
  ),
51
- viz: <MaskOverlay mode="detection" />,
 
 
 
 
 
 
 
 
52
  },
53
  {
54
  id: "ch07-02",
@@ -58,22 +77,27 @@ export const ch07: Chapter = {
58
  content: (
59
  <div className="space-y-4">
60
  <p>
61
- Three conventions you will see daily, all describing the same rectangle:
 
62
  </p>
63
  <ul className="space-y-2 text-[14px] text-ink/85">
64
  <li>
65
- <strong>xyxy</strong> · top-left and bottom-right corners. COCO, supervision.
 
66
  </li>
67
  <li>
68
- <strong>xywh</strong> · centre with width and height. Internal to many models.
 
69
  </li>
70
  <li>
71
- <strong>normalized</strong> · everything divided by image size. YOLO labels.
 
72
  </li>
73
  </ul>
74
  <Callout label="In Nectar">
75
- <code className="font-mono text-[12px]">FormatConverter</code> in the SDK
76
- handles COCO ↔ YOLO conversion automatically. Chapter 11 returns to it.
 
77
  </Callout>
78
  </div>
79
  ),
@@ -87,14 +111,28 @@ export const ch07: Chapter = {
87
  content: (
88
  <div className="space-y-4">
89
  <p>
90
- Every pixel gets a class label. Output is an image with the same spatial
91
- size as the input but with channels equal to the number of classes.
 
 
 
 
 
 
 
 
 
92
  </p>
93
- <MBlock>{"\\hat Y \\in \\mathbb{R}^{H \\times W \\times C}, \\quad \\hat y_{ij} = \\arg\\max_c \\hat Y_{ijc}"}</MBlock>
94
  <p className="text-muted">
95
- All pixels of the same class share one mask — &quot;all gate pixels&quot;,
96
- not &quot;this gate vs. that gate&quot;.
 
97
  </p>
 
 
 
 
 
98
  </div>
99
  ),
100
  viz: <MaskOverlay mode="semantic" />,
@@ -107,17 +145,31 @@ export const ch07: Chapter = {
107
  content: (
108
  <div className="space-y-4">
109
  <p>
110
- Detection + per-pixel mask, separate per object. Mask R-CNN added a small
111
- mask head on top of Faster R-CNN; YOLO-seg and DETR-seg do similarly.
 
 
 
 
 
 
112
  </p>
113
  <p className="text-muted">
114
  The Nectar SDK supports this through{" "}
115
- <code className="font-mono text-[12px]">Segmentor</code> with the same
116
- three frameworks (YOLO, DETR, RF-DETR). See chapter 11.
117
  </p>
118
  </div>
119
  ),
120
- viz: <MaskOverlay mode="instance" />,
 
 
 
 
 
 
 
 
121
  },
122
  {
123
  id: "ch07-05",
@@ -127,12 +179,13 @@ export const ch07: Chapter = {
127
  content: (
128
  <div className="space-y-4">
129
  <p>
130
- Predict a small set of named points per object — corners of a gate, joints
131
- of a body. Either as a heatmap per keypoint, or as a regressed coordinate.
 
132
  </p>
133
  <p className="text-muted">
134
- Useful when downstream geometry (pose estimation, alignment) needs precise
135
- anchors.
136
  </p>
137
  </div>
138
  ),
@@ -146,16 +199,16 @@ export const ch07: Chapter = {
146
  content: (
147
  <div className="space-y-4">
148
  <p>
149
- Per-pixel distance in metres. Stereo, time-of-flight, or learned monocular
150
- depth from a single RGB image.
151
  </p>
152
  <p>
153
- Black Bee uses this directly: the RealSense and OAK-D drivers in the
154
- Nectar vision module return depth alongside colour.
155
  </p>
156
  <Callout label="Tip" tone="warm">
157
- Monocular depth is up to a scale factor. Useful for ordering objects, less
158
- useful for absolute distances without calibration.
159
  </Callout>
160
  </div>
161
  ),
 
1
  import type { Chapter } from "@/components/slide/types";
2
  import { MaskOverlay } from "@/components/viz/Scene";
3
  import { BboxFormats } from "@/components/viz/BboxFormats";
4
+ import { ClassificationDemo } from "@/components/viz/ClassificationDemo";
5
+ import { VideoDemo } from "@/components/viz/VideoDemo";
6
  import { Callout } from "@/components/ui/Callout";
7
  import { M, MBlock } from "@/components/math/Math";
8
 
 
21
  content: (
22
  <div className="space-y-4">
23
  <p>
24
+ One label per image. Output is a probability distribution over a
25
+ fixed vocabulary, produced by softmax over the network logits:
26
  </p>
27
  <MBlock>{"\\hat p_k = \\frac{e^{z_k}}{\\sum_j e^{z_j}}"}</MBlock>
28
+ <p>
29
+ The prediction is <M>{"\\arg\\max_k \\hat p_k"}</M>; loss is
30
+ categorical cross-entropy. Top-1 and top-5 accuracy are the
31
+ standard metrics. ImageNet (1000 classes) was the long-standing
32
+ benchmark.
33
  </p>
34
+ <Callout label="Watch">
35
+ Bars on the right are the full distribution, not just the winner.
36
+ Image 5 is the same person as image 1 — see how the model splits
37
+ its mass between two classes when the appearance changes.
38
+ </Callout>
39
  </div>
40
  ),
41
+ viz: <ClassificationDemo />,
42
  },
43
  {
44
  id: "ch07-01",
 
48
  content: (
49
  <div className="space-y-4">
50
  <p>
51
+ Predict a variable-length list of (box, class, confidence). The
52
+ output structure is what makes detection harder than classification
53
+ — a single image can contain zero or many objects.
54
  </p>
55
+ <p>
56
+ Most Black Bee perception lives here: gates, posts, drones. The
57
+ video on the right is one of our trained YOLO models running on a
58
+ real flight log. Chapter 8 unpacks the architecture.
59
  </p>
60
  </div>
61
  ),
62
+ viz: (
63
+ <VideoDemo
64
+ caption="real Black Bee mission · YOLO detection on flight footage"
65
+ clips={[
66
+ { src: "/team/det-1.mp4", label: "clip 1" },
67
+ { src: "/team/det-2.mp4", label: "clip 2" },
68
+ ]}
69
+ />
70
+ ),
71
  },
72
  {
73
  id: "ch07-02",
 
77
  content: (
78
  <div className="space-y-4">
79
  <p>
80
+ Three conventions you will see daily, all describing the same
81
+ rectangle:
82
  </p>
83
  <ul className="space-y-2 text-[14px] text-ink/85">
84
  <li>
85
+ <strong>xyxy</strong> · top-left and bottom-right corners. COCO,
86
+ supervision.
87
  </li>
88
  <li>
89
+ <strong>xywh</strong> · centre with width and height. Internal
90
+ to many models.
91
  </li>
92
  <li>
93
+ <strong>normalized</strong> · everything divided by image size.
94
+ YOLO labels.
95
  </li>
96
  </ul>
97
  <Callout label="In Nectar">
98
+ <code className="font-mono text-[12px]">FormatConverter</code> in
99
+ the SDK handles COCO ↔ YOLO conversion automatically. Chapter 11
100
+ returns to it.
101
  </Callout>
102
  </div>
103
  ),
 
111
  content: (
112
  <div className="space-y-4">
113
  <p>
114
+ Every pixel gets a class label. The output has the same spatial
115
+ size as the input, with one channel per class:
116
+ </p>
117
+ <MBlock>
118
+ {"\\hat Y \\in \\mathbb{R}^{H \\times W \\times C}, \\quad \\hat y_{ij} = \\arg\\max_c \\hat Y_{ijc}"}
119
+ </MBlock>
120
+ <p>
121
+ All pixels of the same class share one mask — &quot;all gate
122
+ pixels&quot;, not &quot;this gate vs that gate&quot;. Loss is
123
+ per-pixel cross-entropy, often combined with Dice loss to handle
124
+ class imbalance.
125
  </p>
 
126
  <p className="text-muted">
127
+ Typical use cases: free-space mapping for autonomous driving, lane
128
+ segmentation, terrain classification from aerial imagery.
129
+ Architectures: U-Net, DeepLab, SegFormer, Mask2Former.
130
  </p>
131
+ <Callout>
132
+ We do not use this on Black Bee yet — most of our targets are
133
+ countable objects, where instance segmentation or detection fits
134
+ better.
135
+ </Callout>
136
  </div>
137
  ),
138
  viz: <MaskOverlay mode="semantic" />,
 
145
  content: (
146
  <div className="space-y-4">
147
  <p>
148
+ Detection plus a per-pixel mask, separate for each object. Mask
149
+ R-CNN added a small mask head on top of Faster R-CNN; YOLO-seg and
150
+ DETR-seg do similarly.
151
+ </p>
152
+ <p>
153
+ The video on the right is one of our segmentation models on a real
154
+ flight; each instance gets its own coloured mask, which lets us
155
+ count, sort, or pick objects individually.
156
  </p>
157
  <p className="text-muted">
158
  The Nectar SDK supports this through{" "}
159
+ <code className="font-mono text-[12px]">Segmentor</code> with the
160
+ same three frameworks (YOLO, DETR, RF-DETR). See chapter 11.
161
  </p>
162
  </div>
163
  ),
164
+ viz: (
165
+ <VideoDemo
166
+ caption="real Black Bee mission · instance segmentation on flight footage"
167
+ clips={[
168
+ { src: "/team/seg-2.mp4", label: "clip 1" },
169
+ { src: "/team/seg-1.mp4", label: "clip 2" },
170
+ ]}
171
+ />
172
+ ),
173
  },
174
  {
175
  id: "ch07-05",
 
179
  content: (
180
  <div className="space-y-4">
181
  <p>
182
+ Predict a small set of named points per object — corners of a
183
+ gate, joints of a body. Either as a heatmap per keypoint, or as a
184
+ regressed coordinate.
185
  </p>
186
  <p className="text-muted">
187
+ Useful when downstream geometry (pose estimation, alignment) needs
188
+ precise anchors.
189
  </p>
190
  </div>
191
  ),
 
199
  content: (
200
  <div className="space-y-4">
201
  <p>
202
+ Per-pixel distance in metres. Stereo, time-of-flight, or learned
203
+ monocular depth from a single RGB image.
204
  </p>
205
  <p>
206
+ Black Bee uses this directly: the RealSense and OAK-D drivers in
207
+ the Nectar vision module return depth alongside colour.
208
  </p>
209
  <Callout label="Tip" tone="warm">
210
+ Monocular depth is up to a scale factor. Useful for ordering
211
+ objects, less useful for absolute distances without calibration.
212
  </Callout>
213
  </div>
214
  ),
public/team/01.jpg ADDED

Git LFS Details

  • SHA256: 081cb25acd77313a8f3c4f89ebfc5e9e609b1521810b630b05dc3dc0bf664454
  • Pointer size: 131 Bytes
  • Size of remote file: 462 kB
public/team/02.jpg ADDED

Git LFS Details

  • SHA256: 8ad502385677205c153a536729c8f3e57df7ac7d02c524f6bd07c416ac604ea7
  • Pointer size: 131 Bytes
  • Size of remote file: 288 kB
public/team/03.jpg ADDED

Git LFS Details

  • SHA256: dd542f3ca8c4faa36338a7fb3e6c860e181221cbefb99aa0f69387bb624ce93d
  • Pointer size: 131 Bytes
  • Size of remote file: 524 kB
public/team/04.jpg ADDED

Git LFS Details

  • SHA256: 808bc1eca59df1f2fa17dac712c6654bd3e7072eee2228bf359b42ea172a33dc
  • Pointer size: 130 Bytes
  • Size of remote file: 50.8 kB
public/team/05.jpg ADDED

Git LFS Details

  • SHA256: b24b2aeed44ac1417e0bb64cc7fe9f30280ae27614c6e44b5c454a64b7566586
  • Pointer size: 131 Bytes
  • Size of remote file: 453 kB
public/team/det-1.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d06d26c358f209b881c5fe426b62bd9f6a17d3b22aa00259caeff109c78fa8ca
3
+ size 3008225
public/team/det-2.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:87d841d138b67d4e49a0ba18bae57c76985b4af95f2885faba0a37a81aa42b8a
3
+ size 3549299
public/team/seg-1.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3603d67b1a2660df5284649d937073fb8227d7bd9cd95eb0ab5b812c83f4b788
3
+ size 18444491
public/team/seg-2.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:34e1a8f2c0ecb4b03c041e530b4caec53470ab3a4a2534574bbd1a8c01b498e7
3
+ size 5527552