diff --git a/config.py b/config.py index e77b1e4..6a66211 100644 --- a/config.py +++ b/config.py @@ -27,6 +27,17 @@ DEFAULT_CONFIG = { "auto_analyze": True, "refresh_interval": 5, # 页面刷新间隔(秒) "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_key": os.environ.get("VISION_API_KEY", "sk-lm-fuP5tGU8:Hi7YU87jHyDP6Ay8Tl2j"), "vision_model": os.environ.get("VISION_MODEL", "qwen-vl-plus") diff --git a/local_analyzer.py b/local_analyzer.py index d47e5d4..7a39ddb 100644 --- a/local_analyzer.py +++ b/local_analyzer.py @@ -19,6 +19,8 @@ import os # 添加项目路径 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from config import config_mgr + try: from person_manager import person_manager HAS_PERSON_MANAGER = True @@ -26,6 +28,12 @@ except ImportError: HAS_PERSON_MANAGER = False print("[LocalAnalyzer] PersonManager not available") +try: + import mediapipe as mp + HAS_MEDIAPIPE = True +except ImportError: + HAS_MEDIAPIPE = False + class LocalAnalyzer: """本地视觉分析器""" @@ -37,7 +45,7 @@ class LocalAnalyzer: self.prev_human_count = 0 # 记录前一帧人数 # 初始化人体检测器 - self._init_human_detector() + self._init_detectors() # 阈值配置 self.config = { @@ -53,23 +61,23 @@ class LocalAnalyzer: self.frame_count = 0 self.motion_count = 0 self.human_count = 0 - - def _init_human_detector(self): - """初始化人体检测器""" + self.person_change_count = 0 # 人员变化次数 + + def _init_detectors(self): + """初始化检测器""" + # Haar Cascade 人体检测 try: - # 使用 OpenCV 内置的 HOG 人体检测器 - self.hog = cv2.HOGDescriptor() - self.hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector()) - - # 或者使用 Haar Cascade(更快但精度稍低) cascade_path = cv2.data.haarcascades + 'haarcascade_fullbody.xml' if Path(cascade_path).exists(): self.human_cascade = cv2.CascadeClassifier(cascade_path) - - print("[LocalAnalyzer] Human detector initialized") + print("[LocalAnalyzer] Haar Cascade body detector initialized") 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): """ 分析单张图片 @@ -118,20 +126,26 @@ class LocalAnalyzer: # 2. 人员检测与识别 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) metrics['person_count'] = person_result['total_count'] metrics['new_persons'] = person_result['new_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 metrics['person_count_change'] = person_count_change + # 记录人员事件 for person in person_result['persons']: if person['is_new']: events.append({ @@ -141,10 +155,11 @@ class LocalAnalyzer: 'source': 'local' }) self.human_count += 1 + self.person_change_count += 1 else: events.append({ 'event_type': '人物活动', - 'description': f'已知人员: {person["name"]},已访问 {person_manager.persons.get(person["person_id"], {}).get("visit_count", 1)} 次', + 'description': f'已知人员: {person["name"]}', 'confidence': '高', 'source': 'local' }) @@ -157,6 +172,7 @@ class LocalAnalyzer: 'confidence': '高', 'source': 'local' }) + self.person_change_count += 1 elif person_count_change < 0: events.append({ 'event_type': '人员进出', @@ -164,42 +180,44 @@ class LocalAnalyzer: 'confidence': '高', 'source': 'local' }) + self.person_change_count += 1 - # 更新前一帧人数 self.prev_human_count = person_result['total_count'] + + # 方法2:Haar Cascade 人体检测(备用或并行) + if use_haar and self.human_cascade is not None: + print(f"[LocalAnalyzer] Using Haar Cascade body detection...") + haar_result = self._detect_human(current_frame) - else: - # 使用传统人体检测(备用) - human_result = self._detect_human(current_frame) - metrics['human_count'] = human_result['count'] - - human_count_change = human_result['count'] - self.prev_human_count - metrics['human_count_change'] = human_count_change - - if human_count_change > 0: - events.append({ - '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'] + # 如果 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 + + 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. 亮度检测 brightness_result = self._detect_brightness_change(current_gray, prev_image_path) @@ -367,28 +385,44 @@ class LocalAnalyzer: def _should_call_model(self, metrics, events): """判断是否需要调用大模型""" - # 条件1:人数变化(最重要) - 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)) + # 检查大模型是否启用 + 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 - # 更新前一帧人数(如果还没更新) - 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") - return True + # 获取触发条件配置 + trigger_condition = config_mgr.get('vision_api_trigger', 'person_change') - # 条件2:检测到新人 - if metrics.get('new_persons', 0) > 0: - print(f"[LocalAnalyzer] New person detected: {metrics.get('new_persons', 0)}, triggering model") - return True + # 条件1:人员变化(person_change 模式) + if trigger_condition in ['person_change', 'always']: + 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)) + + if abs(person_count_change) >= self.config['human_count_change_threshold']: + print(f"[LocalAnalyzer] Person change detected: {person_count_change}, triggering model") + return True + + # 检测到新人 + if metrics.get('new_persons', 0) > 0: + print(f"[LocalAnalyzer] New person detected: {metrics.get('new_persons', 0)}, triggering model") + return True - # 条件3:运动面积超过阈值(排除有人但不动的情况) - 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 + # 条件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 - # 条件4:亮度大幅变化(灯开关等) - if abs(metrics.get('brightness_change', 0)) > self.config['brightness_change_threshold'] * 2: - print(f"[LocalAnalyzer] Brightness changed: {metrics.get('brightness_change', 0)}") + # 条件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 False diff --git a/web/static/app.js b/web/static/app.js index 455d08b..20050f1 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -505,6 +505,17 @@ function loadSettingsForm() { document.getElementById('setting-auto-analyze').checked = config.auto_analyze !== false; document.getElementById('setting-display-limit').value = config.display_limit || 20; 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-key').value = config.vision_api_key || ''; document.getElementById('setting-model').value = config.vision_model || ''; @@ -520,6 +531,17 @@ function saveSettings() { auto_analyze: document.getElementById('setting-auto-analyze').checked, display_limit: parseInt(document.getElementById('setting-display-limit').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_key: document.getElementById('setting-api-key').value, vision_model: document.getElementById('setting-model').value @@ -533,14 +555,13 @@ function saveSettings() { .then(function(res) { return res.json(); }) .then(function(data) { if (data.success) { - alert('Settings saved!'); + showToast('Settings saved!', 1500); closeSettingsModal(); - // Reload config to apply new refresh interval loadConfig(); refreshAll(); } }) - .catch(function(e) { alert('Error: ' + e.message); }); + .catch(function(e) { showToast('Error: ' + e.message, 3000); }); } function browseImagesDir() { diff --git a/web/static/index.html b/web/static/index.html index 1b21255..7444501 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -137,6 +137,43 @@ +
+

检测算法设置

+
+ + + 传统人体检测(备用) +
+
+ + + 高精度人脸检测 +
+
+ + + 人脸识别(识别同一人) +
+
+ +
+

AI大模型分析

+
+ + + 调用 Vision API 详细分析 +
+
+ + +
+
+

API 设置

diff --git a/web/static/style.css b/web/static/style.css index 6830864..e1a0b7f 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -649,6 +649,16 @@ button { gap: 10px; } +.person-actions { + display: flex; + gap: 10px; +} + +.setting-desc { + color: #888; + font-size: 12px; +} + @media (max-width: 768px) { .control-panel { flex-direction: column;