funtexi commited on
Commit
584431e
·
verified ·
1 Parent(s): d35a9bb

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +230 -162
index.html CHANGED
@@ -3,210 +3,207 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>AI 全息相框 - 视觉增强版</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
  <style>
10
- /* --- 基础布局 --- */
11
  body { background-color: #020617; color: white; font-family: 'Segoe UI', sans-serif; overflow-x: hidden; touch-action: none; }
12
 
13
  .stage {
14
- perspective: 1200px; /* 加深透视感 */
15
- width: 100%; max-width: 600px; margin: 0 auto;
16
  position: relative; z-index: 20;
17
  }
18
 
19
- /* --- 3D 卡片容器 --- */
20
  .canvas-card {
21
  position: relative; width: 100%; aspect-ratio: 4/3;
22
  background: #0f172a; border-radius: 12px;
23
- /* 增加多层阴影营造悬浮感 */
24
- box-shadow:
25
- 0 0 0 1px rgba(34, 211, 238, 0.1),
26
- 0 20px 50px -10px rgba(0, 0, 0, 0.8),
27
- 0 0 100px -20px rgba(6, 182, 212, 0.15);
28
  transform-style: preserve-3d;
29
- transition: transform 0.1s cubic-bezier(0.1, 0.5, 0.5, 1);
30
- overflow: hidden;
31
- }
32
-
33
- /* 玻璃质感反光层 */
34
- .glass-shine {
35
- position: absolute; inset: 0; pointer-events: none; z-index: 40;
36
- background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, transparent 40%, transparent 60%, rgba(255,255,255,0.05) 100%);
37
- mix-blend-mode: overlay;
38
  }
39
 
40
  canvas { width: 100%; height: 100%; object-fit: contain; display: block; image-rendering: pixelated; }
41
 
42
- /* --- HUD 装饰 --- */
43
- .hud-corner {
44
- position: absolute; width: 30px; height: 30px;
45
- border: 2px solid #22d3ee; z-index: 30; opacity: 0.6;
46
- transition: all 0.3s ease;
47
- box-shadow: 0 0 10px rgba(34, 211, 238, 0.3);
48
- }
49
- .tl { top: 10px; left: 10px; border-right: none; border-bottom: none; }
50
- .tr { top: 10px; right: 10px; border-left: none; border-bottom: none; }
51
- .bl { bottom: 10px; left: 10px; border-right: none; border-top: none; }
52
- .br { bottom: 10px; right: 10px; border-left: none; border-top: none; }
53
-
54
  /* 扫描线 */
55
  .scan-line {
56
  position: absolute; left: 0; right: 0; height: 2px;
57
  background: #fff;
58
  box-shadow: 0 0 15px #00f3ff, 0 0 30px #00f3ff;
59
- display: none; pointer-events: none; z-index: 25;
60
  mix-blend-mode: overlay;
61
  }
62
 
63
- /* --- 控件样--- */
64
  .mode-switch {
65
- background: rgba(15, 23, 42, 0.8); backdrop-filter: blur(12px);
66
  border: 1px solid rgba(255,255,255,0.1);
67
- border-radius: 16px; padding: 4px; display: inline-flex; gap: 4px;
68
  }
69
  .mode-btn {
70
- padding: 8px 24px; border-radius: 12px; cursor: pointer; transition: all 0.2s;
71
- font-size: 0.9rem; font-weight: 600; color: #64748b;
72
  }
73
- .mode-btn:hover { color: #cbd5e1; background: rgba(255,255,255,0.05); }
74
  .mode-btn.active {
75
- background: linear-gradient(to bottom right, #06b6d4, #2563eb);
76
- color: #fff; box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
77
  }
78
 
 
79
  .loading-overlay {
80
  position: absolute; inset: 0; background: rgba(2, 6, 23, 0.9);
81
  z-index: 50; display: flex; flex-direction: column;
82
- align-items: center; justify-content: center; backdrop-filter: blur(8px);
83
  }
84
  .hidden { display: none !important; opacity: 0; pointer-events: none; }
85
 
86
  .spinner {
87
- width: 48px; height: 48px;
88
- border: 3px solid rgba(34, 211, 238, 0.1);
89
  border-top: 3px solid #22d3ee; border-radius: 50%;
90
- animation: spin 0.8s linear infinite;
91
  }
92
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
 
 
 
 
 
 
 
 
 
 
93
  </style>
94
  </head>
95
- <body class="flex flex-col items-center min-h-screen py-10 px-4">
96
 
97
  <!-- 标题 -->
98
- <div class="text-center mb-10 relative z-10">
99
- <h1 class="text-5xl font-black italic text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 to-blue-600 mb-2 drop-shadow-2xl">
100
- DEPTH <span class="text-white">CORE</span>
101
  </h1>
102
- <p class="text-xs font-mono text-cyan-500/60 tracking-[0.4em]">NEURAL RENDERING V5</p>
103
  </div>
104
 
105
- <!-- 切换 -->
106
- <div class="mb-8 relative z-10">
107
  <div class="mode-switch">
108
  <div class="mode-btn active" onclick="switchMode('holo')">
109
- <i class="fas fa-lightbulb mr-2"></i> 3D 浮雕光影
110
  </div>
111
  <div class="mode-btn" onclick="switchMode('scan')">
112
- <i class="fas fa-radar mr-2"></i> 热成像扫描
113
  </div>
114
  </div>
115
  </div>
116
 
117
  <!-- 舞台 -->
118
  <div class="stage">
119
- <div id="card" class="canvas-card group">
120
-
121
- <!-- 装饰 -->
122
- <div class="glass-shine"></div>
123
- <div class="hud-corner tl group-hover:top-2 group-hover:left-2"></div>
124
- <div class="hud-corner tr group-hover:top-2 group-hover:right-2"></div>
125
- <div class="hud-corner bl group-hover:bottom-2 group-hover:left-2"></div>
126
- <div class="hud-corner br group-hover:bottom-2 group-hover:right-2"></div>
127
-
128
- <!-- 加载层 -->
129
  <div id="loading-layer" class="loading-overlay hidden">
130
  <div id="spinner" class="spinner mb-4"></div>
131
- <div id="loading-text" class="text-xl font-bold text-white tracking-widest">LOADING</div>
132
- <div id="loading-error" class="text-xs text-red-400 mt-3 px-4 text-center hidden"></div>
133
- <div id="progress-text" class="text-xs text-cyan-500 mt-2 font-mono"></div>
134
  </div>
135
 
136
  <canvas id="output-canvas"></canvas>
137
  <div id="scan-line" class="scan-line"></div>
138
 
139
  <div id="placeholder" class="absolute inset-0 flex flex-col items-center justify-center text-slate-600 pointer-events-none transition-opacity duration-300">
140
- <div class="w-16 h-16 rounded-xl border-2 border-dashed border-slate-700 flex items-center justify-center mb-3">
141
- <i class="fas fa-plus text-2xl opacity-50"></i>
142
- </div>
143
- <p class="text-sm font-bold tracking-widest opacity-60">UPLOAD IMAGE</p>
144
  </div>
145
  </div>
146
  </div>
147
 
148
  <!-- 底部按钮 -->
149
- <div class="mt-12 relative z-10">
150
- <label class="group cursor-pointer relative inline-flex items-center justify-center px-8 py-3 bg-slate-800 rounded-full border border-slate-700 hover:border-cyan-500 transition-all duration-300 hover:shadow-[0_0_25px_rgba(6,182,212,0.25)]">
151
  <span class="text-sm font-bold text-white flex items-center gap-2">
152
- <i class="fas fa-upload text-cyan-400"></i> <span id="upload-text">选择图片</span>
153
  </span>
154
  <input type="file" id="upload" accept="image/*" class="hidden">
155
  </label>
156
  </div>
157
 
 
 
 
 
 
158
  <script type="module">
159
  import { pipeline, env, RawImage } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.16.0';
160
 
 
161
  env.allowLocalModels = false;
162
  env.useBrowserCache = true;
163
 
 
164
  const ui = {
165
  card: document.getElementById('card'),
166
  canvas: document.getElementById('output-canvas'),
167
  layer: document.getElementById('loading-layer'),
168
- spinner: document.getElementById('spinner'),
169
  text: document.getElementById('loading-text'),
170
- error: document.getElementById('loading-error'),
171
  progress: document.getElementById('progress-text'),
172
  scanLine: document.getElementById('scan-line'),
173
  placeholder: document.getElementById('placeholder'),
174
  fileInput: document.getElementById('upload'),
 
175
  btns: document.querySelectorAll('.mode-btn'),
176
- uploadText: document.getElementById('upload-text')
177
  };
178
 
179
  const ctx = ui.canvas.getContext('2d', { willReadFrequently: true });
180
 
181
  let depthEstimator = null;
182
  let originalImageData = null;
183
- let depthData = null; // Uint8Array 0-255
184
  let currentMode = 'holo';
185
  let isProcessing = false;
186
- let mouseX = 0, mouseY = 0; // -1 to 1
187
  let renderLoopId = null;
188
 
189
- // 色彩映射表 (Heatmap Gradient: Black -> Blue -> Cyan -> Yellow -> White)
190
- // 预计算256个颜色值,提升渲染速度
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  const colormap = new Uint32Array(256);
192
  function initColormap() {
193
  for (let i = 0; i < 256; i++) {
194
  const t = i / 255;
195
- // 简单的 Turbo-like 伪彩色逻辑
196
  let r = Math.min(255, Math.max(0, Math.floor(255 * (2 * t - 0.5))));
197
  let g = Math.min(255, Math.max(0, Math.floor(255 * (2 * t))));
198
  let b = Math.min(255, Math.max(0, Math.floor(255 * (1.5 - 2 * t))));
199
- // 增强暖色
200
- if (t > 0.7) { r = 255; g = 255 * (t-0.7)/0.3 + 200; b = 100; }
201
-
202
- // ABGR (Little Endian)
203
  colormap[i] = (255 << 24) | (b << 16) | (g << 8) | r;
204
  }
 
205
  }
206
  initColormap();
207
 
 
208
  window.switchMode = (mode) => {
 
209
  currentMode = mode;
 
 
210
  ui.btns.forEach(btn => btn.classList.remove('active'));
211
  if(mode === 'holo') ui.btns[0].classList.add('active');
212
  else ui.btns[1].classList.add('active');
@@ -217,48 +214,40 @@
217
  } else {
218
  ui.scanLine.style.display = 'block';
219
  ui.card.style.transform = `rotateX(0deg) rotateY(0deg)`;
220
- // 切回扫描模式,先恢复黑白原图防止闪烁
221
- // if (originalImageData) ctx.putImageData(originalImageData, 0, 0);
222
  }
223
  };
224
 
225
- function setStatus(state, msg, errMsg = "") {
226
  ui.layer.classList.remove('hidden');
227
  ui.text.textContent = msg;
228
- ui.error.classList.add('hidden');
229
- if (state === 'loading') {
230
- ui.spinner.style.display = 'block';
231
- ui.text.className = "text-xl font-bold text-white tracking-widest";
232
- } else if (state === 'error') {
233
- ui.spinner.style.display = 'none';
234
- ui.text.className = "text-lg font-bold text-red-500";
235
- ui.error.textContent = errMsg;
236
- ui.error.classList.remove('hidden');
237
- } else if (state === 'hide') {
238
- ui.layer.classList.add('hidden');
239
- }
240
  }
241
 
 
242
  async function loadModel() {
243
  if (depthEstimator) return;
244
- setStatus('loading', "LINKING NEURAL NET...");
 
245
  try {
246
  depthEstimator = await pipeline('depth-estimation', 'Xenova/depth-anything-small-hf', {
247
  quantized: true,
248
  progress_callback: (data) => {
249
  if (data.status === 'progress') {
250
  const p = Math.round(data.progress);
251
- ui.progress.textContent = `DOWNLOADING ASSETS... ${p}%`;
 
252
  }
253
  }
254
  });
 
255
  } catch (err) {
256
- console.error(err);
257
- setStatus('error', "INIT FAILED", err.message);
258
  throw err;
259
  }
260
  }
261
 
 
262
  function processImage(file) {
263
  return new Promise((resolve) => {
264
  const reader = new FileReader();
@@ -266,6 +255,8 @@
266
  const img = new Image();
267
  img.src = e.target.result;
268
  img.onload = () => {
 
 
269
  const maxDim = 512;
270
  let w = img.width;
271
  let h = img.height;
@@ -273,11 +264,17 @@
273
  if (w > h) { h = Math.round(h * (maxDim / w)); w = maxDim; }
274
  else { w = Math.round(w * (maxDim / h)); h = maxDim; }
275
  }
 
 
276
  w = w - (w % 32); if(w===0) w=32;
277
  h = h - (h % 32); if(h===0) h=32;
278
 
 
 
279
  ui.canvas.width = w;
280
  ui.canvas.height = h;
 
 
281
  ctx.drawImage(img, 0, 0, w, h);
282
 
283
  const imgData = ctx.getImageData(0, 0, w, h);
@@ -295,56 +292,66 @@
295
  });
296
  }
297
 
 
298
  ui.fileInput.addEventListener('change', async (e) => {
299
  const file = e.target.files[0];
 
 
300
  if (!file) return;
301
  if (isProcessing) return;
 
302
  isProcessing = true;
303
- ui.uploadText.textContent = "分析中...";
304
-
305
  if (renderLoopId) cancelAnimationFrame(renderLoopId);
306
  depthData = null;
307
 
308
  try {
309
  await loadModel();
310
- setStatus('loading', "PRE-PROCESSING...");
311
  ui.progress.textContent = "";
312
 
313
  const processed = await processImage(file);
314
  originalImageData = processed.imgData;
315
-
316
- setStatus('loading', "INFERENCING DEPTH...");
 
 
 
 
 
317
 
318
  setTimeout(async () => {
319
  try {
320
  const image = await RawImage.read(processed.url);
321
  const output = await depthEstimator(image);
322
 
323
- // --- 核心数据处---
 
 
324
  const rawDepth = output.depth;
325
  const dW = rawDepth.width;
326
  const dH = rawDepth.height;
327
  const dData = rawDepth.data;
328
 
329
- // 1. 归一化 (Normalize)
330
  let min = Infinity, max = -Infinity;
331
- for(let i=0; i<dData.length; i++) {
332
- if(dData[i] < min) min = dData[i];
333
- if(dData[i] > max) max = dData[i];
334
  }
335
  const range = max - min + 0.0001;
336
 
337
- // 2. 将单通道扩展为 RGBA 以便 Canvas 缩放
338
  const rgbaBuffer = new Uint8ClampedArray(dW * dH * 4);
339
  for(let i=0; i<dW*dH; i++) {
340
  const val = Math.floor(((dData[i] - min) / range) * 255);
341
- rgbaBuffer[i*4] = val; // R
342
- rgbaBuffer[i*4+1] = val; // G
343
- rgbaBuffer[i*4+2] = val; // B
344
- rgbaBuffer[i*4+3] = 255; // A
345
  }
346
 
347
- // 3. 缩放
348
  const tCanvas = document.createElement('canvas');
349
  tCanvas.width = dW; tCanvas.height = dH;
350
  tCanvas.getContext('2d').putImageData(new ImageData(rgbaBuffer, dW, dH), 0, 0);
@@ -355,62 +362,57 @@
355
 
356
  const finalDepthImg = scaleCanvas.getContext('2d').getImageData(0, 0, processed.width, processed.height);
357
 
358
- // 4. 保存最终的单通道深度数据
359
  depthData = new Uint8Array(finalDepthImg.data.length / 4);
360
- for(let i=0; i<depthData.length; i++) {
361
- depthData[i] = finalDepthImg.data[i * 4];
362
- }
363
 
364
- ctx.putImageData(originalImageData, 0, 0);
365
  setStatus('hide');
366
  startLoop();
367
 
368
  } catch (err) {
 
369
  console.error(err);
370
- setStatus('error', "PROCESS ERROR", err.message);
371
  } finally {
372
  isProcessing = false;
373
- ui.uploadText.textContent = "更换照片";
374
  }
375
  }, 50);
376
  } catch (err) {
377
- console.error(err);
378
- setStatus('error', "LOAD ERROR", err.message);
379
  isProcessing = false;
380
- ui.uploadText.textContent = "更换照片";
381
  }
382
  });
383
 
 
384
  function startLoop() {
385
  const container = document.body;
386
 
387
- // 鼠标移动逻辑
388
- const handleMove = (x, y) => {
389
  const rect = ui.card.getBoundingClientRect();
390
- const cx = rect.left + rect.width / 2;
391
- const cy = rect.top + rect.height / 2;
392
- mouseX = (x - cx) / (window.innerWidth / 2); // 范围加大,减缓移动幅度
393
- mouseY = (y - cy) / (window.innerHeight / 2);
394
-
395
- // 限制在 -1 到 1
396
  mouseX = Math.max(-1, Math.min(1, mouseX));
397
  mouseY = Math.max(-1, Math.min(1, mouseY));
398
  };
399
 
400
- container.onmousemove = (e) => handleMove(e.clientX, e.clientY);
401
  container.ontouchmove = (e) => {
402
  e.preventDefault();
403
- handleMove(e.touches[0].clientX, e.touches[0].clientY);
404
  };
405
 
406
  const loop = () => {
407
- if (depthData && originalImageData &&
408
- depthData.length === (originalImageData.width * originalImageData.height)) {
409
-
410
  if (currentMode === 'holo') {
411
- renderReliefLight(); // 新算法:浮雕光影
412
  } else {
413
- renderHeatmapScan(); // 新算法:热力图扫描
414
  }
415
  }
416
  renderLoopId = requestAnimationFrame(loop);
@@ -418,39 +420,105 @@
418
  loop();
419
  }
420
 
421
- // --- 算法 A: 3D 浮雕光影 (Relief Lighting) ---
422
- // 核心思路:利用深度差计算法线,模拟光源照射
423
- function renderReliefLight() {
424
- // 物理卡片旋转
425
- const rotateX = -mouseY * 10;
426
- const rotateY = mouseX * 10;
427
- ui.card.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
428
 
429
  const w = ui.canvas.width;
430
  const h = ui.canvas.height;
431
  const output = ctx.createImageData(w, h);
432
 
433
- // 使用 Uint32Array 读写像素 (ABGR)
434
  const buf32 = new Uint32Array(output.data.buffer);
435
  const src32 = new Uint32Array(originalImageData.data.buffer);
436
 
437
- // 光源方向 (反向鼠标位置)
438
  const lx = -mouseX * 2;
439
  const ly = -mouseY * 2;
440
- const lz = 0.5; // Z轴光源高度 (0-1)
441
 
442
  for (let y = 1; y < h - 1; y++) {
443
  const yw = y * w;
444
  for (let x = 1; x < w - 1; x++) {
445
  const i = yw + x;
446
 
447
- // 计算法线 (Normal)
448
- // dx = 右边深度 - 左边深度
449
- // dy = 下边深度 - 上边深度
450
  const dzdx = (depthData[i+1] - depthData[i-1]) / 255.0;
451
  const dzdy = (depthData[i+w] - depthData[i-w]) / 255.0;
452
 
453
- // 点积计算光照强度 (Lambertian Diffuse)
454
- // Normal = (-dzdx, -dzdy, 1)
455
- // Light = (lx, ly, lz)
456
- // Dot = -
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI 全息相框 - 调试增强版</title>
7
  <script src="https://cdn.tailwindcss.com"></script>
8
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
9
  <style>
 
10
  body { background-color: #020617; color: white; font-family: 'Segoe UI', sans-serif; overflow-x: hidden; touch-action: none; }
11
 
12
  .stage {
13
+ perspective: 1000px;
14
+ width: 100%; max-width: 512px; margin: 0 auto;
15
  position: relative; z-index: 20;
16
  }
17
 
 
18
  .canvas-card {
19
  position: relative; width: 100%; aspect-ratio: 4/3;
20
  background: #0f172a; border-radius: 12px;
21
+ box-shadow: 0 25px 60px rgba(0,0,0,0.7);
 
 
 
 
22
  transform-style: preserve-3d;
23
+ transition: transform 0.1s cubic-bezier(0.25, 0.46, 0.45, 0.94);
24
+ overflow: hidden; border: 1px solid #334155;
 
 
 
 
 
 
 
25
  }
26
 
27
  canvas { width: 100%; height: 100%; object-fit: contain; display: block; image-rendering: pixelated; }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  /* 扫描线 */
30
  .scan-line {
31
  position: absolute; left: 0; right: 0; height: 2px;
32
  background: #fff;
33
  box-shadow: 0 0 15px #00f3ff, 0 0 30px #00f3ff;
34
+ display: none; pointer-events: none; z-index: 30;
35
  mix-blend-mode: overlay;
36
  }
37
 
38
+ /* 切换按钮 */
39
  .mode-switch {
40
+ background: rgba(30, 41, 59, 0.8); backdrop-filter: blur(8px);
41
  border: 1px solid rgba(255,255,255,0.1);
42
+ border-radius: 99px; padding: 4px; display: inline-flex; gap: 4px;
43
  }
44
  .mode-btn {
45
+ padding: 8px 20px; border-radius: 99px; cursor: pointer; transition: all 0.2s;
46
+ font-size: 0.85rem; font-weight: 600; color: #94a3b8; display: flex; items-center; gap: 6px;
47
  }
48
+ .mode-btn:hover { color: #fff; background: rgba(255,255,255,0.05); }
49
  .mode-btn.active {
50
+ background: #06b6d4; color: #fff; box-shadow: 0 2px 10px rgba(6, 182, 212, 0.4);
 
51
  }
52
 
53
+ /* 加载层 */
54
  .loading-overlay {
55
  position: absolute; inset: 0; background: rgba(2, 6, 23, 0.9);
56
  z-index: 50; display: flex; flex-direction: column;
57
+ align-items: center; justify-content: center; backdrop-filter: blur(4px);
58
  }
59
  .hidden { display: none !important; opacity: 0; pointer-events: none; }
60
 
61
  .spinner {
62
+ width: 40px; height: 40px; border: 3px solid rgba(34, 211, 238, 0.1);
 
63
  border-top: 3px solid #22d3ee; border-radius: 50%;
64
+ animation: spin 1s linear infinite;
65
  }
66
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
67
+
68
+ /* 调试日志窗口 */
69
+ #debug-console {
70
+ width: 100%; max-width: 600px; height: 150px;
71
+ background: #000; border: 1px solid #333;
72
+ font-family: 'Consolas', monospace; font-size: 11px; color: #0f0;
73
+ overflow-y: auto; padding: 10px; margin-top: 20px;
74
+ border-radius: 8px; opacity: 0.9; box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
75
+ }
76
+ #debug-console div { margin-bottom: 2px; border-bottom: 1px solid #111; }
77
  </style>
78
  </head>
79
+ <body class="flex flex-col items-center min-h-screen py-6 px-4">
80
 
81
  <!-- 标题 -->
82
+ <div class="text-center mb-6 relative z-10">
83
+ <h1 class="text-4xl font-black italic tracking-tighter text-transparent bg-clip-text bg-gradient-to-r from-cyan-300 to-blue-500 mb-2">
84
+ DEPTH <span class="text-white">LOGS</span>
85
  </h1>
86
+ <p class="text-xs font-mono text-cyan-500/60 tracking-widest">DIAGNOSTIC MODE</p>
87
  </div>
88
 
89
+ <!-- 模式切换 -->
90
+ <div class="mb-6 relative z-10">
91
  <div class="mode-switch">
92
  <div class="mode-btn active" onclick="switchMode('holo')">
93
+ <i class="fas fa-cube"></i> 3D 浮雕
94
  </div>
95
  <div class="mode-btn" onclick="switchMode('scan')">
96
+ <i class="fas fa-radar"></i> 热扫描
97
  </div>
98
  </div>
99
  </div>
100
 
101
  <!-- 舞台 -->
102
  <div class="stage">
103
+ <div id="card" class="canvas-card">
 
 
 
 
 
 
 
 
 
104
  <div id="loading-layer" class="loading-overlay hidden">
105
  <div id="spinner" class="spinner mb-4"></div>
106
+ <div id="loading-text" class="text-lg font-bold text-white tracking-widest">LOADING</div>
107
+ <div id="progress-text" class="text-xs text-cyan-400 mt-2 font-mono"></div>
 
108
  </div>
109
 
110
  <canvas id="output-canvas"></canvas>
111
  <div id="scan-line" class="scan-line"></div>
112
 
113
  <div id="placeholder" class="absolute inset-0 flex flex-col items-center justify-center text-slate-600 pointer-events-none transition-opacity duration-300">
114
+ <i class="fas fa-image text-5xl mb-3 opacity-30"></i>
115
+ <p class="text-xs font-mono opacity-50">WAITING FOR INPUT</p>
 
 
116
  </div>
117
  </div>
118
  </div>
119
 
120
  <!-- 底部按钮 -->
121
+ <div class="mt-8 relative z-10">
122
+ <label class="group cursor-pointer relative inline-flex items-center justify-center px-8 py-3 bg-slate-800 rounded-full border border-slate-700 hover:border-cyan-500 transition-all shadow-lg">
123
  <span class="text-sm font-bold text-white flex items-center gap-2">
124
+ <i class="fas fa-upload text-cyan-400"></i> <span id="upload-text">上传照片</span>
125
  </span>
126
  <input type="file" id="upload" accept="image/*" class="hidden">
127
  </label>
128
  </div>
129
 
130
+ <!-- 日志窗口 -->
131
+ <div id="debug-console">
132
+ <div style="color:#aaa">> System initialized. Waiting for user input...</div>
133
+ </div>
134
+
135
  <script type="module">
136
  import { pipeline, env, RawImage } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.16.0';
137
 
138
+ // 配置:强制远程加载
139
  env.allowLocalModels = false;
140
  env.useBrowserCache = true;
141
 
142
+ // UI 引用
143
  const ui = {
144
  card: document.getElementById('card'),
145
  canvas: document.getElementById('output-canvas'),
146
  layer: document.getElementById('loading-layer'),
 
147
  text: document.getElementById('loading-text'),
 
148
  progress: document.getElementById('progress-text'),
149
  scanLine: document.getElementById('scan-line'),
150
  placeholder: document.getElementById('placeholder'),
151
  fileInput: document.getElementById('upload'),
152
+ uploadText: document.getElementById('upload-text'),
153
  btns: document.querySelectorAll('.mode-btn'),
154
+ console: document.getElementById('debug-console')
155
  };
156
 
157
  const ctx = ui.canvas.getContext('2d', { willReadFrequently: true });
158
 
159
  let depthEstimator = null;
160
  let originalImageData = null;
161
+ let depthData = null;
162
  let currentMode = 'holo';
163
  let isProcessing = false;
164
+ let mouseX = 0, mouseY = 0;
165
  let renderLoopId = null;
166
 
167
+ // --- 日志工具 ---
168
+ function log(msg, type='info') {
169
+ const time = new Date().toLocaleTimeString();
170
+ let color = '#aaa';
171
+ if (type === 'success') color = '#4ade80'; // Green
172
+ if (type === 'error') color = '#ef4444'; // Red
173
+ if (type === 'warn') color = '#facc15'; // Yellow
174
+ if (type === 'cmd') color = '#22d3ee'; // Cyan
175
+
176
+ const el = document.createElement('div');
177
+ el.style.color = color;
178
+ el.innerHTML = `[${time}] ${msg}`;
179
+ ui.console.appendChild(el);
180
+ ui.console.scrollTop = ui.console.scrollHeight;
181
+ console.log(`[${type}] ${msg}`);
182
+ }
183
+
184
+ // --- 热力图色谱初始化 ---
185
  const colormap = new Uint32Array(256);
186
  function initColormap() {
187
  for (let i = 0; i < 256; i++) {
188
  const t = i / 255;
189
+ // Turbo-like gradient
190
  let r = Math.min(255, Math.max(0, Math.floor(255 * (2 * t - 0.5))));
191
  let g = Math.min(255, Math.max(0, Math.floor(255 * (2 * t))));
192
  let b = Math.min(255, Math.max(0, Math.floor(255 * (1.5 - 2 * t))));
193
+ if (t > 0.8) { r=255; g=255; b=200; } // Highlights
194
+ // ABGR Little Endian: A B G R
 
 
195
  colormap[i] = (255 << 24) | (b << 16) | (g << 8) | r;
196
  }
197
+ log("Color map initialized", "success");
198
  }
199
  initColormap();
200
 
201
+ // --- 模式切换 ---
202
  window.switchMode = (mode) => {
203
+ if (currentMode === mode) return;
204
  currentMode = mode;
205
+ log(`切换模式: ${mode.toUpperCase()}`, "cmd");
206
+
207
  ui.btns.forEach(btn => btn.classList.remove('active'));
208
  if(mode === 'holo') ui.btns[0].classList.add('active');
209
  else ui.btns[1].classList.add('active');
 
214
  } else {
215
  ui.scanLine.style.display = 'block';
216
  ui.card.style.transform = `rotateX(0deg) rotateY(0deg)`;
 
 
217
  }
218
  };
219
 
220
+ function setStatus(state, msg) {
221
  ui.layer.classList.remove('hidden');
222
  ui.text.textContent = msg;
223
+ if (state === 'hide') ui.layer.classList.add('hidden');
 
 
 
 
 
 
 
 
 
 
 
224
  }
225
 
226
+ // --- 1. 加载模型 ---
227
  async function loadModel() {
228
  if (depthEstimator) return;
229
+ log("开始加载 AI 模型...", "warn");
230
+ setStatus('loading', "CONNECTING...");
231
  try {
232
  depthEstimator = await pipeline('depth-estimation', 'Xenova/depth-anything-small-hf', {
233
  quantized: true,
234
  progress_callback: (data) => {
235
  if (data.status === 'progress') {
236
  const p = Math.round(data.progress);
237
+ ui.progress.textContent = `${data.file} - ${p}%`;
238
+ if (p % 20 === 0) log(`Downloading: ${p}%`);
239
  }
240
  }
241
  });
242
+ log("模型加载成功!", "success");
243
  } catch (err) {
244
+ log("模型加载失败: " + err.message, "error");
245
+ setStatus('loading', "LOAD FAILED");
246
  throw err;
247
  }
248
  }
249
 
250
+ // --- 2. 图片处理 (关键:32倍数对齐) ---
251
  function processImage(file) {
252
  return new Promise((resolve) => {
253
  const reader = new FileReader();
 
255
  const img = new Image();
256
  img.src = e.target.result;
257
  img.onload = () => {
258
+ log(`原图尺寸: ${img.width}x${img.height}`);
259
+
260
  const maxDim = 512;
261
  let w = img.width;
262
  let h = img.height;
 
264
  if (w > h) { h = Math.round(h * (maxDim / w)); w = maxDim; }
265
  else { w = Math.round(w * (maxDim / h)); h = maxDim; }
266
  }
267
+
268
+ // 强制对齐 32
269
  w = w - (w % 32); if(w===0) w=32;
270
  h = h - (h % 32); if(h===0) h=32;
271
 
272
+ log(`调整后尺寸: ${w}x${h} (32倍数)`, "info");
273
+
274
  ui.canvas.width = w;
275
  ui.canvas.height = h;
276
+
277
+ ctx.clearRect(0, 0, w, h);
278
  ctx.drawImage(img, 0, 0, w, h);
279
 
280
  const imgData = ctx.getImageData(0, 0, w, h);
 
292
  });
293
  }
294
 
295
+ // --- 3. 主逻辑 ---
296
  ui.fileInput.addEventListener('change', async (e) => {
297
  const file = e.target.files[0];
298
+ ui.fileInput.value = ''; // 允许重复上传
299
+
300
  if (!file) return;
301
  if (isProcessing) return;
302
+
303
  isProcessing = true;
304
+ ui.uploadText.textContent = "处理中...";
305
+
306
  if (renderLoopId) cancelAnimationFrame(renderLoopId);
307
  depthData = null;
308
 
309
  try {
310
  await loadModel();
311
+ setStatus('loading', "PROCESSING...");
312
  ui.progress.textContent = "";
313
 
314
  const processed = await processImage(file);
315
  originalImageData = processed.imgData;
316
+
317
+ // 立即显示原图,防止白屏
318
+ ctx.putImageData(originalImageData, 0, 0);
319
+ setStatus('hide'); // 先隐藏加载层,让用户看到图
320
+
321
+ // 短暂延迟后开始推理
322
+ setStatus('loading', "AI INFERENCE...");
323
 
324
  setTimeout(async () => {
325
  try {
326
  const image = await RawImage.read(processed.url);
327
  const output = await depthEstimator(image);
328
 
329
+ log("深度推完成", "success");
330
+
331
+ // 手动转换 Tensor -> RGBA
332
  const rawDepth = output.depth;
333
  const dW = rawDepth.width;
334
  const dH = rawDepth.height;
335
  const dData = rawDepth.data;
336
 
337
+ // 归一化深度值
338
  let min = Infinity, max = -Infinity;
339
+ for(let v of dData) {
340
+ if(v<min) min=v; if(v>max) max=v;
 
341
  }
342
  const range = max - min + 0.0001;
343
 
344
+ // 构建 4 通道数据
345
  const rgbaBuffer = new Uint8ClampedArray(dW * dH * 4);
346
  for(let i=0; i<dW*dH; i++) {
347
  const val = Math.floor(((dData[i] - min) / range) * 255);
348
+ rgbaBuffer[i*4] = val;
349
+ rgbaBuffer[i*4+1] = val;
350
+ rgbaBuffer[i*4+2] = val;
351
+ rgbaBuffer[i*4+3] = 255;
352
  }
353
 
354
+ // 缩放深度图
355
  const tCanvas = document.createElement('canvas');
356
  tCanvas.width = dW; tCanvas.height = dH;
357
  tCanvas.getContext('2d').putImageData(new ImageData(rgbaBuffer, dW, dH), 0, 0);
 
362
 
363
  const finalDepthImg = scaleCanvas.getContext('2d').getImageData(0, 0, processed.width, processed.height);
364
 
365
+ // 保存单通道深度
366
  depthData = new Uint8Array(finalDepthImg.data.length / 4);
367
+ for(let i=0; i<depthData.length; i++) depthData[i] = finalDepthImg.data[i*4];
 
 
368
 
369
+ log("数据处理完毕,启动渲染循环", "success");
370
  setStatus('hide');
371
  startLoop();
372
 
373
  } catch (err) {
374
+ log("推理错误: " + err.message, "error");
375
  console.error(err);
376
+ setStatus('loading', "ERROR");
377
  } finally {
378
  isProcessing = false;
379
+ ui.uploadText.textContent = "上传照片";
380
  }
381
  }, 50);
382
  } catch (err) {
383
+ log("主流程错误: " + err.message, "error");
384
+ setStatus('loading', "ERROR");
385
  isProcessing = false;
386
+ ui.uploadText.textContent = "上传照片";
387
  }
388
  });
389
 
390
+ // --- 4. 渲染循环 ---
391
  function startLoop() {
392
  const container = document.body;
393
 
394
+ const updateMouse = (x, y) => {
 
395
  const rect = ui.card.getBoundingClientRect();
396
+ const cx = rect.left + rect.width/2;
397
+ const cy = rect.top + rect.height/2;
398
+ mouseX = (x - cx) / (window.innerWidth/2);
399
+ mouseY = (y - cy) / (window.innerHeight/2);
 
 
400
  mouseX = Math.max(-1, Math.min(1, mouseX));
401
  mouseY = Math.max(-1, Math.min(1, mouseY));
402
  };
403
 
404
+ container.onmousemove = (e) => updateMouse(e.clientX, e.clientY);
405
  container.ontouchmove = (e) => {
406
  e.preventDefault();
407
+ updateMouse(e.touches[0].clientX, e.touches[0].clientY);
408
  };
409
 
410
  const loop = () => {
411
+ if (depthData && originalImageData) {
 
 
412
  if (currentMode === 'holo') {
413
+ renderRelief();
414
  } else {
415
+ renderHeatmap();
416
  }
417
  }
418
  renderLoopId = requestAnimationFrame(loop);
 
420
  loop();
421
  }
422
 
423
+ // 渲染模式 A: 浮雕光影 (无撕裂)
424
+ function renderRelief() {
425
+ const rotateX = -mouseY * 12;
426
+ const rotateY = mouseX * 12;
427
+ ui.card.style.transform = `rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
 
 
428
 
429
  const w = ui.canvas.width;
430
  const h = ui.canvas.height;
431
  const output = ctx.createImageData(w, h);
432
 
 
433
  const buf32 = new Uint32Array(output.data.buffer);
434
  const src32 = new Uint32Array(originalImageData.data.buffer);
435
 
 
436
  const lx = -mouseX * 2;
437
  const ly = -mouseY * 2;
438
+ const lz = 0.5;
439
 
440
  for (let y = 1; y < h - 1; y++) {
441
  const yw = y * w;
442
  for (let x = 1; x < w - 1; x++) {
443
  const i = yw + x;
444
 
445
+ // 计算法线
 
 
446
  const dzdx = (depthData[i+1] - depthData[i-1]) / 255.0;
447
  const dzdy = (depthData[i+w] - depthData[i-w]) / 255.0;
448
 
449
+ // 光照计算
450
+ let intensity = (-dzdx * lx - dzdy * ly + lz);
451
+ intensity = Math.max(0.5, Math.min(1.5, intensity + 0.6));
452
+
453
+ const pixel = src32[i];
454
+ const r = (pixel & 0xff);
455
+ const g = (pixel >> 8) & 0xff;
456
+ const b = (pixel >> 16) & 0xff;
457
+
458
+ const nr = Math.min(255, r * intensity);
459
+ const ng = Math.min(255, g * intensity);
460
+ const nb = Math.min(255, b * intensity);
461
+
462
+ buf32[i] = (255 << 24) | (nb << 16) | (ng << 8) | nr;
463
+ }
464
+ }
465
+ ctx.putImageData(output, 0, 0);
466
+ }
467
+
468
+ // 渲染模式 B: 热力扫描
469
+ function renderHeatmap() {
470
+ const scanRatio = (mouseY + 1) / 2;
471
+ const scanYPx = Math.max(0, Math.min(ui.canvas.height, scanRatio * ui.canvas.height));
472
+ ui.scanLine.style.top = `${scanYPx}px`;
473
+
474
+ const targetDepth = Math.floor(scanRatio * 255);
475
+ const w = ui.canvas.width;
476
+ const h = ui.canvas.height;
477
+ const output = ctx.createImageData(w, h);
478
+
479
+ const buf32 = new Uint32Array(output.data.buffer);
480
+ const src32 = new Uint32Array(originalImageData.data.buffer);
481
+
482
+ const range = 25;
483
+
484
+ for (let i = 0; i < w * h; i++) {
485
+ const depth = depthData[i];
486
+
487
+ if (Math.abs(depth - targetDepth) < range) {
488
+ // 热力图颜色混合
489
+ const color = colormap[depth];
490
+ const src = src32[i];
491
+
492
+ // 简单混合:原图 50% + 热力色 50%
493
+ const cr = (color & 0xff);
494
+ const cg = (color >> 8) & 0xff;
495
+ const cb = (color >> 16) & 0xff;
496
+
497
+ const sr = (src & 0xff);
498
+ const sg = (src >> 8) & 0xff;
499
+ const sb = (src >> 16) & 0xff;
500
+
501
+ const nr = (sr + cr) >> 1;
502
+ const ng = (sg + cg) >> 1;
503
+ const nb = (sb + cb) >> 1;
504
+
505
+ buf32[i] = (255 << 24) | (nb << 16) | (ng << 8) | nr;
506
+
507
+ } else if (depth > targetDepth) {
508
+ buf32[i] = src32[i]; // 原图
509
+ } else {
510
+ // 变暗
511
+ const src = src32[i];
512
+ const r = (src & 0xff);
513
+ const g = (src >> 8) & 0xff;
514
+ const b = (src >> 16) & 0xff;
515
+ const gray = (r * 0.3 + g * 0.59 + b * 0.11) * 0.2;
516
+
517
+ buf32[i] = (255 << 24) | (gray << 16) | (gray << 8) | gray;
518
+ }
519
+ }
520
+ ctx.putImageData(output, 0, 0);
521
+ }
522
+ </script>
523
+ </body>
524
+ </html>