基于香橙派的声源定位与分类系统:软件设计与实现

1. 引言

随着人工智能和物联网技术的飞速发展,嵌入式智能系统在安防监控、智能家居、工业检测、人机交互等领域的应用日益广泛。声源定位与声音分类技术作为感知环境的关键技术,能够使机器像人一样“听声辨位”、“闻声识物”,具有极高的研究价值和广阔的应用前景。

传统的声源定位与分类系统多基于高性能PC或工作站,存在成本高、功耗大、体积臃肿、难以部署等问题。而基于ARM架构的嵌入式平台,如树莓派、香橙派等,以其低廉的成本、紧凑的尺寸、较低的功耗和足够的计算能力,为构建轻量级、低功耗、可离线运行的智能声学感知系统提供了理想的硬件基础。

本项目旨在基于香橙派硬件平台,开发一套完整的声源定位与声音信号分类系统。用户已完成了约60%的硬件开发工作,包括麦克风阵列的搭建、香橙派的系统配置与基础驱动编写等。本方案将聚焦于剩下的软件部分,详细阐述如何使用Python和scikit-learn等工具,完成阵列麦克风声源定位、基于小波变换-CEEMDAN-LSTM的声信号分类、嵌入式系统优化以及GUI界面开发四大核心任务,最终形成一个功能完备、运行高效、用户体验良好的集成系统。

2. 系统总体架构设计

在开始具体实现之前,我们需要对整个系统的软件架构进行顶层设计,确保各模块之间耦合度低、协同高效。

2.1 硬件基础回顾

用户提供的硬件基础通常包括:

  • 核心处理单元:香橙派(Orange Pi)某型号(如Orange Pi 4B、Orange Pi Zero 2等),搭载ARM Cortex-A系列多核处理器,运行Linux操作系统。
  • 声学传感单元:一个由多个麦克风(通常是4个或8个)组成的阵列,其几何结构(如线性、圆形、方形)已知。麦克风通过I2S接口或USB声卡与香橙派连接。
  • 其他外围设备:可能包括用于显示结果的屏幕、用于指示状态的LED灯、用于数据传输的网络接口或USB接口等。

2.2 软件架构

本系统的软件采用分层架构,自底向上主要包括:

  1. 硬件抽象层(HAL)

    • 负责与底层硬件驱动交互,提供统一的API读取多通道麦克风阵列的原始音频数据(PCM格式)。
    • 可能涉及使用alsa-libpyaudiosounddevice等库进行音频采集参数的配置(采样率、量化位数、通道数、块大小)和数据读取。
  2. 信号处理与算法核心层

    • 声源定位模块:接收多通道音频数据,通过广义互相关(GCC-PHAT)、可控响应功率(SRP-PHAT)等算法计算声源到达不同麦克风的时间差(TDOA),进而根据阵列几何解算声源的方向(DOA)或位置(X, Y, Z)。
    • 声音分类模块:接收单通道或波束成形后的音频数据,进行预处理、特征提取,并利用训练好的LSTM模型进行分类识别。特征提取融合了小波变换和CEEMDAN两种先进方法。
  3. 模型训练层(通常在开发PC上完成):

    • 这是一个离线过程。使用大规模声音数据集(如UrbanSound8K, ESC-50)训练和验证CEEMDAN-LSTM分类模型。
    • 训练好的模型参数将被导出并部署到香橙派上。
  4. 应用逻辑与GUI层

    • 业务逻辑:协调调度声源定位和声音分类任务,管理数据流,处理用户输入。
    • GUI界面:基于PyQt或Tkinter框架开发图形用户界面,实时显示音频波形、声源定位结果(如极坐标图、位置点)、分类结果、系统状态等信息。
  5. 系统服务层

    • 负责系统自启动、日志记录、资源监控(CPU、内存占用)、网络通信等功能,确保系统稳定可靠运行。

整个系统的数据流是:麦克风阵列采集数据 -> HAL层读取 -> (副本1)声源定位模块 -> 得到方位角/俯仰角 -> GUI显示;(副本2)声音分类模块(可选先进行波束成形增强信噪比)-> 预处理 -> 特征提取 -> LSTM分类 -> 得到类别标签 -> GUI显示。

3. 模块一:阵列麦克风声源定位实现

声源定位是系统的核心功能之一,其目标是估计出空间中声源相对于麦克风阵列的方向或位置。

3.1 算法选择:GCC-PHAT

在众多TDOA估计算法中,广义互相关-相位变换(GCC-PHAT) 因其在混响环境中的鲁棒性和相对较低的计算复杂度,非常适合在香橙派这样的嵌入式平台上实现。

GCC-PHAT的核心思想是通过突出互相关谱的相位信息,削弱混响和噪声的影响,从而更精确地估计时间差。其计算步骤如下:

  1. 预处理:对两路麦克风信号 x1(n)x_1(n)x1(n)x2(n)x_2(n)x2(n) 进行预加重、分帧、加窗(如汉宁窗)。
  2. 傅里叶变换(FFT):将两路信号转换到频域,得到 X1(f)X_1(f)X1(f)X2(f)X_2(f)X2(f)
  3. 计算互功率谱Gx1x2(f)=X1(f)⋅X2∗(f)G_{x_1x_2}(f) = X_1(f) \cdot X_2^*(f)Gx1x2(f)=X1(f)X2(f),其中 ∗* 表示共轭复数。
  4. PHAT加权Φ(f)=1∣Gx1x2(f)∣\Phi(f) = \frac{1}{|G_{x_1x_2}(f)|}Φ(f)=Gx1x2(f)1。这一步是关键,它使所有频率分量的幅值变为1,只保留相位信息。
  5. 计算广义互相关Rx1x2PHAT(τ)=F−1[Φ(f)⋅Gx1x2(f)]R_{x_1x_2}^{PHAT}(\tau) = \mathcal{F}^{-1} [ \Phi(f) \cdot G_{x_1x_2}(f) ]Rx1x2PHAT(τ)=F1[Φ(f)Gx1x2(f)],即对加权后的互功率谱进行逆傅里叶变换。
  6. 寻找峰值:在 Rx1x2PHAT(τ)R_{x_1x_2}^{PHAT}(\tau)Rx1x2PHAT(τ) 中寻找最大峰值的位置 τpeak\tau_{peak}τpeak,该位置即为估计出的时间差 τ^12\hat{\tau}_{12}τ^12

对于M个麦克风的阵列,需要计算 CM2C_M^2CM2 对麦克风组合的TDOA。

3.2 位置解算

获得TDOA估计值 τ^ij\hat{\tau}_{ij}τ^ij 后,即可根据麦克风阵列的几何模型解算声源方向(DOA)。

以最简单的双麦克风线性阵列为例:
假设麦克风间距为 ddd,声速为 ccc,估计出的TDOA为 τ^\hat{\tau}τ^,则声源方位角 θ\thetaθ(相对于阵列法线)可通过下式计算:
sin⁡θ=c⋅τ^d \sin\theta = \frac{c \cdot \hat{\tau}}{d} sinθ=dcτ^
需要注意,τ^\hat{\tau}τ^ 有正负,因此 θ\thetaθ 的范围是 [−90∘,90∘][-90^{\circ}, 90^{\circ}][90,90],存在Front-Back Ambiguity(前后模糊)。

对于更多麦克风的阵列(如圆形阵列),可以通过最小二乘法等方法求解超定方程组,得到更精确的DOA估计,甚至可以直接估计声源的2D或3D坐标。

3.3 Python代码实现要点

import numpy as np
from scipy import signal
from scipy.fft import fft, ifft
import math

def gcc_phat(sig1, sig2, fs, interp=1):
    """
    计算两路信号之间的GCC-PHAT时间差
    参数:
        sig1, sig2: 输入信号
        fs: 采样率
        interp: 插值因子,用于提高峰值检测精度
    返回:
        tau: 估计的时间差 (秒)
        corr: 互相关序列
    """
    n = len(sig1) + len(sig2)
    
    # FFT
    SIG1 = fft(sig1, n)
    SIG2 = fft(sig2, n)
    
    # 计算互功率谱
    R = SIG1 * np.conj(SIG2)
    
    # PHAT加权
    R_phat = R / (np.abs(R) + 1e-10)  # 加上小常数避免除零
    
    # IFFT
    cc = np.real(ifft(R_phat, n))
    
    # 将互相关序列移位,使零延迟在中心
    cc = np.fft.fftshift(cc)
    
    # 插值以提高精度
    if interp > 1:
        cc = signal.resample(cc, interp * n)
        
    # 寻找最大峰值的偏移量
    max_shift = np.argmax(np.abs(cc)) - (interp * n // 2)
    
    # 计算时间差
    tau = max_shift / (interp * fs)
    
    return tau, cc

# 假设我们有一个4麦克风的环形阵列,已知其几何位置
mic_positions = np.array([ ... ])  # 形状为 (4, 2) 或 (4, 3)

def estimate_doa(tdoa_vector, mic_positions, c=343):
    """
    根据TDOA向量和麦克风位置估计声源方向(2D)
    这是一个简化的示例,实际解算更复杂。
    """
    # 这里需要根据阵列几何编写具体的解算算法
    # 例如,对于线性阵列:
    d = np.linalg.norm(mic_positions[1] - mic_positions[0]) # 麦克风间距
    theta = np.arcsin((c * tdoa_vector[0,1]) / d) # 以第一对麦克风为例
    return np.rad2deg(theta)

# 主循环中
def audio_callback(indata, frames, time, status):
    """
    音频流回调函数
    """
    if status:
        print(f"Audio stream status: {status}")
    
    # indata 形状为 (frames, n_channels)
    channels_data = indata.T # 转换为 (n_channels, frames)
    
    # 计算所有麦克风对之间的TDOA
    n_mics = channels_data.shape[0]
    tdoa_matrix = np.zeros((n_mics, n_mics))
    
    for i in range(n_mics):
        for j in range(i+1, n_mics):
            tau, _ = gcc_phat(channels_data[i], channels_data[j], fs=16000, interp=16)
            tdoa_matrix[i, j] = tau
            tdoa_matrix[j, i] = -tau
            
    # 选择一对关键麦克风或使用所有信息解算DOA
    # 例如,使用麦克风0和1
    estimated_angle = estimate_doa(tdoa_matrix, mic_positions)
    
    # 将角度结果传递给GUI或其它模块
    # ...

嵌入式优化:直接使用上述代码在香橙派上可能效率不高。为了实时性,我们可以:

  • 固定点FFT:使用pyfftw库或ARM平台优化的FFT库(如Ne10)。
  • 降低采样率和点数:在满足需求的前提下,使用较低的采样率(如16kHz)和较短的帧长。
  • Cython或C扩展:将计算最密集的GCC-PHAT循环用Cython或C语言编写,编译成Python扩展模块。

4. 模块二:小波变换+CEEMDAN+LSTM的声音信号分类实现

声音分类是系统的另一个核心功能,旨在识别出声音事件的类别(如“汽车鸣笛”、“狗吠”、“敲门声”等)。

4.1 技术路线:混合特征提取与深度学习

传统的MFCC特征在许多场景下表现良好,但对于非平稳、非线性的复杂环境音,其表征能力有时不足。我们采用一种融合小波变换CEEMDAN的混合特征提取方法,再输入到LSTM网络中进行分类,以期获得更优的性能。

  • 小波变换(WT):擅长处理非平稳信号,能同时在时域和频域提供良好的分辨率,可以提取信号在不同尺度和位置上的细节信息。
  • CEEMDAN(Complete Ensemble Empirical Mode Decomposition with Adaptive Noise):是EMD的改进算法,能有效克服模态混叠问题,将复杂信号自适应地分解为一系列本征模态函数(IMF),每个IMF包含了原信号不同时间尺度的局部特征。
  • LSTM(长短期记忆网络):是循环神经网络(RNN)的一种变体,特别擅长处理和分类序列数据(如音频时序信号),能够学习长期依赖关系。

流程:音频信号 -> (分支1) 小波变换 -> 提取小波系数统计特征 -> 特征融合 -> LSTM -> 分类结果。
-> (分支2) CEEMDAN -> 提取IMF分量统计特征 ->

4.2 特征提取流程

  1. 预处理:对音频信号进行预加重、分帧、静音切除、归一化。
  2. 小波特征提取
    • 选择合适的小波基(如’db4’)和分解层数(如5层)。
    • 对每一帧信号进行离散小波变换(DWT),得到近似系数和细节系数。
    • 计算各层系数的统计特征作为特征向量的一部分:均值、标准差、能量、熵等。
  3. CEEMDAN特征提取
    • 对每一帧信号进行CEEMDAN分解,得到若干IMF分量和一个残差。
    • 计算前几个重要IMF分量的统计特征:均值、标准差、能量、过零率等。
  4. 特征融合与标准化:将小波特征和CEEMDAN特征拼接成一个长特征向量,然后进行标准化(如StandardScaler),消除量纲影响。

4.3 LSTM模型构建与训练

# 以下代码通常在开发PC上运行,用于训练模型
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout, Input
from tensorflow.keras.optimizers import Adam
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.model_selection import train_test_split
from PyEMD import CEEMDAN  # 需要安装PyEMD库
import pywt

# 1. 特征提取函数 (示例)
def extract_hybrid_features(audio_frame, fs, ceemdan_max_imf=5, wavelet='db4', level=5):
    """
    提取小波+CEEMDAN混合特征
    """
    features = []
    
    # --- 小波特征 ---
    coeffs = pywt.wavedec(audio_frame, wavelet, level=level)
    for i, coeff in enumerate(coeffs):
        features.append(np.mean(coeff))
        features.append(np.std(coeff))
        features.append(np.sum(coeff**2)) # 能量
        # ... 可以添加更多统计量
        
    # --- CEEMDAN特征 ---
    ceemdan = CEEMDAN()
    imfs = ceemdan(audio_frame, max_imf=ceemdan_max_imf)
    for i, imf in enumerate(imfs[:ceemdan_max_imf]): # 只取前几个IMF
        features.append(np.mean(imf))
        features.append(np.std(imf))
        features.append(np.sum(imf**2))
        features.append(np.mean(0.5 * (np.sign(imf[1:]) - np.sign(imf[:-1])))) # 过零率近似
        # ... 
        
    return np.array(features)

# 2. 加载数据集并提取所有特征 (示例使用UrbanSound8K)
# 假设已经将音频文件加载到X_audio(列表)和标签y中
all_features = []
for audio in X_audio:
    feat = extract_hybrid_features(audio, fs=16000)
    all_features.append(feat)
    
X = np.array(all_features)
y = np.array(y)

# 3. 数据预处理
le = LabelEncoder()
y_encoded = le.fit_transform(y) # 将字符串标签转为整数
# 假设是多元分类,需要one-hot
# y_categorical = to_categorical(y_encoded)

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

X_train, X_test, y_train, y_test = train_test_split(X_scaled, y_encoded, test_size=0.2, random_state=42)

# 4. 构建LSTM模型
# 注意:我们的特征已经不再是原始波形,而是提取后的统计特征向量。
# 因此,我们需要将特征向量视为一个时间步(或者重塑为多个虚拟时间步)。
# 这里我们选择将其视为单时间步长序列。

n_features = X_train.shape[1]
n_classes = len(np.unique(y_encoded))

model = Sequential()
# 输入形状: (None, 1, n_features) -> (批次大小, 时间步长, 特征维度)
# 我们只有一个时间步,所以需要重塑
model.add(Input(shape=(1, n_features))) 
model.add(LSTM(128, return_sequences=False)) # 可以尝试堆叠LSTM层
model.add(Dropout(0.5))
model.add(Dense(64, activation='relu'))
model.add(Dropout(0.3))
model.add(Dense(n_classes, activation='softmax'))

model.compile(optimizer=Adam(learning_rate=0.001),
              loss='sparse_categorical_crossentropy', # 如果y是整数标签用这个
              # loss='categorical_crossentropy', # 如果y是one-hot用这个
              metrics=['accuracy'])

model.summary()

# 5. 训练模型
history = model.fit(
    X_train.reshape(-1, 1, n_features), # 重塑为LSTM输入格式
    y_train,
    epochs=100,
    batch_size=32,
    validation_data=(X_test.reshape(-1, 1, n_features), y_test),
    verbose=1,
    callbacks=[tf.keras.callbacks.EarlyStopping(patience=10, restore_best_weights=True)]
)

# 6. 评估模型
test_loss, test_acc = model.evaluate(X_test.reshape(-1, 1, n_features), y_test, verbose=0)
print(f'\nTest accuracy: {test_acc}')

# 7. 保存模型和Scaler
model.save('sound_classification_lstm_model.h5')
import joblib
joblib.dump(scaler, 'feature_scaler.save')
joblib.dump(le, 'label_encoder.save')

4.4 嵌入式部署与推理

训练好的模型和Scaler将被移植到香橙派上。

# 在香橙派上的推理代码
from tensorflow.keras.models import load_model
import joblib
import numpy as np

class SoundClassifier:
    def __init__(self, model_path, scaler_path, label_encoder_path):
        self.model = load_model(model_path)
        self.scaler = joblib.load(scaler_path)
        self.le = joblib.load(label_encoder_path)
        
    def preprocess_and_predict(self, audio_frame):
        """
        audio_frame: 一维 numpy 数组,单声道音频数据
        """
        # 1. 提取混合特征 (使用与训练时完全相同的函数和参数)
        features = extract_hybrid_features(audio_frame, fs=16000)
        
        # 2. 标准化特征
        features_scaled = self.scaler.transform(features.reshape(1, -1))
        
        # 3. 重塑为LSTM输入格式 (1, 1, n_features)
        features_reshaped = features_scaled.reshape(1, 1, -1)
        
        # 4. 预测
        prediction = self.model.predict(features_reshaped, verbose=0)
        predicted_class_index = np.argmax(prediction, axis=1)[0]
        predicted_label = self.le.inverse_transform([predicted_class_index])[0]
        confidence = prediction[0][predicted_class_index]
        
        return predicted_label, confidence

# 在主程序中使用
classifier = SoundClassifier('sound_classification_lstm_model.h5', 'feature_scaler.save', 'label_encoder.save')

# 在音频回调或处理线程中
def classification_callback(audio_data):
    label, conf = classifier.preprocess_and_predict(audio_data)
    print(f"Predicted: {label} with confidence {conf:.2f}")
    # 将结果发送到GUI

嵌入式优化

  • 模型量化:使用TensorFlow Lite将Keras模型转换为TFLite格式并进行动态范围量化或全整数量化,可以显著减小模型体积和提高推理速度。
    # 转换模型
    converter = tf.lite.TFLiteConverter.from_keras_model(model)
    converter.optimizations = [tf.lite.Optimize.DEFAULT] # 动态范围量化
    tflite_model = converter.convert()
    with open('model_quantized.tflite', 'wb') as f:
        f.write(tflite_model)
    
  • 使用TFLite解释器:在香橙派上使用TFLite解释器加载量化后的模型进行推理,效率更高。
  • 多线程:将特征提取和模型推理放在独立的线程中,避免阻塞音频采集和GUI主线程。

5. 模块三:嵌入式系统开发完善

用户已完成60%的嵌入式开发,本部分主要完善系统集成、优化和稳定性。

5.1 系统配置与依赖管理

  • 操作系统:香橙派通常运行Armbian或基于Debian的系统。确保系统已更新,并安装必要的依赖库:
    sudo apt-get update
    sudo apt-get install python3-pip python3-dev libatlas-base-dev libportaudio2 libportaudiocpp0 portaudio19-dev
    
  • Python环境:建议使用virtualenv创建虚拟环境,隔离项目依赖。
    python3 -m venv orange_pi_venv
    source orange_pi_venv/bin/activate
    
  • 安装Python包:在虚拟环境中安装项目所需的包。由于平台是ARM架构,有些包可能需要从源码编译(如numpy, scipy),这会非常耗时。优先寻找预编译的wheel文件(例如从piwheels.org仓库)。
    pip install numpy scipy scikit-learn tensorflow tensorflowlite pyaudio sounddevice pyqt5 matplotlib pyemd
    # 如果tensorflow没有ARM预编译版,可以尝试tflite-runtime
    pip install tflite-runtime
    

5.2 音频采集模块

使用sounddevicePyAudio库来可靠地读取麦克风阵列。

import sounddevice as sd

# 配置音频流参数
fs = 16000
blocksize = 1024  # 每次回调处理的样本数
channels = 4      # 麦克风数量

# 检查可用设备
print(sd.query_devices())
# 选择正确的设备索引
input_device_index = None # 通常需要指定,例如USB声卡的索引

def audio_callback(indata, frames, time, status):
    # indata 形状为 (frames, channels)
    if status:
        print(f"Audio stream status: {status}")
    # 将数据放入队列,供定位和分类线程消费
    # audio_queue.put(indata.copy())

# 创建音频输入流
stream = sd.InputStream(
    device=input_device_index,
    samplerate=fs,
    blocksize=blocksize,
    channels=channels,
    callback=audio_callback,
    dtype='float32'
)

# 启动和停止流
try:
    with stream:
        while True:
            # 主循环可以做其他事,或者等待停止信号
            sd.sleep(1000)
except KeyboardInterrupt:
    print("Stopped by user")

5.3 多线程设计与资源管理

为了保证实时性,必须采用多线程架构。

  • 主线程:负责GUI事件循环(如果GUI是阻塞的,如PyQt)或系统控制。
  • 音频采集线程:由sounddevice回调函数内部管理,通常运行在一个高优先级的独立线程中。
  • 声源定位线程:从音频队列中获取数据,进行GCC-PHAT计算和DOA估计。
  • 声音分类线程:从音频队列中获取数据(可能是另一路经过波束成形的数据),进行特征提取和模型推理。

使用threading模块和queue模块进行线程间通信。

import threading
import queue
from collections import deque

# 创建数据队列
audio_queue = queue.Queue(maxsize=20) # 防止队列无限增长耗尽内存

# 定位线程函数
def localization_worker():
    while True:
        try:
            audio_data = audio_queue.get(timeout=1.0)
            # 计算DOA...
            # 将结果通过线程安全的方式传递给GUI更新
            # gui_signal.update_angle.emit(estimated_angle)
        except queue.Empty:
            continue

# 分类线程函数
def classification_worker():
    while True:
        # 可能从另一个队列获取数据,或者以更低频率从同一个队列取
        try:
            audio_data_for_classification = classification_queue.get(timeout=2.0)
            # 进行分类...
            # gui_signal.update_class.emit(predicted_label, confidence)
        except queue.Empty:
            continue

# 在音频回调中放入队列
def audio_callback(indata, frames, time, status):
    # ...
    try:
        audio_queue.put_nowait(indata.copy())
    except queue.Full:
        print("Audio queue is full, dropping block.")

# 启动工作线程
loc_thread = threading.Thread(target=localization_worker, daemon=True)
class_thread = threading.Thread(target=classification_worker, daemon=True)
loc_thread.start()
class_thread.start()

5.4 系统自启动与守护

编写一个Shell脚本,在系统启动时自动运行我们的Python程序。

#!/bin/bash
# /home/orange/start_system.sh

cd /path/to/your/project
source orange_pi_venv/bin/activate
python3 main.py

使用systemd创建服务单元文件,实现开机自启和进程守护。

# /etc/systemd/system/sound-system.service
[Unit]
Description=Sound Localization and Classification System
After=network.target sound.target

[Service]
User=orange
Group=orange
WorkingDirectory=/path/to/your/project
ExecStart=/bin/bash /home/orange/start_system.sh
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

然后启用服务:

sudo systemctl enable sound-system.service
sudo systemctl start sound-system.service

6. 模块四:GUI界面开发

GUI是用户与系统交互的窗口,需要直观地展示核心信息。

6.1 框架选择:PyQt5

PyQt5功能强大、跨平台、UI设计灵活,适合开发复杂的桌面应用程序。

6.2 界面设计

主界面主要包含以下区域:

  1. 波形显示区:实时绘制至少一个通道的音频波形图(使用matplotlibPyQtGraph)。
  2. 声源定位结果显示区
    • 极坐标图:用一个动态更新的点或指针显示当前声源的方向角。
    • 数值显示:以数字形式显示方位角和俯仰角(如果有)。
  3. 声音分类结果显示区:以标签或列表形式显示当前识别出的声音类别及其置信度。
  4. 控制区:开始/停止按钮、系统状态指示灯、参数设置入口等。

6.3 代码实现要点

import sys
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QPushButton, QGroupBox, QStatusBar)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
import pyqtgraph as pg # 用于高性能绘图
import numpy as np

# 定义一个用于线程间通信的信号类
class GuiSignals(QObject):
    update_waveform = pyqtSignal(np.ndarray) # 传递波形数据
    update_angle = pyqtSignal(float)          # 传递角度数据
    update_classification = pyqtSignal(str, float) # 传递类别和置信度

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.signals = GuiSignals()
        self.initUI()
        self.connectSignals()
        
    def initUI(self):
        self.setWindowTitle('香橙派声源定位与分类系统')
        self.setGeometry(100, 100, 1200, 800)
        
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QHBoxLayout(central_widget)
        
        # 左侧:控制和状态面板
        left_panel = QVBoxLayout()
        control_group = QGroupBox("系统控制")
        control_layout = QVBoxLayout()
        self.start_btn = QPushButton("开始")
        self.stop_btn = QPushButton("停止")
        self.stop_btn.setEnabled(False)
        control_layout.addWidget(self.start_btn)
        control_layout.addWidget(self.stop_btn)
        control_group.setLayout(control_layout)
        left_panel.addWidget(control_group)
        
        # ... 可以添加其他状态信息显示
        main_layout.addLayout(left_panel, 1)
        
        # 右侧:数据显示面板
        right_panel = QVBoxLayout()
        
        # 1. 波形图
        wave_group = QGroupBox("实时波形")
        wave_layout = QVBoxLayout()
        self.wave_plot = pg.PlotWidget()
        self.wave_plot.setYRange(-1, 1)
        self.wave_curve = self.wave_plot.plot(pen='y')
        wave_layout.addWidget(self.wave_plot)
        wave_group.setLayout(wave_layout)
        right_panel.addWidget(wave_group)
        
        # 2. 极坐标图 (声源定位)
        loc_group = QGroupBox("声源定位")
        loc_layout = QVBoxLayout()
        self.polar_plot = pg.PlotWidget()
        self.polar_plot.setAspectLocked(True)
        self.polar_plot.setXRange(-1.2, 1.2)
        self.polar_plot.setYRange(-1.2, 1.2)
        # 绘制极坐标网格(略)
        self.angle_scatter = pg.ScatterPlotItem(size=15, pen=pg.mkPen(None), brush=pg.mkBrush(255, 0, 0, 200))
        self.polar_plot.addItem(self.angle_scatter)
        self.angle_label = QLabel("方位角: -- °")
        loc_layout.addWidget(self.polar_plot)
        loc_layout.addWidget(self.angle_label)
        loc_group.setLayout(loc_layout)
        right_panel.addWidget(loc_group)
        
        # 3. 分类结果
        class_group = QGroupBox("声音分类")
        class_layout = QVBoxLayout()
        self.class_label = QLabel("类别: --")
        self.confidence_label = QLabel("置信度: --")
        class_layout.addWidget(self.class_label)
        class_layout.addWidget(self.confidence_label)
        class_group.setLayout(class_layout)
        right_panel.addWidget(class_group)
        
        main_layout.addLayout(right_panel, 3)
        
        self.statusBar().showMessage('系统就绪')
        
        # 连接按钮信号
        self.start_btn.clicked.connect(self.start_system)
        self.stop_btn.clicked.connect(self.stop_system)
        
    def connectSignals(self):
        # 连接自定义信号到槽函数
        self.signals.update_waveform.connect(self.update_waveform_plot)
        self.signals.update_angle.connect(self.update_angle_display)
        self.signals.update_classification.connect(self.update_classification_display)
        
    def update_waveform_plot(self, data):
        """ 更新波形图 """
        # 假设data是单通道数据
        x = np.arange(len(data))
        self.wave_curve.setData(x, data)
        
    def update_angle_display(self, angle_deg):
        """ 更新声源定位显示 """
        self.angle_label.setText(f"方位角: {angle_deg:.1f} °")
        # 在极坐标图上更新点
        angle_rad = np.deg2rad(angle_deg)
        x = np.cos(angle_rad)
        y = np.sin(angle_rad)
        self.angle_scatter.setData([x], [y])
        
    def update_classification_display(self, label, confidence):
        """ 更新分类结果显示 """
        self.class_label.setText(f"类别: {label}")
        self.confidence_label.setText(f"置信度: {confidence:.2%}")
        
    def start_system(self):
        # 这里触发音频流开始、工作线程启动
        self.statusBar().showMessage('系统运行中...')
        self.start_btn.setEnabled(False)
        self.stop_btn.setEnabled(True)
        # ... 调用后台逻辑的开始函数
        
    def stop_system(self):
        # 这里触发音频流停止、工作线程结束
        self.statusBar().showMessage('系统已停止')
        self.start_btn.setEnabled(True)
        self.stop_btn.setEnabled(False)
        # ... 调用后台逻辑的停止函数

if __name__ == '__main__':
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    
    # 在这里创建并启动后台线程和音频流,并将main_window.signals传递给它们
    
    sys.exit(app.exec_())

6.4 跨线程更新GUI

重要:所有对GUI组件的更新都必须在主线程中进行。我们使用PyQt的pyqtSignal机制,工作线程发射信号,主线程中的槽函数负责接收信号并更新UI,这是线程安全的。

7. 系统集成、测试与优化

7.1 系统集成

将上述所有模块整合到一个主程序main.py中:

  • 初始化GUI。
  • 初始化声源定位器和声音分类器。
  • 创建音频流和数据队列。
  • 启动工作线程。
  • 将工作线程与GUI信号连接起来。

7.2 测试方案

  1. 单元测试:分别测试GCC-PHAT函数、特征提取函数、模型推理函数等。
  2. 功能测试
    • 声源定位测试:在消音室或安静环境中,使用扬声器在不同方位播放白噪声或脉冲信号,检验系统估计的角度是否准确。
    • 声音分类测试:播放已知类别的声音文件,检验系统识别结果是否正确。
  3. 性能测试
    • 实时性测试:使用系统时钟测量从音频采集到结果显示的总延迟,确保满足实时性要求(通常<100ms)。
    • 资源消耗测试:使用top, htop等工具监控CPU和内存占用率。确保在香橙派上运行稳定,不会因资源耗尽而崩溃。
  4. 鲁棒性测试:在存在背景噪声、混响、多个声源的环境中进行测试,评估系统性能下降程度。

7.3 性能优化总结

  • 算法层面:选择计算复杂度低的算法(如GCC-PHAT),调整帧长、重叠率等参数。
  • 代码层面:使用向量化操作(NumPy),避免Python循环。对性能瓶颈部分使用C扩展(Cython)。
  • 模型层面:使用轻量级模型(如TFLite量化模型),减少层数和神经元数量。
  • 系统层面:使用多线程充分利用多核CPU。调整线程优先级。
  • 编译安装:为ARM平台编译安装优化的科学计算库(如OpenBLAS)。

8. 结论与展望

本项目基于香橙派嵌入式平台,成功设计并实现了一套集成声源定位和环境声音分类的智能声学感知系统。软件部分完成了从底层音频采集、核心算法(GCC-PHAT、小波-CEEMDAN-LSTM)实现、到上层GUI界面开发的全部工作。系统能够实时显示声音波形、估计声源方向并识别声音类别,达到了预期目标。

未来展望

  1. 深度学习端到端定位:探索使用深度学习模型(如CNN、CRNN)直接从多通道原始音频或特征中估计声源方向,避免复杂的TDOA几何解算。
  2. 多声源处理:升级算法以支持同时定位和分离多个声源。
  3. 模型轻量化与知识蒸馏:进一步压缩模型,探索更适合嵌入式设备的网络结构(如MobileNet、SqueezeNet应用于声学特征)。
  4. 云端协同:将复杂的模型推理任务卸载到云端服务器,香橙派只负责前端采集和简单处理,实现更复杂的功能。
  5. 应用拓展:将系统具体应用于智能家居控制(声控)、安防监控(异常声音检测与定位)、视频会议跟踪等特定场景。

本系统 demonstrates了在资源受限的嵌入式设备上实现复杂AI算法的可行性,为构建低成本、低功耗、智能化的边缘感知设备提供了一个优秀的范例。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐