feat: 检测算法开关设置 + AI大模型分析开关设置

This commit is contained in:
2026-04-16 21:41:25 +08:00
parent 7e7a967eb4
commit b3d5c80abf
5 changed files with 185 additions and 72 deletions

View File

@@ -27,6 +27,17 @@ DEFAULT_CONFIG = {
"auto_analyze": True, "auto_analyze": True,
"refresh_interval": 5, # 页面刷新间隔(秒) "refresh_interval": 5, # 页面刷新间隔(秒)
"display_limit": 20, # 显示最近多少条 "display_limit": 20, # 显示最近多少条
# 检测算法开关
"use_haar_cascade": True, # Haar Cascade 人体检测
"use_mediapipe_face": True, # MediaPipe 人脸检测
"use_face_recognition": True, # face_recognition 人脸识别
# AI大模型分析开关
"use_vision_api": False, # 是否使用大模型分析(默认关闭)
"vision_api_trigger": "person_change", # 触发条件person_change/motion/brightness/always
# Vision API 配置
"vision_api_url": os.environ.get("VISION_API_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"), "vision_api_url": os.environ.get("VISION_API_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
"vision_api_key": os.environ.get("VISION_API_KEY", "sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j"), "vision_api_key": os.environ.get("VISION_API_KEY", "sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j"),
"vision_model": os.environ.get("VISION_MODEL", "qwen-vl-plus") "vision_model": os.environ.get("VISION_MODEL", "qwen-vl-plus")

View File

@@ -19,6 +19,8 @@ import os
# 添加项目路径 # 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from config import config_mgr
try: try:
from person_manager import person_manager from person_manager import person_manager
HAS_PERSON_MANAGER = True HAS_PERSON_MANAGER = True
@@ -26,6 +28,12 @@ except ImportError:
HAS_PERSON_MANAGER = False HAS_PERSON_MANAGER = False
print("[LocalAnalyzer] PersonManager not available") print("[LocalAnalyzer] PersonManager not available")
try:
import mediapipe as mp
HAS_MEDIAPIPE = True
except ImportError:
HAS_MEDIAPIPE = False
class LocalAnalyzer: class LocalAnalyzer:
"""本地视觉分析器""" """本地视觉分析器"""
@@ -37,7 +45,7 @@ class LocalAnalyzer:
self.prev_human_count = 0 # 记录前一帧人数 self.prev_human_count = 0 # 记录前一帧人数
# 初始化人体检测器 # 初始化人体检测器
self._init_human_detector() self._init_detectors()
# 阈值配置 # 阈值配置
self.config = { self.config = {
@@ -53,22 +61,22 @@ class LocalAnalyzer:
self.frame_count = 0 self.frame_count = 0
self.motion_count = 0 self.motion_count = 0
self.human_count = 0 self.human_count = 0
self.person_change_count = 0 # 人员变化次数
def _init_human_detector(self): def _init_detectors(self):
"""初始化人体检测器""" """初始化检测器"""
# Haar Cascade 人体检测
try: try:
# 使用 OpenCV 内置的 HOG 人体检测器
self.hog = cv2.HOGDescriptor()
self.hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())
# 或者使用 Haar Cascade更快但精度稍低
cascade_path = cv2.data.haarcascades + 'haarcascade_fullbody.xml' cascade_path = cv2.data.haarcascades + 'haarcascade_fullbody.xml'
if Path(cascade_path).exists(): if Path(cascade_path).exists():
self.human_cascade = cv2.CascadeClassifier(cascade_path) self.human_cascade = cv2.CascadeClassifier(cascade_path)
print("[LocalAnalyzer] Haar Cascade body detector initialized")
print("[LocalAnalyzer] Human detector initialized")
except Exception as e: except Exception as e:
print(f"[LocalAnalyzer] Human detector init failed: {e}") print(f"[LocalAnalyzer] Haar Cascade init failed: {e}")
# MediaPipe 人脸检测状态(由 PersonManager 管理)
if HAS_PERSON_MANAGER:
print("[LocalAnalyzer] PersonManager available (MediaPipe face detection)")
def analyze(self, image_path, prev_image_path=None): def analyze(self, image_path, prev_image_path=None):
""" """
@@ -118,20 +126,26 @@ class LocalAnalyzer:
# 2. 人员检测与识别 # 2. 人员检测与识别
person_result = {'persons': [], 'total_count': 0, 'new_count': 0, 'known_count': 0} person_result = {'persons': [], 'total_count': 0, 'new_count': 0, 'known_count': 0}
haar_result = {'count': 0, 'positions': []}
if HAS_PERSON_MANAGER: # 获取检测算法配置
print(f"[LocalAnalyzer] Using PersonManager for face detection...") use_haar = config_mgr.get('use_haar_cascade', True)
use_mediapipe = config_mgr.get('use_mediapipe_face', True)
# 方法1MediaPipe 人脸检测 + 人员识别(优先)
if HAS_PERSON_MANAGER and use_mediapipe:
print(f"[LocalAnalyzer] Using MediaPipe face detection...")
person_result = person_manager.analyze_image(image_path, save_new_person=True) person_result = person_manager.analyze_image(image_path, save_new_person=True)
metrics['person_count'] = person_result['total_count'] metrics['person_count'] = person_result['total_count']
metrics['new_persons'] = person_result['new_count'] metrics['new_persons'] = person_result['new_count']
metrics['known_persons'] = person_result['known_count'] metrics['known_persons'] = person_result['known_count']
# 记录人员变化 prev_person_count = self.prev_human_count
prev_person_count = self.prev_human_count # 用之前的变量名
person_count_change = person_result['total_count'] - prev_person_count person_count_change = person_result['total_count'] - prev_person_count
metrics['person_count_change'] = person_count_change metrics['person_count_change'] = person_count_change
# 记录人员事件
for person in person_result['persons']: for person in person_result['persons']:
if person['is_new']: if person['is_new']:
events.append({ events.append({
@@ -141,10 +155,11 @@ class LocalAnalyzer:
'source': 'local' 'source': 'local'
}) })
self.human_count += 1 self.human_count += 1
self.person_change_count += 1
else: else:
events.append({ events.append({
'event_type': '人物活动', 'event_type': '人物活动',
'description': f'已知人员: {person["name"]},已访问 {person_manager.persons.get(person["person_id"], {}).get("visit_count", 1)}', 'description': f'已知人员: {person["name"]}',
'confidence': '', 'confidence': '',
'source': 'local' 'source': 'local'
}) })
@@ -157,6 +172,7 @@ class LocalAnalyzer:
'confidence': '', 'confidence': '',
'source': 'local' 'source': 'local'
}) })
self.person_change_count += 1
elif person_count_change < 0: elif person_count_change < 0:
events.append({ events.append({
'event_type': '人员进出', 'event_type': '人员进出',
@@ -164,42 +180,44 @@ class LocalAnalyzer:
'confidence': '', 'confidence': '',
'source': 'local' 'source': 'local'
}) })
self.person_change_count += 1
# 更新前一帧人数
self.prev_human_count = person_result['total_count'] self.prev_human_count = person_result['total_count']
else: # 方法2Haar Cascade 人体检测(备用或并行)
# 使用传统人体检测(备用) if use_haar and self.human_cascade is not None:
human_result = self._detect_human(current_frame) print(f"[LocalAnalyzer] Using Haar Cascade body detection...")
metrics['human_count'] = human_result['count'] haar_result = self._detect_human(current_frame)
human_count_change = human_result['count'] - self.prev_human_count # 如果 MediaPipe 没检测到,用 Haar Cascade 结果
metrics['human_count_change'] = human_count_change if not (HAS_PERSON_MANAGER and use_mediapipe):
metrics['human_count'] = haar_result['count']
if human_count_change > 0: human_count_change = haar_result['count'] - self.prev_human_count
events.append({ metrics['human_count_change'] = human_count_change
'event_type': '人物活动',
'description': f'检测到 {human_count_change} 人进入,当前共 {human_result["count"]}',
'confidence': '',
'source': 'local'
})
self.human_count += human_count_change
elif human_count_change < 0:
events.append({
'event_type': '人物活动',
'description': f'检测到 {abs(human_count_change)} 人离开,当前剩 {human_result["count"]}',
'confidence': '',
'source': 'local'
})
elif human_result['count'] > 0:
events.append({
'event_type': '人物活动',
'description': f'检测到 {human_result["count"]} 个人(无变化)',
'confidence': '',
'source': 'local'
})
self.prev_human_count = human_result['count'] if human_count_change > 0:
events.append({
'event_type': '人员进出',
'description': f'检测到 {human_count_change} 人进入Haar',
'confidence': '',
'source': 'local'
})
self.human_count += human_count_change
self.person_change_count += 1
elif human_count_change < 0:
events.append({
'event_type': '人员进出',
'description': f'检测到 {abs(human_count_change)} 人离开Haar',
'confidence': '',
'source': 'local'
})
self.person_change_count += 1
self.prev_human_count = haar_result['count']
else:
# 并行运行时,记录 Haar 结果但不作为主要判断
metrics['haar_count'] = haar_result['count']
# 3. 亮度检测 # 3. 亮度检测
brightness_result = self._detect_brightness_change(current_gray, prev_image_path) brightness_result = self._detect_brightness_change(current_gray, prev_image_path)
@@ -367,28 +385,44 @@ class LocalAnalyzer:
def _should_call_model(self, metrics, events): def _should_call_model(self, metrics, events):
"""判断是否需要调用大模型""" """判断是否需要调用大模型"""
# 条件1人数变化最重要 # 检查大模型是否启用
current_person_count = metrics.get('person_count', metrics.get('human_count', 0)) use_vision_api = config_mgr.get('use_vision_api', False)
person_count_change = metrics.get('person_count_change', metrics.get('human_count_change', 0)) if not use_vision_api:
print("[LocalAnalyzer] Vision API disabled, skipping model call")
return False
# 更新前一帧人数(如果还没更新) # 获取触发条件配置
if abs(person_count_change) >= self.config['human_count_change_threshold']: trigger_condition = config_mgr.get('vision_api_trigger', 'person_change')
print(f"[LocalAnalyzer] Person count changed: {current_person_count - person_count_change} -> {current_person_count}, triggering model")
return True
# 条件2检测到新人 # 条件1人员变化person_change 模式)
if metrics.get('new_persons', 0) > 0: if trigger_condition in ['person_change', 'always']:
print(f"[LocalAnalyzer] New person detected: {metrics.get('new_persons', 0)}, triggering model") current_person_count = metrics.get('person_count', metrics.get('human_count', 0))
return True person_count_change = metrics.get('person_count_change', metrics.get('human_count_change', 0))
# 条件3运动面积超过阈值排除有人但不动的情况 if abs(person_count_change) >= self.config['human_count_change_threshold']:
if metrics.get('motion_ratio', 0) > self.config['trigger_model_threshold'] * 2: print(f"[LocalAnalyzer] Person change detected: {person_count_change}, triggering model")
print(f"[LocalAnalyzer] Large motion detected: {metrics.get('motion_ratio', 0):.2%}") return True
return True
# 条件4亮度大幅变化灯开关等 # 检测到新人
if abs(metrics.get('brightness_change', 0)) > self.config['brightness_change_threshold'] * 2: if metrics.get('new_persons', 0) > 0:
print(f"[LocalAnalyzer] Brightness changed: {metrics.get('brightness_change', 0)}") print(f"[LocalAnalyzer] New person detected: {metrics.get('new_persons', 0)}, triggering model")
return True
# 条件2运动检测motion 模式)
if trigger_condition in ['motion', 'always']:
if metrics.get('motion_ratio', 0) > self.config['trigger_model_threshold'] * 2:
print(f"[LocalAnalyzer] Large motion detected: {metrics.get('motion_ratio', 0):.2%}")
return True
# 条件3亮度变化brightness 模式)
if trigger_condition in ['brightness', 'always']:
if abs(metrics.get('brightness_change', 0)) > self.config['brightness_change_threshold'] * 2:
print(f"[LocalAnalyzer] Brightness changed: {metrics.get('brightness_change', 0)}")
return True
# always 模式:总是调用
if trigger_condition == 'always':
print("[LocalAnalyzer] Always trigger mode enabled")
return True return True
return False return False

View File

@@ -505,6 +505,17 @@ function loadSettingsForm() {
document.getElementById('setting-auto-analyze').checked = config.auto_analyze !== false; document.getElementById('setting-auto-analyze').checked = config.auto_analyze !== false;
document.getElementById('setting-display-limit').value = config.display_limit || 20; document.getElementById('setting-display-limit').value = config.display_limit || 20;
document.getElementById('setting-refresh-interval').value = config.refresh_interval || 5; document.getElementById('setting-refresh-interval').value = config.refresh_interval || 5;
// Detection algorithm settings
document.getElementById('setting-use-haar').checked = config.use_haar_cascade !== false;
document.getElementById('setting-use-mediapipe').checked = config.use_mediapipe_face !== false;
document.getElementById('setting-use-face-rec').checked = config.use_face_recognition !== false;
// Vision API settings
document.getElementById('setting-use-vision-api').checked = config.use_vision_api === true;
document.getElementById('setting-vision-trigger').value = config.vision_api_trigger || 'person_change';
// API config
document.getElementById('setting-api-url').value = config.vision_api_url || ''; document.getElementById('setting-api-url').value = config.vision_api_url || '';
document.getElementById('setting-api-key').value = config.vision_api_key || ''; document.getElementById('setting-api-key').value = config.vision_api_key || '';
document.getElementById('setting-model').value = config.vision_model || ''; document.getElementById('setting-model').value = config.vision_model || '';
@@ -520,6 +531,17 @@ function saveSettings() {
auto_analyze: document.getElementById('setting-auto-analyze').checked, auto_analyze: document.getElementById('setting-auto-analyze').checked,
display_limit: parseInt(document.getElementById('setting-display-limit').value), display_limit: parseInt(document.getElementById('setting-display-limit').value),
refresh_interval: parseInt(document.getElementById('setting-refresh-interval').value), refresh_interval: parseInt(document.getElementById('setting-refresh-interval').value),
// Detection algorithms
use_haar_cascade: document.getElementById('setting-use-haar').checked,
use_mediapipe_face: document.getElementById('setting-use-mediapipe').checked,
use_face_recognition: document.getElementById('setting-use-face-rec').checked,
// Vision API
use_vision_api: document.getElementById('setting-use-vision-api').checked,
vision_api_trigger: document.getElementById('setting-vision-trigger').value,
// API config
vision_api_url: document.getElementById('setting-api-url').value, vision_api_url: document.getElementById('setting-api-url').value,
vision_api_key: document.getElementById('setting-api-key').value, vision_api_key: document.getElementById('setting-api-key').value,
vision_model: document.getElementById('setting-model').value vision_model: document.getElementById('setting-model').value
@@ -533,14 +555,13 @@ function saveSettings() {
.then(function(res) { return res.json(); }) .then(function(res) { return res.json(); })
.then(function(data) { .then(function(data) {
if (data.success) { if (data.success) {
alert('Settings saved!'); showToast('Settings saved!', 1500);
closeSettingsModal(); closeSettingsModal();
// Reload config to apply new refresh interval
loadConfig(); loadConfig();
refreshAll(); refreshAll();
} }
}) })
.catch(function(e) { alert('Error: ' + e.message); }); .catch(function(e) { showToast('Error: ' + e.message, 3000); });
} }
function browseImagesDir() { function browseImagesDir() {

View File

@@ -137,6 +137,43 @@
</div> </div>
</div> </div>
<div class="settings-section">
<h4>检测算法设置</h4>
<div class="setting-item">
<label>Haar Cascade 人体检测:</label>
<input type="checkbox" id="setting-use-haar" checked>
<span class="setting-desc">传统人体检测(备用)</span>
</div>
<div class="setting-item">
<label>MediaPipe 人脸检测:</label>
<input type="checkbox" id="setting-use-mediapipe" checked>
<span class="setting-desc">高精度人脸检测</span>
</div>
<div class="setting-item">
<label>Face Recognition:</label>
<input type="checkbox" id="setting-use-face-rec" checked>
<span class="setting-desc">人脸识别(识别同一人)</span>
</div>
</div>
<div class="settings-section">
<h4>AI大模型分析</h4>
<div class="setting-item">
<label>启用大模型分析:</label>
<input type="checkbox" id="setting-use-vision-api">
<span class="setting-desc">调用 Vision API 详细分析</span>
</div>
<div class="setting-item">
<label>触发条件:</label>
<select id="setting-vision-trigger">
<option value="person_change">人员变化时</option>
<option value="motion">运动检测时</option>
<option value="brightness">亮度变化时</option>
<option value="always">总是分析</option>
</select>
</div>
</div>
<div class="settings-section"> <div class="settings-section">
<h4>API 设置</h4> <h4>API 设置</h4>
<div class="setting-item"> <div class="setting-item">

View File

@@ -649,6 +649,16 @@ button {
gap: 10px; gap: 10px;
} }
.person-actions {
display: flex;
gap: 10px;
}
.setting-desc {
color: #888;
font-size: 12px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.control-panel { .control-panel {
flex-direction: column; flex-direction: column;