项目标题-电子法棒-卷积神经网络轨迹识别方案

基于STM32部署卷积神经网络控制设备方案-AI项目-STM32部署卷积神经网络方案-红外信号复制方案-轨迹识别

先了解一下背景,STM32是一款微控制器,做AI一般都是拥有算力的微处理器,量产非常昂贵,但是此项目解决微控制器能部署AI 卷积神经网络的技术,并且调研过目前位于深圳等一线城市,也有对控制器部署AI的客户需求,市场量很大。

项目包含下述内容

- 硬件部分、PCB制板、BOM表文件等等 (Hardware)
- 外壳、3D打印文件、如果你有想法也可以告诉我、帮你设计~ (3D_print)
- 软件程序、用于电子法棒的软件程序 AI + Keil等等(Software)
- QT上位机动作识别模型训练脚本 (用于训练模型等功能)(Upper_computer
- 图示、Demo中的动作轨迹图例(Model_trajectories)
- 环境安装包、工具包(Install_package) + 嵌入式神经网络部署源码(NNOM_Demo)
- 二次开发方案
- 技术支持、全项目内容答疑
- 项目持续更新中(任何问题和想法 功能都可以跟我提,我会慢慢跟进解决)

如下为整个资料包的目录

目录

项目标题-电子法棒-卷积神经网络轨迹识别方案

1.项目简介

2.系统架构

3.项目资源包整体介绍

4.开发环境配置说明

5.安装与配置(手把手)

6.功能使用说明(必看)

7.二次开发方案

8.训练模型-训练自己想要的轨迹

9.软件部分-从0到1源码详解

10.QT上位机+AI部分-卷积神经网络详解

11.硬件部分-原理图 PCB部分讲解

12.外壳部分-3D打印工程详解 *

13.常见错误问题汇总

14.持续更新


1.项目简介

目的:不管是学习还是工作过程中,项目比重的占比都是绝对性的,没有好的项目是无法学习的,非常多的人来找我要新颖的项目。经调研,发现项目如此重要,很多人的项目无非就是小车跟智能家居,大家都是雷同的培训班之类的项目,没有任务意义。也去展会看过,甚至培训班也都是小车以及智能家居套壳,硬件卖的贵,扩展性也没有。

解决痛点:(经调研

1.市场需求在变,客户方案也在变,AI结合嵌入式是主流,需更迭新鲜的血液,这是你要与老工程师拉开的路径

2.市面上无案例,不同于市面上小车,智能家居等等一味堆叠传感器的方案培训班项目,写简历都是大同小异!

3.市面上针对嵌入式AI项目案例几乎没有,都是人脸识别等类型的项目大同小异,此项目方案为此打破常规 直接在最低成本的stm32运行卷积神经网络!
4.项目可以二次开发,模块化架构编程思维,不同于培训班以及市面项目,可以迭代成自己的产品,不像别的项目学了自己也无法迭代。
5.即使你是业内人员你也无法感受到整个产品流程,项目从硬件到软件到上位机到外壳设计一整套流程,让你明白业务流程与工程流程,正真设计产品的流程。

功能:(通俗易懂)

​      设备会根据你的手势轨迹动作,去发出指令,控制设备。你可以理解是一个哈利波特魔法棒。

1.设备的手势可以自行添加,目前13个手势,你可以任意添加你想要的手势轨迹,配备QT上位机给你添加(还可以学到QT)

2.设备会根据你做的动作判断是否正确,是否是动作集里面的动作,去发出控制设备的信号,比如空调的开关,温度的升降,任何牌子的都可以(格力美的等等都可以 本质是复制红外信号 调研过开源案例没有此方法)

本质是STM32部署卷积神经网络方案,如果学以致用,你也可以做出眼球跟踪项目等等,本质都是一样的方案~

2.系统架构

硬件架构:STM32F103、MPU6050、红外传感器、电池降压电路、电源管理IC、电源选择电路、充电管理电路、充电管理IC等等

软件架构:QT、ARM、Tensorflow、Keras、实战算法、nnom(嵌入式AI推理库)等等

3.项目资源包整体介绍

4.开发环境配置说明

Keil环境配置:

  • Keil 版本:请使用 Keil 5,建议从 Keil 官网下载最新版,避免遇到兼容性问题。

  • 编译器版本:请选择 Arm Compiler 6.22 作为编译器,确保项目能够顺利编译。

  • 调试器设置:根据您的设备选择合适的调试器,例如 ST-Link 或其他兼容设备,以进行程序调试。

  • 库文件安装:首次打开项目时,Keil 可能提示需要安装缺失的库,请根据提示进行安装,以确保项目能够正常运行。

5.安装与配置(手把手)

我已经放在Install_package中,详情可以看资料包。

6.功能使用说明(必看)

更多详情在资料包中,不再赘述。。

7.二次开发方案

- 基于卷积神经网络框架 NNOM_Demo 此项目已经移植好框架内容 本质就是训练模型然后量化推理的过程
  - 眼球追踪 Eye Tracking
    - 使用卷积神经网络对眼睛图像进行检测,定位瞳孔位置
  - 姿态估计 Pose Estimation
    - 利用摄像头或IMU传感器,实时检测人体的姿态,并基于CNN进行分类和估计。可用于运动分析、健康监测等场景

​    通过这些扩展方案,项目可以覆盖更多的应用场景,进一步提升智能化和交互性,特别是在嵌入式设备的实际应用中

例如 射频模块 wifi 蓝牙等等  同样可以进行插桩二次开发在设备上

8.训练模型-训练自己想要的轨迹

选择你想要的运动轨迹 运功轨迹可以做你自己想做的 也可以重新训练原本的 原本的轨迹图像位于 .\电子法棒\Model_trajectories中

更多详情根据资料包所示。

9.软件部分-从0到1源码详解

nnom:

NNOM 是一个轻量级的、专为嵌入式设备设计的神经网络库,特别适用于 **ARM Cortex-M** 等资源有限的微控制器(MCU)。它的主要功能是将深度学习模型(如卷积神经网络,CNN)在低功耗、内存受限的嵌入式环境中进行推理(inference)让嵌入式系统能够执行机器学习任务,尤其是涉及神经网络的推理过程。

轻量化:NNOM 被设计为非常轻量的库,适合在内存和处理能力有限的嵌入式设备上运行,尤其是那些没有硬件加速器的设备。

跨平台:主要针对 Cortex-M 系列的处理器设计,但也可以在其他平台上移植使用。它依赖 CMSIS-NN,后者提供了一些优化的神经网络运算加速。

支持模型量化:NNOM 支持量化的神经网络模型,这在嵌入式设备上非常重要,因为量化模型可以大大减少内存和计算需求,适合资源受限的环境。

兼容性:NNOM 可以与 TensorFlow 和 Keras 等主流深度学习框架进行兼容,通过模型转换工具可以将训练好的模型转换成适合嵌入式系统使用的格式。

灵活性:NNOM 支持多种网络结构和层,如卷积层(Conv)、池化层(Pooling)、全连接层(Fully Connected)、激活函数(Activation)等,用户可以根据需要设计不同的网络架构。

开源和可定制:NNOM 是开源的,可以根据具体应用进行定制和修改。
[GitHub - majianjia/nnom: A higher-level Neural Network library for microcontrollers.](

具体代码位于资料包中。。

10.QT上位机+AI部分-卷积神经网络详解

具体代码位于资料包中。。

import sys
import os
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, losses, callbacks, utils  # type: ignore TensorFlow
from tensorflow.keras.preprocessing.sequence import pad_sequences  # type: ignore TensorFlow
from sklearn.metrics import classification_report
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QPushButton, QLabel, QVBoxLayout, QWidget, QMessageBox
import re

# 动作分类名
motion_names = [
    'RightAngle', 'SharpAngle', 'Lightning', 'Triangle', 'Letter_h', 'letter_R', 
    'letter_W', 'letter_phi', 'Circle', 'UpAndDown', 'Horn', 'Wave', 'NoMotion'
]

# 定义目录路径和文件名
DEF_MODEL_NAME = 'model.h5'
DEF_WEIGHTS_NAME = 'weights.h'
DEF_FILE_MAX = 100
DEF_N_ROWS = 150
DEF_COLUMNS = (3, 4, 5)
DEF_FILE_FORMAT = '.txt'
DEF_FILE_NAME_SEPERATOR = '_'
DEF_BATCH_SIZE = 120
DEF_NUM_EPOCH = 200

# 动作名称到标签的映射
motion_to_label = {name: idx for idx, name in enumerate(motion_names)}

# 定义训练函数
def train(x_train, y_train, x_test, y_test, model_path, weights_path, 
          input_shape=(DEF_N_ROWS, 3, 1), num_classes=len(motion_names), 
          batch_size=DEF_BATCH_SIZE, epochs=DEF_NUM_EPOCH):
    
    inputs = layers.Input(shape=input_shape)
    x = layers.Conv2D(30, kernel_size=(3, 3), strides=(3, 1), padding='same')(inputs)
    x = layers.LeakyReLU()(x)
    x = layers.Conv2D(15, kernel_size=(3, 3), strides=(3, 1), padding='same')(x)
    x = layers.LeakyReLU()(x)
    x = layers.AveragePooling2D(pool_size=(3, 1), strides=(3, 1))(x)
    x = layers.Flatten()(x)
    x = layers.Dense(num_classes)(x)
    outputs = layers.Softmax()(x)
    
    model = models.Model(inputs=inputs, outputs=outputs)
    model.compile(optimizer=optimizers.Adam(), loss=losses.CategoricalCrossentropy(), metrics=['accuracy'])
    
    early_stopping = callbacks.EarlyStopping(monitor='val_loss', patience=10)
    checkpoint = callbacks.ModelCheckpoint(model_path, monitor='val_accuracy', save_best_only=True, mode='max')
    
    history = model.fit(x_train, y_train, batch_size=batch_size, epochs=epochs, verbose=2, 
                        validation_data=(x_test, y_test), shuffle=True, 
                        callbacks=[early_stopping, checkpoint])
    
    # Save the model weights
    model.save_weights(weights_path)
    
    del model
    tf.keras.backend.clear_session()
    
    return history

# 加载数据集函数
def load_dataset(root_dir, max_rows=None):
    file_list = []
    labels = []
    for filename in os.listdir(root_dir):
        if filename.endswith(DEF_FILE_FORMAT):
            match = re.match(rf'^([\w]+)_([\d]+){DEF_FILE_FORMAT}$', filename)
            if match:
                motion_name = match.group(1)
                number_str = match.group(2)
                number = int(number_str)
                if 0 <= number <= DEF_FILE_MAX:
                    if motion_name in motion_to_label:
                        file_path = os.path.join(root_dir, filename)
                        data = np.loadtxt(file_path, delimiter=' ', usecols=DEF_COLUMNS, max_rows=max_rows)
                        file_list.append(data)
                        labels.append(motion_to_label[motion_name])
                    else:
                        print(f"Motion name not recognized: {filename}")
                else:
                    print(f"Number out of range: {filename}")
            else:
                print(f"Invalid file name format: {filename}")
    return file_list, labels

# PyQt5 界面定义
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("训练模型")
        self.setGeometry(100, 100, 400, 300)
        
        self.label = QLabel("请选择数据文件夹", self)
        self.select_button = QPushButton("选择数据文件夹", self)
        self.train_button = QPushButton("开始训练", self)
        self.model_path_button = QPushButton("选择模型保存路径", self)
        self.weights_path_button = QPushButton("选择权重保存路径", self)
        self.model_path_label = QLabel("模型保存路径: 未选择", self)
        self.weights_path_label = QLabel("权重保存路径: 未选择", self)
        
        self.select_button.clicked.connect(self.select_directory)
        self.train_button.clicked.connect(self.start_training)
        self.model_path_button.clicked.connect(self.select_model_path)
        self.weights_path_button.clicked.connect(self.select_weights_path)
        
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.select_button)
        layout.addWidget(self.model_path_label)
        layout.addWidget(self.weights_path_label)
        layout.addWidget(self.model_path_button)
        layout.addWidget(self.weights_path_button)
        layout.addWidget(self.train_button)
        
        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)
        
        self.data_directory = ""
        self.model_path = DEF_MODEL_NAME
        self.weights_path = DEF_WEIGHTS_NAME
        
    def select_directory(self):
        directory = QFileDialog.getExistingDirectory(self, "选择数据文件夹")
        if directory:
            self.data_directory = directory
            self.label.setText(f"选择的文件夹: {self.data_directory}")
            
    def select_model_path(self):
        path, _ = QFileDialog.getSaveFileName(self, "选择模型保存路径", DEF_MODEL_NAME, "HDF5 Files (*.h5);;All Files (*)")
        if path:
            self.model_path = path
            self.model_path_label.setText(f"模型保存路径: {self.model_path}")
        
    def select_weights_path(self):
        path, _ = QFileDialog.getSaveFileName(self, "选择权重保存路径", DEF_WEIGHTS_NAME, "HDF5 Files (*.h5);;All Files (*)")
        if path:
            self.weights_path = path
            self.weights_path_label.setText(f"权重保存路径: {self.weights_path}")
        
    def start_training(self):
        if not self.data_directory:
            QMessageBox.warning(self, "警告", "请先选择数据文件夹")
            return
        if self.model_path == DEF_MODEL_NAME:
            QMessageBox.warning(self, "警告", "请先选择模型保存路径")
            return
        if self.weights_path == DEF_WEIGHTS_NAME:
            QMessageBox.warning(self, "警告", "请先选择权重保存路径")
            return
        
        # 加载数据集
        file_list, labels = load_dataset(self.data_directory, max_rows=DEF_N_ROWS)
        
        # 数据预处理
        max_len = max([len(x) for x in file_list])
        file_list_padded = pad_sequences(file_list, maxlen=max_len, dtype='float32', padding='post', value=0)
        labels_one_hot = utils.to_categorical(labels, num_classes=len(motion_names))
        
        # 数据集分割
        num_elements = len(file_list_padded)
        train_size = int(num_elements * 0.8)
        
        best_val_accuracy = 0
        best_model = None
        for _ in range(3):
            indices = np.arange(num_elements)
            np.random.shuffle(indices)
            
            train_indices = indices[:train_size]
            test_indices = indices[train_size:]
            
            x_train = file_list_padded[train_indices]
            y_train = labels_one_hot[train_indices]
            x_test = file_list_padded[test_indices]
            y_test = labels_one_hot[test_indices]
            
            history = train(x_train, y_train, x_test, y_test, model_path=self.model_path, 
                            weights_path=self.weights_path, batch_size=DEF_BATCH_SIZE, epochs=DEF_NUM_EPOCH)
            
            model = tf.keras.models.load_model(self.model_path)
            
            y_pred = model.predict(x_test)
            y_pred_classes = np.argmax(y_pred, axis=1)
            y_true_classes = np.argmax(y_test, axis=1)
            
            val_accuracy = history.history['val_accuracy'][-1]
            print(f"Validation Accuracy: {val_accuracy:.4f}")
            
            if val_accuracy > best_val_accuracy:
                best_val_accuracy = val_accuracy
                best_model = model
        
        if best_model is not None:
            y_pred = best_model.predict(x_test)
            y_pred_classes = np.argmax(y_pred, axis=1)
            y_true_classes = np.argmax(y_test, axis=1)
            
            print(classification_report(y_true_classes, y_pred_classes, target_names=motion_names))
            
            QMessageBox.information(self, "训练完成", "模型训练完成,最佳模型及其权重已保存。")
        else:
            QMessageBox.warning(self, "训练失败", "没有找到最佳模型。")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

11.硬件部分-原理图 PCB部分讲解

详情资料包。。。。PCB原理图BOM表等等。。

12.外壳部分-3D打印工程详解 *

详情资料包。。。。

13.常见错误问题汇总

14.持续更新

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐