Преглед изворни кода

refactor: 移除web前端界面,切换为纯headless API运行

1. 更新AGENTS.md和README.md文档,说明系统变更
2. 删除web_static静态前端文件及相关服务挂载代码
3. 修改根路由返回状态信息而非前端页面
4. 更新测试用例适配接口变更
5. 新增说明移除了OCR、LLM、穿戴检测功能
wenhongquan пре 22 часа
родитељ
комит
ccba432f8e

+ 4 - 6
AGENTS.md

@@ -9,11 +9,13 @@
 **施工现场安全行为智能识别系统 v2.0.0** - 基于 Python 的双摄像头联动系统。
 
 核心功能:
-- 多组全景摄像头实时监控,YOLO11 检测人体
-- 球机 PTZ 联动跟踪,变焦定位目标
+- 多组全景/球机摄像头实时监控,YOLO11 检测人体
+- 球机 PTZ 巡航由摄像机平台自身完成,本程序仅拉取球机 RTSP 流进行检测
 - 配对图片保存与上传
 - 事件推送至业务平台
 
+> 注:Web 设置页面已移除,系统以 headless API 方式运行。
+
 > 注:OCR 编号识别、LLM 判断、安全帽/反光衣检测已在本版本中移除。
 
 ---
@@ -49,10 +51,6 @@ dual_camera_system/
 ├── web/                         # Web API 服务
 │   ├── routes.py                # HTTP 路由
 │   └── state.py                 # Web 应用状态
-├── web_static/                  # 前端静态页面
-│   ├── app.js                   # 前端交互脚本
-│   ├── index.html               # 前端入口页面
-│   └── style.css                # 前端样式
 ├── app.py                       # FastAPI 应用工厂与全局服务初始化
 ├── main.py                      # 服务入口(启动 uvicorn)
 ├── dahua_sdk.py                 # 大华 SDK ctypes 封装

+ 3 - 7
dual_camera_system/README.md

@@ -2,12 +2,12 @@
 
 ## 系统简介
 
-本系统实现多组全景摄像头和可变焦球机的联动抓拍功能:
+本系统实现多组全景摄像头和球机视频流的实时检测与抓拍上报:
 - 多组全景摄像头实时监控,检测画面中的人体
-- 对应球机按扫描点位轮询定位并抓拍
+- 球机 RTSP 流由摄像机平台自身巡航提供,程序仅进行实时检测与抓拍
 - 保存全景+球机配对图片,支持上传至业务平台
 
-> 注意:本版本已移除 OCR 编号识别、LLM 判断、安全帽/反光衣检测,仅保留人体检测与联动抓拍。
+> 注意:本版本已移除 OCR 编号识别、LLM 判断、安全帽/反光衣检测,仅保留人体检测与抓拍。Web 设置页面已移除,系统以 headless API 方式运行。
 
 ## 系统架构
 
@@ -57,10 +57,6 @@ dual_camera_system/
 ├── web/                  # Web API 服务
 │   ├── routes.py         # HTTP 路由
 │   └── state.py          # Web 应用状态
-├── web_static/           # 前端静态页面
-│   ├── app.js            # 前端交互脚本
-│   ├── index.html        # 前端入口页面
-│   └── style.css         # 前端样式
 ├── app.py                # FastAPI 应用工厂与全局服务初始化
 ├── main.py               # 服务入口(启动 uvicorn)
 ├── dahua_sdk.py          # 大华SDK Python封装

+ 1 - 9
dual_camera_system/app.py

@@ -8,8 +8,6 @@ from contextlib import asynccontextmanager
 
 import cv2
 from fastapi import FastAPI
-from fastapi.staticfiles import StaticFiles
-from fastapi.responses import FileResponse
 
 from config import CAMERA_GROUPS, SDK_PATH, SYSTEM_CONFIG, STORAGE_CONFIG
 from dahua_sdk import DahuaSDK
@@ -332,16 +330,10 @@ def create_app(test_mode: bool = False) -> FastAPI:
         )
 
     app = FastAPI(lifespan=lifespan)
-    static_dir = os.path.join(os.path.dirname(__file__), "web_static")
-    if os.path.isdir(static_dir):
-        app.mount("/static", StaticFiles(directory=static_dir), name="static")
     app.include_router(router)
 
     @app.get("/")
     async def root():
-        index_path = os.path.join(static_dir, "index.html")
-        if os.path.isfile(index_path):
-            return FileResponse(index_path)
-        return {"message": "PTZ 360 scan + panorama polling system"}
+        return {"status": "running", "service": "dual-camera detection"}
 
     return app

BIN
dual_camera_system/data/previews/group_1/preview_1781684018502.jpg


BIN
dual_camera_system/data/previews/group_1/preview_1781684045686.jpg


BIN
dual_camera_system/data/previews/group_1/preview_1781684063369.jpg


BIN
dual_camera_system/data/previews/group_1/preview_1781684259420.jpg


+ 3 - 14
dual_camera_system/tests/test_routes.py

@@ -95,10 +95,11 @@ def test_delete_point(client):
     assert len(response.json()["points"]) == 0
 
 
-def test_static_index(client):
+def test_root_health(client):
     response = client.get("/")
     assert response.status_code == 200
-    assert "text/html" in response.headers["content-type"]
+    data = response.json()
+    assert data["status"] == "running"
 
 
 def test_panorama_not_found(client):
@@ -167,18 +168,6 @@ def test_live_stream_not_found(client):
     assert response.status_code == 404
 
 
-def test_static_css_served(client):
-    response = client.get("/static/style.css")
-    assert response.status_code == 200
-    assert "text/css" in response.headers["content-type"]
-
-
-def test_static_js_served(client):
-    response = client.get("/static/app.js")
-    assert response.status_code == 200
-    assert "javascript" in response.headers["content-type"]
-
-
 def test_mutating_endpoints_require_api_key(client_with_mocks, monkeypatch):
     client, scanner, scheduler, _, _ = client_with_mocks
     monkeypatch.setattr("web.auth.DEVICE_CONFIG", {"api_key": "secret123"})

+ 0 - 623
dual_camera_system/web_static/app.js

@@ -1,623 +0,0 @@
-const parseResponse = async (r) => {
-  const contentType = r.headers.get('content-type') || '';
-  if (contentType.includes('application/json')) {
-    return r.json();
-  }
-  const text = await r.text();
-  return text ? { text } : {};
-};
-
-const API = {
-  get: async (url) => {
-    const r = await fetch(url);
-    if (!r.ok) {
-      const text = await r.text();
-      throw new Error(`${url}: ${r.status} ${text}`);
-    }
-    return parseResponse(r);
-  },
-  post: async (url, body = {}) => {
-    const r = await fetch(url, {
-      method: 'POST',
-      headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify(body)
-    });
-    if (!r.ok) {
-      const text = await r.text();
-      throw new Error(`${url}: ${r.status} ${text}`);
-    }
-    return parseResponse(r);
-  },
-  del: async (url) => {
-    const r = await fetch(url, { method: 'DELETE' });
-    if (!r.ok) {
-      const text = await r.text();
-      throw new Error(`${url}: ${r.status} ${text}`);
-    }
-    return parseResponse(r);
-  }
-};
-
-let currentGroup = null;
-let scanPollInterval = null;
-let controlsGloballyDisabled = false;
-let tempPreview = null;
-let selectedSampleEl = null;
-
-function log(msg) {
-  const panel = document.getElementById('log-panel');
-  const line = document.createElement('div');
-  line.textContent = `${new Date().toLocaleTimeString()} ${msg}`;
-  panel.appendChild(line);
-  while (panel.children.length > 200) {
-    panel.removeChild(panel.firstChild);
-  }
-  panel.scrollTop = panel.scrollHeight;
-}
-
-function setStatus(msg) {
-  document.getElementById('status').textContent = `状态:${msg}`;
-}
-
-function setControlsDisabled(disabled) {
-  controlsGloballyDisabled = disabled;
-  ['btn-scan', 'btn-poll-start', 'btn-poll-stop', 'btn-preview', 'btn-add'].forEach(id => {
-    const el = document.getElementById(id);
-    if (el) el.disabled = disabled;
-  });
-  ['inp-pan', 'inp-tilt', 'inp-zoom', 'inp-dwell'].forEach(id => {
-    const el = document.getElementById(id);
-    if (el) el.disabled = disabled;
-  });
-}
-
-async function withDisabled(id, fn) {
-  const el = document.getElementById(id);
-  el.disabled = true;
-  try {
-    return await fn();
-  } finally {
-    if (!controlsGloballyDisabled) {
-      el.disabled = false;
-    }
-  }
-}
-
-function parseStrictFloat(value, name) {
-  const num = Number(value);
-  if (!Number.isFinite(num)) {
-    throw new Error(`${name} 必须是有效数字`);
-  }
-  return num;
-}
-
-function escapeHtml(text) {
-  const div = document.createElement('div');
-  div.textContent = text;
-  return div.innerHTML;
-}
-
-function setSelectedPreview(url) {
-  const img = document.getElementById('selected-preview');
-  if (url) {
-    img.src = url;
-    img.style.display = 'block';
-  } else {
-    img.src = '';
-    img.style.display = 'none';
-  }
-}
-
-function selectSample(sample) {
-  document.getElementById('inp-pan').value = sample.pan.toFixed(2);
-  document.getElementById('inp-tilt').value = sample.tilt.toFixed(2);
-  document.getElementById('inp-zoom').value = sample.zoom;
-  tempPreview = { path: sample.thumbnail };
-  setSelectedPreview(`/api/sample-image?path=${encodeURIComponent(sample.thumbnail)}`);
-  if (sampleCanvas) sampleCanvas.setSelected(sample.pan, sample.tilt);
-}
-
-class SampleCanvas {
-  constructor(containerId, wrapperId) {
-    this.container = document.getElementById(containerId);
-    this.wrapper = document.getElementById(wrapperId);
-    this.samples = [];
-    this.pans = [];
-    this.tilts = [];
-    this.sampleMap = new Map();
-    this.cellW = 160;
-    this.cellH = 120;
-    this.captionH = 20;
-    this.scale = 1;
-    this.selectedPan = null;
-    this.selectedTilt = null;
-    this.selectedNodeId = null;
-    this.graph = null;
-
-    this.initGraph();
-    window.addEventListener('resize', () => this.resize());
-  }
-
-  initGraph() {
-    const G6 = window.G6;
-    if (!G6) {
-      log('G6 库未加载,扫描矩阵不可用');
-      return;
-    }
-
-    const rect = this.wrapper.getBoundingClientRect();
-    const width = rect.width || this.wrapper.clientWidth || 800;
-    const height = rect.height || this.wrapper.clientHeight || 600;
-    this.graph = new G6.Graph({
-      container: this.container,
-      width,
-      height,
-      animation: false,
-      zoomRange: [0.1, 5],
-      data: { nodes: [], edges: [] },
-      node: {
-        type: 'image',
-        style: {
-          size: [this.cellW, this.cellH],
-          cursor: 'pointer',
-          labelFill: '#cbd5e1',
-          labelFontSize: 11,
-          labelPlacement: 'bottom',
-          labelOffsetY: 4,
-          radius: 4,
-        },
-        state: {
-          selected: {
-            halo: true,
-            haloStroke: '#4ade80',
-            haloLineWidth: 6,
-            haloOpacity: 1,
-          },
-        },
-      },
-      layout: {
-        type: 'grid',
-        cols: 1,
-        nodeSize: [this.cellW, this.cellH + this.captionH],
-        preventOverlap: true,
-      },
-      behaviors: ['drag-canvas', 'zoom-canvas'],
-    });
-
-    this.graph.on('node:click', (e) => {
-      const target = e.item || e.target;
-      const id = target?.id;
-      if (!id) return;
-      const nodeData = this.graph.getNodeData(id);
-      if (nodeData?.data?.sample) {
-        selectSample(nodeData.data.sample);
-      }
-    });
-
-    this.graph.on('aftertransform', () => {
-      if (!this.graph) return;
-      this.scale = this.graph.getZoom();
-      this.updateZoomLabel();
-    });
-  }
-
-  async resize() {
-    if (!this.graph) return;
-    const rect = this.wrapper.getBoundingClientRect();
-    this.graph.setSize(rect.width, rect.height);
-    await this.graph.fitView();
-    this.scale = this.graph.getZoom();
-    this.updateZoomLabel();
-  }
-
-  async setSamples(samples) {
-    this.samples = samples || [];
-    this.pans = Array.from(new Set(this.samples.map(s => s.pan))).sort((a, b) => a - b);
-    this.tilts = Array.from(new Set(this.samples.map(s => s.tilt))).sort((a, b) => b - a);
-    this.sampleMap = new Map();
-    this.samples.forEach(s => this.sampleMap.set(`${s.pan},${s.tilt}`, s));
-    if (!this.graph) return;
-
-    const nodes = [];
-    this.tilts.forEach(tilt => {
-      this.pans.forEach(pan => {
-        const s = this.sampleMap.get(`${pan},${tilt}`);
-        if (!s) return;
-        const id = `p${pan.toFixed(1)}_t${tilt.toFixed(1)}`;
-        nodes.push({
-          id,
-          data: { sample: s },
-          style: {
-            src: `/api/sample-image?path=${encodeURIComponent(s.thumbnail)}`,
-            labelText: `P:${pan.toFixed(0)} T:${tilt.toFixed(0)}`,
-          },
-        });
-      });
-    });
-
-    this.graph.setLayout({
-      type: 'grid',
-      cols: this.pans.length || 1,
-      nodeSize: [this.cellW, this.cellH + this.captionH],
-      preventOverlap: true,
-    });
-    this.graph.setData({ nodes, edges: [] });
-    await this.graph.render();
-    if (nodes.length > 0) await this.graph.fitView();
-    this.selectedNodeId = null;
-    this.selectedPan = null;
-    this.selectedTilt = null;
-    this.scale = this.graph.getZoom();
-    this.updateZoomLabel();
-  }
-
-  setSelected(pan, tilt) {
-    if (!this.graph) return;
-    const prevId = this.selectedNodeId;
-    this.selectedPan = pan;
-    this.selectedTilt = tilt;
-    this.selectedNodeId = pan !== null && tilt !== null
-      ? `p${pan.toFixed(1)}_t${tilt.toFixed(1)}`
-      : null;
-
-    const states = {};
-    if (prevId && prevId !== this.selectedNodeId) states[prevId] = [];
-    if (this.selectedNodeId) states[this.selectedNodeId] = ['selected'];
-    if (Object.keys(states).length > 0) {
-      this.graph.setElementState(states, false);
-    }
-  }
-
-  async resetView() {
-    if (!this.graph) return;
-    await this.graph.fitView();
-    this.scale = this.graph.getZoom();
-    this.updateZoomLabel();
-  }
-
-  updateZoomLabel() {
-    const label = document.getElementById('zoom-level');
-    if (label) label.textContent = `${Math.round(this.scale * 100)}%`;
-  }
-
-  draw() {
-    if (!this.graph) return;
-    this.graph.zoomTo(this.scale);
-  }
-}
-
-let sampleCanvas = null;
-
-async function loadSamples(groupId) {
-  try {
-    const data = await API.get(`/api/samples/${groupId}`);
-    if (!sampleCanvas) {
-      sampleCanvas = new SampleCanvas('sample-canvas', 'sample-grid-wrapper');
-    }
-    await sampleCanvas.setSamples(data.samples || []);
-  } catch (e) {
-    log(`加载扫描样本失败: ${e.message}`);
-  }
-}
-
-async function loadGroups() {
-  try {
-    const status = await API.get('/api/status');
-    const select = document.getElementById('group-select');
-    select.innerHTML = '';
-    const gids = Object.keys(status.groups || {});
-    gids.forEach(gid => {
-      const opt = document.createElement('option');
-      opt.value = gid;
-      opt.textContent = gid;
-      select.appendChild(opt);
-    });
-    if (gids.length === 0) {
-      currentGroup = null;
-      setStatus('未配置摄像头组');
-      setControlsDisabled(true);
-      return;
-    }
-    setControlsDisabled(false);
-    if (gids.includes(currentGroup)) {
-      select.value = currentGroup;
-    } else {
-      currentGroup = select.options[0].value;
-      onGroupChange();
-    }
-  } catch (e) {
-    log(`获取状态失败: ${e.message}`);
-  }
-}
-
-function resetSampleZoom() {
-  if (sampleCanvas) sampleCanvas.resetView();
-}
-
-function onGroupChange() {
-  currentGroup = document.getElementById('group-select').value;
-  selectedSampleEl = null;
-  tempPreview = null;
-  setSelectedPreview(null);
-  resetSampleZoom();
-  loadSamples(currentGroup);
-  renderVideos(currentGroup);
-  loadPoints(currentGroup);
-}
-
-function renderVideos(groupId) {
-  const grid = document.getElementById('video-grid');
-  grid.innerHTML = '';
-  ['panorama', 'ptz'].forEach(cam => {
-    const box = document.createElement('div');
-    box.className = 'video-box';
-
-    const title = document.createElement('div');
-    title.className = 'title';
-    title.textContent = `${cam} - ${groupId}`;
-
-    const img = document.createElement('img');
-    img.src = `/api/live/${cam}/${groupId}?marked=1&t=${Date.now()}`;
-    img.alt = cam;
-
-    box.appendChild(title);
-    box.appendChild(img);
-    grid.appendChild(box);
-  });
-}
-
-async function loadPoints(groupId) {
-  try {
-    const data = await API.get(`/api/points/${groupId}`);
-    const ul = document.getElementById('points');
-    ul.innerHTML = '';
-    data.points.forEach(p => {
-      const li = document.createElement('li');
-
-      const span = document.createElement('span');
-      span.textContent = `P:${p.pan.toFixed(0)} T:${p.tilt.toFixed(0)}`;
-
-      const previewBtn = document.createElement('button');
-      previewBtn.textContent = '预览';
-      previewBtn.onclick = async () => {
-        document.getElementById('inp-pan').value = p.pan.toFixed(2);
-        document.getElementById('inp-tilt').value = p.tilt.toFixed(2);
-        document.getElementById('inp-zoom').value = p.zoom;
-        await runPreview(groupId, p.pan, p.tilt, p.zoom, p.id);
-      };
-
-      const delBtn = document.createElement('button');
-      delBtn.dataset.id = String(p.id);
-      delBtn.textContent = '删除';
-      delBtn.onclick = async () => {
-        if (!currentGroup) {
-          log('未选择摄像头组');
-          return;
-        }
-        if (!confirm('确定删除该扫描点?')) return;
-        delBtn.disabled = true;
-        try {
-          await API.del(`/api/points/${groupId}/${p.id}`);
-          loadPoints(groupId);
-        } catch (e) {
-          log(`删除失败: ${e.message}`);
-          delBtn.disabled = false;
-        }
-      };
-
-      li.appendChild(span);
-      li.appendChild(previewBtn);
-      li.appendChild(delBtn);
-      ul.appendChild(li);
-    });
-  } catch (e) {
-    log(`加载扫描点失败: ${e.message}`);
-  }
-}
-
-async function updateStatus() {
-  if (!currentGroup) return;
-  try {
-    const status = await API.get('/api/status');
-    const g = status.groups[currentGroup];
-    if (g) {
-      setStatus(g.polling_state);
-    }
-  } catch (e) {
-    // ignore
-  }
-}
-
-document.getElementById('group-select').addEventListener('change', onGroupChange);
-
-document.getElementById('btn-scan').addEventListener('click', async () => {
-  if (!currentGroup) {
-    log('未选择摄像头组');
-    return;
-  }
-  if (scanPollInterval) return;
-  const scannedGroup = currentGroup;
-  const scanBtn = document.getElementById('btn-scan');
-  scanBtn.disabled = true;
-  setStatus('扫描中...');
-  try {
-    await API.post(`/api/scan/${scannedGroup}`);
-    log(`开始扫描: ${scannedGroup}`);
-    scanPollInterval = setInterval(async () => {
-      try {
-        const prog = await API.get(`/api/scan/${scannedGroup}/progress`);
-        const progress = prog.total > 0 ? (prog.current / prog.total) * 100 : 0;
-        if (prog.state === 'done' || prog.state === 'failed' || progress >= 100) {
-          clearInterval(scanPollInterval);
-          scanPollInterval = null;
-          if (!controlsGloballyDisabled) scanBtn.disabled = false;
-          if (prog.state === 'done') {
-            log('扫描完成');
-            resetSampleZoom();
-            loadSamples(scannedGroup);
-            loadPoints(scannedGroup);
-          } else if (prog.state === 'failed') {
-            log(`扫描失败: ${prog.error || 'unknown'}`);
-          }
-        } else {
-          setStatus(`扫描中... ${progress.toFixed(0)}%`);
-        }
-      } catch (e) {
-        clearInterval(scanPollInterval);
-        scanPollInterval = null;
-        if (!controlsGloballyDisabled) scanBtn.disabled = false;
-        setStatus('扫描失败');
-        log(`扫描进度获取失败: ${e.message}`);
-      }
-    }, 1000);
-  } catch (e) {
-    log(`扫描失败: ${e.message}`);
-    if (!controlsGloballyDisabled) scanBtn.disabled = false;
-    setStatus('扫描失败');
-  }
-});
-
-document.getElementById('btn-poll-start').addEventListener('click', async () => {
-  if (!currentGroup) {
-    log('未选择摄像头组');
-    return;
-  }
-  await withDisabled('btn-poll-start', async () => {
-    try {
-      await API.post(`/api/poll/${currentGroup}/start`);
-      log(`开始轮询: ${currentGroup}`);
-    } catch (e) {
-      log(`轮询启动失败: ${e.message}`);
-    }
-  });
-});
-
-document.getElementById('btn-poll-stop').addEventListener('click', async () => {
-  if (!currentGroup) {
-    log('未选择摄像头组');
-    return;
-  }
-  await withDisabled('btn-poll-stop', async () => {
-    try {
-      await API.post(`/api/poll/${currentGroup}/stop`);
-      log(`停止轮询: ${currentGroup}`);
-    } catch (e) {
-      log(`停止失败: ${e.message}`);
-    }
-  });
-});
-
-async function runPreview(groupId, pan, tilt, zoom, pointId = null) {
-  const payload = { pan, tilt, zoom };
-  const result = await API.post(`/api/preview/${groupId}`, payload);
-  log(`预览位置: P=${pan.toFixed(1)} T=${tilt.toFixed(1)} Z=${zoom}`);
-  if (result.snapshot_url) {
-    log(`预览抓拍已保存: ${result.snapshot_path}`);
-    // 预览抓拍显示在右侧,但不更新保存点图片
-    setSelectedPreview(result.snapshot_url);
-    tempPreview = { path: result.snapshot_path };
-  }
-  return result;
-}
-
-document.getElementById('btn-preview').addEventListener('click', async () => {
-  if (!currentGroup) {
-    log('未选择摄像头组');
-    return;
-  }
-  let pan, tilt, zoom;
-  try {
-    pan = parseStrictFloat(document.getElementById('inp-pan').value, 'pan');
-    tilt = parseStrictFloat(document.getElementById('inp-tilt').value, 'tilt');
-    zoom = Number(document.getElementById('inp-zoom').value);
-  } catch (e) {
-    log(`错误:${e.message}`);
-    return;
-  }
-  if (!Number.isInteger(zoom) || zoom < 1) {
-    log('错误:zoom 必须是大于等于 1 的整数');
-    return;
-  }
-  await withDisabled('btn-preview', async () => {
-    try {
-      await runPreview(currentGroup, pan, tilt, zoom);
-    } catch (e) {
-      log(`预览失败: ${e.message}`);
-    }
-  });
-});
-
-document.getElementById('btn-add').addEventListener('click', async () => {
-  if (!currentGroup) {
-    log('未选择摄像头组');
-    return;
-  }
-  await withDisabled('btn-add', async () => {
-    let pan, tilt, zoom, dwellTime;
-    try {
-      pan = parseStrictFloat(document.getElementById('inp-pan').value, 'pan');
-      tilt = parseStrictFloat(document.getElementById('inp-tilt').value, 'tilt');
-      zoom = Number(document.getElementById('inp-zoom').value);
-      dwellTime = parseStrictFloat(document.getElementById('inp-dwell').value, '停留时间');
-    } catch (e) {
-      log(`错误:${e.message}`);
-      return;
-    }
-
-    if (pan < 0 || pan > 360) {
-      log('错误:pan 必须是 0-360 之间的有限数值');
-      return;
-    }
-    if (tilt < -90 || tilt > 90) {
-      log('错误:tilt 必须是 -90-90 之间的有限数值');
-      return;
-    }
-    if (!Number.isInteger(zoom) || zoom < 1) {
-      log('错误:zoom 必须是大于等于 1 的整数');
-      return;
-    }
-    if (dwellTime <= 0) {
-      log('错误:停留时间必须是大于 0 的有限数值');
-      return;
-    }
-
-    const payload = { pan, tilt, zoom, dwell_time: dwellTime };
-    if (tempPreview) {
-      payload.preview_image = tempPreview.path;
-    }
-    try {
-      await API.post(`/api/points/${currentGroup}`, payload);
-      log('扫描点已保存');
-      selectedSampleEl = null;
-      tempPreview = null;
-      setSelectedPreview(null);
-      loadPoints(currentGroup);
-    } catch (e) {
-      log(`保存失败: ${e.message}`);
-    }
-  });
-});
-
-document.getElementById('btn-zoom-in').addEventListener('click', () => {
-  if (sampleCanvas) {
-    sampleCanvas.scale = Math.min(5.0, sampleCanvas.scale * 1.2);
-    sampleCanvas.updateZoomLabel();
-    sampleCanvas.draw();
-  }
-});
-
-document.getElementById('btn-zoom-out').addEventListener('click', () => {
-  if (sampleCanvas) {
-    sampleCanvas.scale = Math.max(0.1, sampleCanvas.scale / 1.2);
-    sampleCanvas.updateZoomLabel();
-    sampleCanvas.draw();
-  }
-});
-
-document.getElementById('btn-zoom-reset').addEventListener('click', () => {
-  resetSampleZoom();
-});
-
-setControlsDisabled(true);
-loadGroups();
-setInterval(updateStatus, 2000);

+ 0 - 52
dual_camera_system/web_static/index.html

@@ -1,52 +0,0 @@
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>PTZ 扫描轮询控制台</title>
-    <link rel="stylesheet" href="/static/style.css">
-</head>
-<body>
-    <div id="toolbar">
-        <select id="group-select"></select>
-        <button id="btn-scan">执行 360° 扫描</button>
-        <button id="btn-poll-start">开始轮询</button>
-        <button id="btn-poll-stop">停止轮询</button>
-        <span id="status">状态:空闲</span>
-    </div>
-    <div id="video-grid"></div>
-    <div id="panorama-panel">
-        <div>
-          <div id="sample-header">
-            扫描样本矩阵(同列同 P,同行同 T)
-            <span id="zoom-controls">
-              <button id="btn-zoom-out">-</button>
-              <span id="zoom-level">100%</span>
-              <button id="btn-zoom-in">+</button>
-              <button id="btn-zoom-reset">重置</button>
-            </span>
-          </div>
-          <div id="sample-grid-wrapper">
-            <div id="sample-canvas"></div>
-          </div>
-        </div>
-        <div id="point-list">
-            <h3>扫描点设置</h3>
-            <img id="selected-preview" src="" alt="选中/预览" style="width:100%;max-height:140px;object-fit:cover;background:#000;border-radius:6px;margin-bottom:10px;display:none;">
-            <div id="point-form">
-                <label>Pan: <input id="inp-pan" type="number" step="0.1"></label>
-                <label>Tilt: <input id="inp-tilt" type="number" step="0.1"></label>
-                <label>Zoom: <input id="inp-zoom" type="number" value="1"></label>
-                <label>停留(s): <input id="inp-dwell" type="number" value="3" step="0.1"></label>
-                <button id="btn-preview">球机预览</button>
-                <button id="btn-add">保存扫描点</button>
-            </div>
-            <h4 style="margin:10px 0 6px 0;font-size:13px;color:#94a3b8;">已选扫描点</h4>
-            <ul id="points"></ul>
-        </div>
-    </div>
-    <div id="log-panel"></div>
-    <script src="https://cdn.jsdelivr.net/npm/@antv/g6@5.1.1/dist/g6.min.js"></script>
-    <script type="module" src="/static/app.js?v=4"></script>
-</body>
-</html>

+ 0 - 29
dual_camera_system/web_static/style.css

@@ -1,29 +0,0 @@
-html, body { height: 100%; margin: 0; }
-body { display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
-#toolbar { padding: 10px 16px; background: #1e293b; display: flex; gap: 12px; align-items: center; border-bottom: 1px solid #334155; }
-#toolbar button { padding: 6px 12px; border: none; border-radius: 4px; background: #3b82f6; color: #fff; cursor: pointer; }
-#toolbar button:hover { background: #2563eb; }
-#toolbar button:disabled { background: #64748b; cursor: not-allowed; }
-#status { margin-left: auto; color: #94a3b8; }
-#video-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; padding: 12px; max-height: 220px; overflow: hidden; }
-.video-box { background: #1e293b; border-radius: 8px; overflow: hidden; }
-.video-box .title { padding: 8px 12px; font-size: 13px; color: #cbd5e1; }
-.video-box img { width: 100%; display: block; background: #000; }
-#panorama-panel { flex: 1; min-height: 0; display: grid; grid-template-columns: 1fr 280px; grid-template-rows: minmax(0, 1fr); gap: 12px; padding: 12px; overflow: hidden; }
-#sample-header { padding: 8px 12px; background: #1e293b; border-radius: 8px 8px 0 0; font-size: 14px; color: #cbd5e1; display: flex; justify-content: space-between; align-items: center; }
-#zoom-controls { display: flex; align-items: center; gap: 6px; }
-#zoom-controls button { padding: 2px 8px; font-size: 13px; background: #334155; border: none; border-radius: 4px; color: #fff; cursor: pointer; }
-#zoom-controls button:hover { background: #475569; }
-#sample-grid-wrapper { height: 100%; background: #111; border-radius: 0 0 8px 8px; overflow: hidden; position: relative; cursor: grab; }
-#sample-grid-wrapper:active { cursor: grabbing; }
-#sample-canvas { display: block; width: 100%; height: 100%; }
-#point-list { background: #1e293b; border-radius: 8px; padding: 12px; height: 100%; overflow-y: auto; }
-#point-list h3 { margin-top: 0; }
-#point-list label { display: block; margin-bottom: 8px; font-size: 13px; }
-#point-list input { width: 80px; padding: 4px; border-radius: 4px; border: 1px solid #475569; background: #0f172a; color: #fff; }
-#point-list button { margin-top: 8px; margin-right: 8px; padding: 6px 10px; border: none; border-radius: 4px; background: #22c55e; color: #fff; cursor: pointer; }
-button:disabled { background: #64748b; cursor: not-allowed; }
-#points { list-style: none; padding: 0; margin: 0; }
-#point-list h4 { margin: 10px 0 6px 0; font-size: 13px; color: #94a3b8; }
-#points li { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #334155; font-size: 13px; }
-#log-panel { padding: 12px; background: #1e293b; font-family: monospace; font-size: 12px; max-height: 160px; overflow: auto; border-top: 1px solid #334155; }