feat: 检测算法开关设置 + AI大模型分析开关设置
This commit is contained in:
11
config.py
11
config.py
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
# 方法1:MediaPipe 人脸检测 + 人员识别(优先)
|
||||||
|
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:
|
# 方法2:Haar 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 结果
|
||||||
|
if not (HAS_PERSON_MANAGER and use_mediapipe):
|
||||||
|
metrics['human_count'] = haar_result['count']
|
||||||
|
|
||||||
|
human_count_change = haar_result['count'] - self.prev_human_count
|
||||||
metrics['human_count_change'] = human_count_change
|
metrics['human_count_change'] = human_count_change
|
||||||
|
|
||||||
if human_count_change > 0:
|
if human_count_change > 0:
|
||||||
events.append({
|
events.append({
|
||||||
'event_type': '人物活动',
|
'event_type': '人员进出',
|
||||||
'description': f'检测到 {human_count_change} 人进入,当前共 {human_result["count"]} 人',
|
'description': f'检测到 {human_count_change} 人进入(Haar)',
|
||||||
'confidence': '高',
|
'confidence': '中',
|
||||||
'source': 'local'
|
'source': 'local'
|
||||||
})
|
})
|
||||||
self.human_count += human_count_change
|
self.human_count += human_count_change
|
||||||
|
self.person_change_count += 1
|
||||||
elif human_count_change < 0:
|
elif human_count_change < 0:
|
||||||
events.append({
|
events.append({
|
||||||
'event_type': '人物活动',
|
'event_type': '人员进出',
|
||||||
'description': f'检测到 {abs(human_count_change)} 人离开,当前剩 {human_result["count"]} 人',
|
'description': f'检测到 {abs(human_count_change)} 人离开(Haar)',
|
||||||
'confidence': '高',
|
'confidence': '中',
|
||||||
'source': 'local'
|
|
||||||
})
|
|
||||||
elif human_result['count'] > 0:
|
|
||||||
events.append({
|
|
||||||
'event_type': '人物活动',
|
|
||||||
'description': f'检测到 {human_result["count"]} 个人(无变化)',
|
|
||||||
'confidence': '低',
|
|
||||||
'source': 'local'
|
'source': 'local'
|
||||||
})
|
})
|
||||||
|
self.person_change_count += 1
|
||||||
|
|
||||||
self.prev_human_count = human_result['count']
|
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,30 +385,46 @@ class LocalAnalyzer:
|
|||||||
def _should_call_model(self, metrics, events):
|
def _should_call_model(self, metrics, events):
|
||||||
"""判断是否需要调用大模型"""
|
"""判断是否需要调用大模型"""
|
||||||
|
|
||||||
# 条件1:人数变化(最重要)
|
# 检查大模型是否启用
|
||||||
|
use_vision_api = config_mgr.get('use_vision_api', False)
|
||||||
|
if not use_vision_api:
|
||||||
|
print("[LocalAnalyzer] Vision API disabled, skipping model call")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 获取触发条件配置
|
||||||
|
trigger_condition = config_mgr.get('vision_api_trigger', 'person_change')
|
||||||
|
|
||||||
|
# 条件1:人员变化(person_change 模式)
|
||||||
|
if trigger_condition in ['person_change', 'always']:
|
||||||
current_person_count = metrics.get('person_count', metrics.get('human_count', 0))
|
current_person_count = metrics.get('person_count', metrics.get('human_count', 0))
|
||||||
person_count_change = metrics.get('person_count_change', metrics.get('human_count_change', 0))
|
person_count_change = metrics.get('person_count_change', metrics.get('human_count_change', 0))
|
||||||
|
|
||||||
# 更新前一帧人数(如果还没更新)
|
|
||||||
if abs(person_count_change) >= self.config['human_count_change_threshold']:
|
if abs(person_count_change) >= self.config['human_count_change_threshold']:
|
||||||
print(f"[LocalAnalyzer] Person count changed: {current_person_count - person_count_change} -> {current_person_count}, triggering model")
|
print(f"[LocalAnalyzer] Person change detected: {person_count_change}, triggering model")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 条件2:检测到新人
|
# 检测到新人
|
||||||
if metrics.get('new_persons', 0) > 0:
|
if metrics.get('new_persons', 0) > 0:
|
||||||
print(f"[LocalAnalyzer] New person detected: {metrics.get('new_persons', 0)}, triggering model")
|
print(f"[LocalAnalyzer] New person detected: {metrics.get('new_persons', 0)}, triggering model")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 条件3:运动面积超过阈值(排除有人但不动的情况)
|
# 条件2:运动检测(motion 模式)
|
||||||
|
if trigger_condition in ['motion', 'always']:
|
||||||
if metrics.get('motion_ratio', 0) > self.config['trigger_model_threshold'] * 2:
|
if metrics.get('motion_ratio', 0) > self.config['trigger_model_threshold'] * 2:
|
||||||
print(f"[LocalAnalyzer] Large motion detected: {metrics.get('motion_ratio', 0):.2%}")
|
print(f"[LocalAnalyzer] Large motion detected: {metrics.get('motion_ratio', 0):.2%}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# 条件4:亮度大幅变化(灯开关等)
|
# 条件3:亮度变化(brightness 模式)
|
||||||
|
if trigger_condition in ['brightness', 'always']:
|
||||||
if abs(metrics.get('brightness_change', 0)) > self.config['brightness_change_threshold'] * 2:
|
if abs(metrics.get('brightness_change', 0)) > self.config['brightness_change_threshold'] * 2:
|
||||||
print(f"[LocalAnalyzer] Brightness changed: {metrics.get('brightness_change', 0)}")
|
print(f"[LocalAnalyzer] Brightness changed: {metrics.get('brightness_change', 0)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# always 模式:总是调用
|
||||||
|
if trigger_condition == 'always':
|
||||||
|
print("[LocalAnalyzer] Always trigger mode enabled")
|
||||||
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_stats(self):
|
def get_stats(self):
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user