feat: 重构数据库schema分离空间与业务配置 - 新增algorithm_registry和roi_algo_bind表 - roi_configs简化为纯空间配置 - 新增AlgorithmInfo/ROIAlgoBind数据模型
This commit is contained in:
313
algorithms.py
313
algorithms.py
@@ -290,6 +290,84 @@ class IntrusionAlgorithm:
|
|||||||
self.detection_start.clear()
|
self.detection_start.clear()
|
||||||
|
|
||||||
|
|
||||||
|
class CrowdDetectionAlgorithm:
|
||||||
|
"""人群聚集检测算法"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
max_count: int = 10,
|
||||||
|
cooldown_seconds: int = 300,
|
||||||
|
target_class: Optional[str] = "person",
|
||||||
|
):
|
||||||
|
self.max_count = max_count
|
||||||
|
self.cooldown_seconds = cooldown_seconds
|
||||||
|
self.target_class = target_class
|
||||||
|
|
||||||
|
self.last_alert_time: Dict[str, datetime] = {}
|
||||||
|
self.alert_triggered: Dict[str, bool] = {}
|
||||||
|
|
||||||
|
def _check_detection_in_roi(self, detection: Dict, roi_id: str) -> bool:
|
||||||
|
matched_rois = detection.get("matched_rois", [])
|
||||||
|
for roi in matched_rois:
|
||||||
|
if roi.get("roi_id") == roi_id:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_target_class(self, detection: Dict, target_class: Optional[str]) -> bool:
|
||||||
|
if not target_class:
|
||||||
|
return True
|
||||||
|
return detection.get("class") == target_class
|
||||||
|
|
||||||
|
def _get_bboxes(self, tracks: List[Dict], roi_id: str) -> List[List[float]]:
|
||||||
|
bboxes = []
|
||||||
|
for det in tracks:
|
||||||
|
if self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class):
|
||||||
|
bboxes.append(det.get("bbox", []))
|
||||||
|
return bboxes
|
||||||
|
|
||||||
|
def process(
|
||||||
|
self,
|
||||||
|
roi_id: str,
|
||||||
|
camera_id: str,
|
||||||
|
tracks: List[Dict],
|
||||||
|
current_time: Optional[datetime] = None,
|
||||||
|
) -> List[Dict]:
|
||||||
|
current_time = current_time or datetime.now()
|
||||||
|
key = f"{camera_id}_{roi_id}"
|
||||||
|
|
||||||
|
person_count = 0
|
||||||
|
for det in tracks:
|
||||||
|
if self._check_detection_in_roi(det, roi_id) and self._check_target_class(det, self.target_class):
|
||||||
|
person_count += 1
|
||||||
|
|
||||||
|
if person_count <= self.max_count:
|
||||||
|
self.alert_triggered[key] = False
|
||||||
|
return []
|
||||||
|
|
||||||
|
if self.alert_triggered.get(key, False):
|
||||||
|
elapsed_since_alert = (current_time - self.last_alert_time.get(key, datetime.min)).total_seconds()
|
||||||
|
if elapsed_since_alert < self.cooldown_seconds:
|
||||||
|
return []
|
||||||
|
self.alert_triggered[key] = False
|
||||||
|
|
||||||
|
bboxes = self._get_bboxes(tracks, roi_id)
|
||||||
|
self.last_alert_time[key] = current_time
|
||||||
|
self.alert_triggered[key] = True
|
||||||
|
|
||||||
|
return [{
|
||||||
|
"roi_id": roi_id,
|
||||||
|
"camera_id": camera_id,
|
||||||
|
"bbox": bboxes[0] if bboxes else [],
|
||||||
|
"alert_type": "crowd_detection",
|
||||||
|
"message": f"检测到人群聚集,当前人数: {person_count}",
|
||||||
|
"count": person_count,
|
||||||
|
}]
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.last_alert_time.clear()
|
||||||
|
self.alert_triggered.clear()
|
||||||
|
|
||||||
|
|
||||||
class AlgorithmManager:
|
class AlgorithmManager:
|
||||||
def __init__(self, working_hours: Optional[List[Dict]] = None):
|
def __init__(self, working_hours: Optional[List[Dict]] = None):
|
||||||
self.algorithms: Dict[str, Dict[str, Any]] = {}
|
self.algorithms: Dict[str, Dict[str, Any]] = {}
|
||||||
@@ -308,6 +386,11 @@ class AlgorithmManager:
|
|||||||
"confirm_seconds": 5,
|
"confirm_seconds": 5,
|
||||||
"target_class": None,
|
"target_class": None,
|
||||||
},
|
},
|
||||||
|
"crowd_detection": {
|
||||||
|
"max_count": 10,
|
||||||
|
"cooldown_seconds": 300,
|
||||||
|
"target_class": "person",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
self._pubsub = None
|
self._pubsub = None
|
||||||
@@ -358,6 +441,13 @@ class AlgorithmManager:
|
|||||||
self.reload_algorithm(roi_id)
|
self.reload_algorithm(roi_id)
|
||||||
else:
|
else:
|
||||||
self.reload_all_algorithms()
|
self.reload_all_algorithms()
|
||||||
|
elif data.get("type") == "bind":
|
||||||
|
bind_ids = data.get("ids", [])
|
||||||
|
if bind_ids:
|
||||||
|
for bind_id in bind_ids:
|
||||||
|
self.reload_bind_algorithm(bind_id)
|
||||||
|
else:
|
||||||
|
self.reload_all_algorithms()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理配置更新消息失败: {e}")
|
logger.error(f"处理配置更新消息失败: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -372,59 +462,95 @@ class AlgorithmManager:
|
|||||||
self._pubsub_thread.join(timeout=5)
|
self._pubsub_thread.join(timeout=5)
|
||||||
logger.info("配置订阅已停止")
|
logger.info("配置订阅已停止")
|
||||||
|
|
||||||
def load_from_redis(self, roi_id: str) -> bool:
|
def load_bind_from_redis(self, bind_id: str) -> bool:
|
||||||
"""从Redis加载单个ROI的算法配置"""
|
"""从Redis加载单个绑定配置的算法"""
|
||||||
try:
|
try:
|
||||||
from core.config_sync import get_config_sync_manager
|
from core.config_sync import get_config_sync_manager
|
||||||
config_manager = get_config_sync_manager()
|
config_manager = get_config_sync_manager()
|
||||||
roi_config = config_manager.get_roi_from_redis(roi_id)
|
bind_config = config_manager.get_algo_bind_from_redis(bind_id)
|
||||||
|
|
||||||
if not roi_config:
|
if not bind_config:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
with self._update_lock:
|
with self._update_lock:
|
||||||
algorithm_type = roi_config.get("algorithm_type", "leave_post")
|
roi_id = bind_config.get("roi_id")
|
||||||
|
algo_code = bind_config.get("algo_code", "leave_post")
|
||||||
|
params = bind_config.get("params", {})
|
||||||
|
|
||||||
if algorithm_type == "leave_post":
|
if roi_id not in self.algorithms:
|
||||||
params = {
|
self.algorithms[roi_id] = {}
|
||||||
"working_hours": roi_config.get("working_hours"),
|
|
||||||
"confirm_on_duty_sec": roi_config.get("confirm_on_duty_sec", 10),
|
key = f"{roi_id}_{bind_id}"
|
||||||
"confirm_leave_sec": roi_config.get("confirm_leave_sec", 10),
|
|
||||||
"cooldown_sec": roi_config.get("cooldown_sec", 300),
|
if algo_code == "leave_post":
|
||||||
"target_class": roi_config.get("target_class", "person"),
|
algo_params = {
|
||||||
|
"confirm_on_duty_sec": params.get("confirm_on_duty_sec", 10),
|
||||||
|
"confirm_leave_sec": params.get("confirm_leave_sec", 10),
|
||||||
|
"cooldown_sec": params.get("cooldown_sec", 300),
|
||||||
|
"working_hours": params.get("working_hours", []),
|
||||||
|
"target_class": params.get("target_class", bind_config.get("target_class", "person")),
|
||||||
}
|
}
|
||||||
if roi_id in self.algorithms and "leave_post" in self.algorithms[roi_id]:
|
if key in self.algorithms.get(roi_id, {}) and "leave_post" in self.algorithms[roi_id].get(key, {}):
|
||||||
algo = self.algorithms[roi_id]["leave_post"]
|
algo = self.algorithms[roi_id][key]["leave_post"]
|
||||||
algo.confirm_on_duty_sec = params["confirm_on_duty_sec"]
|
algo.confirm_on_duty_sec = algo_params["confirm_on_duty_sec"]
|
||||||
algo.confirm_leave_sec = params["confirm_leave_sec"]
|
algo.confirm_leave_sec = algo_params["confirm_leave_sec"]
|
||||||
algo.cooldown_sec = params["cooldown_sec"]
|
algo.cooldown_sec = algo_params["cooldown_sec"]
|
||||||
algo.target_class = params["target_class"]
|
algo.target_class = algo_params["target_class"]
|
||||||
if params["working_hours"]:
|
if algo_params["working_hours"]:
|
||||||
algo.working_hours = params["working_hours"]
|
algo.working_hours = algo_params["working_hours"]
|
||||||
logger.info(f"已热更新算法参数: {roi_id}")
|
logger.info(f"已热更新算法参数: {key}")
|
||||||
else:
|
else:
|
||||||
self.register_algorithm(roi_id, "leave_post", params)
|
self.algorithms[roi_id][key] = {}
|
||||||
logger.info(f"已从Redis加载算法: {roi_id}")
|
self.algorithms[roi_id][key]["leave_post"] = LeavePostAlgorithm(
|
||||||
else:
|
confirm_on_duty_sec=algo_params["confirm_on_duty_sec"],
|
||||||
params = {}
|
confirm_leave_sec=algo_params["confirm_leave_sec"],
|
||||||
if roi_id in self.algorithms and algorithm_type in self.algorithms[roi_id]:
|
cooldown_sec=algo_params["cooldown_sec"],
|
||||||
pass
|
working_hours=algo_params["working_hours"],
|
||||||
else:
|
target_class=algo_params["target_class"],
|
||||||
self.register_algorithm(roi_id, algorithm_type, params)
|
)
|
||||||
logger.info(f"已从Redis加载算法: {roi_id}")
|
logger.info(f"已从Redis加载算法: {key}")
|
||||||
|
elif algo_code == "intrusion":
|
||||||
|
algo_params = {
|
||||||
|
"cooldown_seconds": params.get("cooldown_seconds", 120),
|
||||||
|
"confirm_seconds": params.get("confirm_seconds", 5),
|
||||||
|
"target_class": params.get("target_class", bind_config.get("target_class")),
|
||||||
|
}
|
||||||
|
self.algorithms[roi_id][key] = {}
|
||||||
|
self.algorithms[roi_id][key]["intrusion"] = IntrusionAlgorithm(
|
||||||
|
cooldown_seconds=algo_params["cooldown_seconds"],
|
||||||
|
confirm_seconds=algo_params["confirm_seconds"],
|
||||||
|
target_class=algo_params["target_class"],
|
||||||
|
)
|
||||||
|
logger.info(f"已从Redis加载算法: {key}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"从Redis加载算法配置失败: {e}")
|
logger.error(f"从Redis加载算法配置失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def reload_bind_algorithm(self, bind_id: str) -> bool:
|
||||||
|
"""重新加载单个绑定的算法配置"""
|
||||||
|
return self.load_bind_from_redis(bind_id)
|
||||||
|
|
||||||
def reload_algorithm(self, roi_id: str) -> bool:
|
def reload_algorithm(self, roi_id: str) -> bool:
|
||||||
"""重新加载单个ROI的算法配置"""
|
"""重新加载单个ROI的所有算法绑定配置"""
|
||||||
if roi_id not in self.algorithms:
|
try:
|
||||||
return self.load_from_redis(roi_id)
|
from core.config_sync import get_config_sync_manager
|
||||||
|
config_manager = get_config_sync_manager()
|
||||||
self.reset_algorithm(roi_id)
|
bindings = config_manager.get_bindings_from_redis(roi_id)
|
||||||
return self.load_from_redis(roi_id)
|
|
||||||
|
if not bindings:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for bind in bindings:
|
||||||
|
bind_id = bind.get("bind_id")
|
||||||
|
self.reset_algorithm(roi_id, bind_id)
|
||||||
|
self.load_bind_from_redis(bind_id)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重新加载ROI算法配置失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def reload_all_algorithms(self) -> int:
|
def reload_all_algorithms(self) -> int:
|
||||||
"""重新加载所有算法配置"""
|
"""重新加载所有算法配置"""
|
||||||
@@ -432,11 +558,13 @@ class AlgorithmManager:
|
|||||||
try:
|
try:
|
||||||
from core.config_sync import get_config_sync_manager
|
from core.config_sync import get_config_sync_manager
|
||||||
config_manager = get_config_sync_manager()
|
config_manager = get_config_sync_manager()
|
||||||
roi_configs = config_manager.get_all_roi_configs()
|
bindings = config_manager.get_bindings_from_redis("")
|
||||||
|
|
||||||
for roi_config in roi_configs:
|
for bind in bindings:
|
||||||
roi_id = roi_config.get("roi_id")
|
bind_id = bind.get("bind_id")
|
||||||
if self.reload_algorithm(roi_id):
|
roi_id = bind.get("roi_id")
|
||||||
|
self.reset_algorithm(roi_id, bind_id)
|
||||||
|
if self.load_bind_from_redis(bind_id):
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
logger.info(f"已重新加载 {count} 个算法配置")
|
logger.info(f"已重新加载 {count} 个算法配置")
|
||||||
@@ -448,22 +576,29 @@ class AlgorithmManager:
|
|||||||
def register_algorithm(
|
def register_algorithm(
|
||||||
self,
|
self,
|
||||||
roi_id: str,
|
roi_id: str,
|
||||||
|
bind_id: str,
|
||||||
algorithm_type: str,
|
algorithm_type: str,
|
||||||
params: Optional[Dict[str, Any]] = None,
|
params: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
if roi_id in self.algorithms and algorithm_type in self.algorithms[roi_id]:
|
"""注册算法(支持绑定ID)"""
|
||||||
|
key = f"{roi_id}_{bind_id}"
|
||||||
|
|
||||||
|
if key in self.algorithms and algorithm_type in self.algorithms[key]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if roi_id not in self.algorithms:
|
if roi_id not in self.algorithms:
|
||||||
self.algorithms[roi_id] = {}
|
self.algorithms[roi_id] = {}
|
||||||
|
|
||||||
|
if key not in self.algorithms[roi_id]:
|
||||||
|
self.algorithms[roi_id][key] = {}
|
||||||
|
|
||||||
algo_params = self.default_params.get(algorithm_type, {}).copy()
|
algo_params = self.default_params.get(algorithm_type, {}).copy()
|
||||||
if params:
|
if params:
|
||||||
algo_params.update(params)
|
algo_params.update(params)
|
||||||
|
|
||||||
if algorithm_type == "leave_post":
|
if algorithm_type == "leave_post":
|
||||||
roi_working_hours = algo_params.get("working_hours") or self.working_hours
|
roi_working_hours = algo_params.get("working_hours") or self.working_hours
|
||||||
self.algorithms[roi_id]["leave_post"] = LeavePostAlgorithm(
|
self.algorithms[roi_id][key]["leave_post"] = LeavePostAlgorithm(
|
||||||
confirm_on_duty_sec=algo_params.get("confirm_on_duty_sec", 10),
|
confirm_on_duty_sec=algo_params.get("confirm_on_duty_sec", 10),
|
||||||
confirm_leave_sec=algo_params.get("confirm_leave_sec", 10),
|
confirm_leave_sec=algo_params.get("confirm_leave_sec", 10),
|
||||||
cooldown_sec=algo_params.get("cooldown_sec", 300),
|
cooldown_sec=algo_params.get("cooldown_sec", 300),
|
||||||
@@ -471,21 +606,31 @@ class AlgorithmManager:
|
|||||||
target_class=algo_params.get("target_class", "person"),
|
target_class=algo_params.get("target_class", "person"),
|
||||||
)
|
)
|
||||||
elif algorithm_type == "intrusion":
|
elif algorithm_type == "intrusion":
|
||||||
self.algorithms[roi_id]["intrusion"] = IntrusionAlgorithm(
|
self.algorithms[roi_id][key]["intrusion"] = IntrusionAlgorithm(
|
||||||
cooldown_seconds=algo_params.get("cooldown_seconds", 120),
|
cooldown_seconds=algo_params.get("cooldown_seconds", 120),
|
||||||
confirm_seconds=algo_params.get("confirm_seconds", 5),
|
confirm_seconds=algo_params.get("confirm_seconds", 5),
|
||||||
target_class=algo_params.get("target_class"),
|
target_class=algo_params.get("target_class"),
|
||||||
)
|
)
|
||||||
|
elif algorithm_type == "crowd_detection":
|
||||||
|
from algorithms import CrowdDetectionAlgorithm
|
||||||
|
self.algorithms[roi_id][key]["crowd_detection"] = CrowdDetectionAlgorithm(
|
||||||
|
max_count=algo_params.get("max_count", 10),
|
||||||
|
cooldown_seconds=algo_params.get("cooldown_seconds", 300),
|
||||||
|
target_class=algo_params.get("target_class", "person"),
|
||||||
|
)
|
||||||
|
|
||||||
def process(
|
def process(
|
||||||
self,
|
self,
|
||||||
roi_id: str,
|
roi_id: str,
|
||||||
|
bind_id: str,
|
||||||
camera_id: str,
|
camera_id: str,
|
||||||
algorithm_type: str,
|
algorithm_type: str,
|
||||||
tracks: List[Dict],
|
tracks: List[Dict],
|
||||||
current_time: Optional[datetime] = None,
|
current_time: Optional[datetime] = None,
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
algo = self.algorithms.get(roi_id, {}).get(algorithm_type)
|
"""处理检测结果(支持绑定ID)"""
|
||||||
|
key = f"{roi_id}_{bind_id}"
|
||||||
|
algo = self.algorithms.get(roi_id, {}).get(key, {}).get(algorithm_type)
|
||||||
if algo is None:
|
if algo is None:
|
||||||
return []
|
return []
|
||||||
return algo.process(roi_id, camera_id, tracks, current_time)
|
return algo.process(roi_id, camera_id, tracks, current_time)
|
||||||
@@ -493,47 +638,69 @@ class AlgorithmManager:
|
|||||||
def update_roi_params(
|
def update_roi_params(
|
||||||
self,
|
self,
|
||||||
roi_id: str,
|
roi_id: str,
|
||||||
|
bind_id: str,
|
||||||
algorithm_type: str,
|
algorithm_type: str,
|
||||||
params: Dict[str, Any],
|
params: Dict[str, Any],
|
||||||
):
|
):
|
||||||
if roi_id in self.algorithms and algorithm_type in self.algorithms[roi_id]:
|
"""更新算法参数(支持绑定ID)"""
|
||||||
algo = self.algorithms[roi_id][algorithm_type]
|
key = f"{roi_id}_{bind_id}"
|
||||||
|
if roi_id in self.algorithms and key in self.algorithms[roi_id] and algorithm_type in self.algorithms[roi_id][key]:
|
||||||
|
algo = self.algorithms[roi_id][key][algorithm_type]
|
||||||
for key, value in params.items():
|
for key, value in params.items():
|
||||||
if hasattr(algo, key):
|
if hasattr(algo, key):
|
||||||
setattr(algo, key, value)
|
setattr(algo, key, value)
|
||||||
|
|
||||||
def reset_algorithm(self, roi_id: str, algorithm_type: Optional[str] = None):
|
def reset_algorithm(self, roi_id: str, bind_id: Optional[str] = None):
|
||||||
|
"""重置算法状态(支持绑定ID)"""
|
||||||
if roi_id not in self.algorithms:
|
if roi_id not in self.algorithms:
|
||||||
return
|
return
|
||||||
|
|
||||||
if algorithm_type:
|
if bind_id:
|
||||||
if algorithm_type in self.algorithms[roi_id]:
|
key = f"{roi_id}_{bind_id}"
|
||||||
self.algorithms[roi_id][algorithm_type].reset()
|
if key in self.algorithms[roi_id]:
|
||||||
|
if algorithm_type in self.algorithms[roi_id][key]:
|
||||||
|
self.algorithms[roi_id][key][algorithm_type].reset()
|
||||||
else:
|
else:
|
||||||
for algo in self.algorithms[roi_id].values():
|
for key in self.algorithms[roi_id]:
|
||||||
algo.reset()
|
for algo in self.algorithms[roi_id][key].values():
|
||||||
|
algo.reset()
|
||||||
|
|
||||||
def reset_all(self):
|
def reset_all(self):
|
||||||
|
"""重置所有算法"""
|
||||||
for roi_algorithms in self.algorithms.values():
|
for roi_algorithms in self.algorithms.values():
|
||||||
for algo in roi_algorithms.values():
|
for bind_algorithms in roi_algorithms.values():
|
||||||
algo.reset()
|
for algo in bind_algorithms.values():
|
||||||
|
algo.reset()
|
||||||
|
|
||||||
def remove_roi(self, roi_id: str):
|
def remove_roi(self, roi_id: str):
|
||||||
|
"""移除ROI的所有算法"""
|
||||||
if roi_id in self.algorithms:
|
if roi_id in self.algorithms:
|
||||||
self.reset_algorithm(roi_id)
|
for key in list(self.algorithms[roi_id].keys()):
|
||||||
|
self.reset_algorithm(roi_id, key.split("_")[-1] if "_" in key else None)
|
||||||
del self.algorithms[roi_id]
|
del self.algorithms[roi_id]
|
||||||
|
|
||||||
|
def remove_bind(self, roi_id: str, bind_id: str):
|
||||||
|
"""移除绑定的算法"""
|
||||||
|
key = f"{roi_id}_{bind_id}"
|
||||||
|
if roi_id in self.algorithms and key in self.algorithms[roi_id]:
|
||||||
|
for algo in self.algorithms[roi_id][key].values():
|
||||||
|
algo.reset()
|
||||||
|
del self.algorithms[roi_id][key]
|
||||||
|
|
||||||
def get_status(self, roi_id: str) -> Dict[str, Any]:
|
def get_status(self, roi_id: str) -> Dict[str, Any]:
|
||||||
|
"""获取算法状态"""
|
||||||
status = {}
|
status = {}
|
||||||
if roi_id in self.algorithms:
|
if roi_id in self.algorithms:
|
||||||
for algo_type, algo in self.algorithms[roi_id].items():
|
for key, bind_algorithms in self.algorithms[roi_id].items():
|
||||||
if algo_type == "leave_post":
|
bind_id = key.split("_", 1)[-1] if "_" in key else ""
|
||||||
status[algo_type] = {
|
for algo_type, algo in bind_algorithms.items():
|
||||||
"state": getattr(algo, "state", "WAITING"),
|
if algo_type == "leave_post":
|
||||||
"alarm_sent": getattr(algo, "alarm_sent", False),
|
status[f"{algo_type}_{bind_id}"] = {
|
||||||
}
|
"state": getattr(algo, "state", "WAITING"),
|
||||||
else:
|
"alarm_sent": getattr(algo, "alarm_sent", False),
|
||||||
status[algo_type] = {
|
}
|
||||||
"detection_count": len(getattr(algo, "detection_start", {})),
|
else:
|
||||||
}
|
status[f"{algo_type}_{bind_id}"] = {
|
||||||
|
"detection_count": len(getattr(algo, "detection_start", {})),
|
||||||
|
}
|
||||||
return status
|
return status
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ class AlertInfo:
|
|||||||
alert_id: str
|
alert_id: str
|
||||||
camera_id: str
|
camera_id: str
|
||||||
roi_id: str
|
roi_id: str
|
||||||
alert_type: str
|
bind_id: Optional[str] = None
|
||||||
|
alert_type: str = "detection"
|
||||||
target_class: Optional[str] = None
|
target_class: Optional[str] = None
|
||||||
confidence: Optional[float] = None
|
confidence: Optional[float] = None
|
||||||
bbox: Optional[List[float]] = None
|
bbox: Optional[List[float]] = None
|
||||||
@@ -224,6 +225,7 @@ class AlertInfo:
|
|||||||
level: AlertLevel = AlertLevel.MEDIUM
|
level: AlertLevel = AlertLevel.MEDIUM
|
||||||
timestamp: Optional[str] = None
|
timestamp: Optional[str] = None
|
||||||
extra_data: Optional[Dict[str, Any]] = None
|
extra_data: Optional[Dict[str, Any]] = None
|
||||||
|
detections: Optional[str] = None
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""转换为字典"""
|
"""转换为字典"""
|
||||||
@@ -231,6 +233,7 @@ class AlertInfo:
|
|||||||
"alert_id": self.alert_id,
|
"alert_id": self.alert_id,
|
||||||
"camera_id": self.camera_id,
|
"camera_id": self.camera_id,
|
||||||
"roi_id": self.roi_id,
|
"roi_id": self.roi_id,
|
||||||
|
"bind_id": self.bind_id,
|
||||||
"alert_type": self.alert_type,
|
"alert_type": self.alert_type,
|
||||||
"target_class": self.target_class,
|
"target_class": self.target_class,
|
||||||
"confidence": self.confidence,
|
"confidence": self.confidence,
|
||||||
@@ -240,6 +243,7 @@ class AlertInfo:
|
|||||||
"level": self.level.value if isinstance(self.level, AlertLevel) else self.level,
|
"level": self.level.value if isinstance(self.level, AlertLevel) else self.level,
|
||||||
"timestamp": self.timestamp,
|
"timestamp": self.timestamp,
|
||||||
"extra_data": self.extra_data,
|
"extra_data": self.extra_data,
|
||||||
|
"detections": self.detections,
|
||||||
}
|
}
|
||||||
|
|
||||||
def to_json(self) -> str:
|
def to_json(self) -> str:
|
||||||
@@ -291,3 +295,184 @@ class ConfigVersion:
|
|||||||
"description": self.description,
|
"description": self.description,
|
||||||
"affected_items": self.affected_items,
|
"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 os
|
||||||
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
@@ -39,7 +40,8 @@ class AlertRecord:
|
|||||||
alert_id: str
|
alert_id: str
|
||||||
camera_id: str
|
camera_id: str
|
||||||
roi_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
|
target_class: Optional[str] = None
|
||||||
confidence: Optional[float] = None
|
confidence: Optional[float] = None
|
||||||
bbox: Optional[List[float]] = None
|
bbox: Optional[List[float]] = None
|
||||||
@@ -49,6 +51,7 @@ class AlertRecord:
|
|||||||
created_at: datetime = field(default_factory=datetime.now)
|
created_at: datetime = field(default_factory=datetime.now)
|
||||||
processed_at: Optional[datetime] = None
|
processed_at: Optional[datetime] = None
|
||||||
duration_minutes: Optional[float] = None
|
duration_minutes: Optional[float] = None
|
||||||
|
detections: Optional[str] = None # JSON格式的检测结果
|
||||||
|
|
||||||
|
|
||||||
class SQLiteManager:
|
class SQLiteManager:
|
||||||
@@ -108,12 +111,73 @@ class SQLiteManager:
|
|||||||
|
|
||||||
cursor = self._conn.cursor()
|
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("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS alert_records (
|
CREATE TABLE IF NOT EXISTS alert_records (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
alert_id TEXT UNIQUE NOT NULL,
|
alert_id TEXT UNIQUE NOT NULL,
|
||||||
camera_id TEXT NOT NULL,
|
camera_id TEXT NOT NULL,
|
||||||
roi_id TEXT NOT NULL,
|
roi_id TEXT NOT NULL,
|
||||||
|
bind_id TEXT,
|
||||||
alert_type TEXT NOT NULL,
|
alert_type TEXT NOT NULL,
|
||||||
target_class TEXT,
|
target_class TEXT,
|
||||||
confidence REAL,
|
confidence REAL,
|
||||||
@@ -123,7 +187,9 @@ class SQLiteManager:
|
|||||||
status TEXT DEFAULT 'pending',
|
status TEXT DEFAULT 'pending',
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
processed_at TEXT,
|
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
|
CREATE INDEX IF NOT EXISTS idx_alert_status
|
||||||
ON alert_records(status)
|
ON alert_records(status)
|
||||||
""")
|
""")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_alert_bind
|
||||||
|
ON alert_records(bind_id)
|
||||||
|
""")
|
||||||
|
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS camera_configs (
|
CREATE TABLE IF NOT EXISTS camera_configs (
|
||||||
@@ -148,31 +218,12 @@ class SQLiteManager:
|
|||||||
status BOOLEAN DEFAULT 1,
|
status BOOLEAN DEFAULT 1,
|
||||||
enabled BOOLEAN DEFAULT 1,
|
enabled BOOLEAN DEFAULT 1,
|
||||||
location TEXT,
|
location TEXT,
|
||||||
|
roi_group_id TEXT,
|
||||||
extra_params TEXT,
|
extra_params TEXT,
|
||||||
updated_at 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("""
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS config_update_log (
|
CREATE TABLE IF NOT EXISTS config_update_log (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -186,6 +237,66 @@ class SQLiteManager:
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
self._conn.commit()
|
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):
|
def _start_background_threads(self):
|
||||||
"""启动后台线程"""
|
"""启动后台线程"""
|
||||||
@@ -242,15 +353,16 @@ class SQLiteManager:
|
|||||||
for record in batch:
|
for record in batch:
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT OR REPLACE INTO alert_records (
|
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,
|
target_class, confidence, bbox, message,
|
||||||
image_path, status, created_at, processed_at,
|
image_path, status, created_at, processed_at,
|
||||||
duration_minutes
|
duration_minutes, detections
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (
|
""", (
|
||||||
record['alert_id'],
|
record['alert_id'],
|
||||||
record['camera_id'],
|
record['camera_id'],
|
||||||
record['roi_id'],
|
record['roi_id'],
|
||||||
|
record.get('bind_id'),
|
||||||
record['alert_type'],
|
record['alert_type'],
|
||||||
record.get('target_class'),
|
record.get('target_class'),
|
||||||
record.get('confidence'),
|
record.get('confidence'),
|
||||||
@@ -261,6 +373,7 @@ class SQLiteManager:
|
|||||||
record['created_at'],
|
record['created_at'],
|
||||||
record.get('processed_at'),
|
record.get('processed_at'),
|
||||||
record.get('duration_minutes'),
|
record.get('duration_minutes'),
|
||||||
|
record.get('detections'),
|
||||||
))
|
))
|
||||||
|
|
||||||
self._conn.commit()
|
self._conn.commit()
|
||||||
@@ -285,6 +398,7 @@ class SQLiteManager:
|
|||||||
'alert_id': alert.alert_id,
|
'alert_id': alert.alert_id,
|
||||||
'camera_id': alert.camera_id,
|
'camera_id': alert.camera_id,
|
||||||
'roi_id': alert.roi_id,
|
'roi_id': alert.roi_id,
|
||||||
|
'bind_id': alert.bind_id,
|
||||||
'alert_type': alert.alert_type,
|
'alert_type': alert.alert_type,
|
||||||
'target_class': alert.target_class,
|
'target_class': alert.target_class,
|
||||||
'confidence': alert.confidence,
|
'confidence': alert.confidence,
|
||||||
@@ -295,6 +409,7 @@ class SQLiteManager:
|
|||||||
'created_at': alert.created_at.isoformat(),
|
'created_at': alert.created_at.isoformat(),
|
||||||
'processed_at': alert.processed_at.isoformat() if alert.processed_at else None,
|
'processed_at': alert.processed_at.isoformat() if alert.processed_at else None,
|
||||||
'duration_minutes': alert.duration_minutes,
|
'duration_minutes': alert.duration_minutes,
|
||||||
|
'detections': alert.detections,
|
||||||
}
|
}
|
||||||
self._write_queue.put(record)
|
self._write_queue.put(record)
|
||||||
|
|
||||||
@@ -427,14 +542,15 @@ class SQLiteManager:
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT OR REPLACE INTO camera_configs (
|
INSERT OR REPLACE INTO camera_configs (
|
||||||
camera_id, rtsp_url, camera_name, status, enabled,
|
camera_id, rtsp_url, camera_name, status, enabled,
|
||||||
location, extra_params, updated_at
|
location, roi_group_id, extra_params, updated_at
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
""", (
|
""", (
|
||||||
camera_id, rtsp_url,
|
camera_id, rtsp_url,
|
||||||
kwargs.get('camera_name'),
|
kwargs.get('camera_name'),
|
||||||
kwargs.get('status', True),
|
kwargs.get('status', True),
|
||||||
kwargs.get('enabled', True),
|
kwargs.get('enabled', True),
|
||||||
kwargs.get('location'),
|
kwargs.get('location'),
|
||||||
|
kwargs.get('roi_group_id'),
|
||||||
str(kwargs.get('extra_params')) if kwargs.get('extra_params') else None,
|
str(kwargs.get('extra_params')) if kwargs.get('extra_params') else None,
|
||||||
now
|
now
|
||||||
))
|
))
|
||||||
@@ -452,7 +568,7 @@ class SQLiteManager:
|
|||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
columns = ['camera_id', 'rtsp_url', 'camera_name', 'status',
|
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 dict(zip(columns, row))
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -465,7 +581,7 @@ class SQLiteManager:
|
|||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
cursor.execute("SELECT * FROM camera_configs ORDER BY camera_id")
|
cursor.execute("SELECT * FROM camera_configs ORDER BY camera_id")
|
||||||
columns = ['camera_id', 'rtsp_url', 'camera_name', 'status',
|
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()]
|
return [dict(zip(columns, row)) for row in cursor.fetchall()]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取所有摄像头配置失败: {e}")
|
logger.error(f"获取所有摄像头配置失败: {e}")
|
||||||
@@ -483,29 +599,21 @@ class SQLiteManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def save_roi_config(self, roi_id: str, camera_id: str, roi_type: str,
|
def save_roi_config(self, roi_id: str, camera_id: str, roi_type: str,
|
||||||
coordinates: List, algorithm_type: str, **kwargs) -> bool:
|
coordinates: List, **kwargs) -> bool:
|
||||||
"""保存ROI配置"""
|
"""保存ROI配置(空间信息,不包含业务参数)"""
|
||||||
try:
|
try:
|
||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
INSERT OR REPLACE INTO roi_configs (
|
INSERT OR REPLACE INTO roi_configs (
|
||||||
roi_id, camera_id, roi_type, coordinates, algorithm_type,
|
roi_id, camera_id, roi_type, coordinates,
|
||||||
alert_threshold, alert_cooldown, enabled, extra_params,
|
enabled, priority, extra_params, updated_at
|
||||||
working_hours, confirm_on_duty_sec, confirm_leave_sec,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
cooldown_sec, target_class, updated_at
|
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""", (
|
""", (
|
||||||
roi_id, camera_id, roi_type, str(coordinates), algorithm_type,
|
roi_id, camera_id, roi_type, str(coordinates),
|
||||||
kwargs.get('alert_threshold', 3),
|
|
||||||
kwargs.get('alert_cooldown', 300),
|
|
||||||
kwargs.get('enabled', True),
|
kwargs.get('enabled', True),
|
||||||
|
kwargs.get('priority', 0),
|
||||||
str(kwargs.get('extra_params')) if kwargs.get('extra_params') else None,
|
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
|
now
|
||||||
))
|
))
|
||||||
self._conn.commit()
|
self._conn.commit()
|
||||||
@@ -522,20 +630,12 @@ class SQLiteManager:
|
|||||||
row = cursor.fetchone()
|
row = cursor.fetchone()
|
||||||
if row:
|
if row:
|
||||||
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
||||||
'algorithm_type', 'alert_threshold', 'alert_cooldown',
|
'enabled', 'priority', 'extra_params', 'updated_at']
|
||||||
'enabled', 'extra_params', 'working_hours',
|
|
||||||
'confirm_on_duty_sec', 'confirm_leave_sec', 'cooldown_sec',
|
|
||||||
'target_class', 'updated_at']
|
|
||||||
result = dict(zip(columns, row))
|
result = dict(zip(columns, row))
|
||||||
try:
|
try:
|
||||||
result['coordinates'] = eval(result['coordinates'])
|
result['coordinates'] = eval(result['coordinates'])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
if result.get('working_hours'):
|
|
||||||
result['working_hours'] = eval(result['working_hours'])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return result
|
return result
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -548,10 +648,7 @@ class SQLiteManager:
|
|||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
cursor.execute("SELECT * FROM roi_configs WHERE camera_id = ?", (camera_id,))
|
cursor.execute("SELECT * FROM roi_configs WHERE camera_id = ?", (camera_id,))
|
||||||
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
||||||
'algorithm_type', 'alert_threshold', 'alert_cooldown',
|
'enabled', 'priority', 'extra_params', 'updated_at']
|
||||||
'enabled', 'extra_params', 'working_hours',
|
|
||||||
'confirm_on_duty_sec', 'confirm_leave_sec', 'cooldown_sec',
|
|
||||||
'target_class', 'updated_at']
|
|
||||||
results = []
|
results = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
r = dict(zip(columns, row))
|
r = dict(zip(columns, row))
|
||||||
@@ -559,11 +656,6 @@ class SQLiteManager:
|
|||||||
r['coordinates'] = eval(r['coordinates'])
|
r['coordinates'] = eval(r['coordinates'])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
if r.get('working_hours'):
|
|
||||||
r['working_hours'] = eval(r['working_hours'])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
results.append(r)
|
results.append(r)
|
||||||
return results
|
return results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -576,10 +668,7 @@ class SQLiteManager:
|
|||||||
cursor = self._conn.cursor()
|
cursor = self._conn.cursor()
|
||||||
cursor.execute("SELECT * FROM roi_configs ORDER BY camera_id, roi_id")
|
cursor.execute("SELECT * FROM roi_configs ORDER BY camera_id, roi_id")
|
||||||
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
columns = ['roi_id', 'camera_id', 'roi_type', 'coordinates',
|
||||||
'algorithm_type', 'alert_threshold', 'alert_cooldown',
|
'enabled', 'priority', 'extra_params', 'updated_at']
|
||||||
'enabled', 'extra_params', 'working_hours',
|
|
||||||
'confirm_on_duty_sec', 'confirm_leave_sec', 'cooldown_sec',
|
|
||||||
'target_class', 'updated_at']
|
|
||||||
results = []
|
results = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
r = dict(zip(columns, row))
|
r = dict(zip(columns, row))
|
||||||
@@ -587,11 +676,6 @@ class SQLiteManager:
|
|||||||
r['coordinates'] = eval(r['coordinates'])
|
r['coordinates'] = eval(r['coordinates'])
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
|
||||||
if r.get('working_hours'):
|
|
||||||
r['working_hours'] = eval(r['working_hours'])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
results.append(r)
|
results.append(r)
|
||||||
return results
|
return results
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -609,6 +693,200 @@ class SQLiteManager:
|
|||||||
logger.error(f"删除ROI配置失败: {e}")
|
logger.error(f"删除ROI配置失败: {e}")
|
||||||
return False
|
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(
|
def log_config_update(
|
||||||
self,
|
self,
|
||||||
config_type: str,
|
config_type: str,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from redis.client import PubSub
|
|||||||
|
|
||||||
from config.settings import get_settings, RedisConfig
|
from config.settings import get_settings, RedisConfig
|
||||||
from config.database import get_sqlite_manager, SQLiteManager
|
from config.database import get_sqlite_manager, SQLiteManager
|
||||||
from config.config_models import CameraInfo as CameraInfoModel, ROIInfo, ConfigVersion
|
from config.config_models import CameraInfo as CameraInfoModel, ROIInfo, ConfigVersion, ROIInfoNew, ROIAlgoBind
|
||||||
from utils.version_control import get_version_control
|
from utils.version_control import get_version_control
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -282,7 +282,7 @@ class ConfigSyncManager:
|
|||||||
|
|
||||||
def get_roi_configs(self, camera_id: Optional[str] = None,
|
def get_roi_configs(self, camera_id: Optional[str] = None,
|
||||||
force_refresh: bool = False) -> List[ROIInfo]:
|
force_refresh: bool = False) -> List[ROIInfo]:
|
||||||
"""获取ROI配置列表"""
|
"""获取ROI配置列表(兼容旧版本)"""
|
||||||
cache_key = f"rois_{camera_id}" if camera_id else "rois_all"
|
cache_key = f"rois_{camera_id}" if camera_id else "rois_all"
|
||||||
|
|
||||||
if not force_refresh:
|
if not force_refresh:
|
||||||
@@ -312,6 +312,59 @@ class ConfigSyncManager:
|
|||||||
cached = self._cache.get(cache_key)
|
cached = self._cache.get(cache_key)
|
||||||
return cached or []
|
return cached or []
|
||||||
|
|
||||||
|
def get_roi_configs_with_bindings(self, camera_id: Optional[str] = None,
|
||||||
|
force_refresh: bool = False) -> List[ROIInfoNew]:
|
||||||
|
"""获取ROI配置列表(包含算法绑定信息)"""
|
||||||
|
cache_key = f"rois_bindings_{camera_id}" if camera_id else "rois_bindings_all"
|
||||||
|
|
||||||
|
if not force_refresh:
|
||||||
|
cached = self._cache.get(cache_key)
|
||||||
|
if cached is not None:
|
||||||
|
return cached
|
||||||
|
|
||||||
|
self._init_database()
|
||||||
|
|
||||||
|
if self._db_manager is None:
|
||||||
|
logger.warning("数据库管理器不可用,返回空ROI配置列表")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
if camera_id:
|
||||||
|
roi_configs = self._db_manager.get_rois_by_camera(camera_id)
|
||||||
|
bindings_list = self._db_manager.get_bindings_by_camera(camera_id)
|
||||||
|
else:
|
||||||
|
roi_configs = self._db_manager.get_all_roi_configs()
|
||||||
|
bindings_list = []
|
||||||
|
for roi in roi_configs:
|
||||||
|
bindings = self._db_manager.get_bindings_by_roi(roi['roi_id'])
|
||||||
|
bindings_list.extend(bindings)
|
||||||
|
|
||||||
|
roi_dict = {r['roi_id']: r for r in roi_configs}
|
||||||
|
bindings_dict = {}
|
||||||
|
for b in bindings_list:
|
||||||
|
roi_id = b['roi_id']
|
||||||
|
if roi_id not in bindings_dict:
|
||||||
|
bindings_dict[roi_id] = []
|
||||||
|
bindings_dict[roi_id].append(b)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for roi_id, roi_data in roi_dict.items():
|
||||||
|
roi_info = ROIInfoNew.from_dict(roi_data)
|
||||||
|
if roi_id in bindings_dict:
|
||||||
|
roi_info.bindings = [ROIAlgoBind.from_dict(b) for b in bindings_dict[roi_id]]
|
||||||
|
result.append(roi_info)
|
||||||
|
|
||||||
|
result.sort(key=lambda x: x.priority, reverse=True)
|
||||||
|
|
||||||
|
self._cache.set(cache_key, result)
|
||||||
|
logger.info(f"已加载ROI配置(含绑定): {len(result)} 个")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取ROI配置(含绑定)失败: {e}")
|
||||||
|
cached = self._cache.get(cache_key)
|
||||||
|
return cached or []
|
||||||
|
|
||||||
def get_camera_rois(self, camera_id: str) -> List[ROIInfo]:
|
def get_camera_rois(self, camera_id: str) -> List[ROIInfo]:
|
||||||
"""获取指定摄像头的ROI配置"""
|
"""获取指定摄像头的ROI配置"""
|
||||||
return self.get_roi_configs(camera_id=camera_id)
|
return self.get_roi_configs(camera_id=camera_id)
|
||||||
@@ -397,13 +450,8 @@ class ConfigSyncManager:
|
|||||||
"camera_id": roi_config.get("camera_id", ""),
|
"camera_id": roi_config.get("camera_id", ""),
|
||||||
"roi_type": roi_config.get("roi_type", ""),
|
"roi_type": roi_config.get("roi_type", ""),
|
||||||
"coordinates": str(roi_config.get("coordinates", [])),
|
"coordinates": str(roi_config.get("coordinates", [])),
|
||||||
"algorithm_type": roi_config.get("algorithm_type", ""),
|
|
||||||
"working_hours": str(roi_config.get("working_hours", [])),
|
|
||||||
"confirm_on_duty_sec": str(roi_config.get("confirm_on_duty_sec", 10)),
|
|
||||||
"confirm_leave_sec": str(roi_config.get("confirm_leave_sec", 10)),
|
|
||||||
"cooldown_sec": str(roi_config.get("cooldown_sec", 300)),
|
|
||||||
"target_class": roi_config.get("target_class", "person"),
|
|
||||||
"enabled": str(roi_config.get("enabled", True)),
|
"enabled": str(roi_config.get("enabled", True)),
|
||||||
|
"priority": str(roi_config.get("priority", 0)),
|
||||||
})
|
})
|
||||||
|
|
||||||
self._redis_client.expire(key, 3600)
|
self._redis_client.expire(key, 3600)
|
||||||
@@ -413,6 +461,33 @@ class ConfigSyncManager:
|
|||||||
logger.error(f"缓存ROI配置到Redis失败: {e}")
|
logger.error(f"缓存ROI配置到Redis失败: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _cache_algo_bind_to_redis(self, bind_config: Dict[str, Any]) -> bool:
|
||||||
|
"""将ROI算法绑定配置缓存到Redis"""
|
||||||
|
if not self._redis_client:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
bind_id = bind_config.get("bind_id")
|
||||||
|
key = f"config:bind:{bind_id}"
|
||||||
|
|
||||||
|
self._redis_client.hset(key, mapping={
|
||||||
|
"bind_id": bind_id,
|
||||||
|
"roi_id": bind_config.get("roi_id", ""),
|
||||||
|
"algo_code": bind_config.get("algo_code", ""),
|
||||||
|
"params": str(bind_config.get("params", {})),
|
||||||
|
"priority": str(bind_config.get("priority", 0)),
|
||||||
|
"enabled": str(bind_config.get("enabled", True)),
|
||||||
|
"algo_name": bind_config.get("algo_name", ""),
|
||||||
|
"target_class": bind_config.get("target_class", "person"),
|
||||||
|
})
|
||||||
|
|
||||||
|
self._redis_client.expire(key, 3600)
|
||||||
|
logger.debug(f"ROI算法绑定配置已缓存到Redis: {key}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"缓存ROI算法绑定配置到Redis失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def _cache_camera_to_redis(self, camera_config: Dict[str, Any]) -> bool:
|
def _cache_camera_to_redis(self, camera_config: Dict[str, Any]) -> bool:
|
||||||
"""将摄像头配置缓存到Redis"""
|
"""将摄像头配置缓存到Redis"""
|
||||||
if not self._redis_client:
|
if not self._redis_client:
|
||||||
@@ -454,6 +529,12 @@ class ConfigSyncManager:
|
|||||||
if self._cache_roi_to_redis(roi):
|
if self._cache_roi_to_redis(roi):
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
|
self.clear_redis_cache("bind")
|
||||||
|
bindings = self._db_manager.get_bindings_by_camera("")
|
||||||
|
for bind in bindings:
|
||||||
|
if self._cache_algo_bind_to_redis(bind):
|
||||||
|
count += 1
|
||||||
|
|
||||||
logger.info(f"已同步 {count} 条配置到Redis缓存")
|
logger.info(f"已同步 {count} 条配置到Redis缓存")
|
||||||
return count
|
return count
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -469,13 +550,8 @@ class ConfigSyncManager:
|
|||||||
key = f"config:roi:{roi_id}"
|
key = f"config:roi:{roi_id}"
|
||||||
data = self._redis_client.hgetall(key)
|
data = self._redis_client.hgetall(key)
|
||||||
if data:
|
if data:
|
||||||
if data.get('coordinates'):
|
data['coordinates'] = eval(data['coordinates']) if data.get('coordinates') else []
|
||||||
data['coordinates'] = eval(data['coordinates'])
|
data['priority'] = int(data.get('priority', 0))
|
||||||
if data.get('working_hours'):
|
|
||||||
data['working_hours'] = eval(data['working_hours'])
|
|
||||||
data['confirm_on_duty_sec'] = int(data.get('confirm_on_duty_sec', 10))
|
|
||||||
data['confirm_leave_sec'] = int(data.get('confirm_leave_sec', 10))
|
|
||||||
data['cooldown_sec'] = int(data.get('cooldown_sec', 300))
|
|
||||||
data['enabled'] = data.get('enabled', 'True') == 'True'
|
data['enabled'] = data.get('enabled', 'True') == 'True'
|
||||||
return data
|
return data
|
||||||
return None
|
return None
|
||||||
@@ -483,6 +559,47 @@ class ConfigSyncManager:
|
|||||||
logger.error(f"从Redis获取ROI配置失败: {e}")
|
logger.error(f"从Redis获取ROI配置失败: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_algo_bind_from_redis(self, bind_id: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""从Redis获取ROI算法绑定配置"""
|
||||||
|
if not self._redis_client:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
key = f"config:bind:{bind_id}"
|
||||||
|
data = self._redis_client.hgetall(key)
|
||||||
|
if data:
|
||||||
|
data['params'] = eval(data['params']) if data.get('params') else {}
|
||||||
|
data['priority'] = int(data.get('priority', 0))
|
||||||
|
data['enabled'] = data.get('enabled', 'True') == 'True'
|
||||||
|
data['target_class'] = data.get('target_class', 'person')
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从Redis获取ROI算法绑定配置失败: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_bindings_from_redis(self, roi_id: str) -> List[Dict[str, Any]]:
|
||||||
|
"""从Redis获取ROI的所有算法绑定"""
|
||||||
|
if not self._redis_client:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
pattern = "config:bind:*"
|
||||||
|
keys = self._redis_client.keys(pattern)
|
||||||
|
results = []
|
||||||
|
for key in keys:
|
||||||
|
data = self._redis_client.hgetall(key)
|
||||||
|
if data and data.get('roi_id') == roi_id:
|
||||||
|
data['params'] = eval(data['params']) if data.get('params') else {}
|
||||||
|
data['priority'] = int(data.get('priority', 0))
|
||||||
|
data['enabled'] = data.get('enabled', 'True') == 'True'
|
||||||
|
data['target_class'] = data.get('target_class', 'person')
|
||||||
|
results.append(data)
|
||||||
|
return sorted(results, key=lambda x: x['priority'], reverse=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"从Redis获取ROI算法绑定列表失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
def get_camera_from_redis(self, camera_id: str) -> Optional[Dict[str, Any]]:
|
def get_camera_from_redis(self, camera_id: str) -> Optional[Dict[str, Any]]:
|
||||||
"""从Redis获取摄像头配置"""
|
"""从Redis获取摄像头配置"""
|
||||||
if not self._redis_client:
|
if not self._redis_client:
|
||||||
@@ -529,6 +646,10 @@ class ConfigSyncManager:
|
|||||||
keys = self._redis_client.keys("config:camera:*")
|
keys = self._redis_client.keys("config:camera:*")
|
||||||
if keys:
|
if keys:
|
||||||
self._redis_client.delete(*keys)
|
self._redis_client.delete(*keys)
|
||||||
|
elif config_type == "bind":
|
||||||
|
keys = self._redis_client.keys("config:bind:*")
|
||||||
|
if keys:
|
||||||
|
self._redis_client.delete(*keys)
|
||||||
else:
|
else:
|
||||||
keys = self._redis_client.keys("config:*")
|
keys = self._redis_client.keys("config:*")
|
||||||
if keys:
|
if keys:
|
||||||
@@ -543,6 +664,7 @@ class ConfigSyncManager:
|
|||||||
self.clear_redis_cache()
|
self.clear_redis_cache()
|
||||||
count = self.sync_all_to_redis()
|
count = self.sync_all_to_redis()
|
||||||
self.notify_config_change("roi", [])
|
self.notify_config_change("roi", [])
|
||||||
|
self.notify_config_change("bind", [])
|
||||||
logger.info(f"算法配置已重新加载,更新了 {count} 条缓存")
|
logger.info(f"算法配置已重新加载,更新了 {count} 条缓存")
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
|||||||
55
main.py
55
main.py
@@ -182,19 +182,26 @@ class EdgeInferenceService:
|
|||||||
def _process_frame(self, camera_id: str, frame: VideoFrame):
|
def _process_frame(self, camera_id: str, frame: VideoFrame):
|
||||||
"""处理视频帧 - 批量处理多 ROI"""
|
"""处理视频帧 - 批量处理多 ROI"""
|
||||||
try:
|
try:
|
||||||
roi_configs = self._config_manager.get_roi_configs(camera_id)
|
roi_configs = self._config_manager.get_roi_configs_with_bindings(camera_id)
|
||||||
|
|
||||||
roi_items = []
|
roi_items = []
|
||||||
for roi in roi_configs:
|
for roi in roi_configs:
|
||||||
if not roi.enabled:
|
if not roi.enabled:
|
||||||
continue
|
continue
|
||||||
try:
|
if not roi.bindings:
|
||||||
cropped, scale_info = self._preprocessor.preprocess_single(
|
continue
|
||||||
frame.image, roi
|
|
||||||
)
|
for bind in roi.bindings:
|
||||||
roi_items.append((camera_id, roi, frame, cropped, scale_info))
|
if not bind.enabled:
|
||||||
except Exception as e:
|
continue
|
||||||
self._logger.error(f"预处理 ROI 失败 {roi.roi_id}: {e}")
|
|
||||||
|
try:
|
||||||
|
cropped, scale_info = self._preprocessor.preprocess_single(
|
||||||
|
frame.image, roi
|
||||||
|
)
|
||||||
|
roi_items.append((camera_id, roi, bind, frame, cropped, scale_info))
|
||||||
|
except Exception as e:
|
||||||
|
self._logger.error(f"预处理 ROI 失败 {roi.roi_id}: {e}")
|
||||||
|
|
||||||
if not roi_items:
|
if not roi_items:
|
||||||
return
|
return
|
||||||
@@ -222,8 +229,8 @@ class EdgeInferenceService:
|
|||||||
batch_size = len(roi_items)
|
batch_size = len(roi_items)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
images = [item[3] for item in roi_items]
|
images = [item[4] for item in roi_items]
|
||||||
scale_infos = [item[4] for item in roi_items]
|
scale_infos = [item[5] for item in roi_items]
|
||||||
|
|
||||||
batch_data, _ = self._preprocessor._batch_preprocessor.preprocess_batch(
|
batch_data, _ = self._preprocessor._batch_preprocessor.preprocess_batch(
|
||||||
images
|
images
|
||||||
@@ -241,12 +248,12 @@ class EdgeInferenceService:
|
|||||||
conf_threshold=self._settings.inference.conf_threshold
|
conf_threshold=self._settings.inference.conf_threshold
|
||||||
)
|
)
|
||||||
|
|
||||||
for idx, (camera_id, roi, frame, _, scale_info) in enumerate(roi_items):
|
for idx, (camera_id, roi, bind, frame, _, scale_info) in enumerate(roi_items):
|
||||||
boxes, scores, class_ids = batch_results[idx]
|
boxes, scores, class_ids = batch_results[idx]
|
||||||
|
|
||||||
if len(boxes) > 0:
|
if len(boxes) > 0:
|
||||||
self._handle_detections(
|
self._handle_detections(
|
||||||
camera_id, roi, frame,
|
camera_id, roi, bind, frame,
|
||||||
boxes, scores, class_ids,
|
boxes, scores, class_ids,
|
||||||
scale_info
|
scale_info
|
||||||
)
|
)
|
||||||
@@ -287,6 +294,7 @@ class EdgeInferenceService:
|
|||||||
self,
|
self,
|
||||||
camera_id: str,
|
camera_id: str,
|
||||||
roi,
|
roi,
|
||||||
|
bind,
|
||||||
frame: VideoFrame,
|
frame: VideoFrame,
|
||||||
boxes: any,
|
boxes: any,
|
||||||
scores: any,
|
scores: any,
|
||||||
@@ -300,19 +308,14 @@ class EdgeInferenceService:
|
|||||||
return
|
return
|
||||||
|
|
||||||
roi_id = roi.roi_id
|
roi_id = roi.roi_id
|
||||||
algorithm_type = roi.algorithm_type
|
algo_code = bind.algo_code
|
||||||
algo_type_str = algorithm_type.value if hasattr(algorithm_type, 'value') else algorithm_type
|
algo_params = bind.params or {}
|
||||||
|
|
||||||
self._algorithm_manager.register_algorithm(
|
self._algorithm_manager.register_algorithm(
|
||||||
roi_id=roi_id,
|
roi_id=roi_id,
|
||||||
algorithm_type=algo_type_str,
|
bind_id=bind.bind_id,
|
||||||
params={
|
algorithm_type=algo_code,
|
||||||
"working_hours": roi.working_hours,
|
params=algo_params
|
||||||
"confirm_on_duty_sec": roi.confirm_on_duty_sec,
|
|
||||||
"confirm_leave_sec": roi.confirm_leave_sec,
|
|
||||||
"cooldown_sec": roi.cooldown_sec,
|
|
||||||
"target_class": roi.target_class,
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
tracks = self._build_tracks(roi, boxes, scores, class_ids, scale_info)
|
tracks = self._build_tracks(roi, boxes, scores, class_ids, scale_info)
|
||||||
@@ -322,8 +325,9 @@ class EdgeInferenceService:
|
|||||||
|
|
||||||
alerts = self._algorithm_manager.process(
|
alerts = self._algorithm_manager.process(
|
||||||
roi_id=roi_id,
|
roi_id=roi_id,
|
||||||
|
bind_id=bind.bind_id,
|
||||||
camera_id=camera_id,
|
camera_id=camera_id,
|
||||||
algorithm_type=algo_type_str,
|
algorithm_type=algo_code,
|
||||||
tracks=tracks,
|
tracks=tracks,
|
||||||
current_time=frame.timestamp
|
current_time=frame.timestamp
|
||||||
)
|
)
|
||||||
@@ -333,11 +337,12 @@ class EdgeInferenceService:
|
|||||||
|
|
||||||
from core.result_reporter import AlertInfo
|
from core.result_reporter import AlertInfo
|
||||||
alert_info = AlertInfo(
|
alert_info = AlertInfo(
|
||||||
alert_id=f"{roi_id}_{int(frame.timestamp.timestamp())}",
|
alert_id=f"{roi_id}_{bind.bind_id}_{int(frame.timestamp.timestamp())}",
|
||||||
camera_id=camera_id,
|
camera_id=camera_id,
|
||||||
roi_id=roi_id,
|
roi_id=roi_id,
|
||||||
|
bind_id=bind.bind_id,
|
||||||
alert_type=alert.get("alert_type", "detection"),
|
alert_type=alert.get("alert_type", "detection"),
|
||||||
target_class=alert.get("class", "unknown"),
|
target_class=alert.get("class", bind.target_class or "unknown"),
|
||||||
confidence=alert.get("confidence", 1.0),
|
confidence=alert.get("confidence", 1.0),
|
||||||
bbox=alert.get("bbox", []),
|
bbox=alert.get("bbox", []),
|
||||||
message=alert.get("message", ""),
|
message=alert.get("message", ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user