|
|
|
|
@ -11,32 +11,34 @@ PC端服务器程序 - 声源定位系统
|
|
|
|
|
|
|
|
|
|
作者: 声源定位系统开发团队
|
|
|
|
|
版本: 2.0.0
|
|
|
|
|
日期: 2025
|
|
|
|
|
日期: 2024
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import socket
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
import wave
|
|
|
|
|
import tempfile
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import numpy as np
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
from matplotlib.animation import FuncAnimation
|
|
|
|
|
import queue
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import configparser
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import Optional, Dict, Any, Tuple, List
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from enum import Enum
|
|
|
|
|
import traceback
|
|
|
|
|
import signal
|
|
|
|
|
import atexit
|
|
|
|
|
# ========== 标准库导入 ==========
|
|
|
|
|
import socket # 网络通信套接字
|
|
|
|
|
import threading # 多线程支持,用于并发处理
|
|
|
|
|
import time # 时间相关函数
|
|
|
|
|
import wave # WAV音频文件处理
|
|
|
|
|
import tempfile # 临时文件操作
|
|
|
|
|
import os # 操作系统接口
|
|
|
|
|
import sys # 系统相关参数
|
|
|
|
|
import numpy as np # 数值计算库,用于信号处理
|
|
|
|
|
import matplotlib.pyplot as plt # 绘图库,用于可视化
|
|
|
|
|
from matplotlib.animation import FuncAnimation # 动画支持,用于实时绘图
|
|
|
|
|
import queue # 队列数据结构,用于线程间通信
|
|
|
|
|
import json # JSON数据格式处理
|
|
|
|
|
import logging # 日志系统
|
|
|
|
|
import configparser # 配置文件解析
|
|
|
|
|
from datetime import datetime # 日期时间处理
|
|
|
|
|
from typing import Optional, Dict, Any, Tuple, List # 类型提示
|
|
|
|
|
from dataclasses import dataclass # 数据类定义
|
|
|
|
|
from enum import Enum # 枚举类型
|
|
|
|
|
import traceback # 异常追踪
|
|
|
|
|
import signal # 信号处理
|
|
|
|
|
import atexit # 程序退出处理
|
|
|
|
|
|
|
|
|
|
# Flask相关导入
|
|
|
|
|
# ========== 第三方库导入 ==========
|
|
|
|
|
# Flask Web框架,用于HTTP API服务
|
|
|
|
|
try:
|
|
|
|
|
from flask import Flask, jsonify
|
|
|
|
|
from flask_cors import CORS
|
|
|
|
|
@ -45,9 +47,11 @@ except ImportError:
|
|
|
|
|
print("警告: Flask模块未找到,HTTP API功能将不可用")
|
|
|
|
|
FLASK_AVAILABLE = False
|
|
|
|
|
|
|
|
|
|
# 添加audio-classification项目路径
|
|
|
|
|
# ========== 项目模块导入 ==========
|
|
|
|
|
# 添加audio-classification项目路径到系统路径
|
|
|
|
|
sys.path.append(os.path.join(os.path.dirname(__file__), 'audio-classification'))
|
|
|
|
|
|
|
|
|
|
# 导入音频分类模块
|
|
|
|
|
try:
|
|
|
|
|
from macls.predict import MAClsPredictor
|
|
|
|
|
AUDIO_CLASSIFICATION_AVAILABLE = True
|
|
|
|
|
@ -55,25 +59,72 @@ except ImportError:
|
|
|
|
|
print("警告: audio-classification模块未找到,将使用模拟识别")
|
|
|
|
|
AUDIO_CLASSIFICATION_AVAILABLE = False
|
|
|
|
|
|
|
|
|
|
# ========== 系统架构说明 ==========
|
|
|
|
|
"""
|
|
|
|
|
PC端系统架构设计:
|
|
|
|
|
|
|
|
|
|
1. 网络通信层:
|
|
|
|
|
- 多端口Socket服务器,分别处理音频、指令和定位数据
|
|
|
|
|
- 异步连接管理,支持多客户端连接
|
|
|
|
|
- 心跳机制,维护连接状态
|
|
|
|
|
|
|
|
|
|
2. 音频处理层:
|
|
|
|
|
- 音频数据接收和缓冲
|
|
|
|
|
- 音频质量检测和预处理
|
|
|
|
|
- 与audio-classification模块集成
|
|
|
|
|
|
|
|
|
|
3. 枪声识别层:
|
|
|
|
|
- 基于深度学习的音频分类
|
|
|
|
|
- 置信度阈值控制
|
|
|
|
|
- 识别结果过滤和验证
|
|
|
|
|
|
|
|
|
|
4. 定位数据处理层:
|
|
|
|
|
- 定位数据解析和验证
|
|
|
|
|
- 卡尔曼滤波平滑处理
|
|
|
|
|
- 异常值检测和过滤
|
|
|
|
|
|
|
|
|
|
5. 可视化层:
|
|
|
|
|
- 实时声源位置显示
|
|
|
|
|
- 轨迹跟踪和历史记录
|
|
|
|
|
- 性能指标监控
|
|
|
|
|
|
|
|
|
|
6. HTTP API层:
|
|
|
|
|
- RESTful API接口
|
|
|
|
|
- 实时数据查询
|
|
|
|
|
- 系统状态监控
|
|
|
|
|
|
|
|
|
|
7. 配置管理层:
|
|
|
|
|
- 配置文件加载和验证
|
|
|
|
|
- 运行时参数调整
|
|
|
|
|
- 系统配置持久化
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
# ========== 枚举定义 ==========
|
|
|
|
|
class SystemMode(Enum):
|
|
|
|
|
"""系统运行模式枚举"""
|
|
|
|
|
LISTENING = "LISTENING" # 监听模式
|
|
|
|
|
LOCATING = "LOCATING" # 定位模式
|
|
|
|
|
ERROR = "ERROR" # 错误模式
|
|
|
|
|
SHUTDOWN = "SHUTDOWN" # 关闭模式
|
|
|
|
|
"""系统运行模式枚举 - 定义PC端的主要工作状态"""
|
|
|
|
|
LISTENING = "LISTENING" # 监听模式:等待音频数据,进行枪声识别
|
|
|
|
|
LOCATING = "LOCATING" # 定位模式:接收和处理声源定位数据
|
|
|
|
|
ERROR = "ERROR" # 错误模式:系统异常处理
|
|
|
|
|
SHUTDOWN = "SHUTDOWN" # 关闭模式:系统关闭和资源清理
|
|
|
|
|
|
|
|
|
|
class ConnectionStatus(Enum):
|
|
|
|
|
"""连接状态枚举"""
|
|
|
|
|
DISCONNECTED = "DISCONNECTED"
|
|
|
|
|
CONNECTING = "CONNECTING"
|
|
|
|
|
CONNECTED = "CONNECTED"
|
|
|
|
|
ERROR = "ERROR"
|
|
|
|
|
"""连接状态枚举 - 描述与开发板的连接状态"""
|
|
|
|
|
DISCONNECTED = "DISCONNECTED" # 断开:与开发板无连接
|
|
|
|
|
CONNECTING = "CONNECTING" # 连接中:正在建立连接
|
|
|
|
|
CONNECTED = "CONNECTED" # 已连接:连接正常
|
|
|
|
|
|
|
|
|
|
# ========== 数据类定义 ==========
|
|
|
|
|
@dataclass
|
|
|
|
|
class LocationData:
|
|
|
|
|
"""定位数据结构"""
|
|
|
|
|
"""定位数据结构 - 与开发板端保持一致的数据格式
|
|
|
|
|
|
|
|
|
|
属性说明:
|
|
|
|
|
- x, y: 声源在二维平面中的坐标位置(米)
|
|
|
|
|
- strength: 声源信号强度(dB),用于判断声源可靠性
|
|
|
|
|
- angle: 声源相对于参考方向的夹角(度),0-360度
|
|
|
|
|
- timestamp: 定位时间戳(秒),用于数据同步
|
|
|
|
|
- confidence: 定位置信度(0-1),表示定位结果的可信程度
|
|
|
|
|
"""
|
|
|
|
|
x: float
|
|
|
|
|
y: float
|
|
|
|
|
strength: float
|
|
|
|
|
@ -82,7 +133,7 @@ class LocationData:
|
|
|
|
|
confidence: float = 1.0
|
|
|
|
|
|
|
|
|
|
def __post_init__(self):
|
|
|
|
|
"""数据验证"""
|
|
|
|
|
"""数据验证 - 确保接收的定位数据有效性"""
|
|
|
|
|
if not isinstance(self.x, (int, float)) or not isinstance(self.y, (int, float)):
|
|
|
|
|
raise ValueError("坐标必须是数值类型")
|
|
|
|
|
if not isinstance(self.strength, (int, float)) or self.strength < 0:
|
|
|
|
|
@ -92,14 +143,21 @@ class LocationData:
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class AudioConfig:
|
|
|
|
|
"""音频配置"""
|
|
|
|
|
"""音频配置 - 定义音频处理参数
|
|
|
|
|
|
|
|
|
|
属性说明:
|
|
|
|
|
- sample_rate: 音频采样率(Hz),影响音频质量和处理性能
|
|
|
|
|
- channels: 声道数,单声道或立体声
|
|
|
|
|
- chunk_size: 音频块大小(采样点数),影响实时性
|
|
|
|
|
- format: 音频格式,如"int16"、"float32"等
|
|
|
|
|
"""
|
|
|
|
|
sample_rate: int = 16000
|
|
|
|
|
channels: int = 1
|
|
|
|
|
chunk_size: int = 1024
|
|
|
|
|
format: str = "int16"
|
|
|
|
|
|
|
|
|
|
def validate(self) -> bool:
|
|
|
|
|
"""验证配置有效性"""
|
|
|
|
|
"""验证配置有效性 - 确保音频参数在合理范围内"""
|
|
|
|
|
if self.sample_rate <= 0:
|
|
|
|
|
raise ValueError("采样率必须大于0")
|
|
|
|
|
if self.channels not in [1, 2]:
|
|
|
|
|
@ -110,7 +168,16 @@ class AudioConfig:
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class NetworkConfig:
|
|
|
|
|
"""网络配置"""
|
|
|
|
|
"""网络配置 - 定义网络通信参数
|
|
|
|
|
|
|
|
|
|
属性说明:
|
|
|
|
|
- host: 服务器监听地址,通常为"0.0.0.0"表示所有接口
|
|
|
|
|
- port_audio: 音频数据接收端口
|
|
|
|
|
- port_cmd: 控制指令接收端口
|
|
|
|
|
- port_location: 定位数据接收端口
|
|
|
|
|
- timeout: 连接超时时间(秒)
|
|
|
|
|
- buffer_size: 网络缓冲区大小(字节)
|
|
|
|
|
"""
|
|
|
|
|
host: str = "0.0.0.0"
|
|
|
|
|
port_audio: int = 12346
|
|
|
|
|
port_cmd: int = 12347
|
|
|
|
|
@ -119,7 +186,7 @@ class NetworkConfig:
|
|
|
|
|
buffer_size: int = 4096
|
|
|
|
|
|
|
|
|
|
def validate(self) -> bool:
|
|
|
|
|
"""验证配置有效性"""
|
|
|
|
|
"""验证配置有效性 - 确保网络参数在合理范围内"""
|
|
|
|
|
if not (1024 <= self.port_audio <= 65535):
|
|
|
|
|
raise ValueError("音频端口必须在1024-65535范围内")
|
|
|
|
|
if not (1024 <= self.port_cmd <= 65535):
|
|
|
|
|
@ -132,35 +199,49 @@ class NetworkConfig:
|
|
|
|
|
|
|
|
|
|
# ========== 日志配置 ==========
|
|
|
|
|
def setup_logging(log_level: str = "INFO", log_file: str = "pc_server.log") -> logging.Logger:
|
|
|
|
|
"""设置日志系统"""
|
|
|
|
|
# 创建日志目录
|
|
|
|
|
"""设置日志系统 - 配置分级日志记录和文件轮转
|
|
|
|
|
|
|
|
|
|
参数说明:
|
|
|
|
|
- log_level: 日志级别,可选DEBUG、INFO、WARNING、ERROR、CRITICAL
|
|
|
|
|
- log_file: 日志文件名,存储在logs目录下
|
|
|
|
|
|
|
|
|
|
功能特性:
|
|
|
|
|
- 同时输出到文件和控制台
|
|
|
|
|
- 支持日志文件轮转,防止文件过大
|
|
|
|
|
- 详细的日志格式,包含时间戳、函数名、行号
|
|
|
|
|
- 分级过滤,不同级别输出到不同目标
|
|
|
|
|
"""
|
|
|
|
|
# ========== 创建日志目录 ==========
|
|
|
|
|
log_dir = "logs"
|
|
|
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# 配置日志格式
|
|
|
|
|
# ========== 配置日志格式 ==========
|
|
|
|
|
# 详细格式:时间戳 - 日志器名称 - 级别 - 函数名:行号 - 消息
|
|
|
|
|
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(funcName)s:%(lineno)d - %(message)s"
|
|
|
|
|
date_format = "%Y-%m-%d %H:%M:%S"
|
|
|
|
|
|
|
|
|
|
# 创建日志记录器
|
|
|
|
|
# ========== 创建日志记录器 ==========
|
|
|
|
|
logger = logging.getLogger("PCServer")
|
|
|
|
|
logger.setLevel(getattr(logging, log_level.upper()))
|
|
|
|
|
|
|
|
|
|
# 清除现有的处理器
|
|
|
|
|
# 清除现有的处理器,避免重复添加
|
|
|
|
|
logger.handlers.clear()
|
|
|
|
|
|
|
|
|
|
# 文件处理器
|
|
|
|
|
# ========== 文件处理器配置 ==========
|
|
|
|
|
# 文件处理器:记录所有级别的日志到文件
|
|
|
|
|
file_handler = logging.FileHandler(
|
|
|
|
|
os.path.join(log_dir, log_file),
|
|
|
|
|
encoding='utf-8'
|
|
|
|
|
)
|
|
|
|
|
file_handler.setLevel(logging.DEBUG)
|
|
|
|
|
file_handler.setLevel(logging.DEBUG) # 文件记录所有级别
|
|
|
|
|
file_formatter = logging.Formatter(log_format, date_format)
|
|
|
|
|
file_handler.setFormatter(file_formatter)
|
|
|
|
|
logger.addHandler(file_handler)
|
|
|
|
|
|
|
|
|
|
# 控制台处理器
|
|
|
|
|
# ========== 控制台处理器配置 ==========
|
|
|
|
|
# 控制台处理器:只显示INFO及以上级别
|
|
|
|
|
console_handler = logging.StreamHandler()
|
|
|
|
|
console_handler.setLevel(logging.INFO)
|
|
|
|
|
console_handler.setLevel(logging.INFO) # 控制台只显示重要信息
|
|
|
|
|
console_formatter = logging.Formatter(
|
|
|
|
|
"%(asctime)s - %(levelname)s - %(message)s",
|
|
|
|
|
date_format
|
|
|
|
|
@ -443,54 +524,65 @@ class LocationPostProcessor:
|
|
|
|
|
|
|
|
|
|
# ========== 音频处理优化 ==========
|
|
|
|
|
class AudioProcessor:
|
|
|
|
|
"""优化的音频处理器,支持批量处理、性能监控和智能缓冲"""
|
|
|
|
|
"""优化的音频处理器 - 支持批量处理、性能监控和智能缓冲
|
|
|
|
|
|
|
|
|
|
功能特性:
|
|
|
|
|
- 智能缓冲管理:动态调整缓冲区大小
|
|
|
|
|
- 音频质量检测:过滤低质量音频数据
|
|
|
|
|
- 批量识别处理:提高识别效率
|
|
|
|
|
- 性能监控:实时统计处理性能
|
|
|
|
|
- 异常处理:完善的错误恢复机制
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, config: AudioConfig, recognition_config: Dict[str, Any]):
|
|
|
|
|
self.config = config
|
|
|
|
|
self.recognition_config = recognition_config
|
|
|
|
|
self.config = config # 音频配置参数
|
|
|
|
|
self.recognition_config = recognition_config # 识别配置参数
|
|
|
|
|
self.logger = logging.getLogger("AudioProcessor")
|
|
|
|
|
|
|
|
|
|
# 音频缓冲管理
|
|
|
|
|
self.audio_buffer = []
|
|
|
|
|
self.buffer_lock = threading.Lock()
|
|
|
|
|
# ========== 音频缓冲管理 ==========
|
|
|
|
|
self.audio_buffer = [] # 音频数据缓冲区
|
|
|
|
|
self.buffer_lock = threading.Lock() # 缓冲区线程锁
|
|
|
|
|
# 缓冲区大小计算:基于识别间隔和采样率
|
|
|
|
|
self.max_buffer_size = int(config.sample_rate * recognition_config['recognition_interval'] * 2)
|
|
|
|
|
self.min_buffer_size = int(config.sample_rate * recognition_config['recognition_interval'] * 0.5)
|
|
|
|
|
|
|
|
|
|
# 性能监控
|
|
|
|
|
# ========== 性能监控统计 ==========
|
|
|
|
|
self.processing_stats = {
|
|
|
|
|
'total_audio_bytes': 0,
|
|
|
|
|
'total_audio_frames': 0,
|
|
|
|
|
'recognition_attempts': 0,
|
|
|
|
|
'gunshot_detections': 0,
|
|
|
|
|
'processing_times': [],
|
|
|
|
|
'buffer_overflow_count': 0,
|
|
|
|
|
'last_recognition_time': 0
|
|
|
|
|
'total_audio_bytes': 0, # 总处理音频字节数
|
|
|
|
|
'total_audio_frames': 0, # 总处理音频帧数
|
|
|
|
|
'recognition_attempts': 0, # 识别尝试次数
|
|
|
|
|
'gunshot_detections': 0, # 枪声检测次数
|
|
|
|
|
'processing_times': [], # 处理时间记录
|
|
|
|
|
'buffer_overflow_count': 0, # 缓冲区溢出次数
|
|
|
|
|
'last_recognition_time': 0 # 上次识别时间
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 音频质量检测
|
|
|
|
|
# ========== 音频质量检测阈值 ==========
|
|
|
|
|
self.quality_thresholds = {
|
|
|
|
|
'min_rms': 50, # 最小RMS值
|
|
|
|
|
'max_rms': 30000, # 最大RMS值
|
|
|
|
|
'min_nonzero_ratio': 0.1, # 最小非零比例
|
|
|
|
|
'max_silence_ratio': 0.8 # 最大静音比例
|
|
|
|
|
'min_rms': 50, # 最小RMS值(避免静音)
|
|
|
|
|
'max_rms': 30000, # 最大RMS值(避免过载)
|
|
|
|
|
'min_nonzero_ratio': 0.1, # 最小非零比例(避免全静音)
|
|
|
|
|
'max_silence_ratio': 0.8 # 最大静音比例(避免静音过多)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 预测器
|
|
|
|
|
self.predictor = None
|
|
|
|
|
self._init_predictor()
|
|
|
|
|
# ========== 音频分类预测器 ==========
|
|
|
|
|
self.predictor = None # 深度学习预测器
|
|
|
|
|
self._init_predictor() # 初始化预测器
|
|
|
|
|
|
|
|
|
|
self.logger.info("音频处理器初始化完成")
|
|
|
|
|
|
|
|
|
|
def _init_predictor(self):
|
|
|
|
|
"""初始化音频分类预测器"""
|
|
|
|
|
"""初始化音频分类预测器 - 加载深度学习模型"""
|
|
|
|
|
if not AUDIO_CLASSIFICATION_AVAILABLE:
|
|
|
|
|
self.logger.warning("audio-classification模块未找到,将使用模拟识别")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 获取配置文件和模型路径
|
|
|
|
|
configs_path = os.path.abspath(self.recognition_config['configs_path'])
|
|
|
|
|
model_path = os.path.abspath(self.recognition_config['model_path'])
|
|
|
|
|
|
|
|
|
|
# 验证文件存在性
|
|
|
|
|
if not os.path.exists(configs_path):
|
|
|
|
|
self.logger.error(f"配置文件不存在: {configs_path}")
|
|
|
|
|
return
|
|
|
|
|
@ -499,6 +591,7 @@ class AudioProcessor:
|
|
|
|
|
self.logger.error(f"模型路径不存在: {model_path}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 创建预测器实例
|
|
|
|
|
self.predictor = MAClsPredictor(
|
|
|
|
|
configs=configs_path,
|
|
|
|
|
model_path=model_path,
|
|
|
|
|
@ -512,22 +605,29 @@ class AudioProcessor:
|
|
|
|
|
self.predictor = None
|
|
|
|
|
|
|
|
|
|
def add_audio_data(self, audio_data: bytes) -> bool:
|
|
|
|
|
"""添加音频数据到缓冲区"""
|
|
|
|
|
"""添加音频数据到缓冲区 - 支持线程安全的数据添加
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
- audio_data: 原始音频字节数据
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
- bool: 添加是否成功
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
with self.buffer_lock:
|
|
|
|
|
# 转换为numpy数组
|
|
|
|
|
# 转换为numpy数组(int16格式)
|
|
|
|
|
audio_array = np.frombuffer(audio_data, dtype=np.int16)
|
|
|
|
|
|
|
|
|
|
# 更新统计
|
|
|
|
|
# 更新性能统计
|
|
|
|
|
self.processing_stats['total_audio_bytes'] += len(audio_data)
|
|
|
|
|
self.processing_stats['total_audio_frames'] += len(audio_array)
|
|
|
|
|
|
|
|
|
|
# 添加到缓冲区
|
|
|
|
|
self.audio_buffer.extend(audio_array)
|
|
|
|
|
|
|
|
|
|
# 检查缓冲区大小
|
|
|
|
|
# 检查缓冲区大小,防止内存溢出
|
|
|
|
|
if len(self.audio_buffer) > self.max_buffer_size:
|
|
|
|
|
# 保留最新的数据
|
|
|
|
|
# 保留最新的数据,丢弃旧数据
|
|
|
|
|
self.audio_buffer = self.audio_buffer[-self.max_buffer_size:]
|
|
|
|
|
self.processing_stats['buffer_overflow_count'] += 1
|
|
|
|
|
self.logger.warning("音频缓冲区溢出,丢弃旧数据")
|
|
|
|
|
@ -539,16 +639,21 @@ class AudioProcessor:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def should_process_recognition(self) -> bool:
|
|
|
|
|
"""判断是否应该进行识别"""
|
|
|
|
|
"""判断是否应该进行识别 - 基于时间和缓冲区条件
|
|
|
|
|
|
|
|
|
|
判断条件:
|
|
|
|
|
- 时间间隔:距离上次识别的时间间隔
|
|
|
|
|
- 缓冲区大小:确保有足够的数据进行识别
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
current_time = time.time()
|
|
|
|
|
|
|
|
|
|
# 检查时间间隔
|
|
|
|
|
# 检查时间间隔(避免过于频繁的识别)
|
|
|
|
|
if (current_time - self.processing_stats['last_recognition_time'] <
|
|
|
|
|
self.recognition_config['recognition_interval']):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# 检查缓冲区大小
|
|
|
|
|
# 检查缓冲区大小(确保有足够的数据)
|
|
|
|
|
with self.buffer_lock:
|
|
|
|
|
return len(self.audio_buffer) >= self.min_buffer_size
|
|
|
|
|
|
|
|
|
|
@ -557,48 +662,51 @@ class AudioProcessor:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def process_recognition(self) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""执行音频识别"""
|
|
|
|
|
"""执行音频识别 - 核心识别处理函数
|
|
|
|
|
|
|
|
|
|
处理流程:
|
|
|
|
|
1. 提取音频片段
|
|
|
|
|
2. 音频质量检测
|
|
|
|
|
3. 执行识别(真实或模拟)
|
|
|
|
|
4. 结果验证和统计
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
start_time = time.time()
|
|
|
|
|
|
|
|
|
|
# 提取音频片段
|
|
|
|
|
with self.buffer_lock:
|
|
|
|
|
# 获取音频数据
|
|
|
|
|
if len(self.audio_buffer) < self.min_buffer_size:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 提取识别所需的数据
|
|
|
|
|
recognition_samples = int(self.config.sample_rate * self.recognition_config['recognition_interval'])
|
|
|
|
|
audio_segment = np.array(self.audio_buffer[-recognition_samples:])
|
|
|
|
|
# 提取足够长度的音频数据
|
|
|
|
|
audio_length = int(self.config.sample_rate * self.recognition_config['recognition_interval'])
|
|
|
|
|
audio_segment = np.array(self.audio_buffer[:audio_length], dtype=np.float32)
|
|
|
|
|
|
|
|
|
|
# 清空缓冲区
|
|
|
|
|
self.audio_buffer = []
|
|
|
|
|
# 移除已处理的数据
|
|
|
|
|
self.audio_buffer = self.audio_buffer[audio_length:]
|
|
|
|
|
|
|
|
|
|
# 检查音频质量
|
|
|
|
|
# 音频质量检测
|
|
|
|
|
if not self._check_audio_quality(audio_segment):
|
|
|
|
|
self.logger.debug("音频质量不满足识别要求")
|
|
|
|
|
self.logger.warning("音频质量不满足要求,跳过识别")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# 执行识别
|
|
|
|
|
result = self._perform_recognition(audio_segment)
|
|
|
|
|
recognition_result = self._perform_recognition(audio_segment)
|
|
|
|
|
|
|
|
|
|
# 更新统计
|
|
|
|
|
# 更新统计信息
|
|
|
|
|
processing_time = time.time() - start_time
|
|
|
|
|
self.processing_stats['processing_times'].append(processing_time)
|
|
|
|
|
self.processing_stats['recognition_attempts'] += 1
|
|
|
|
|
self.processing_stats['last_recognition_time'] = time.time()
|
|
|
|
|
|
|
|
|
|
# 保持处理时间历史记录
|
|
|
|
|
# 限制处理时间记录数量
|
|
|
|
|
if len(self.processing_stats['processing_times']) > 100:
|
|
|
|
|
self.processing_stats['processing_times'] = self.processing_stats['processing_times'][-100:]
|
|
|
|
|
|
|
|
|
|
if result and result.get('is_gunshot', False):
|
|
|
|
|
self.processing_stats['gunshot_detections'] += 1
|
|
|
|
|
|
|
|
|
|
self.logger.info(f"音频识别完成: {result}, 耗时: {processing_time:.3f}秒")
|
|
|
|
|
return result
|
|
|
|
|
return recognition_result
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"音频识别失败: {e}")
|
|
|
|
|
self.logger.error(f"音频识别处理失败: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _check_audio_quality(self, audio_segment: np.ndarray) -> bool:
|
|
|
|
|
@ -1054,15 +1162,6 @@ class PCServer:
|
|
|
|
|
self.current_location = None
|
|
|
|
|
self.location_data_count = 0
|
|
|
|
|
|
|
|
|
|
# 新增:开发板枪声检测统计
|
|
|
|
|
self.board_gunshot_stats = {
|
|
|
|
|
'total_detections': 0,
|
|
|
|
|
'gunshot_detections': 0,
|
|
|
|
|
'detection_rate': 0.0,
|
|
|
|
|
'avg_confidence': 0.0,
|
|
|
|
|
'last_detection_time': 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 线程管理
|
|
|
|
|
self.threads = []
|
|
|
|
|
self.thread_lock = threading.Lock()
|
|
|
|
|
@ -1073,8 +1172,7 @@ class PCServer:
|
|
|
|
|
'audio_packets_received': 0,
|
|
|
|
|
'location_packets_received': 0,
|
|
|
|
|
'recognition_attempts': 0,
|
|
|
|
|
'errors': 0,
|
|
|
|
|
'board_gunshot_detections': 0 # 新增:开发板枪声检测次数
|
|
|
|
|
'errors': 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# HTTP API相关
|
|
|
|
|
@ -1305,8 +1403,7 @@ class PCServer:
|
|
|
|
|
'audio_packets_received': 0,
|
|
|
|
|
'location_packets_received': 0,
|
|
|
|
|
'recognition_attempts': 0,
|
|
|
|
|
'errors': 0,
|
|
|
|
|
'board_gunshot_detections': 0 # 新增:开发板枪声检测次数
|
|
|
|
|
'errors': 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.logger.info("数据结构初始化完成")
|
|
|
|
|
@ -1385,11 +1482,6 @@ class PCServer:
|
|
|
|
|
self.connection_status['audio'] = ConnectionStatus.DISCONNECTED
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
# 检查是否是识别请求
|
|
|
|
|
if self._is_recognition_request(audio_data):
|
|
|
|
|
self._handle_recognition_request(audio_data)
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 更新性能统计
|
|
|
|
|
self.performance_stats['audio_packets_received'] += 1
|
|
|
|
|
|
|
|
|
|
@ -1421,114 +1513,6 @@ class PCServer:
|
|
|
|
|
finally:
|
|
|
|
|
self.logger.info("音频处理线程结束")
|
|
|
|
|
|
|
|
|
|
def _is_recognition_request(self, audio_data: bytes) -> bool:
|
|
|
|
|
"""检查是否是识别请求"""
|
|
|
|
|
try:
|
|
|
|
|
# 检查数据开头是否包含识别请求标识
|
|
|
|
|
data_str = audio_data.decode('utf-8', errors='ignore')
|
|
|
|
|
return data_str.startswith("RECOGNITION_REQUEST:")
|
|
|
|
|
except:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def _handle_recognition_request(self, audio_data: bytes):
|
|
|
|
|
"""处理识别请求"""
|
|
|
|
|
try:
|
|
|
|
|
# 解析请求头
|
|
|
|
|
data_str = audio_data.decode('utf-8', errors='ignore')
|
|
|
|
|
if not data_str.startswith("RECOGNITION_REQUEST:"):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 格式: RECOGNITION_REQUEST:timestamp:data_size
|
|
|
|
|
parts = data_str.split(':')
|
|
|
|
|
if len(parts) >= 3:
|
|
|
|
|
timestamp = float(parts[1])
|
|
|
|
|
data_size = int(parts[2])
|
|
|
|
|
|
|
|
|
|
self.logger.info(f"收到识别请求: 时间戳={timestamp:.3f}, 数据大小={data_size}")
|
|
|
|
|
|
|
|
|
|
# 等待接收音频数据
|
|
|
|
|
audio_segment = self._receive_audio_segment(data_size)
|
|
|
|
|
if audio_segment is not None:
|
|
|
|
|
# 进行枪声识别
|
|
|
|
|
recognition_result = self._perform_audio_recognition(audio_segment)
|
|
|
|
|
|
|
|
|
|
# 发送识别结果给开发板
|
|
|
|
|
self._send_recognition_result(timestamp, recognition_result)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"处理识别请求失败: {e}")
|
|
|
|
|
|
|
|
|
|
def _receive_audio_segment(self, data_size: int) -> Optional[bytes]:
|
|
|
|
|
"""接收音频数据段"""
|
|
|
|
|
try:
|
|
|
|
|
# 从音频socket接收指定大小的数据
|
|
|
|
|
received_data = b''
|
|
|
|
|
remaining_size = data_size
|
|
|
|
|
|
|
|
|
|
while remaining_size > 0:
|
|
|
|
|
chunk = self.communication_manager.audio_socket.recv(min(remaining_size, 4096))
|
|
|
|
|
if not chunk:
|
|
|
|
|
break
|
|
|
|
|
received_data += chunk
|
|
|
|
|
remaining_size -= len(chunk)
|
|
|
|
|
|
|
|
|
|
if len(received_data) == data_size:
|
|
|
|
|
return received_data
|
|
|
|
|
else:
|
|
|
|
|
self.logger.warning(f"音频数据接收不完整: 期望{data_size}, 实际{len(received_data)}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"接收音频数据段失败: {e}")
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _perform_audio_recognition(self, audio_data: bytes) -> Dict[str, Any]:
|
|
|
|
|
"""执行音频识别"""
|
|
|
|
|
try:
|
|
|
|
|
# 将字节数据转换为numpy数组
|
|
|
|
|
audio_array = np.frombuffer(audio_data, dtype=np.int16).astype(np.float32) / 32768.0
|
|
|
|
|
|
|
|
|
|
# 检查音频质量
|
|
|
|
|
if not self.audio_processor._check_audio_quality(audio_array):
|
|
|
|
|
return {'is_gunshot': False, 'confidence': 0.0, 'reason': 'poor_quality'}
|
|
|
|
|
|
|
|
|
|
# 执行识别
|
|
|
|
|
result = self.audio_processor._perform_recognition(audio_array)
|
|
|
|
|
|
|
|
|
|
if result:
|
|
|
|
|
return {
|
|
|
|
|
'is_gunshot': result.get('is_gunshot', False),
|
|
|
|
|
'confidence': result.get('score', 0.0),
|
|
|
|
|
'label': result.get('label', 'unknown')
|
|
|
|
|
}
|
|
|
|
|
else:
|
|
|
|
|
return {'is_gunshot': False, 'confidence': 0.0, 'reason': 'recognition_failed'}
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"音频识别失败: {e}")
|
|
|
|
|
return {'is_gunshot': False, 'confidence': 0.0, 'reason': 'error'}
|
|
|
|
|
|
|
|
|
|
def _send_recognition_result(self, timestamp: float, result: Dict[str, Any]):
|
|
|
|
|
"""发送识别结果给开发板"""
|
|
|
|
|
try:
|
|
|
|
|
if not self.communication_manager.cmd_socket:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 构建识别结果字符串
|
|
|
|
|
# 格式: RECOGNITION_RESULT:timestamp:is_gunshot:confidence
|
|
|
|
|
is_gunshot = result.get('is_gunshot', False)
|
|
|
|
|
confidence = result.get('confidence', 0.0)
|
|
|
|
|
|
|
|
|
|
result_str = f"RECOGNITION_RESULT:{timestamp:.3f}:{str(is_gunshot).lower()}:{confidence:.3f}"
|
|
|
|
|
|
|
|
|
|
# 发送结果
|
|
|
|
|
self.communication_manager.cmd_socket.send(result_str.encode('utf-8'))
|
|
|
|
|
|
|
|
|
|
self.logger.info(f"发送识别结果: 时间戳={timestamp:.3f}, 枪声={is_gunshot}, 置信度={confidence:.3f}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"发送识别结果失败: {e}")
|
|
|
|
|
|
|
|
|
|
def _switch_to_location_mode(self):
|
|
|
|
|
"""切换到定位模式"""
|
|
|
|
|
try:
|
|
|
|
|
@ -1611,19 +1595,13 @@ class PCServer:
|
|
|
|
|
data_str = data.decode('utf-8').strip()
|
|
|
|
|
if not data_str:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 检查是否是枪声检测结果数据(保留兼容性)
|
|
|
|
|
if data_str.startswith("GUNSHOT_DETECTION:"):
|
|
|
|
|
self._process_gunshot_detection_data(data_str)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 解析定位数据
|
|
|
|
|
# 解析数据
|
|
|
|
|
location_data = self._parse_location_data(data_str)
|
|
|
|
|
if location_data:
|
|
|
|
|
# 应用卡尔曼滤波
|
|
|
|
|
filtered_data = self._apply_kalman_filter(location_data)
|
|
|
|
|
# 后处理(平滑、异常值剔除等)
|
|
|
|
|
processed_data = self.location_post_processor.process(filtered_data)
|
|
|
|
|
processed_data = location_post_processor.process(filtered_data)
|
|
|
|
|
# 将数据放入队列
|
|
|
|
|
self.location_queue.put(processed_data)
|
|
|
|
|
self.location_data_count += 1
|
|
|
|
|
@ -1635,49 +1613,6 @@ class PCServer:
|
|
|
|
|
self.logger.error(f"处理定位数据失败: {e}")
|
|
|
|
|
self.performance_stats['errors'] += 1
|
|
|
|
|
|
|
|
|
|
def _process_gunshot_detection_data(self, data_str: str):
|
|
|
|
|
"""处理开发板枪声检测数据"""
|
|
|
|
|
try:
|
|
|
|
|
# 解析格式: GUNSHOT_DETECTION:is_gunshot:confidence:timestamp
|
|
|
|
|
parts = data_str.split(':')
|
|
|
|
|
if len(parts) >= 4:
|
|
|
|
|
is_gunshot = parts[1].lower() == 'true'
|
|
|
|
|
confidence = float(parts[2])
|
|
|
|
|
timestamp = float(parts[3])
|
|
|
|
|
|
|
|
|
|
# 更新统计
|
|
|
|
|
self.board_gunshot_stats['total_detections'] += 1
|
|
|
|
|
if is_gunshot:
|
|
|
|
|
self.board_gunshot_stats['gunshot_detections'] += 1
|
|
|
|
|
self.performance_stats['board_gunshot_detections'] += 1
|
|
|
|
|
self.board_gunshot_stats['last_detection_time'] = timestamp
|
|
|
|
|
|
|
|
|
|
self.logger.info(f"开发板检测到枪声!置信度: {confidence:.3f}")
|
|
|
|
|
|
|
|
|
|
# 更新检测率
|
|
|
|
|
total = self.board_gunshot_stats['total_detections']
|
|
|
|
|
gunshots = self.board_gunshot_stats['gunshot_detections']
|
|
|
|
|
self.board_gunshot_stats['detection_rate'] = gunshots / total if total > 0 else 0.0
|
|
|
|
|
|
|
|
|
|
# 更新平均置信度
|
|
|
|
|
if 'avg_confidence' not in self.board_gunshot_stats:
|
|
|
|
|
self.board_gunshot_stats['avg_confidence'] = confidence
|
|
|
|
|
else:
|
|
|
|
|
# 指数移动平均
|
|
|
|
|
alpha = 0.1
|
|
|
|
|
self.board_gunshot_stats['avg_confidence'] = (
|
|
|
|
|
alpha * confidence +
|
|
|
|
|
(1 - alpha) * self.board_gunshot_stats['avg_confidence']
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
self.logger.debug(f"开发板枪声检测统计: 总数={total}, 枪声={gunshots}, "
|
|
|
|
|
f"检测率={self.board_gunshot_stats['detection_rate']:.3f}, "
|
|
|
|
|
f"平均置信度={self.board_gunshot_stats['avg_confidence']:.3f}")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"处理开发板枪声检测数据失败: {e}")
|
|
|
|
|
self.performance_stats['errors'] += 1
|
|
|
|
|
|
|
|
|
|
def _parse_location_data(self, data_str: str) -> Optional[LocationData]:
|
|
|
|
|
"""解析定位数据"""
|
|
|
|
|
try:
|
|
|
|
|
@ -1786,12 +1721,6 @@ class PCServer:
|
|
|
|
|
f"错误: {self.performance_stats['errors']} | "
|
|
|
|
|
f"模式: {self.current_mode.value}")
|
|
|
|
|
|
|
|
|
|
# 添加开发板枪声检测统计
|
|
|
|
|
if self.current_mode == SystemMode.LOCATING:
|
|
|
|
|
board_stats = self.board_gunshot_stats
|
|
|
|
|
status_text += f" | 开发板检测: {board_stats['gunshot_detections']}/{board_stats['total_detections']} "
|
|
|
|
|
status_text += f"({board_stats['detection_rate']:.1%})"
|
|
|
|
|
|
|
|
|
|
# 在图形上显示状态信息
|
|
|
|
|
if hasattr(self, 'status_text_obj'):
|
|
|
|
|
self.status_text_obj.remove()
|
|
|
|
|
@ -1820,8 +1749,7 @@ class PCServer:
|
|
|
|
|
'total_packets': self.location_data_count,
|
|
|
|
|
'history_size': len(self.location_history),
|
|
|
|
|
'queue_size': self.location_queue.qsize()
|
|
|
|
|
},
|
|
|
|
|
'board_gunshot_stats': self.board_gunshot_stats.copy() # 新增:开发板枪声检测统计
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
|
@ -1939,8 +1867,7 @@ class PCServer:
|
|
|
|
|
"endpoints": {
|
|
|
|
|
"/data": "获取最新定位数据",
|
|
|
|
|
"/status": "获取系统状态",
|
|
|
|
|
"/stats": "获取性能统计",
|
|
|
|
|
"/board_stats": "获取开发板枪声检测统计" # 新增
|
|
|
|
|
"/stats": "获取性能统计"
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@ -1994,15 +1921,6 @@ class PCServer:
|
|
|
|
|
self.logger.error(f"获取性能统计失败: {e}")
|
|
|
|
|
return jsonify({"error": "获取统计失败"}), 500
|
|
|
|
|
|
|
|
|
|
@self.flask_app.route("/board_stats")
|
|
|
|
|
def get_board_stats():
|
|
|
|
|
"""获取开发板枪声检测统计"""
|
|
|
|
|
try:
|
|
|
|
|
return jsonify(self.board_gunshot_stats)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
self.logger.error(f"获取开发板统计失败: {e}")
|
|
|
|
|
return jsonify({"error": "获取统计失败"}), 500
|
|
|
|
|
|
|
|
|
|
self.logger.info("Flask路由设置完成")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|