ROI选区01
This commit is contained in:
16
api/roi.py
16
api/roi.py
@@ -14,11 +14,18 @@ from db.crud import (
|
|||||||
)
|
)
|
||||||
from db.models import get_db
|
from db.models import get_db
|
||||||
from inference.pipeline import get_pipeline
|
from inference.pipeline import get_pipeline
|
||||||
|
from inference.roi.cache_manager import get_roi_cache
|
||||||
from inference.roi.roi_filter import ROIFilter
|
from inference.roi.roi_filter import ROIFilter
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/camera", tags=["ROI管理"])
|
router = APIRouter(prefix="/api/camera", tags=["ROI管理"])
|
||||||
|
|
||||||
|
|
||||||
|
def _invalidate_roi_cache(camera_id: int):
|
||||||
|
pipeline = get_pipeline()
|
||||||
|
pipeline.roi_filter.clear_cache(camera_id)
|
||||||
|
get_roi_cache().invalidate(camera_id)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{camera_id}/rois", response_model=List[dict])
|
@router.get("/{camera_id}/rois", response_model=List[dict])
|
||||||
def list_rois(camera_id: int, db: Session = Depends(get_db)):
|
def list_rois(camera_id: int, db: Session = Depends(get_db)):
|
||||||
roi_configs = get_all_rois(db, camera_id)
|
roi_configs = get_all_rois(db, camera_id)
|
||||||
@@ -92,8 +99,7 @@ def add_roi(
|
|||||||
return_sec=return_sec,
|
return_sec=return_sec,
|
||||||
)
|
)
|
||||||
|
|
||||||
pipeline = get_pipeline()
|
_invalidate_roi_cache(camera_id)
|
||||||
pipeline.roi_filter.update_cache(camera_id, get_roi_points(db, camera_id))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": roi.id,
|
"id": roi.id,
|
||||||
@@ -137,8 +143,7 @@ def modify_roi(
|
|||||||
if not roi:
|
if not roi:
|
||||||
raise HTTPException(status_code=404, detail="ROI不存在")
|
raise HTTPException(status_code=404, detail="ROI不存在")
|
||||||
|
|
||||||
pipeline = get_pipeline()
|
_invalidate_roi_cache(camera_id)
|
||||||
pipeline.roi_filter.update_cache(camera_id, get_roi_points(db, camera_id))
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": roi.id,
|
"id": roi.id,
|
||||||
@@ -156,8 +161,7 @@ def remove_roi(camera_id: int, roi_id: int, db: Session = Depends(get_db)):
|
|||||||
if not delete_roi(db, roi_id):
|
if not delete_roi(db, roi_id):
|
||||||
raise HTTPException(status_code=404, detail="ROI不存在")
|
raise HTTPException(status_code=404, detail="ROI不存在")
|
||||||
|
|
||||||
pipeline = get_pipeline()
|
_invalidate_roi_cache(camera_id)
|
||||||
pipeline.roi_filter.update_cache(camera_id, get_roi_points(db, camera_id))
|
|
||||||
|
|
||||||
return {"message": "删除成功"}
|
return {"message": "删除成功"}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Layout, Menu, theme } from 'antd';
|
import { Layout, Menu, theme } from 'antd';
|
||||||
import {
|
import {
|
||||||
VideoCameraOutlined,
|
VideoCameraOutlined,
|
||||||
@@ -18,6 +18,7 @@ const App: React.FC = () => {
|
|||||||
} = theme.useToken();
|
} = theme.useToken();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
@@ -49,18 +50,36 @@ const App: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
<Sider collapsible theme="dark">
|
<Sider
|
||||||
|
collapsible
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapse={setCollapsed}
|
||||||
|
theme="dark"
|
||||||
|
style={{
|
||||||
|
zIndex: 100,
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
overflow: 'auto',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
width={200}
|
||||||
|
collapsedWidth={80}
|
||||||
|
>
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 32,
|
height: 64,
|
||||||
margin: 16,
|
|
||||||
background: 'rgba(255, 255, 255, 0.2)',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
|
fontSize: 16,
|
||||||
|
background: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
}}>
|
}}>
|
||||||
安保监控系统
|
{collapsed ? '' : '安保监控系统'}
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<Menu
|
||||||
theme="dark"
|
theme="dark"
|
||||||
@@ -71,23 +90,31 @@ const App: React.FC = () => {
|
|||||||
onClick={({ key }) => navigate(key)}
|
onClick={({ key }) => navigate(key)}
|
||||||
/>
|
/>
|
||||||
</Sider>
|
</Sider>
|
||||||
<Layout>
|
<Layout style={{ marginLeft: collapsed ? 80 : 200, transition: 'margin-left 0.2s' }}>
|
||||||
<Header style={{ padding: 0, background: colorBgContainer }}>
|
<Header style={{
|
||||||
<div style={{ paddingLeft: 24, fontSize: 18, fontWeight: 'bold' }}>
|
padding: '0 24px',
|
||||||
|
background: colorBgContainer,
|
||||||
|
marginLeft: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'sticky',
|
||||||
|
top: 0,
|
||||||
|
zIndex: 99,
|
||||||
|
boxShadow: '0 1px 4px rgba(0,0,0,0.08)',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: 18, fontWeight: 'bold', color: '#333' }}>
|
||||||
安保异常行为识别系统 v1.0
|
安保异常行为识别系统 v1.0
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
<Content style={{ margin: '16px' }}>
|
<Content style={{
|
||||||
<div
|
margin: 24,
|
||||||
style={{
|
|
||||||
padding: 24,
|
padding: 24,
|
||||||
minHeight: 360,
|
minHeight: 360,
|
||||||
background: colorBgContainer,
|
background: colorBgContainer,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
}}
|
overflow: 'auto',
|
||||||
>
|
}}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import App from './App';
|
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import CameraManagement from './pages/CameraManagement';
|
import CameraManagement from './pages/CameraManagement';
|
||||||
import ROIEditor from './pages/ROIEditor';
|
import ROIEditor from './pages/ROIEditor';
|
||||||
import AlertCenter from './pages/AlertCenter';
|
import AlertCenter from './pages/AlertCenter';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
const Settings: React.FC = () => (
|
const Settings: React.FC = () => (
|
||||||
<div>
|
<div>
|
||||||
@@ -16,15 +16,16 @@ const Settings: React.FC = () => (
|
|||||||
const AppWrapper: React.FC = () => {
|
const AppWrapper: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<App>
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<App />}>
|
||||||
<Route path="/cameras" element={<CameraManagement />} />
|
<Route index element={<Dashboard />} />
|
||||||
<Route path="/rois" element={<ROIEditor />} />
|
<Route path="cameras" element={<CameraManagement />} />
|
||||||
<Route path="/alerts" element={<AlertCenter />} />
|
<Route path="rois" element={<ROIEditor />} />
|
||||||
<Route path="/settings" element={<Settings />} />
|
<Route path="alerts" element={<AlertCenter />} />
|
||||||
|
<Route path="settings" element={<Settings />} />
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</App>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,34 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout-sider {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-layout {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dropdown,
|
||||||
|
.ant-select-dropdown,
|
||||||
|
.ant-picker-dropdown,
|
||||||
|
.ant-popover {
|
||||||
|
z-index: 1050 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-message {
|
||||||
|
z-index: 1060;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-notification {
|
||||||
|
z-index: 1070;
|
||||||
}
|
}
|
||||||
|
|
||||||
.camera-grid {
|
.camera-grid {
|
||||||
|
|||||||
14
frontend/src/main.tsx
Normal file
14
frontend/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { ConfigProvider } from 'antd'
|
||||||
|
import AppWrapper from './AppWrapper'
|
||||||
|
import './index.css'
|
||||||
|
import zhCN from 'antd/locale/zh_CN'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ConfigProvider locale={zhCN}>
|
||||||
|
<AppWrapper />
|
||||||
|
</ConfigProvider>
|
||||||
|
</React.StrictMode>,
|
||||||
|
)
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useMemo } from 'react';
|
||||||
import { Table, Button, Modal, Form, Input, InputNumber, Switch, Space, Tag, message } from 'antd';
|
import {
|
||||||
import { PlusOutlined, DeleteOutlined, EditOutlined, PlayCircleOutlined, PauseCircleOutlined } from '@ant-design/icons';
|
Table, Button, Modal, Form, Input, InputNumber, Switch, Space, Tag, message,
|
||||||
|
Popconfirm, Tooltip, Badge
|
||||||
|
} from 'antd';
|
||||||
|
import {
|
||||||
|
PlusOutlined, DeleteOutlined, EditOutlined, PlayCircleOutlined,
|
||||||
|
PauseCircleOutlined, SearchOutlined, SyncOutlined, CameraOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
interface Camera {
|
interface Camera {
|
||||||
@@ -10,20 +16,34 @@ interface Camera {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
fps_limit: number;
|
fps_limit: number;
|
||||||
process_every_n_frames: number;
|
process_every_n_frames: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CameraStatus {
|
||||||
|
is_running: boolean;
|
||||||
|
fps: number;
|
||||||
|
error_message: string | null;
|
||||||
|
last_check_time: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CameraManagement: React.FC = () => {
|
const CameraManagement: React.FC = () => {
|
||||||
const [cameras, setCameras] = useState<Camera[]>([]);
|
const [cameras, setCameras] = useState<Camera[]>([]);
|
||||||
|
const [cameraStatus, setCameraStatus] = useState<Record<number, CameraStatus>>({});
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const [editingCamera, setEditingCamera] = useState<Camera | null>(null);
|
const [editingCamera, setEditingCamera] = useState<Camera | null>(null);
|
||||||
|
const [searchText, setSearchText] = useState('');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const fetchCameras = async () => {
|
const fetchCameras = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('/api/cameras?enabled_only=false');
|
const [camerasRes, statusRes] = await Promise.all([
|
||||||
setCameras(res.data);
|
axios.get('/api/cameras?enabled_only=false'),
|
||||||
|
axios.get('/api/pipeline/status')
|
||||||
|
]);
|
||||||
|
setCameras(camerasRes.data);
|
||||||
|
setCameraStatus(statusRes.data.cameras || {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error('获取摄像头列表失败');
|
message.error('获取摄像头列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -33,11 +53,50 @@ const CameraManagement: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCameras();
|
fetchCameras();
|
||||||
|
const interval = setInterval(fetchCameras, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const extractIP = (url: string): string => {
|
||||||
|
try {
|
||||||
|
const match = url.match(/:\/\/([^:]+):?(\d+)?\//);
|
||||||
|
return match ? match[1] : '未知';
|
||||||
|
} catch {
|
||||||
|
return '未知';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusInfo = (camera: Camera) => {
|
||||||
|
const status = cameraStatus[camera.id];
|
||||||
|
if (!camera.enabled) {
|
||||||
|
return { color: '#8c8c8f', text: '已停用', status: 'default' };
|
||||||
|
}
|
||||||
|
if (!status) {
|
||||||
|
return { color: '#fa8c16', text: '加载中...', status: 'processing' };
|
||||||
|
}
|
||||||
|
if (status.is_running && status.fps > 0) {
|
||||||
|
return { color: '#52c41a', text: `运行中 (${status.fps.toFixed(1)} FPS)`, status: 'success' };
|
||||||
|
}
|
||||||
|
if (status.error_message) {
|
||||||
|
return { color: '#f5222d', text: `连接失败`, status: 'error' };
|
||||||
|
}
|
||||||
|
return { color: '#fa8c16', text: '初始化中...', status: 'processing' };
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredCameras = useMemo(() => {
|
||||||
|
if (!searchText) return cameras;
|
||||||
|
const lowerSearch = searchText.toLowerCase();
|
||||||
|
return cameras.filter(cam =>
|
||||||
|
cam.name.toLowerCase().includes(lowerSearch) ||
|
||||||
|
cam.rtsp_url.toLowerCase().includes(lowerSearch) ||
|
||||||
|
extractIP(cam.rtsp_url).toLowerCase().includes(lowerSearch)
|
||||||
|
);
|
||||||
|
}, [cameras, searchText]);
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setEditingCamera(null);
|
setEditingCamera(null);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
form.setFieldsValue({ fps_limit: 30, process_every_n_frames: 3, enabled: true });
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,51 +147,114 @@ const CameraManagement: React.FC = () => {
|
|||||||
title: 'ID',
|
title: 'ID',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
key: 'id',
|
key: 'id',
|
||||||
width: 60,
|
width: 70,
|
||||||
|
sorter: (a: Camera, b: Camera) => a.id - b.id,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
|
sorter: (a: Camera, b: Camera) => a.name.localeCompare(b.name),
|
||||||
|
render: (name: string, record: Camera) => (
|
||||||
|
<Space direction="vertical" size={0}>
|
||||||
|
<span style={{ fontWeight: 500 }}>{name}</span>
|
||||||
|
<Tag icon={<CameraOutlined />} style={{ fontSize: 12 }}>
|
||||||
|
#{record.id}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'RTSP地址',
|
title: 'IP地址',
|
||||||
dataIndex: 'rtsp_url',
|
key: 'ip',
|
||||||
key: 'rtsp_url',
|
width: 150,
|
||||||
ellipsis: true,
|
render: (_: any, record: Camera) => {
|
||||||
|
const ip = extractIP(record.rtsp_url);
|
||||||
|
return (
|
||||||
|
<Tooltip title={record.rtsp_url}>
|
||||||
|
<code style={{ fontSize: 12, color: '#666' }}>{ip}</code>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
|
key: 'status',
|
||||||
|
width: 160,
|
||||||
|
sorter: (a: Camera, b: Camera) => {
|
||||||
|
const statusA = getStatusInfo(a).status;
|
||||||
|
const statusB = getStatusInfo(b).status;
|
||||||
|
return statusA.localeCompare(statusB);
|
||||||
|
},
|
||||||
|
render: (_: any, record: Camera) => {
|
||||||
|
const { color, text, status } = getStatusInfo(record);
|
||||||
|
return (
|
||||||
|
<Badge status={status as any} text={<span style={{ color }}>{text}</span>} />
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '启用',
|
||||||
dataIndex: 'enabled',
|
dataIndex: 'enabled',
|
||||||
key: 'enabled',
|
key: 'enabled',
|
||||||
render: (enabled: boolean) => (
|
width: 80,
|
||||||
<Tag color={enabled ? 'green' : 'red'}>
|
render: (enabled: boolean, record: Camera) => (
|
||||||
{enabled ? '启用' : '停用'}
|
<Switch
|
||||||
</Tag>
|
size="small"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={() => handleToggle(record)}
|
||||||
|
checkedChildren="启用"
|
||||||
|
unCheckedChildren="停用"
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'FPS限制',
|
title: 'FPS限制',
|
||||||
dataIndex: 'fps_limit',
|
dataIndex: 'fps_limit',
|
||||||
key: 'fps_limit',
|
key: 'fps_limit',
|
||||||
|
width: 100,
|
||||||
|
sorter: (a: Camera, b: Camera) => a.fps_limit - b.fps_limit,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '处理间隔',
|
title: '处理间隔',
|
||||||
dataIndex: 'process_every_n_frames',
|
dataIndex: 'process_every_n_frames',
|
||||||
key: 'process_every_n_frames',
|
key: 'process_every_n_frames',
|
||||||
|
width: 100,
|
||||||
|
render: (n: number) => `每 ${n} 帧`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
|
width: 150,
|
||||||
render: (_: any, record: Camera) => (
|
render: (_: any, record: Camera) => (
|
||||||
<Space>
|
<Space size="small">
|
||||||
|
<Tooltip title={record.enabled ? '停用' : '启用'}>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={record.enabled ? <PauseCircleOutlined /> : <PlayCircleOutlined />}
|
size="small"
|
||||||
|
icon={record.enabled ? <PauseCircleOutlined style={{ color: '#faad14' }} /> : <PlayCircleOutlined style={{ color: '#52c41a' }} />}
|
||||||
onClick={() => handleToggle(record)}
|
onClick={() => handleToggle(record)}
|
||||||
/>
|
/>
|
||||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
|
</Tooltip>
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} />
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定删除此摄像头?"
|
||||||
|
onConfirm={() => handleDelete(record.id)}
|
||||||
|
okText="删除"
|
||||||
|
cancelText="取消"
|
||||||
|
okButtonProps={{ danger: true }}
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -140,35 +262,102 @@ const CameraManagement: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16, gap: 16, flexWrap: 'wrap' }}>
|
||||||
|
<Space>
|
||||||
|
<Input
|
||||||
|
placeholder="搜索摄像头名称、IP或RTSP地址"
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
style={{ width: 300 }}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
|
<Tooltip title="刷新列表">
|
||||||
|
<Button icon={<SyncOutlined spin={loading} />} onClick={fetchCameras} loading={loading} />
|
||||||
|
</Tooltip>
|
||||||
|
</Space>
|
||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||||
添加摄像头
|
新增摄像头
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={cameras}
|
dataSource={filteredCameras}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
pageSize: 10,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`,
|
||||||
|
}}
|
||||||
|
scroll={{ x: 1200 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={editingCamera ? '编辑摄像头' : '添加摄像头'}
|
title={
|
||||||
|
<Space>
|
||||||
|
{editingCamera ? <EditOutlined /> : <PlusOutlined />}
|
||||||
|
{editingCamera ? '编辑摄像头' : '新增摄像头'}
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
onCancel={() => setModalVisible(false)}
|
onCancel={() => setModalVisible(false)}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
|
width={500}
|
||||||
|
okText="保存"
|
||||||
|
cancelText="取消"
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
<Form.Item
|
||||||
<Input />
|
name="name"
|
||||||
|
label="摄像头名称"
|
||||||
|
rules={[{ required: true, message: '请输入摄像头名称' }]}
|
||||||
|
tooltip="给摄像头起一个易于识别的名称"
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:办公楼入口摄像头" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="rtsp_url" label="RTSP地址" rules={[{ required: true }]}>
|
|
||||||
<Input.TextArea rows={2} />
|
<Form.Item
|
||||||
|
name="rtsp_url"
|
||||||
|
label="RTSP地址"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入RTSP地址' },
|
||||||
|
{ type: 'url', message: '请输入有效的URL地址' }
|
||||||
|
]}
|
||||||
|
tooltip="RTSP流地址,格式如:rtsp://admin:password@192.168.1.100:554/stream"
|
||||||
|
>
|
||||||
|
<Input.TextArea rows={3} placeholder="rtsp://admin:password@IP地址:554/通道号" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="fps_limit" label="FPS限制" rules={[{ required: true }]}>
|
|
||||||
<InputNumber min={1} max={60} style={{ width: '100%' }} />
|
<Space style={{ width: '100%' }} size="large">
|
||||||
|
<Form.Item
|
||||||
|
name="fps_limit"
|
||||||
|
label="FPS限制"
|
||||||
|
tooltip="摄像头最大处理帧率"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} max={60} style={{ width: '100%' }} addonAfter="帧/秒" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="process_every_n_frames" label="处理间隔帧数" rules={[{ required: true }]}>
|
|
||||||
<InputNumber min={1} max={30} style={{ width: '100%' }} />
|
<Form.Item
|
||||||
|
name="process_every_n_frames"
|
||||||
|
label="处理间隔"
|
||||||
|
tooltip="每N帧处理一次,用于降低CPU负载"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} max={30} style={{ width: '100%' }} addonAfter="帧" />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="enabled"
|
||||||
|
label="启用状态"
|
||||||
|
valuePropName="checked"
|
||||||
|
tooltip="关闭后将停止该摄像头的检测任务"
|
||||||
|
>
|
||||||
|
<Switch checkedChildren="启用" unCheckedChildren="停用" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
304
frontend/src/pages/ROIEditor.tsx
Normal file
304
frontend/src/pages/ROIEditor.tsx
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Card, Button, Space, Select, message, Modal, Form, Input, InputNumber, Drawer } from 'antd';
|
||||||
|
import { Stage, Layer, Rect, Line, Circle, Text as KonvaText } from 'react-konva';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
interface ROI {
|
||||||
|
id: number;
|
||||||
|
roi_id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
points: number[][];
|
||||||
|
rule: string;
|
||||||
|
direction: string | null;
|
||||||
|
enabled: boolean;
|
||||||
|
threshold_sec: number;
|
||||||
|
confirm_sec: number;
|
||||||
|
return_sec: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Camera {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROIEditor: React.FC = () => {
|
||||||
|
const [cameras, setCameras] = useState<Camera[]>([]);
|
||||||
|
const [selectedCamera, setSelectedCamera] = useState<number | null>(null);
|
||||||
|
const [rois, setRois] = useState<ROI[]>([]);
|
||||||
|
const [snapshot, setSnapshot] = useState<string>('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [imageDim, setImageDim] = useState({ width: 800, height: 600 });
|
||||||
|
const [selectedROI, setSelectedROI] = useState<ROI | null>(null);
|
||||||
|
const [drawerVisible, setDrawerVisible] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
const stageRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const fetchCameras = async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/cameras?enabled_only=true');
|
||||||
|
setCameras(res.data);
|
||||||
|
if (res.data.length > 0 && !selectedCamera) {
|
||||||
|
setSelectedCamera(res.data[0].id);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error('获取摄像头列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCameras();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedCamera) {
|
||||||
|
fetchSnapshot();
|
||||||
|
fetchROIs();
|
||||||
|
}
|
||||||
|
}, [selectedCamera]);
|
||||||
|
|
||||||
|
const fetchSnapshot = async () => {
|
||||||
|
if (!selectedCamera) return;
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`/api/camera/${selectedCamera}/snapshot/base64`);
|
||||||
|
setSnapshot(res.data.image);
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const maxWidth = 800;
|
||||||
|
const maxHeight = 600;
|
||||||
|
const scale = Math.min(maxWidth / img.width, maxHeight / img.height);
|
||||||
|
setImageDim({ width: img.width * scale, height: img.height * scale });
|
||||||
|
};
|
||||||
|
img.src = `data:image/jpeg;base64,${res.data.image}`;
|
||||||
|
} catch (err) {
|
||||||
|
message.error('获取截图失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchROIs = async () => {
|
||||||
|
if (!selectedCamera) return;
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`/api/camera/${selectedCamera}/rois`);
|
||||||
|
setRois(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
message.error('获取ROI列表失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveROI = async (values: any) => {
|
||||||
|
if (!selectedCamera || !selectedROI) return;
|
||||||
|
try {
|
||||||
|
await axios.put(`/api/camera/${selectedCamera}/roi/${selectedROI.id}`, values);
|
||||||
|
message.success('保存成功');
|
||||||
|
setDrawerVisible(false);
|
||||||
|
fetchROIs();
|
||||||
|
} catch (err) {
|
||||||
|
message.error('保存失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddROI = async () => {
|
||||||
|
if (!selectedCamera) return;
|
||||||
|
const roi_id = `roi_${Date.now()}`;
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/camera/${selectedCamera}/roi`, {
|
||||||
|
roi_id,
|
||||||
|
name: '新区域',
|
||||||
|
roi_type: 'polygon',
|
||||||
|
points: [[100, 100], [300, 100], [300, 300], [100, 300]],
|
||||||
|
rule_type: 'leave_post',
|
||||||
|
threshold_sec: 360,
|
||||||
|
confirm_sec: 30,
|
||||||
|
return_sec: 5,
|
||||||
|
});
|
||||||
|
message.success('添加成功');
|
||||||
|
fetchROIs();
|
||||||
|
} catch (err) {
|
||||||
|
message.error('添加失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteROI = async (roiId: number) => {
|
||||||
|
if (!selectedCamera) return;
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/camera/${selectedCamera}/roi/${roiId}`);
|
||||||
|
message.success('删除成功');
|
||||||
|
fetchROIs();
|
||||||
|
} catch (err) {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getROIStrokeColor = (rule: string) => {
|
||||||
|
return rule === 'intrusion' ? '#ff4d4f' : '#faad14';
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderROI = (roi: ROI) => {
|
||||||
|
const points = roi.points.flat();
|
||||||
|
const color = getROIStrokeColor(roi.rule);
|
||||||
|
|
||||||
|
if (roi.type === 'polygon') {
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={roi.id}
|
||||||
|
points={points}
|
||||||
|
closed
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={2}
|
||||||
|
fill={`${color}33`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedROI(roi);
|
||||||
|
form.setFieldsValue(roi);
|
||||||
|
setDrawerVisible(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (roi.type === 'line') {
|
||||||
|
return (
|
||||||
|
<Line
|
||||||
|
key={roi.id}
|
||||||
|
points={points}
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={3}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedROI(roi);
|
||||||
|
form.setFieldsValue(roi);
|
||||||
|
setDrawerVisible(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Card>
|
||||||
|
<Space style={{ marginBottom: 16 }}>
|
||||||
|
<Select
|
||||||
|
placeholder="选择摄像头"
|
||||||
|
value={selectedCamera}
|
||||||
|
onChange={setSelectedCamera}
|
||||||
|
style={{ width: 200 }}
|
||||||
|
options={cameras.map((c) => ({ label: c.name, value: c.id }))}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={fetchSnapshot}>
|
||||||
|
刷新截图
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleAddROI}>添加ROI</Button>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<div className="roi-editor-container" style={{ display: 'flex', gap: 16 }}>
|
||||||
|
<div style={{ flex: 1, background: '#f0f0f0', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
{snapshot ? (
|
||||||
|
<Stage width={imageDim.width} height={imageDim.height} ref={stageRef}>
|
||||||
|
<Layer>
|
||||||
|
<Rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={imageDim.width}
|
||||||
|
height={imageDim.height}
|
||||||
|
fillPatternImage={
|
||||||
|
(() => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = `data:image/jpeg;base64,${snapshot}`;
|
||||||
|
return img;
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
fillPatternOffset={{ x: 0, y: 0 }}
|
||||||
|
fillPatternScale={{ x: 1, y: 1 }}
|
||||||
|
/>
|
||||||
|
{rois.map(renderROI)}
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
) : (
|
||||||
|
<div>加载中...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{ width: 300 }}>
|
||||||
|
<Card title="ROI列表" size="small">
|
||||||
|
{rois.map((roi) => (
|
||||||
|
<div
|
||||||
|
key={roi.id}
|
||||||
|
style={{
|
||||||
|
padding: 8,
|
||||||
|
marginBottom: 8,
|
||||||
|
background: '#fafafa',
|
||||||
|
borderRadius: 4,
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: selectedROI?.id === roi.id ? '2px solid #1890ff' : '1px solid #d9d9d9',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedROI(roi);
|
||||||
|
form.setFieldsValue(roi);
|
||||||
|
setDrawerVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ fontWeight: 'bold' }}>{roi.name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: '#666' }}>
|
||||||
|
类型: {roi.type} | 规则: {roi.rule}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteROI(roi.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title="编辑ROI"
|
||||||
|
open={drawerVisible}
|
||||||
|
onClose={() => setDrawerVisible(false)}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical" onFinish={handleSaveROI}>
|
||||||
|
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="roi_type" label="类型">
|
||||||
|
<Select options={[{ label: '多边形', value: 'polygon' }, { label: '线段', value: 'line' }]} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="rule_type" label="规则">
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ label: '离岗检测', value: 'leave_post' },
|
||||||
|
{ label: '周界入侵', value: 'intrusion' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="threshold_sec" label="超时时间(秒)">
|
||||||
|
<InputNumber min={60} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="confirm_sec" label="确认时间(秒)">
|
||||||
|
<InputNumber min={5} style={{ width: '100%' }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
||||||
|
<input type="checkbox" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" htmlType="submit">
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => setDrawerVisible(false)}>取消</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Drawer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ROIEditor;
|
||||||
@@ -12,11 +12,11 @@ import numpy as np
|
|||||||
from config import get_config
|
from config import get_config
|
||||||
from db.crud import (
|
from db.crud import (
|
||||||
create_alarm,
|
create_alarm,
|
||||||
get_all_rois,
|
|
||||||
update_camera_status,
|
update_camera_status,
|
||||||
)
|
)
|
||||||
from db.models import init_db
|
from db.models import init_db, get_session_factory
|
||||||
from inference.engine import YOLOEngine
|
from inference.engine import YOLOEngine
|
||||||
|
from inference.roi.cache_manager import get_roi_cache
|
||||||
from inference.roi.roi_filter import ROIFilter
|
from inference.roi.roi_filter import ROIFilter
|
||||||
from inference.rules.algorithms import AlgorithmManager
|
from inference.rules.algorithms import AlgorithmManager
|
||||||
from inference.stream import StreamManager
|
from inference.stream import StreamManager
|
||||||
@@ -34,6 +34,8 @@ class InferencePipeline:
|
|||||||
reconnect_delay=self.config.stream.reconnect_delay,
|
reconnect_delay=self.config.stream.reconnect_delay,
|
||||||
)
|
)
|
||||||
self.roi_filter = ROIFilter()
|
self.roi_filter = ROIFilter()
|
||||||
|
self.roi_cache = get_roi_cache()
|
||||||
|
|
||||||
self.algo_manager = AlgorithmManager(working_hours=[
|
self.algo_manager = AlgorithmManager(working_hours=[
|
||||||
{
|
{
|
||||||
"start": [wh.start[0], wh.start[1]],
|
"start": [wh.start[0], wh.start[1]],
|
||||||
@@ -56,6 +58,8 @@ class InferencePipeline:
|
|||||||
if not self.db_initialized:
|
if not self.db_initialized:
|
||||||
init_db()
|
init_db()
|
||||||
self.db_initialized = True
|
self.db_initialized = True
|
||||||
|
self.roi_cache.initialize(get_session_factory(), refresh_interval=10.0)
|
||||||
|
self.roi_cache.start_background_refresh()
|
||||||
|
|
||||||
def _get_db_session(self):
|
def _get_db_session(self):
|
||||||
from db.models import get_session_factory
|
from db.models import get_session_factory
|
||||||
@@ -169,24 +173,7 @@ class InferencePipeline:
|
|||||||
def _process_frame(self, camera_id: int, frame: np.ndarray, camera):
|
def _process_frame(self, camera_id: int, frame: np.ndarray, camera):
|
||||||
from ultralytics.engine.results import Results
|
from ultralytics.engine.results import Results
|
||||||
|
|
||||||
db = self._get_db_session()
|
roi_configs = self.roi_cache.get_rois(camera_id)
|
||||||
try:
|
|
||||||
rois = get_all_rois(db, camera_id)
|
|
||||||
roi_configs = [
|
|
||||||
{
|
|
||||||
"id": roi.id,
|
|
||||||
"roi_id": roi.roi_id,
|
|
||||||
"type": roi.roi_type,
|
|
||||||
"points": json.loads(roi.points),
|
|
||||||
"rule": roi.rule_type,
|
|
||||||
"direction": roi.direction,
|
|
||||||
"enabled": roi.enabled,
|
|
||||||
"threshold_sec": roi.threshold_sec,
|
|
||||||
"confirm_sec": roi.confirm_sec,
|
|
||||||
"return_sec": roi.return_sec,
|
|
||||||
}
|
|
||||||
for roi in rois
|
|
||||||
]
|
|
||||||
|
|
||||||
if roi_configs:
|
if roi_configs:
|
||||||
self.roi_filter.update_cache(camera_id, roi_configs)
|
self.roi_filter.update_cache(camera_id, roi_configs)
|
||||||
@@ -246,9 +233,6 @@ class InferencePipeline:
|
|||||||
for alert in alerts:
|
for alert in alerts:
|
||||||
self._handle_alert(camera_id, alert, frame, roi_conf)
|
self._handle_alert(camera_id, alert, frame, roi_conf)
|
||||||
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
def _handle_alert(
|
def _handle_alert(
|
||||||
self,
|
self,
|
||||||
camera_id: int,
|
camera_id: int,
|
||||||
|
|||||||
13
logs/app.log
13
logs/app.log
@@ -60,3 +60,16 @@
|
|||||||
2026-01-20 17:30:01,820 - security_monitor - INFO - 推理Pipeline启动,活跃摄像头数: 2
|
2026-01-20 17:30:01,820 - security_monitor - INFO - 推理Pipeline启动,活跃摄像头数: 2
|
||||||
2026-01-20 17:31:10,482 - security_monitor - INFO - 正在关闭系统...
|
2026-01-20 17:31:10,482 - security_monitor - INFO - 正在关闭系统...
|
||||||
2026-01-20 17:31:10,612 - security_monitor - INFO - 系统已关闭
|
2026-01-20 17:31:10,612 - security_monitor - INFO - 系统已关闭
|
||||||
|
2026-01-20 18:02:39,208 - security_monitor - INFO - 启动安保异常行为识别系统
|
||||||
|
2026-01-20 18:02:39,221 - security_monitor - INFO - 数据库初始化完成
|
||||||
|
2026-01-20 18:02:51,772 - security_monitor - INFO - 推理Pipeline启动,活跃摄像头数: 2
|
||||||
|
2026-01-20 18:04:32,838 - security_monitor - INFO - 正在关闭系统...
|
||||||
|
2026-01-20 18:04:33,043 - security_monitor - INFO - 系统已关闭
|
||||||
|
2026-01-20 18:04:47,234 - security_monitor - INFO - 启动安保异常行为识别系统
|
||||||
|
2026-01-20 18:04:47,247 - security_monitor - INFO - 数据库初始化完成
|
||||||
|
2026-01-20 18:04:59,775 - security_monitor - INFO - 推理Pipeline启动,活跃摄像头数: 2
|
||||||
|
2026-01-20 18:23:13,953 - security_monitor - INFO - 正在关闭系统...
|
||||||
|
2026-01-20 18:23:14,097 - security_monitor - INFO - 系统已关闭
|
||||||
|
2026-01-20 18:24:01,952 - security_monitor - INFO - 启动安保异常行为识别系统
|
||||||
|
2026-01-20 18:24:01,963 - security_monitor - INFO - 数据库初始化完成
|
||||||
|
2026-01-20 18:24:14,477 - security_monitor - INFO - 推理Pipeline启动,活跃摄像头数: 2
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user