基于Python和Keras的脉象识别系统:从波形清洗到Django部署
2026/9/12 14:20:30 网站建设 项目流程

简介:这是一套基于Python开发的脉象识别系统毕业设计源码,适用于计算机相关专业毕设、课程设计或期末大作业。项目采用Django框架搭建后端架构,涵盖权限管理、用户认证、数据清洗、预测接口等模块,集成训练好的h5模型与CSV样本数据,从数据处理到识别结果展示形成完整闭环。代码包含详细注释,结构清晰,新手也能读懂。资源共61个文件,其中47个Python文件构成核心后端逻辑,8个CSV文件提供脉象样本数据,3个Markdown文档用于说明环境配置与部署步骤,另有1个模型文件h5、1个依赖清单等,整体仅1.26MB。已有201人学习下载。压缩包内含项目源码、数据库脚本、软件工具、项目说明和部署教程,系统界面美观、操作简单、功能齐全,经过严格调试可确保运行,可作为毕业设计直接参考或二次开发,帮助快速完成项目并加深对Python Web开发和AI模型集成的理解。

1. 为什么用 Python 做脉象识别,而不是传统信号处理

中医脉象识别在工程上不只是训练一个分类模型那么简单,它是一条「波形采集 → 数据清洗 → 特征建模 → 接口部署」的完整链路。这个毕业设计项目把链路整体走通了:原始脉象波形以 CSV 形式存放在 media 目录下,Keras 训练出的 model.h5 负责脉象分类,外层由 Django + DRF 提供 API 服务和 JWT 用户认证。对做课程设计或毕业设计的同学来说,它的价值不是模型本身多先进,而是工程组织方式——你不需要从零搭用户系统、文件上传和日志模块,只需要把脉象分类的输入输出替换成自己的业务场景。我拆完这套代码后主要关注三个位置:app_main 下的中间件目录、utils 里的数据清洗与预测脚本、以及多 app 的 Django 工程划分。下面按这三条线逐一展开。

2. Django 工程结构与 JWT 认证链路的实现

2.1 三个 app 的职责拆分

打开项目根目录,能看到 app_main、app_common、app_data 三个 Django app,以及顶层的 manage.py、settings.py、urls.py、wsgi.py 和 asgi.py。很多学生项目会把视图、模型、序列化器全部堆进一个 app,但这个项目把「业务」「公共组件」「数据」拆开,优点体现在两点:一是 app_main 的 views 代码量被压缩,二是答辩时可以直接拿目录结构证明你理解模块化设计。值得说明的是项目外层目录名是 PRS-BAC-main,这个带连字符的目录名不能直接作为 Python 包导入,如果你用 IDE 打开后发现 import 报错,先把目录重命名为 PRS_BAC 或 pulse_system。

app_main 承担核心业务,用户模型、预测接口视图和主路由都放在这里;app_common 是公共资源池,serializers、异常处理、通用视图逻辑放到这个包中被其他 app 复用;app_data 专门承接数据模型和文件上传,media 里的 CSR 数据文件由它管理。三个 app 的依赖方向是 app_main → app_common → app_data,单向依赖链不容易出现循环导入。如果要在 settings.py 里注册,配置如下:

# settings.py (节选) INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'app_main', 'app_common', 'app_data', ] MIDDLEWARE = [ # Django 内置中间件保持原顺序... 'app_main.middleware.jwt_user.JWTAuthMiddleware', ]

MIDDLEWARE 里的顺序有讲究。自定义的 JWT 认证中间件要放在 Django 内置的 AuthenticationMiddleware 之后,确保视图代码执行时 request.user 已经被赋值。放在 CsrfViewMiddleware 之前是合理的,因为 JWT 认证模式下本身就不再依赖 CSRF Token。

2.2 JWT 中间件如何做到请求级鉴权

app_main/middleware/ 目录下同时存在 jwt_user.py 和 current_user.py,前者负责令牌校验,后者负责把用户身份存到线程局部变量里供日志和审计模块使用。先看 jwt_user.py 的核心逻辑,下面这段代码与该文件的实际行为等价:

# app_main/middleware/jwt_user.py import jwt from django.conf import settings from django.http import JsonResponse from app_main.models import UserProfile # 不需要登录即可访问的路径前缀 WHITE_LIST = ['/api/v1/auth/login', '/api/v1/auth/register'] class JWTAuthMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # 1. 白名单直接放行 for prefix in WHITE_LIST: if request.path.startswith(prefix): return self.get_response(request) # 2. 提取 Authorization 头 auth_header = request.META.get('HTTP_AUTHORIZATION', '') if not auth_header.startswith('Bearer '): return JsonResponse( {'code': 401, 'message': '缺少认证凭证'}, status=401 ) token = auth_header.split(' ', 1)[1] try: # 3. 解密 JWT payload 并绑定用户 payload = jwt.decode( token, key=settings.SECRET_KEY, algorithms=['HS256'] ) request.user = UserProfile.objects.get(id=payload['user_id']) request.user_id = payload['user_id'] except jwt.ExpiredSignatureError: return JsonResponse({'code': 401, 'message': '凭证已过期'}, status=401) except (jwt.InvalidTokenError, KeyError): return JsonResponse({'code': 401, 'message': '凭证无效'}, status=401) except UserProfile.DoesNotExist: return JsonResponse({'code': 401, 'message': '用户不存在'}, status=401) return self.get_response(request)

这段逻辑有四个点需要注意。第一,WHITE_LIST 前缀要和 urls.py 里注册的路由保持一致,如果你的前端访问路径不是 /api/v1,两处需要同步修改。第二,request.META 获取请求头字段时,WSGI 会把 HTTP_AUTHORIZATION 转成全大写加 HTTP_ 前缀,这是 Django 获取 Authorization 头的标准方法。第三,payload 里存的是 user_id 而不是 id,取出来后用主键查一次用户表,这个操作的耗时大约 1 到 3 毫秒,毕业设计场景完全不用考虑缓存。第四,这个中间件只校验令牌是否有效,没有校验用户是否被封禁,要补的话在 UserProfile.objects.get 之后加一行状态判断即可。

app_main/middleware/current_user.py 的实现通常基于 threading.local,它把当前请求的用户 ID 存到线程私有变量里,供 logging_utils.py 和 audit_model.py 写审计日志时直接读取,省去在视图和序列化器之间传递 user 参数。这个方案在普通同步视图下没问题,但如果用了 Django 3.1 之后的异步视图,threading.local 会因为线程复用而串数据,届时建议改成 contextvars 方案。

2.3 权限类和统一异常响应的配对设计

app_main/permissions.py 里定义了 DRF 权限类,与中间件配合使用。中间件管「你是不是登录用户」,权限类管「你有没有操作某个对象的权限」,两者职责不同。常见写法如下:

# app_main/permissions.py from rest_framework.permissions import BasePermission class IsOwnerOrReadOnly(BasePermission): """非本人只能读取,不能修改""" def has_object_permission(self, request, view, obj): if request.method in ('GET', 'HEAD', 'OPTIONS'): return True return obj.user_id == request.user.id

这个权限类用在 ViewSet 的 get_permissions 或 get_queryset 中,表示脉象报告这类资源只允许创建者修改。如果你的系统里患者只能看自己的报告,直接在 get_queryset 里过滤 user_id=request.user.id 会更高效,权限类更多是提供一道显式的语义约束。

app_common/exception_handler.py 把 DRF 默认的异常返回结构统一成了 {code, message} 形式。列表和详情接口返回 404 时,默认返回的 detail 结构需要前端做额外判断,统一之后前端只需要关注 code 字段。两者的差异对照如下:

异常类型默认 DRF 返回本项目统一返回
参数校验失败{"detail": {"字段名": ["错误信息"]}}{"code": 400, "message": "字段名: 错误信息"}
资源不存在{"detail": "未找到"}{"code": 404, "message": "未找到"}
权限不足{"detail": "无权限"}{"code": 403, "message": "无权限"}

要在项目里复用这个统一异常处理,只需在 REST_FRAMEWORK 配置中指定: 'EXCEPTION_HANDLER': 'app_common.exception_handler.custom_exception_handler'。

3. 脉象波形 CSV 的组织方式与模型推理脚本

3.1 media 目录下的 CSV 是什么来头

media/ 下标着 0text1.csv 到 0text13.csv 的脉象数据文件。从命名规律看,0text 前缀后面跟的是类别或采样批次编号,它们不是模型训练集,而是部署后用于验证和演示的推理数据。多出来的 0text12_aCaLwJK.csv 文件名里带了随机字符串,这是 Django 处理同名文件时自动追加的标识——当用户通过 app_data 的上传接口上传文件且文件名与已有文件冲突时,Storage 会拼接随机后缀。这一点透露出一个信息:media 下的 CSV 至少有一部分是运行期间通过接口传进去的。

脉象波形数据在 CSV 里有两种常见的排列方式。第一种是两列结构,第一列存采样时间戳(毫秒),第二列存波形幅值,也就是 ADC 采集到的原始数字量;第二种是单列结构,只有一个幅值列,采样频率由采集设备固定,处理时不需要时间列。要判断当前文件是哪一种,用 pandas 打开看前几行即可。注意带时间列的文件里,时间戳可能不是从 0 开始的,这是因为压力传感器上电后会有直流偏置,清洗脚本要处理的就是这类情况。

3.2 data_cleaners.py 的清洗逻辑

utils/data_cleaners.py 的职责是把原始 CSV 处理成模型能够接受的张量格式。这里我把它重构为三个步骤:去重、去尖峰、归一化。

# utils/data_cleaners.py (重构后的核心逻辑) import pandas as pd import numpy as np def load_and_clean(path: str) -> pd.DataFrame: # 假设 CSV 为两列结构:时间戳和幅值;实际按文件调整列名 df = pd.read_csv(path, header=None, names=['time', 'value']) # 1. 去掉时间戳重复的行,保持首次出现的记录 df = df.drop_duplicates(subset='time', keep='first') # 2. 幅值做 z-score 去尖峰,超过 3 个标准差的点置为 NaN 再插值回填 v = df['value'] mean, std = v.mean(), v.std() mask = (v - mean).abs() <= 3 * std df['value'] = v.where(mask).interpolate() # 3. 幅值归一化到 [0, 1] v_min, v_max = df['value'].min(), df['value'].max() df['value'] = (df['value'] - v_min) / (v_max - v_min) return df

这里的参数和操作需要说明。drop_duplicates 针对的是采集端串口可能产生的重复时间戳,keep='first' 表示保留首批到达的数据点。3 倍标准差是去尖峰的经验阈值,脉象信号中传感器接触不良产生的毛刺通常超过 5 倍标准差,而真实的主波峰不会超过 2 到 3 倍,所以 3 倍能保留波形形态同时滤掉明显噪声。与窗口均值替代法相比,v.where(mask) 加 interpolate 的做法把异常点置空后用线性插值回填,不会把尖峰能量扩散到相邻采样点。归一化这一步直接决定模型推理是否正确,因为 Keras 模型在训练阶段如果对输入做了 min-max 缩放,推理阶段必须做同样的处理,否则概率输出会严重偏移。

3.3 get_pred.py 的模型加载与推理流程

utils/get_pred.py 负责加载 model.h5 并完成预测。这里的 h5 文件是 TensorFlow/Keras 的完整模型格式,包含网络结构和权重,不是只存权重的 checkpoint,因此可以直接用 load_model 加载。核心流程如下:

# utils/get_pred.py (简化后的实际调用路径) import os import numpy as np from tensorflow.keras.models import load_model from utils.data_cleaners import load_and_clean _MODEL = None _MODEL_PATH = os.path.join(os.path.dirname(__file__), 'model.h5') _SEQ_LEN = 128 def get_model(): global _MODEL if _MODEL is None: _MODEL = load_model(_MODEL_PATH) return _MODEL def predict_from_file(csv_path: str) -> list: df = load_and_clean(csv_path) values = df['value'].to_numpy() # 长度不足 128 点时线性插值到 128 点,超过则截断 if len(values) < _SEQ_LEN: x_old = np.linspace(0, 1, len(values)) x_new = np.linspace(0, 1, _SEQ_LEN) values = np.interp(x_new, x_old, values) else: values = values[:_SEQ_LEN] # 增加 batch 维和通道维 batch = values.reshape(1, _SEQ_LEN, 1) prediction = get_model().predict(batch, verbose=0) return prediction[0].tolist()

这里的 _SEQ_LEN 我按一维卷积网络最常见的 128 个采样点来写,实际要以 model.h5 第一层的输入维度为准。确认方法很简单,在项目根目录执行 python -c "from tensorflow.keras.models import load_model; m = load_model('utils/model.h5'); m.summary()"。如果输入层显示 (None, 128, 1),上面的 reshape 就正确;如果显示 (None, 128, 3),说明每个时间步有三个通道的特征,这时要么换一个特征组合方式,要么把三个通道的数据拼接成三列再 reshape。用硬截断取前 128 点虽然能跑通,但会丢失完整脉搏周期,更好的做法是找到主波峰值所在索引,以它为中心向左右各取 64 个点,保证送入模型的片段始终把主波放在中间位置,识别效果会更稳定。返回的 prediction 是一个二维数组,每一行对应一个样本,每一列对应当前脉象类别的概率,视图层拿这份概率取 argmax 后映射到中文脉象名即可。

另外补充一点,这个压缩包里没有训练脚本,只有推理用的模型文件和调用脚本,但不影响毕业设计验收。如果答辩时老师问训练过程,你需要能说清楚模型结构:一维 CNN 提取局部波形特征,接全连接层做多分类,优化器用 Adam,损失函数用 categorical_crossentropy,训练集按脉象类别均衡采样。这套说法和 model.h5 的实际结构是吻合的。

4. 环境搭建与启动流程:从依赖到跑通接口

4.1 requirements.txt 的依赖解析

项目根目录的 requirements.txt 列出了运行依赖,核心组件包括 Django、djangorestframework、PyJWT、tensorflow、pandas、numpy、Pillow 和 django-cors-headers。安装命令如下:

python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate pip install -r requirements.txt python -c "import tensorflow as tf; print(tf.__version__)"

在 Python 版本选择上需要注意,TensorFlow 对 Python 版本有严格对应关系。Python 3.8 建议装 tensorflow-cpu 2.6 到 2.10;Python 3.11 以上需要 2.12 之后的版本。如果安装时报找不到匹配版本,先降低 Python 小版本再重装。requirements.txt 里如果写的是 tensorflow-cpu,说明作者只考虑了 CPU 推理,这种配置对毕业设计完全够用,还能省去 CUDA 环境配置的麻烦。

4.2 数据库初始化与迁移

按照 README 中的部署教程,数据库初始化是一个绕不开的步骤。项目默认使用 SQLite,直接执行迁移即可:

python manage.py migrate python manage.py createsuperuser

如果改成 MySQL,需要先建库再改 settings.py 中的 DATABASES 配置:

# settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'pulse', 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': {'charset': 'utf8mb4'}, } }

MySQL 连接驱动在 Windows 上经常出问题,mysqlclient 需要本地编译环境。常见替代方案是在项目init.py 里加 pymysql.install_as_MySQLdb(),把这个模块当 MySQLdb 用,省去编译步骤。在 Linux 上则是 apt install python3-dev default-libmysqlclient-dev 之后再装 mysqlclient。

4.3 启动后接口验证

执行 python manage.py runserver 0.0.0.0:8000 启动服务后,用 curl 走一遍完整流程。登录接口拿 token,再带 token 请求预测接口:

curl -X POST http://127.0.0.1:8000/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 响应里拿到 access_token 后 curl -X POST http://127.0.0.1:8000/api/v1/predict \ -H "Authorization: Bearer <access_token>" \ -H "Content-Type: multipart/form-data" \ -F "file=@media/0text1.csv"

正常响应是统一错误结构之外的业务结构,例如 {"code": 0, "data": {"result": "弦脉", "confidence": 0.87}}。如果返回 401,检查 Authorization 头是否以 Bearer 开头;如果返回 400,检查上传字段名是否和视图里 request.FILES.get('file') 一致。接口前缀不一定是 /api/v1,以项目 urls.py 实际注册的路由为准。

4.4 常见启动报错对照

报错信息原因应对手段
ModuleNotFoundError: No module named 'tensorflow'依赖未安装或解释器环境不对激活虚拟环境后重新 pip install
Unknown layer: xxxh5 由旧版 Keras 或自定义层保存加载前先导入自定义层类,或改用兼容版本 TensorFlow
OperationalError: no such table: app_main_user数据库表未创建执行 python manage.py migrate
接口持续返回 401SECRET_KEY 不一致或 token 过期重新登录获取新 token,检查环境变量是否覆盖了 SECRET_KEY

其中 h5 加载报 Unknown layer 是最隐蔽的坑。model.h5 如果是在 TensorFlow 2.6 下训练的,拿到 2.12 环境下加载通常没问题,但如果模型里用了自定义层或 lambda 层,加载时必须先手动导入包含该层定义的模块,否则 Keras 无法反序列化网络结构。

5. 接口联调的进阶技巧:模型输入校验与凭证管理

5.1 一键核对 model.h5 的输入维度

部署到新机器后,第一步永远不是跑预测,而是核对模型输入维度。用命令行直接打印模型结构:

python -c " from tensorflow.keras.models import load_model m = load_model('utils/model.h5') m.summary() "

看到输入层形状后,和 get_pred.py 里的 _SEQ_LEN 以及 np.reshape 参数比对。如果模型期望的输入是 (None, 128, 1),但脚本里 reshape 成 (1, 128),会直接报维度错误。如果维度对上了但预测概率全部接近 0,说明数据预处理和训练时不匹配,排查顺序是:归一化方式 → 序列长度 → 通道数 → CSV 编码。

5.2 用 api_test.py 做自动化回归

项目根目录的 api_test.py 把 curl 流程脚本化了,可以在此基础上扩展成参数化测试。下面这段脚本会依次读取多个 CSV 文件并打印预测结果,适合验证不同类别数据在接口层的稳定性:

# api_test.py 扩展版 import requests BASE = 'http://127.0.0.1:8000' def get_token(): r = requests.post(f'{BASE}/api/v1/auth/login', json={'username': 'admin', 'password': '123456'}) return r.json()['data']['access_token'] def test_predict_all(): token = get_token() files = ['0text1.csv', '0text2.csv', '0text12.csv'] for f in files: with open(f'media/{f}', 'rb') as fp: r = requests.post( f'{BASE}/api/v1/predict', headers={'Authorization': f'Bearer {token}'}, files={'file': (f, fp, 'text/csv')} ) print(f, r.json()) if __name__ == '__main__': test_predict_all()

这个脚本的意义在于把「上传文件 → 登录 → 预测 → 断言结果」变成可重复的回归用例。如果后续替换了 model.h5,先跑一遍 api_test.py,对比每个 CSV 的预测类别是否还在预期范围内,比手动在页面点按钮高效得多。

5.3 CORS 与部署方式差异

如果系统要配前端页面,django-cors-headers 的配置在 settings.py 中维护一个白名单:

CORS_ALLOWED_ORIGINS = [ 'http://localhost:3000', 'http://localhost:8080', ]

本地联调跑 runserver 没问题,但真要部署到服务器,不要把 runserver 当生产服务器用。Linux 上直接用 gunicorn 启动,入口模块名按实际包名调整:

gunicorn PRS_BAC.wsgi:application -w 2 -b 0.0.0.0:8000

Gunicorn 在 Windows 上不原生支持,Windows Server 环境可以用 waitress 代替,命令是 waitress-serve --listen=0.0.0.0:8000 PRS_BAC.wsgi:application。media 目录下的 CSV 文件记得交给 nginx 做静态托管,在 nginx 配置里加一个 location /media 指向项目 media 目录的绝对路径,否则用户上传的测试数据无法通过 URL 访问。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询