Skip to content

Commit 52b7f99

Browse files
authored
Add 3D convolution and kernel functions
1 parent 3d7a8e6 commit 52b7f99

File tree

1 file changed

+340
-0
lines changed

1 file changed

+340
-0
lines changed

3d/cnn3d.js

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
let iterations = 5;
2+
let pad = 1;
3+
4+
let kernelParams3D = {
5+
corners: 0,
6+
edges: 0,
7+
faces: 0,
8+
center: 0
9+
};
10+
11+
let kernel27 = [];
12+
13+
let field3DRaw = []; // [x][y][z]
14+
15+
/* ---------- Math ---------- */
16+
17+
function activation(t) {
18+
return t;
19+
}
20+
21+
/* ---------- 3D Kernel (27 from 4 sliders) ---------- */
22+
23+
function buildKernel3D({ center, faces, edges, corners }) {
24+
const K = [];
25+
26+
for (let dz = -1; dz <= 1; dz++)
27+
for (let dy = -1; dy <= 1; dy++)
28+
for (let dx = -1; dx <= 1; dx++) {
29+
30+
const ax = Math.abs(dx);
31+
const ay = Math.abs(dy);
32+
const az = Math.abs(dz);
33+
const s = ax + ay + az;
34+
35+
let v;
36+
if (s === 0) v = center; // 1
37+
else if (s === 1) v = faces; // 6
38+
else if (s === 2) v = edges; // 12
39+
else v = corners; // 8
40+
41+
K.push(v);
42+
}
43+
44+
return K; // length 27
45+
}
46+
47+
/* ---------- 3D Convolution ---------- */
48+
49+
function convolution3D(field, kernel27) {
50+
const n = field.length;
51+
const out = new Array(n);
52+
53+
for (let x = 0; x < n; x++) {
54+
out[x] = new Array(n);
55+
56+
for (let y = 0; y < n; y++) {
57+
out[x][y] = new Array(n);
58+
59+
for (let z = 0; z < n; z++) {
60+
let s = 0;
61+
let i = 0;
62+
63+
for (let dx = -1; dx <= 1; dx++)
64+
for (let dy = -1; dy <= 1; dy++)
65+
for (let dz = -1; dz <= 1; dz++) {
66+
67+
const xx = (x + dx + n) % n;
68+
const yy = (y + dy + n) % n;
69+
const zz = (z + dz + n) % n;
70+
71+
s += field[xx][yy][zz] * kernel27[i++];
72+
}
73+
74+
out[x][y][z] = activation(s);
75+
}
76+
}
77+
}
78+
79+
return out;
80+
}
81+
82+
/* ---------- 3D Padding ---------- */
83+
84+
function padding3D(field, k) {
85+
// base case
86+
if (field.length === 1) {
87+
// make a minimal 2×2×2
88+
return [
89+
[[1, 0], [0, 0]],
90+
[[0, 0], [0, 0]]
91+
];
92+
}
93+
94+
const n = field.length;
95+
const nn = n * 2;
96+
97+
const out = new Array(nn);
98+
for (let x = 0; x < nn; x++) {
99+
out[x] = new Array(nn);
100+
for (let y = 0; y < nn; y++) {
101+
out[x][y] = new Array(nn).fill(0);
102+
}
103+
}
104+
105+
for (let x = 0; x < n; x++)
106+
for (let y = 0; y < n; y++)
107+
for (let z = 0; z < n; z++) {
108+
const v = field[x][y][z];
109+
const vv = k * v;
110+
111+
for (let dx = 0; dx <= 1; dx++)
112+
for (let dy = 0; dy <= 1; dy++)
113+
for (let dz = 0; dz <= 1; dz++) {
114+
out[x * 2 + dx][y * 2 + dy][z * 2 + dz] =
115+
(dx || dy || dz) ? vv : v;
116+
}
117+
}
118+
119+
return out;
120+
}
121+
122+
/* ---------- UI helpers ---------- */
123+
124+
function getIterations() {
125+
const el = document.getElementById("iterations");
126+
let it = parseInt(el.value, 10);
127+
if (!Number.isFinite(it)) it = 5;
128+
129+
// Hard clamp to prevent accidental nuclear memory use.
130+
// 2^7 = 128 => 2,097,152 cells (still heavy but survivable)
131+
// 2^8 = 256 => 16,777,216 cells (too much for nested arrays). But manageable.
132+
it = Math.max(1, Math.min(8, it));
133+
el.value = it;
134+
return it;
135+
}
136+
137+
function setRangeSliderDefaults(id) {
138+
const s = document.getElementById(id);
139+
s.min = -1;
140+
s.max = 1;
141+
s.step = 0.001;
142+
}
143+
144+
/* ---------- Main regen (3D) ---------- */
145+
146+
function regenerateField3DKeepCore() {
147+
iterations = getIterations();
148+
149+
kernel27 = buildKernel3D({
150+
center: kernelParams3D.center,
151+
faces: kernelParams3D.faces,
152+
edges: kernelParams3D.edges,
153+
corners: kernelParams3D.corners
154+
});
155+
156+
let field = [[[1]]];
157+
158+
for (let i = 0; i < iterations; i++) {
159+
field = padding3D(field, pad);
160+
field = convolution3D(field, kernel27);
161+
}
162+
163+
field3DRaw = field;
164+
165+
// update Z slider limits
166+
const n = field3DRaw.length;
167+
const zSlice = document.getElementById("zSlice");
168+
const zLabel = document.getElementById("zLabel");
169+
const zMaxLabel = document.getElementById("zMaxLabel");
170+
171+
zSlice.max = Math.max(0, n - 1);
172+
if (parseInt(zSlice.value, 10) > n - 1) zSlice.value = String(n - 1);
173+
174+
zLabel.textContent = zSlice.value;
175+
zMaxLabel.textContent = String(Math.max(0, n - 1));
176+
177+
drawCurrentSlice();
178+
dumpKernelLine3D();
179+
}
180+
181+
/* ---------- Rendering ---------- */
182+
183+
function drawCurrentSlice() {
184+
const zSlice = parseInt(document.getElementById("zSlice").value, 10) || 0;
185+
document.getElementById("zLabel").textContent = String(zSlice);
186+
187+
drawSlice(field3DRaw, zSlice);
188+
dumpKernelLine3D(); // update min/max for this Z too
189+
}
190+
191+
function drawSlice(field, z) {
192+
const canvas = document.getElementById("myCanvas");
193+
const px = parseInt(document.getElementById("pixelSize").value, 10);
194+
195+
const n = field.length;
196+
canvas.width = n * px;
197+
canvas.height = n * px;
198+
199+
const ctx = canvas.getContext("2d");
200+
const mode = document.getElementById("colorMode").value;
201+
202+
// per-slice normalization (simple and predictable)
203+
let lo = Infinity;
204+
let hi = -Infinity;
205+
206+
if (mode === "grayscale") {
207+
for (let x = 0; x < n; x++)
208+
for (let y = 0; y < n; y++) {
209+
const v = field[x][y][z];
210+
if (v < lo) lo = v;
211+
if (v > hi) hi = v;
212+
}
213+
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi === lo) {
214+
lo = 0; hi = 1;
215+
}
216+
}
217+
218+
for (let x = 0; x < n; x++) {
219+
for (let y = 0; y < n; y++) {
220+
221+
const raw = field[x][y][z];
222+
223+
let color = "#000";
224+
225+
if (mode === "grayscale") {
226+
const t = (raw - lo) / (hi - lo + 1e-9);
227+
const g = Math.floor(Math.max(0, Math.min(1, t)) * 255);
228+
color = `rgb(${g},${g},${g})`;
229+
}
230+
else if (mode === "mod4") {
231+
const s = Math.floor(raw);
232+
const r = ((s % 4) + 4) % 4;
233+
color = (r === 0 || r === 1) ? "#000" : "#fff";
234+
}
235+
else if (mode === "alleycat") {
236+
const s = Math.floor(raw);
237+
const p = ["#000", "#5ff", "#f5f", "#fff"];
238+
color = p[((s % 4) + 4) % 4];
239+
}
240+
241+
ctx.fillStyle = color;
242+
ctx.fillRect(x * px, y * px, px, px);
243+
}
244+
}
245+
}
246+
247+
function dumpKernelLine3D() {
248+
const n = field3DRaw.length;
249+
const z = parseInt(document.getElementById("zSlice").value, 10) || 0;
250+
251+
let min = Infinity;
252+
let max = -Infinity;
253+
254+
for (let x = 0; x < n; x++)
255+
for (let y = 0; y < n; y++) {
256+
const v = field3DRaw[x][y][z];
257+
if (v < min) min = v;
258+
if (v > max) max = v;
259+
}
260+
261+
const kern =
262+
`corners=${kernelParams3D.corners.toFixed(3)} ` +
263+
`edges=${kernelParams3D.edges.toFixed(3)} ` +
264+
`faces=${kernelParams3D.faces.toFixed(3)} ` +
265+
`center=${kernelParams3D.center.toFixed(3)}`;
266+
267+
document.getElementById("kernelDump").textContent =
268+
`iter=${iterations} size=${n}×${n}×${n} z=${z} ` +
269+
`min=${min.toFixed(3)} max=${max.toFixed(3)} ` +
270+
`pad=${pad} ` +
271+
`kernel3D(${kern})`;
272+
}
273+
274+
/* ---------- UI wiring ---------- */
275+
276+
function initUI() {
277+
// slider defaults
278+
setRangeSliderDefaults("kCorners");
279+
setRangeSliderDefaults("kEdges");
280+
setRangeSliderDefaults("kFaces");
281+
setRangeSliderDefaults("kCenter");
282+
283+
// kernel sliders
284+
document.getElementById("kCorners").oninput = e => {
285+
kernelParams3D.corners = parseFloat(e.target.value);
286+
regenerateField3DKeepCore();
287+
};
288+
document.getElementById("kEdges").oninput = e => {
289+
kernelParams3D.edges = parseFloat(e.target.value);
290+
regenerateField3DKeepCore();
291+
};
292+
document.getElementById("kFaces").oninput = e => {
293+
kernelParams3D.faces = parseFloat(e.target.value);
294+
regenerateField3DKeepCore();
295+
};
296+
document.getElementById("kCenter").oninput = e => {
297+
kernelParams3D.center = parseFloat(e.target.value);
298+
regenerateField3DKeepCore();
299+
};
300+
301+
// globals
302+
document.getElementById("iterations").oninput = regenerateField3DKeepCore;
303+
document.getElementById("pixelSize").oninput = drawCurrentSlice;
304+
document.getElementById("colorMode").onchange = drawCurrentSlice;
305+
306+
// z slicer
307+
document.getElementById("zSlice").oninput = drawCurrentSlice;
308+
309+
// pad mode
310+
document.querySelectorAll('input[name="padmode"]').forEach(r => {
311+
r.addEventListener('change', () => {
312+
pad = Number(r.value);
313+
regenerateField3DKeepCore();
314+
console.log('pad changed to', pad);
315+
});
316+
});
317+
318+
// refresh seed
319+
document.getElementById("refreshBtn").onclick = refreshSeed;
320+
}
321+
322+
/* ---------- Seed ---------- */
323+
324+
function refreshSeed() {
325+
function rf() { return Math.random() * 2 - 1; }
326+
327+
kernelParams3D = {
328+
corners: rf(),
329+
edges: rf(),
330+
faces: rf(),
331+
center: rf()
332+
};
333+
334+
document.getElementById("kCorners").value = kernelParams3D.corners;
335+
document.getElementById("kEdges").value = kernelParams3D.edges;
336+
document.getElementById("kFaces").value = kernelParams3D.faces;
337+
document.getElementById("kCenter").value = kernelParams3D.center;
338+
339+
regenerateField3DKeepCore();
340+
}

0 commit comments

Comments
 (0)