feat: 重构数据库schema分离空间与业务配置 - 新增algorithm_registry和roi_algo_bind表 - roi_configs简化为纯空间配置 - 新增AlgorithmInfo/ROIAlgoBind数据模型
This commit is contained in:
@@ -215,7 +215,8 @@ class AlertInfo:
|
||||
alert_id: str
|
||||
camera_id: str
|
||||
roi_id: str
|
||||
alert_type: str
|
||||
bind_id: Optional[str] = None
|
||||
alert_type: str = "detection"
|
||||
target_class: Optional[str] = None
|
||||
confidence: Optional[float] = None
|
||||
bbox: Optional[List[float]] = None
|
||||
@@ -224,6 +225,7 @@ class AlertInfo:
|
||||
level: AlertLevel = AlertLevel.MEDIUM
|
||||
timestamp: Optional[str] = None
|
||||
extra_data: Optional[Dict[str, Any]] = None
|
||||
detections: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
@@ -231,6 +233,7 @@ class AlertInfo:
|
||||
"alert_id": self.alert_id,
|
||||
"camera_id": self.camera_id,
|
||||
"roi_id": self.roi_id,
|
||||
"bind_id": self.bind_id,
|
||||
"alert_type": self.alert_type,
|
||||
"target_class": self.target_class,
|
||||
"confidence": self.confidence,
|
||||
@@ -240,6 +243,7 @@ class AlertInfo:
|
||||
"level": self.level.value if isinstance(self.level, AlertLevel) else self.level,
|
||||
"timestamp": self.timestamp,
|
||||
"extra_data": self.extra_data,
|
||||
"detections": self.detections,
|
||||
}
|
||||
|
||||
def to_json(self) -> str:
|
||||
@@ -291,3 +295,184 @@ class ConfigVersion:
|
||||
"description": self.description,
|
||||
"affected_items": self.affected_items,
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class AlgorithmInfo:
|
||||
"""算法配置信息数据模型"""
|
||||
algo_code: str
|
||||
algo_name: str
|
||||
target_class: str = "person"
|
||||
param_schema: Optional[Dict[str, Any]] = None
|
||||
description: Optional[str] = None
|
||||
is_active: bool = True
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"algo_code": self.algo_code,
|
||||
"algo_name": self.algo_name,
|
||||
"target_class": self.target_class,
|
||||
"param_schema": self.param_schema,
|
||||
"description": self.description,
|
||||
"is_active": self.is_active,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'AlgorithmInfo':
|
||||
"""从字典创建实例"""
|
||||
return cls(
|
||||
algo_code=data.get("algo_code", ""),
|
||||
algo_name=data.get("algo_name", ""),
|
||||
target_class=data.get("target_class", "person"),
|
||||
param_schema=data.get("param_schema"),
|
||||
description=data.get("description"),
|
||||
is_active=data.get("is_active", True),
|
||||
created_at=data.get("created_at"),
|
||||
updated_at=data.get("updated_at"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ROIAlgoBind:
|
||||
"""ROI与算法绑定关系数据模型"""
|
||||
bind_id: str
|
||||
roi_id: str
|
||||
algo_code: str
|
||||
params: Dict[str, Any] = field(default_factory=dict)
|
||||
priority: int = 0
|
||||
enabled: bool = True
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
algo_name: Optional[str] = None
|
||||
target_class: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"bind_id": self.bind_id,
|
||||
"roi_id": self.roi_id,
|
||||
"algo_code": self.algo_code,
|
||||
"params": self.params,
|
||||
"priority": self.priority,
|
||||
"enabled": self.enabled,
|
||||
"created_at": self.created_at,
|
||||
"updated_at": self.updated_at,
|
||||
"algo_name": self.algo_name,
|
||||
"target_class": self.target_class,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ROIAlgoBind':
|
||||
"""从字典创建实例"""
|
||||
return cls(
|
||||
bind_id=data.get("bind_id", ""),
|
||||
roi_id=data.get("roi_id", ""),
|
||||
algo_code=data.get("algo_code", ""),
|
||||
params=data.get("params", {}),
|
||||
priority=data.get("priority", 0),
|
||||
enabled=data.get("enabled", True),
|
||||
created_at=data.get("created_at"),
|
||||
updated_at=data.get("updated_at"),
|
||||
algo_name=data.get("algo_name"),
|
||||
target_class=data.get("target_class"),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ROIInfoNew:
|
||||
"""ROI区域信息数据模型(新版本,包含绑定信息)"""
|
||||
roi_id: str
|
||||
camera_id: str
|
||||
roi_type: ROIType
|
||||
coordinates: List[List[float]]
|
||||
enabled: bool = True
|
||||
priority: int = 0
|
||||
extra_params: Optional[Dict[str, Any]] = None
|
||||
bindings: List[ROIAlgoBind] = field(default_factory=list)
|
||||
updated_at: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"roi_id": self.roi_id,
|
||||
"camera_id": self.camera_id,
|
||||
"roi_type": self.roi_type.value if isinstance(self.roi_type, ROIType) else self.roi_type,
|
||||
"coordinates": self.coordinates,
|
||||
"enabled": self.enabled,
|
||||
"priority": self.priority,
|
||||
"extra_params": self.extra_params,
|
||||
"bindings": [b.to_dict() for b in self.bindings],
|
||||
"updated_at": self.updated_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, Any]) -> 'ROIInfoNew':
|
||||
"""从字典创建实例"""
|
||||
roi_type_str = data.get("roi_type", "polygon")
|
||||
roi_type = ROIType(roi_type_str) if roi_type_str in [e.value for e in ROIType] else ROIType.POLYGON
|
||||
|
||||
bindings = data.get("bindings", [])
|
||||
if isinstance(bindings, list):
|
||||
bindings = [ROIAlgoBind.from_dict(b) for b in bindings]
|
||||
|
||||
return cls(
|
||||
roi_id=data.get("roi_id", ""),
|
||||
camera_id=data.get("camera_id", ""),
|
||||
roi_type=roi_type,
|
||||
coordinates=data.get("coordinates", []),
|
||||
enabled=data.get("enabled", True),
|
||||
priority=data.get("priority", 0),
|
||||
extra_params=data.get("extra_params"),
|
||||
bindings=bindings,
|
||||
updated_at=data.get("updated_at"),
|
||||
)
|
||||
|
||||
def is_point_inside(self, point: List[float]) -> bool:
|
||||
"""判断点是否在ROI区域内"""
|
||||
if self.roi_type == ROIType.RECTANGLE:
|
||||
return self._is_point_in_rectangle(point)
|
||||
elif self.roi_type == ROIType.POLYGON:
|
||||
return self._is_point_in_polygon(point)
|
||||
return False
|
||||
|
||||
def _is_point_in_rectangle(self, point: List[float]) -> bool:
|
||||
"""判断点是否在矩形区域内"""
|
||||
if len(self.coordinates) < 2:
|
||||
return False
|
||||
|
||||
x, y = point[0], point[1]
|
||||
x1, y1 = self.coordinates[0]
|
||||
x2, y2 = self.coordinates[1]
|
||||
|
||||
left = min(x1, x2)
|
||||
right = max(x1, x2)
|
||||
top = min(y1, y2)
|
||||
bottom = max(y1, y2)
|
||||
|
||||
return left <= x <= right and top <= y <= bottom
|
||||
|
||||
def _is_point_in_polygon(self, point: List[float]) -> bool:
|
||||
"""判断点是否在多边形区域内(射线法)"""
|
||||
if len(self.coordinates) < 3:
|
||||
return False
|
||||
|
||||
x, y = point[0], point[1]
|
||||
n = len(self.coordinates)
|
||||
inside = False
|
||||
|
||||
j = n - 1
|
||||
for i in range(n):
|
||||
xi, yi = self.coordinates[i]
|
||||
xj, yj = self.coordinates[j]
|
||||
|
||||
if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi):
|
||||
inside = not inside
|
||||
|
||||
j = i
|
||||
|
||||
return inside
|
||||
|
||||
@@ -9,6 +9,7 @@ SQLite 数据库模块
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
import threading
|
||||
import queue
|
||||
@@ -39,7 +40,8 @@ class AlertRecord:
|
||||
alert_id: str
|
||||
camera_id: str
|
||||
roi_id: str
|
||||
alert_type: str
|
||||
bind_id: Optional[str] = None # 关联 roi_algo_bind 表
|
||||
alert_type: str = "detection"
|
||||
target_class: Optional[str] = None
|
||||
confidence: Optional[float] = None
|
||||
bbox: Optional[List[float]] = None
|
||||
@@ -49,6 +51,7 @@ class AlertRecord:
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
processed_at: Optional[datetime] = None
|
||||
duration_minutes: Optional[float] = None
|
||||
detections: Optional[str] = None # JSON格式的检测结果
|
||||
|
||||
|
||||
class SQLiteManager:
|
||||
@@ -108,12 +111,73 @@ class SQLiteManager:
|
||||
|
||||
cursor = self._conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS algorithm_registry (
|
||||
algo_code TEXT PRIMARY KEY,
|
||||
algo_name TEXT NOT NULL,
|
||||
target_class TEXT DEFAULT 'person',
|
||||
param_schema TEXT,
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_algo_active
|
||||
ON algorithm_registry(is_active)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS roi_configs (
|
||||
roi_id TEXT PRIMARY KEY,
|
||||
camera_id TEXT NOT NULL,
|
||||
roi_type TEXT NOT NULL,
|
||||
coordinates TEXT NOT NULL,
|
||||
enabled BOOLEAN DEFAULT 1,
|
||||
priority INTEGER DEFAULT 0,
|
||||
extra_params TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_roi_camera
|
||||
ON roi_configs(camera_id)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS roi_algo_bind (
|
||||
bind_id TEXT PRIMARY KEY,
|
||||
roi_id TEXT NOT NULL,
|
||||
algo_code TEXT NOT NULL,
|
||||
params TEXT NOT NULL,
|
||||
priority INTEGER DEFAULT 0,
|
||||
enabled BOOLEAN DEFAULT 1,
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
FOREIGN KEY (roi_id) REFERENCES roi_configs(roi_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (algo_code) REFERENCES algorithm_registry(algo_code) ON DELETE RESTRICT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_bind_roi
|
||||
ON roi_algo_bind(roi_id)
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_bind_algo
|
||||
ON roi_algo_bind(algo_code)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS alert_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
alert_id TEXT UNIQUE NOT NULL,
|
||||
camera_id TEXT NOT NULL,
|
||||
roi_id TEXT NOT NULL,
|
||||
bind_id TEXT,
|
||||
alert_type TEXT NOT NULL,
|
||||
target_class TEXT,
|
||||
confidence REAL,
|
||||
@@ -123,7 +187,9 @@ class SQLiteManager:
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TEXT NOT NULL,
|
||||
processed_at TEXT,
|
||||
duration_minutes REAL
|
||||
duration_minutes REAL,
|
||||
detections TEXT,
|
||||
FOREIGN KEY (bind_id) REFERENCES roi_algo_bind(bind_id) ON DELETE SET NULL
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -139,6 +205,10 @@ class SQLiteManager:
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_status
|
||||
ON alert_records(status)
|
||||
""")
|
||||
cursor.execute("""
|
||||
CREATE INDEX IF NOT EXISTS idx_alert_bind
|
||||
ON alert_records(bind_id)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS camera_configs (
|
||||
@@ -148,31 +218,12 @@ class SQLiteManager:
|
||||
status BOOLEAN DEFAULT 1,
|
||||
enabled BOOLEAN DEFAULT 1,
|
||||
location TEXT,
|
||||
roi_group_id TEXT,
|
||||
extra_params TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS roi_configs (
|
||||
roi_id TEXT PRIMARY KEY,
|
||||
camera_id TEXT NOT NULL,
|
||||
roi_type TEXT NOT NULL,
|
||||
coordinates TEXT NOT NULL,
|
||||
algorithm_type TEXT NOT NULL,
|
||||
alert_threshold INTEGER DEFAULT 3,
|
||||
alert_cooldown INTEGER DEFAULT 300,
|
||||
enabled BOOLEAN DEFAULT 1,
|
||||
extra_params TEXT,
|
||||
working_hours TEXT,
|
||||
confirm_on_duty_sec INTEGER DEFAULT 10,
|
||||
confirm_leave_sec INTEGER DEFAULT 10,
|
||||
cooldown_sec INTEGER DEFAULT 300,
|
||||
target_class TEXT DEFAULT 'person',
|
||||
updated_at TEXT
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS config_update_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -186,6 +237,66 @@ class SQLiteManager:
|
||||
""")
|
||||
|
||||
self._conn.commit()
|
||||
|
||||
self._init_default_algorithms()
|
||||
|
||||
def _init_default_algorithms(self):
|
||||
"""初始化默认算法配置"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
|
||||
algorithms = [
|
||||
{
|
||||
'algo_code': 'leave_post',
|
||||
'algo_name': '离岗检测',
|
||||
'target_class': 'person',
|
||||
'param_schema': json.dumps({
|
||||
"confirm_on_duty_sec": {"type": "int", "default": 10, "min": 1},
|
||||
"confirm_leave_sec": {"type": "int", "default": 10, "min": 1},
|
||||
"cooldown_sec": {"type": "int", "default": 300, "min": 0},
|
||||
"working_hours": {"type": "list", "default": []},
|
||||
}),
|
||||
'description': '检测人员是否在岗,支持工作时间段配置'
|
||||
},
|
||||
{
|
||||
'algo_code': 'intrusion',
|
||||
'algo_name': '周界入侵检测',
|
||||
'target_class': 'person',
|
||||
'param_schema': json.dumps({
|
||||
"cooldown_seconds": {"type": "int", "default": 120, "min": 0},
|
||||
"confirm_seconds": {"type": "int", "default": 5, "min": 1},
|
||||
}),
|
||||
'description': '检测人员进入指定区域,支持确认时间和冷却时间配置'
|
||||
},
|
||||
{
|
||||
'algo_code': 'crowd_detection',
|
||||
'algo_name': '人群聚集检测',
|
||||
'target_class': 'person',
|
||||
'param_schema': json.dumps({
|
||||
"max_count": {"type": "int", "default": 10, "min": 1},
|
||||
"cooldown_seconds": {"type": "int", "default": 300, "min": 0},
|
||||
}),
|
||||
'description': '检测区域内人员数量是否超过阈值'
|
||||
},
|
||||
]
|
||||
|
||||
for algo in algorithms:
|
||||
cursor.execute("""
|
||||
INSERT OR IGNORE INTO algorithm_registry (
|
||||
algo_code, algo_name, target_class, param_schema, description,
|
||||
is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, 1, ?, ?)
|
||||
""", (
|
||||
algo['algo_code'], algo['algo_name'], algo['target_class'],
|
||||
algo['param_schema'], algo['description'],
|
||||
datetime.now().isoformat(), datetime.now().isoformat()
|
||||
))
|
||||
|
||||
self._conn.commit()
|
||||
logger.info(f"已初始化 {len(algorithms)} 个默认算法配置")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"初始化默认算法失败: {e}")
|
||||
|
||||
def _start_background_threads(self):
|
||||
"""启动后台线程"""
|
||||
@@ -242,15 +353,16 @@ class SQLiteManager:
|
||||
for record in batch:
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO alert_records (
|
||||
alert_id, camera_id, roi_id, alert_type,
|
||||
alert_id, camera_id, roi_id, bind_id, alert_type,
|
||||
target_class, confidence, bbox, message,
|
||||
image_path, status, created_at, processed_at,
|
||||
duration_minutes
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
duration_minutes, detections
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
record['alert_id'],
|
||||
record['camera_id'],
|
||||
record['roi_id'],
|
||||
record.get('bind_id'),
|
||||
record['alert_type'],
|
||||
record.get('target_class'),
|
||||
record.get('confidence'),
|
||||
@@ -261,6 +373,7 @@ class SQLiteManager:
|
||||
record['created_at'],
|
||||
record.get('processed_at'),
|
||||
record.get('duration_minutes'),
|
||||
record.get('detections'),
|
||||
))
|
||||
|
||||
self._conn.commit()
|
||||
@@ -285,6 +398,7 @@ class SQLiteManager:
|
||||
'alert_id': alert.alert_id,
|
||||
'camera_id': alert.camera_id,
|
||||
'roi_id': alert.roi_id,
|
||||
'bind_id': alert.bind_id,
|
||||
'alert_type': alert.alert_type,
|
||||
'target_class': alert.target_class,
|
||||
'confidence': alert.confidence,
|
||||
@@ -295,6 +409,7 @@ class SQLiteManager:
|
||||
'created_at': alert.created_at.isoformat(),
|
||||
'processed_at': alert.processed_at.isoformat() if alert.processed_at else None,
|
||||
'duration_minutes': alert.duration_minutes,
|
||||
'detections': alert.detections,
|
||||
}
|
||||
self._write_queue.put(record)
|
||||
|
||||
@@ -427,14 +542,15 @@ class SQLiteManager:
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO camera_configs (
|
||||
camera_id, rtsp_url, camera_name, status, enabled,
|
||||
location, extra_params, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
location, roi_group_id, extra_params, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
camera_id, rtsp_url,
|
||||
kwargs.get('camera_name'),
|
||||
kwargs.get('status', True),
|
||||
kwargs.get('enabled', True),
|
||||
kwargs.get('location'),
|
||||
kwargs.get('roi_group_id'),
|
||||
str(kwargs.get('extra_params')) if kwargs.get('extra_params') else None,
|
||||
now
|
||||
))
|
||||
@@ -452,7 +568,7 @@ class SQLiteManager:
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
columns = ['camera_id', 'rtsp_url', 'camera_name', 'status',
|
||||
'enabled', 'location', 'extra_params', 'updated_at']
|
||||
'enabled', 'location', 'roi_group_id', 'extra_params', 'updated_at']
|
||||
return dict(zip(columns, row))
|
||||
return None
|
||||
except Exception as e:
|
||||
@@ -465,7 +581,7 @@ class SQLiteManager:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("SELECT * FROM camera_configs ORDER BY camera_id")
|
||||
columns = ['camera_id', 'rtsp_url', 'camera_name', 'status',
|
||||
'enabled', 'location', 'extra_params', 'updated_at']
|
||||
'enabled', 'location', 'roi_group_id', 'extra_params', 'updated_at']
|
||||
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.error(f"获取所有摄像头配置失败: {e}")
|
||||
@@ -483,29 +599,21 @@ class SQLiteManager:
|
||||
return False
|
||||
|
||||
def save_roi_config(self, roi_id: str, camera_id: str, roi_type: str,
|
||||
coordinates: List, algorithm_type: str, **kwargs) -> bool:
|
||||
"""保存ROI配置"""
|
||||
coordinates: List, **kwargs) -> bool:
|
||||
"""保存ROI配置(空间信息,不包含业务参数)"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO roi_configs (
|
||||
roi_id, camera_id, roi_type, coordinates, algorithm_type,
|
||||
alert_threshold, alert_cooldown, enabled, extra_params,
|
||||
working_hours, confirm_on_duty_sec, confirm_leave_sec,
|
||||
cooldown_sec, target_class, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
roi_id, camera_id, roi_type, coordinates,
|
||||
enabled, priority, extra_params, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
roi_id, camera_id, roi_type, str(coordinates), algorithm_type,
|
||||
kwargs.get('alert_threshold', 3),
|
||||
kwargs.get('alert_cooldown', 300),
|
||||
roi_id, camera_id, roi_type, str(coordinates),
|
||||
kwargs.get('enabled', True),
|
||||
kwargs.get('priority', 0),
|
||||
str(kwargs.get('extra_params')) if kwargs.get('extra_params') else None,
|
||||
str(kwargs.get('working_hours')) if kwargs.get('working_hours') else None,
|
||||
kwargs.get('confirm_on_duty_sec', 10),
|
||||
kwargs.get('confirm_leave_sec', 10),
|
||||
kwargs.get('cooldown_sec', 300),
|
||||
kwargs.get('target_class', 'person'),
|
||||
now
|
||||
))
|
||||
self._conn.commit()
|
||||
@@ -522,20 +630,12 @@ class SQLiteManager:
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
||||
'algorithm_type', 'alert_threshold', 'alert_cooldown',
|
||||
'enabled', 'extra_params', 'working_hours',
|
||||
'confirm_on_duty_sec', 'confirm_leave_sec', 'cooldown_sec',
|
||||
'target_class', 'updated_at']
|
||||
'enabled', 'priority', 'extra_params', 'updated_at']
|
||||
result = dict(zip(columns, row))
|
||||
try:
|
||||
result['coordinates'] = eval(result['coordinates'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if result.get('working_hours'):
|
||||
result['working_hours'] = eval(result['working_hours'])
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
@@ -548,10 +648,7 @@ class SQLiteManager:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("SELECT * FROM roi_configs WHERE camera_id = ?", (camera_id,))
|
||||
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
||||
'algorithm_type', 'alert_threshold', 'alert_cooldown',
|
||||
'enabled', 'extra_params', 'working_hours',
|
||||
'confirm_on_duty_sec', 'confirm_leave_sec', 'cooldown_sec',
|
||||
'target_class', 'updated_at']
|
||||
'enabled', 'priority', 'extra_params', 'updated_at']
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
r = dict(zip(columns, row))
|
||||
@@ -559,11 +656,6 @@ class SQLiteManager:
|
||||
r['coordinates'] = eval(r['coordinates'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if r.get('working_hours'):
|
||||
r['working_hours'] = eval(r['working_hours'])
|
||||
except:
|
||||
pass
|
||||
results.append(r)
|
||||
return results
|
||||
except Exception as e:
|
||||
@@ -576,10 +668,7 @@ class SQLiteManager:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("SELECT * FROM roi_configs ORDER BY camera_id, roi_id")
|
||||
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
||||
'algorithm_type', 'alert_threshold', 'alert_cooldown',
|
||||
'enabled', 'extra_params', 'working_hours',
|
||||
'confirm_on_duty_sec', 'confirm_leave_sec', 'cooldown_sec',
|
||||
'target_class', 'updated_at']
|
||||
'enabled', 'priority', 'extra_params', 'updated_at']
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
r = dict(zip(columns, row))
|
||||
@@ -587,11 +676,6 @@ class SQLiteManager:
|
||||
r['coordinates'] = eval(r['coordinates'])
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
if r.get('working_hours'):
|
||||
r['working_hours'] = eval(r['working_hours'])
|
||||
except:
|
||||
pass
|
||||
results.append(r)
|
||||
return results
|
||||
except Exception as e:
|
||||
@@ -609,6 +693,200 @@ class SQLiteManager:
|
||||
logger.error(f"删除ROI配置失败: {e}")
|
||||
return False
|
||||
|
||||
def save_algorithm(self, algo_code: str, algo_name: str, target_class: str = "person",
|
||||
param_schema: Optional[str] = None, description: Optional[str] = None,
|
||||
is_active: bool = True) -> bool:
|
||||
"""保存算法配置"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO algorithm_registry (
|
||||
algo_code, algo_name, target_class, param_schema, description,
|
||||
is_active, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
algo_code, algo_name, target_class, param_schema, description,
|
||||
is_active, now, now
|
||||
))
|
||||
self._conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存算法配置失败: {e}")
|
||||
return False
|
||||
|
||||
def get_algorithm(self, algo_code: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取算法配置"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("SELECT * FROM algorithm_registry WHERE algo_code = ?", (algo_code,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
columns = ['algo_code', 'algo_name', 'target_class', 'param_schema',
|
||||
'description', 'is_active', 'created_at', 'updated_at']
|
||||
result = dict(zip(columns, row))
|
||||
try:
|
||||
if result.get('param_schema'):
|
||||
result['param_schema'] = json.loads(result['param_schema'])
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取算法配置失败: {e}")
|
||||
return None
|
||||
|
||||
def get_all_algorithms(self, active_only: bool = True) -> List[Dict[str, Any]]:
|
||||
"""获取所有算法配置"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
query = "SELECT * FROM algorithm_registry"
|
||||
if active_only:
|
||||
query += " WHERE is_active = 1"
|
||||
cursor.execute(query)
|
||||
columns = ['algo_code', 'algo_name', 'target_class', 'param_schema',
|
||||
'description', 'is_active', 'created_at', 'updated_at']
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
r = dict(zip(columns, row))
|
||||
try:
|
||||
if r.get('param_schema'):
|
||||
r['param_schema'] = json.loads(r['param_schema'])
|
||||
except:
|
||||
pass
|
||||
results.append(r)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"获取所有算法配置失败: {e}")
|
||||
return []
|
||||
|
||||
def save_roi_algo_bind(self, bind_id: str, roi_id: str, algo_code: str,
|
||||
params: Dict[str, Any], priority: int = 0,
|
||||
enabled: bool = True) -> bool:
|
||||
"""保存ROI与算法的绑定关系"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
now = datetime.now().isoformat()
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO roi_algo_bind (
|
||||
bind_id, roi_id, algo_code, params, priority,
|
||||
enabled, created_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (
|
||||
bind_id, roi_id, algo_code, json.dumps(params),
|
||||
priority, enabled, now, now
|
||||
))
|
||||
self._conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存ROI算法绑定失败: {e}")
|
||||
return False
|
||||
|
||||
def get_roi_algo_bind(self, bind_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""获取ROI算法绑定配置"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("SELECT * FROM roi_algo_bind WHERE bind_id = ?", (bind_id,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
columns = ['bind_id', 'roi_id', 'algo_code', 'params',
|
||||
'priority', 'enabled', 'created_at', 'updated_at']
|
||||
result = dict(zip(columns, row))
|
||||
try:
|
||||
if result.get('params'):
|
||||
result['params'] = json.loads(result['params'])
|
||||
except:
|
||||
pass
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取ROI算法绑定失败: {e}")
|
||||
return None
|
||||
|
||||
def get_bindings_by_roi(self, roi_id: str) -> List[Dict[str, Any]]:
|
||||
"""获取指定ROI的所有算法绑定"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT b.*, a.algo_name, a.target_class
|
||||
FROM roi_algo_bind b
|
||||
LEFT JOIN algorithm_registry a ON b.algo_code = a.algo_code
|
||||
WHERE b.roi_id = ? AND b.enabled = 1
|
||||
ORDER BY b.priority DESC
|
||||
""", (roi_id,))
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
result = dict(zip(
|
||||
['bind_id', 'roi_id', 'algo_code', 'params', 'priority',
|
||||
'enabled', 'created_at', 'updated_at', 'algo_name', 'target_class'],
|
||||
row
|
||||
))
|
||||
try:
|
||||
if result.get('params'):
|
||||
result['params'] = json.loads(result['params'])
|
||||
except:
|
||||
pass
|
||||
results.append(result)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"获取ROI算法绑定失败: {e}")
|
||||
return []
|
||||
|
||||
def get_bindings_by_camera(self, camera_id: str) -> List[Dict[str, Any]]:
|
||||
"""获取指定摄像头的所有ROI算法绑定"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT b.*, a.algo_name, a.target_class, r.roi_type, r.coordinates
|
||||
FROM roi_algo_bind b
|
||||
LEFT JOIN algorithm_registry a ON b.algo_code = a.algo_code
|
||||
LEFT JOIN roi_configs r ON b.roi_id = r.roi_id
|
||||
WHERE r.camera_id = ? AND b.enabled = 1 AND r.enabled = 1
|
||||
ORDER BY r.priority DESC, b.priority DESC
|
||||
""", (camera_id,))
|
||||
results = []
|
||||
for row in cursor.fetchall():
|
||||
result = dict(zip(
|
||||
['bind_id', 'roi_id', 'algo_code', 'params', 'priority',
|
||||
'enabled', 'created_at', 'updated_at', 'algo_name', 'target_class',
|
||||
'roi_type', 'coordinates'],
|
||||
row
|
||||
))
|
||||
try:
|
||||
if result.get('params'):
|
||||
result['params'] = json.loads(result['params'])
|
||||
if result.get('coordinates'):
|
||||
result['coordinates'] = eval(result['coordinates'])
|
||||
except:
|
||||
pass
|
||||
results.append(result)
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"获取摄像头算法绑定失败: {e}")
|
||||
return []
|
||||
|
||||
def delete_roi_algo_bind(self, bind_id: str) -> bool:
|
||||
"""删除ROI算法绑定"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("DELETE FROM roi_algo_bind WHERE bind_id = ?", (bind_id,))
|
||||
self._conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except Exception as e:
|
||||
logger.error(f"删除ROI算法绑定失败: {e}")
|
||||
return False
|
||||
|
||||
def delete_bindings_by_roi(self, roi_id: str) -> int:
|
||||
"""删除指定ROI的所有算法绑定"""
|
||||
try:
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("DELETE FROM roi_algo_bind WHERE roi_id = ?", (roi_id,))
|
||||
self._conn.commit()
|
||||
return cursor.rowcount
|
||||
except Exception as e:
|
||||
logger.error(f"删除ROI算法绑定失败: {e}")
|
||||
return 0
|
||||
|
||||
def log_config_update(
|
||||
self,
|
||||
config_type: str,
|
||||
|
||||
Reference in New Issue
Block a user