从0到1制作单只鳌虾运动轨迹追踪软件

前言

需要准备windows10操作系统,python3.11.9,cuDNN8.9.2.26,CUDA11.8,paddleDetection2.7

流程:

  1. 准备数据集-澳洲鳌虾VOC数据集 
  2. 基于RT-DETR目标检测模型训练
  3. 导出onnx模型进行python部署
  4. 平滑滤波处理视频帧保留的物体质心坐标
  5. 基于pywebview为软件前端,falsk为软件后端制作UI
  6. 使用pyinstaller打包成exe
  7. 使用into setup生成安装包

本人代码禁止任何商业化用途,个人开发者随意。所有代码均开源

项目目录

XXX 项目总目录static 存放js静态文件plotly.jstemplates 存放html文件index.htmltemp 用户上传文件保存路径venv 虚拟环境main.py 主程序model.onnx 模型文件1.ico 打包的程序图标

准备数据集

点击下载澳洲鳌虾VOC数据集

下载后解压,文件目录为

dataAnnotations0.xml1.xml...imgs0.jpg1.jpg...lables.txt

然后使用如下的脚本把数据集划分为训练集和测试集

import os
import random
import shutildef splitDatasets(images_dir,xmls_dir,train_dir,test_dir):if os.path.exists(train_dir):shutil.rmtree(train_dir)os.makedirs(train_dir)os.makedirs(train_dir+'/imgs')os.makedirs(train_dir+'/annotations')if os.path.exists(test_dir):shutil.rmtree(test_dir)os.makedirs(test_dir)os.makedirs(test_dir+'/imgs')os.makedirs(test_dir+'/annotations')images=os.listdir(images_dir)random.shuffle(images)split_index=int(0.9*len(images))train_images=images[:split_index]test_images=images[split_index:]with open(train_dir+'/train.txt','w') as file:for img in train_images:shutil.copy(os.path.join(images_dir,img),os.path.join(train_dir,'imgs',img))ann=img.replace('jpg','xml')shutil.copy(os.path.join(xmls_dir,ann),os.path.join(train_dir,'annotations',ann))line=os.path.join(train_dir,'imgs',img)+' '+os.path.join(train_dir,'annotations',ann)+'\n'file.write(line)with open(test_dir+'/test.txt','w') as file:for img in test_images:shutil.copy(os.path.join(images_dir,img),os.path.join(test_dir,'imgs',img))ann=img.replace('jpg','xml')shutil.copy(os.path.join(xmls_dir,ann),os.path.join(test_dir,'annotations',ann))line=os.path.join(test_dir,'imgs',img)+' '+os.path.join(test_dir,'annotations',ann)+'\n'file.write(line)shutil.rmtree(images_dir)shutil.rmtree(xmls_dir)if __name__=='__main__':# 填写img文件夹所在绝对路径images_dir='/home/aistudio/work/voc/imgs'# 填写Annotations文件夹所在绝对路径xmls_dir='/home/aistudio/work/voc/Annotations'# 填写 训练集 的存放的绝对路径train_dir='/home/aistudio/work/voc/trains'# 填写 测试集 的存放的绝对路径test_dir='/home/aistudio/work/voc/tests'splitDatasets(images_dir,xmls_dir,train_dir,test_dir)

训练模型

可在aistudio云平台训练,我放好了所有的相关文件,点击进入,里面的说明很详细

也可在本地进行训练,下面来配置本地的训练环境

配置相关文件

下载paddleDetection2.7

原始目录如下

paddleDetection2.7.github.travisactivitybenchmarkconfigs 模型配置文件dataset 里面有数据集下载的脚本文件demodeploy 推理的相关文件docs 说明文档industrial_tutorialppdet 模型运行的核心文件scriptstest_pictools 模型训练入口,测试,验证,导出等脚本文件.gitignore.pre-commit-config.yaml.style.yapf.travis.ymlLICENSEREADME_cn.md 说明文档中文版README_en.md 说明文档英文版requirements.txt 相关依赖库setup.py 模型编译的相关脚本

需要删除一些目录,把README_en.md改名为README.md,处理过的目录如下

paddleDetection2.7configsdatasetdeployppdettoolsREADME.mdrequirements.txtsetup.py

把dataset里所有东西都删除,再将划分好的数据集放到该文件下,处理好的目录如下

datasetvoctrainsannotationsimgstrain.txttestsannotationsimgstest.txtlabels.txt

进入tools目录,只保留如下文件,其余全删除,处理后的文件目录如下

toolstrain.pyinfer.pyeval.pyexport_model.py

进入configs目录,只保留下面三个文件和目录,处理后的目录如下

configsdatasetsrtdetrruntime.yml

进入datasets目录,只保留voc.yml,其余文件全删除,处理后的目录如下

datasetsvoc.yml

并用如下内容覆盖voc.yml

metric: VOC
map_type: 11point
num_classes: 1TrainDataset:name: VOCDataSetdataset_dir: dataset/vocanno_path: trains/train.txtlabel_list: labels.txtdata_fields: ['image', 'gt_bbox', 'gt_class', 'difficult']EvalDataset:name: VOCDataSetdataset_dir: dataset/vocanno_path: tests/test.txtlabel_list: labels.txtdata_fields: ['image', 'gt_bbox', 'gt_class', 'difficult']TestDataset:name: ImageFolderanno_path: dataset/labels.txt

进入rtdetr目录,只保留如下2个文件和目录,处理后的目录如下:

rtdetr_base_rtdetr_hgnetv2_x_6x_coco.yml

进入_base_目录,找到optimizer_6x.yml,修改第一行为epoch: 200,意思是训练200轮

找到rtdetr_reader.yml,根据自己的CPU和GPU调整相关参数,如果是4核CPU,worker_num可为8,batch_size根据显存调整,占用80%到90%的显存即可

安装依赖库

建议在虚拟环境中操作

!pip install -r requirements.txt
!pip install pycocotools
!pip install filterpy
!pip install flask
!pip install pyinstaller
!pip install pywebview
!pip install onnxruntime-gpu
!pip install onnxruntime
!pip install onnx
!pip install paddle2onnx
!python setup.py install

开始训练

建议命令行输入,先进入paddleDetection所在位置,再执行以下命令

python tools/train.py -c configs/rtdetr/rtdetr_hgnetv2_x_6x_coco.yml --eval --use_vdl True --vdl_log_dir vdl_log_dir/scalar

然后就是漫长的等待

导出模型

生成的模型在paddleDetection/output/best_model/model.pdparams

先进入paddleDetection所在位置,再执行以下命令

python tools/export_model.py -c configs/rtdetr/rtdetr_hgnetv2_x_6x_coco.yml -o weights=output/best_model/model.pdparams

转onnx

先进入paddleDetection所在位置,再执行以下命令,可以根据需要选择保存路径

paddle2onnx --model_dir=output_inference/rtdetr_hgnetv2_x_6x_coco/ \--model_filename model.pdmodel  \--params_filename model.pdiparams \--opset_version 16 \--save_file /home/work/infer/model.onnx

模型部署

导包

import webview
from flask import Flask, request, jsonify,render_template,stream_with_context,Response
import os
import time
import cv2
from onnxruntime import InferenceSession
import numpy as np
from werkzeug.utils import secure_filename

总览代码

class TrackShrimp():def __init__(self,video_path,model_path,onnx_threshold=0.7):# 获取帧数据self.cap=self.init_video(video_path)frame_width=int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))frame_height=int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))# 图形尺寸im_shape = np.array([[frame_height, frame_width]], dtype='float32')# y轴缩放量self.im_scale_y=640.0/frame_height# x轴缩放量self.im_scale_x=640.0/frame_widthscale_factor = np.array([[self.im_scale_y,self.im_scale_x]]).astype('float32')# 定义模型输入self.inputs_dict = {'im_shape': im_shape,'image': None,'scale_factor': scale_factor}# 初始化模型self.sess=self.init_session(model_path)# 模型输出阈值self.onnx_threshold=onnx_thresholddef init_video(self,input_path):cap=cv2.VideoCapture(input_path)if not cap.isOpened():raise ValueError(f'无法打开视频{input_path}')return capdef init_session(self,model_path):try:return InferenceSession(model_path, providers=['CUDAExecutionProvider']) except:return InferenceSession(model_path, providers=['CPUExecutionProvider'])def precess_img(self,frame):img = cv2.resize(frame, None,None,fx=self.im_scale_x,fy=self.im_scale_y,interpolation=2)img = img.astype(np.float32) / 255.0img = np.transpose(img, [2, 0, 1])img = img[np.newaxis, :, :, :]return imgdef postcess(self,results:np.ndarray,all_centers:list[np.ndarray]):results=results[(results[:, 0] == 0) & (results[:, 1] > self.onnx_threshold)]x_centers = (results[:, 2] + results[:, 4]) / 2y_centers = (results[:, 3] + results[:, 5]) / 2centers = np.column_stack((x_centers, y_centers))all_centers.extend(centers)def by_smoothfilter(self,centers:list[np.ndarray],window_size=24):""":param centers: list[np.ndarray,np.ndarray,...]:param window_size: 平滑窗口大小:return: 平滑后的质心坐标NumPy数组"""centers=np.stack(centers)# 计算滑动窗口的平均值,pad函数在序列前后补零以处理边界情况padded_centers = np.pad(centers, ((window_size//2, window_size//2), (0, 0)), mode='edge')window_sum = np.cumsum(padded_centers, axis=0)smoothed_centers = (window_sum[window_size:] - window_sum[:-window_size]) / window_sizereturn smoothed_centersdef calculate_distance(self,centers:np.ndarray):'''centers:np.ndarray n*2'''# 计算相邻点之间的差diffs = centers[1:] - centers[:-1]# 计算每个差值的欧几里得距离distances = np.linalg.norm(diffs, axis=1)# 计算总路程return int(np.sum(distances))def gain_position(self,centers:np.ndarray):position_list=centers.tolist()return position_listdef run(self):global scheduleglobal run_task# 帧数frame_count=int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))frame_number=0center_list=[]for frame_number in range(frame_count):if not run_task:returnsuccess, frame = self.cap.read()if not success:breakschedule=int(frame_number/frame_count*100)# 打印进度if frame_number%10==0:print('Process: ',schedule)# 图片预处理img=self.precess_img(frame)self.inputs_dict['image']=imgresults=self.sess.run(None,self.inputs_dict)[0]if results is not None:self.postcess(results,center_list)# 使用平滑滤波filtered_centers = self.by_smoothfilter(center_list)self.cap.release()# 返回路程,轨迹坐标return self.calculate_distance(filtered_centers),self.gain_position(filtered_centers)

由于是对视频进行推理,所以首先得初始化视频打开的方法

def init_video(self,input_path):cap=cv2.VideoCapture(input_path)if not cap.isOpened():raise ValueError(f'无法打开视频{input_path}')return cap

初始化onnx运行引擎,优先使用显卡,如果CUDA环境有问题,就使用CPU运行

    def init_session(self,model_path):try:return InferenceSession(model_path, providers=['CUDAExecutionProvider']) except:return InferenceSession(model_path, providers=['CPUExecutionProvider'])

onnx引擎需要一定的输入格式,放到类的init里

    def __init__(self,video_path,model_path,onnx_threshold=0.7):# 获取帧数据self.cap=self.init_video(video_path)frame_width=int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))frame_height=int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))# 图形尺寸im_shape = np.array([[frame_height, frame_width]], dtype='float32')# y轴缩放量self.im_scale_y=640.0/frame_height# x轴缩放量self.im_scale_x=640.0/frame_widthscale_factor = np.array([[self.im_scale_y,self.im_scale_x]]).astype('float32')# 定义模型输入self.inputs_dict = {'im_shape': im_shape,'image': None,'scale_factor': scale_factor}# 初始化模型self.sess=self.init_session(model_path)# 模型输出阈值self.onnx_threshold=onnx_threshold

在提取每一帧后需要进行图像处理,resize图片为模型输入的要求,归一化

    def precess_img(self,frame):img = cv2.resize(frame, None,None,fx=self.im_scale_x,fy=self.im_scale_y,interpolation=2)img = img.astype(np.float32) / 255.0img = np.transpose(img, [2, 0, 1])img = img[np.newaxis, :, :, :]return img

在提取到视频的每一帧中的鳌虾的质心坐标后,由于每一帧的图像都不一样,输入模型后再输出的结果就不一样,会抖动,也就是噪声,我们需要滤波去噪,这里使用平滑滤波,相比卡尔曼滤波简单使用快速出结果。

    def by_smoothfilter(self,centers:list[np.ndarray],window_size=24):""":param centers: list[np.ndarray,np.ndarray,...]:param window_size: 平滑窗口大小:return: 平滑后的质心坐标NumPy数组"""centers=np.stack(centers)# 计算滑动窗口的平均值,pad函数在序列前后补零以处理边界情况padded_centers = np.pad(centers, ((window_size//2, window_size//2), (0, 0)), mode='edge')window_sum = np.cumsum(padded_centers, axis=0)smoothed_centers = (window_sum[window_size:] - window_sum[:-window_size]) / window_sizereturn smoothed_centers

我们需要计算鳌虾的运动总路程,用滤波后的质心坐标计算

    def calculate_distance(self,centers:np.ndarray):'''centers:np.ndarray n*2'''# 计算相邻点之间的差diffs = centers[1:] - centers[:-1]# 计算每个差值的欧几里得距离distances = np.linalg.norm(diffs, axis=1)# 计算总路程return int(np.sum(distances))

滤波后的质心坐标是numpy数组,需要一定的转换再发送到前端进行渲染(matplotlib画的图太丑了,不如plotly.js)

    def gain_position(self,centers:np.ndarray):position_list=centers.tolist()return position_list

在获取每一帧图像后,送入模型。模型会输出一对numpy数组,需要进行一对的后处理,低于阈值的就抛弃,然后取阈值最高的,计算质心坐标并保存

    def postcess(self,results:np.ndarray,all_centers:list[np.ndarray]):results=results[(results[:, 0] == 0) & (results[:, 1] > self.onnx_threshold)]x_centers = (results[:, 2] + results[:, 4]) / 2y_centers = (results[:, 3] + results[:, 5]) / 2centers = np.column_stack((x_centers, y_centers))all_centers.extend(centers)

需要在一个主函数里将上述打开视频,图像预处理,送入模型,后处理连起来

    def run(self):global scheduleglobal run_task# 帧数frame_count=int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))frame_number=0center_list=[]for frame_number in range(frame_count):if not run_task:returnsuccess, frame = self.cap.read()if not success:breakschedule=int(frame_number/frame_count*100)# 打印进度if frame_number%10==0:print('Process: ',schedule)# 图片预处理img=self.precess_img(frame)self.inputs_dict['image']=imgresults=self.sess.run(None,self.inputs_dict)[0]if results is not None:self.postcess(results,center_list)# 使用平滑滤波filtered_centers = self.by_smoothfilter(center_list)self.cap.release()# 返回路程,轨迹坐标return self.calculate_distance(filtered_centers),self.gain_position(filtered_centers)

前端的设计

以pywebview为平台,html和css设计前端

 

 

 

代码总览

index.html

<!DOCTYPE html>
<html>
<head><title></title><link rel="shortcut icon" href="#" /><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><script src="../static/plotly.js"></script><style>html,body{width: 100%;height: 100%;margin: 0 auto;}body{display: flex;align-items: center;justify-content: center;height: 100vh;background-color: rgb(6, 32, 80);}main{display: grid;grid-template-columns: 1fr 3fr;column-gap: 2%;width: 98%;height: 98%;}fieldset{border: 2px solid rgb(32, 139, 139);color: rgb(32, 139, 139);margin: 8% 0 8% 0;}#s2{text-align: center;display: flex;justify-content: center;align-items: center;background-color: rgba(32, 139, 139, 0.301);border: 2px solid rgb(32, 139, 139);}#progress-circle{border: 1em solid rgb(32, 139, 139);width: 40vh;height: 40vh;border-radius: 20vh;display: flex;  justify-content: center; align-items: center;}#progress-num{font-size: 18vh;color: rgb(32, 139, 139);}</style>
</head>
<body><main><section id="s1"><form id="form" enctype="multipart/form-data"><fieldset><legend>选择你要检测的视频</legend><input type="file" accept=".mp4" id="video" name="vedio"></fieldset><fieldset><legend>功能按键</legend><button onclick="submit_to()" id="submit">开始上传</button><button onclick="stopRun()">终止运行</button></fieldset></form><script>async function stopRun(){try{const response=await fetch('/stopRun',{method:'POST'})if (!response.ok) {  throw new Error('Network response was not ok.');  }data=await response.json()alert(data.data)}catch(error){console.log(error)}}async function submit_to(){// 防重复激发const button = document.getElementById('submit');  button.disabled = true;try{// 获取文件const input=document.getElementById('video')const file=input.files[0]if (!file){throw new Error('未选择文件')}if(file.type!=='video/mp4'){throw new Error('请选择MP4文件')}// 刷新界面 const s2=document.getElementById('s2')Plotly.purge(s2)// 初始化进度显示const progressCircle=document.getElementById('progress-circle')const progressNum=document.getElementById('progress-num')progressCircle.style.display='flex'progressNum.innerHTML='0%'// 更新进度let source = new EventSource("/progress")source.onmessage = function(event) {progressNum.innerHTML = event.data+'%'}// 发送请求const formData=new FormData()formData.append('video', file)const response=await fetch('/shrimp',{method:'POST',body:formData})if (!response.ok) {throw new Error('Network response was not ok.');  }source.close()const data=await response.json()button.disabled=falseif(data.data==='任务被终止'){alert(data.data)}else{progressCircle.style.display='none'$('#distance').text('总路程'+data.distance)// 画图var trace=[{x: data.position_data.map(item=>item[0]),y: data.position_data.map(item=>item[1]),mode:"lines",line:{color:'rgb(32, 139, 139)'}}]var layout = {xaxis: {range: [0, 600],title: "x(像素)",titlefont: {  color: 'rgb(32, 139, 139)' // 轴标签颜色  },  linecolor: 'rgb(32, 139, 139)', // 轴线颜色  tickfont: {  color: 'lrgb(32, 139, 139)' // 轴刻度标签颜色  }},yaxis: {range: [0, 600],title: "y(像素)",titlefont: {  color: 'rgb(32, 139, 139)' // 轴标签颜色  },  linecolor: 'rgb(32, 139, 139)', // 轴线颜色  tickfont: {  color: 'lrgb(32, 139, 139)' // 轴刻度标签颜色  }},  title: "鳌虾运动轨迹",titlefont:{color:'rgb(32, 139, 139)'},plot_bgcolor: 'rgba(0,0,0,0)',paper_bgcolor:'rgba(0,0,0,0)'}Plotly.newPlot("s2", trace, layout,{scrollZoom: true,editable: true }) }}catch(error){button.disabled = falseif(error.message.startsWith('Failed to fetch')){}else{alert(error)}}}</script><fieldset><legend>输出结果</legend><P id="distance">总路程:</P></fieldset><fieldset><legend>注意事项</legend><p>本程序运行将消耗大量算力和内存,最好使用高配电脑。不支持windows10以下的操作系统。在后台有任务在跑时,切勿重复上传视频,等待后台跑完出图时再上传新的视频。如果选错视频并上传了,请点击'终止运行'再重新上传视频。有问题联系wx:m989783106</p></fieldset></section><section id="s2"><div id="progress-circle"><p id="progress-num"></p></div></section></main>
</body>
</html>

plotly.js从官网下载

代码分览

总体设计是以<html>和<body>为底,<main>为主容器内使用grid2列布局,2个<section>作为内容器占据左右2个网格。

左边的<section>容纳文件上传表单,功能按钮,数据显示,使用说明

        <section id="s1"><form id="form" enctype="multipart/form-data"><fieldset><legend>选择你要检测的视频</legend><input type="file" accept=".mp4" id="video" name="vedio"></fieldset><fieldset><legend>功能按键</legend><button onclick="submit_to()" id="submit">开始上传</button><button onclick="stopRun()">终止运行</button></fieldset></form><fieldset><legend>输出结果</legend><P id="distance">总路程:</P></fieldset><fieldset><legend>注意事项</legend><p>本程序运行将消耗大量算力和内存,最好使用高配电脑。不支持windows10以下的操作系统。在后台有任务在跑时,切勿重复上传视频,等待后台跑完出图时再上传新的视频。如果选错视频并上传了,请点击'终止运行'再重新上传视频。有问题联系wx:m989783106</p></fieldset></section>

之间用<fieldset>做了区域划分,简单又美观。

<button>均使用onclick属性进行触发

在上传前会检测用户是否选择文件,是否选择的是MP4文件

// 获取文件
const input=document.getElementById('video')
const file=input.files[0]
if (!file){throw new Error('未选择文件')
}
if(file.type!=='video/mp4'){throw new Error('请选择MP4文件')
}

 一共有3个请求:

  • 请求上传文件,将MP4上传给后端,然后后端运行模型发送质心坐标给前端渲染
  • 请求终止程序,当用户想终止后端运行模型,重新上传文件时
  • 请求获取模型处理进度,后端返回进度给前端,前端进行渲染展示

画轨迹图,前端用plotly.js将质心坐标进行渲染,同时轨迹图还有一定的交互能力。

// 画图
var trace=[{x: data.position_data.map(item=>item[0]),y: data.position_data.map(item=>item[1]),mode:"lines",line:{color:'rgb(32, 139, 139)'}
}]
var layout = {xaxis: {range: [0, 600],title: "x(像素)",titlefont: {  color: 'rgb(32, 139, 139)' // 轴标签颜色  },  linecolor: 'rgb(32, 139, 139)', // 轴线颜色  tickfont: {  color: 'lrgb(32, 139, 139)' // 轴刻度标签颜色  }},yaxis: {range: [0, 600],title: "y(像素)",titlefont: {  color: 'rgb(32, 139, 139)' // 轴标签颜色  },  linecolor: 'rgb(32, 139, 139)', // 轴线颜色  tickfont: {  color: 'lrgb(32, 139, 139)' // 轴刻度标签颜色  }},  title: "鳌虾运动轨迹",titlefont:{color:'rgb(32, 139, 139)'},plot_bgcolor: 'rgba(0,0,0,0)',paper_bgcolor:'rgba(0,0,0,0)'}
Plotly.newPlot("s2", trace, layout,{scrollZoom: true,editable: true })

其余的就是代码的排布顺序,异步执行调度,错误处理能力,系统稳定性,用户交互能力的提升,细节很多,均包含在代码中


右边的<section>容纳进度圈,轨迹图

        <section id="s2"><div id="progress-circle"><p id="progress-num"></p></div></section>

在文件上传时,就初始化渲染进度条,然后异步请求获取进度,渲染到页面;当进度到达一定值,比如99%,就关闭获取进度的请求,同时设置进度条的display=none。当用户打断程序执行或者重新运行程序,就清理轨迹图,初始化进度条,循环往复。

后端设计

后端整体使用flask,jinjia模板,将flask与pywebview结合。把模型检测代码封装到一个类TrackShrimp,其余的就是各种请求函数。

代码总览

import webview
from flask import Flask, request, jsonify,render_template,stream_with_context,Response
import os
import time
import cv2
from onnxruntime import InferenceSession
import numpy as np
from werkzeug.utils import secure_filenameclass TrackShrimp():def __init__(self,video_path,model_path,onnx_threshold=0.7):# 获取帧数据self.cap=self.init_video(video_path)frame_width=int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))frame_height=int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))# 图形尺寸im_shape = np.array([[frame_height, frame_width]], dtype='float32')# y轴缩放量self.im_scale_y=640.0/frame_height# x轴缩放量self.im_scale_x=640.0/frame_widthscale_factor = np.array([[self.im_scale_y,self.im_scale_x]]).astype('float32')# 定义模型输入self.inputs_dict = {'im_shape': im_shape,'image': None,'scale_factor': scale_factor}# 初始化模型self.sess=self.init_session(model_path)# 模型输出阈值self.onnx_threshold=onnx_thresholddef init_video(self,input_path):cap=cv2.VideoCapture(input_path)if not cap.isOpened():raise ValueError(f'无法打开视频{input_path}')return capdef init_session(self,model_path):try:return InferenceSession(model_path, providers=['CUDAExecutionProvider']) except:return InferenceSession(model_path, providers=['CPUExecutionProvider'])def precess_img(self,frame):img = cv2.resize(frame, None,None,fx=self.im_scale_x,fy=self.im_scale_y,interpolation=2)img = img.astype(np.float32) / 255.0img = np.transpose(img, [2, 0, 1])img = img[np.newaxis, :, :, :]return imgdef postcess(self,results:np.ndarray,all_centers:list[np.ndarray]):results=results[(results[:, 0] == 0) & (results[:, 1] > self.onnx_threshold)]x_centers = (results[:, 2] + results[:, 4]) / 2y_centers = (results[:, 3] + results[:, 5]) / 2centers = np.column_stack((x_centers, y_centers))all_centers.extend(centers)def by_smoothfilter(self,centers:list[np.ndarray],window_size=24):""":param centers: list[np.ndarray,np.ndarray,...]:param window_size: 平滑窗口大小:return: 平滑后的质心坐标NumPy数组"""centers=np.stack(centers)# 计算滑动窗口的平均值,pad函数在序列前后补零以处理边界情况padded_centers = np.pad(centers, ((window_size//2, window_size//2), (0, 0)), mode='edge')window_sum = np.cumsum(padded_centers, axis=0)smoothed_centers = (window_sum[window_size:] - window_sum[:-window_size]) / window_sizereturn smoothed_centersdef calculate_distance(self,centers:np.ndarray):'''centers:np.ndarray n*2'''# 计算相邻点之间的差diffs = centers[1:] - centers[:-1]# 计算每个差值的欧几里得距离distances = np.linalg.norm(diffs, axis=1)# 计算总路程return int(np.sum(distances))def gain_position(self,centers:np.ndarray):position_list=centers.tolist()return position_listdef run(self):global scheduleglobal run_task# 帧数frame_count=int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))frame_number=0center_list=[]for frame_number in range(frame_count):if not run_task:returnsuccess, frame = self.cap.read()if not success:breakschedule=int(frame_number/frame_count*100)# 打印进度if frame_number%10==0:print('Process: ',schedule)# 图片预处理img=self.precess_img(frame)self.inputs_dict['image']=imgresults=self.sess.run(None,self.inputs_dict)[0]if results is not None:self.postcess(results,center_list)# 使用平滑滤波filtered_centers = self.by_smoothfilter(center_list)self.cap.release()# 返回路程,轨迹坐标return self.calculate_distance(filtered_centers),self.gain_position(filtered_centers)app = Flask(__name__)
UPLOAD_FOLDER = './temp'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
schedule=0
run_task=Falsedef run_flask():app.run(debug=False, threaded=True,host='127.0.0.1',port=5000)def video_process(video_path):return TrackShrimp(video_path,'./model.onnx').run()# 主页面
@app.route('/',methods=['POST','GET'])
def return_main_page():return render_template('index.html')# 检测视频页面
@app.route('/shrimp',methods=['POST'])
def shrimp_track():global run_taskrun_task=Truefile=request.files.get('video')filename = secure_filename(file.filename)video_path=os.path.join(app.config['UPLOAD_FOLDER'],filename)file.save(video_path)try:results=video_process(video_path)if results is not None:distance,position_data=resultsdata = {'distance': distance,'position_data': position_data}run_task=Falsereturn jsonify(data)else:return jsonify({'data':'任务被终止'})except Exception as e:print('error:',e)return jsonify({'data':'任务被终止'})finally:if os.path.exists(video_path):os.remove(video_path)@app.route('/stopRun',methods=['GET','POST'])
def stopRun():global run_taskglobal scheduleif run_task:run_task=Falseschedule=0return jsonify({'data':'正在停止任务'})else:return jsonify({'data':'当前没有任务运行'})# 进度查询路由
@app.route('/progress',methods=['GET'])
def progress():@stream_with_contextdef generate():global run_taskratio = schedulewhile ratio < 95 and run_task:yield "data:" + str(ratio) + "\n\n"ratio = scheduletime.sleep(5)return Response(generate(), mimetype='text/event-stream')if __name__=='__main__':# 启动后端  # flask_thread = threading.Thread(target=run_flask)  # flask_thread.start()# time.sleep(1)# 启动前端webview.create_window('鳌虾轨迹侦测',url=app,width=900,height=600)# webview.create_window('鳌虾轨迹侦测',url=f'http://127.0.0.1:5000',width=900,height=600)webview.start()

代码分览

一个onnx部署的类TrackShrimp,详细见前面。

一些常量的定义

app = Flask(__name__)
UPLOAD_FOLDER = './temp' # 文件的上传路径,后端需要该路径保留用户上传的文件
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
schedule=0 # 实时进度,初始化进度为0
run_task=False # 一个onnx模型是否在运行的标志,用于接收用户中断信号从而终止模型运行

定义一个flask的·启动函数,用于web调试,浏览器F12启动调试窗口

def run_flask():app.run(debug=False, threaded=True,host='127.0.0.1',port=5000)

主页面的请求函数,该页面为主要的UI

# 主页面
@app.route('/',methods=['POST','GET'])
def return_main_page():return render_template('index.html')

用户请求中断的请求函数

首先通过标志位(run_task)检测模型是否在跑,如果检测到模型正在运行,就把标志位设为False,然后把进度归0

@app.route('/stopRun',methods=['GET','POST'])
def stopRun():global run_taskglobal scheduleif run_task:run_task=Falseschedule=0return jsonify({'data':'正在停止任务'})else:return jsonify({'data':'当前没有任务运行'})

进度查询

这里设置当进度为95%时,就停止查询。

@app.route('/progress',methods=['GET'])
def progress():@stream_with_contextdef generate():global run_taskratio = schedulewhile ratio < 95 and run_task:yield "data:" + str(ratio) + "\n\n"ratio = scheduletime.sleep(5)return Response(generate(), mimetype='text/event-stream')

一个检测的入口函数

def video_process(video_path):return TrackShrimp(video_path,'./model.onnx').run()

接收用户上传文件的函数

一旦用户上传文件,就设置运行标志位为True,然后将文件保存,再送入模型运行接口函数,当用户请求终止时,results为None,所以使用if else进行区分。模型结果出来后就把标志位设为False,同时将数据传到前端

@app.route('/shrimp',methods=['POST'])
def shrimp_track():global run_taskrun_task=Truefile=request.files.get('video')filename = secure_filename(file.filename)video_path=os.path.join(app.config['UPLOAD_FOLDER'],filename)file.save(video_path)try:results=video_process(video_path)if results is not None:distance,position_data=resultsdata = {'distance': distance,'position_data': position_data}run_task=Falsereturn jsonify(data)else:return jsonify({'data':'任务被终止'})except Exception as e:print('error:',e)return jsonify({'data':'任务被终止'})finally:if os.path.exists(video_path):os.remove(video_path)

接着就是启动所有代码了,为了调试方便,我写了2份代码,一份用于调试,一份用于成品

if __name__=='__main__':# 启动前端webview.create_window('鳌虾轨迹侦测',url=app,width=900,height=600)webview.start()
if __name__=='__main__':# 启动后端  flask_thread = threading.Thread(target=run_flask)  flask_thread.start()time.sleep(1)# 启动前端webview.create_window('鳌虾轨迹侦测',url=f'http://127.0.0.1:5000',width=900,height=600)webview.start()

pyinstaller打包

进入项目目录,命令行输入

piinstaller -D -w main.py

找到生成的main.spec文件,按如下修改

# -*- mode: python ; coding: utf-8 -*-a = Analysis(['main.py'],pathex=[],binaries=[],datas=[('templates/','templates/'),('static/','static/'),('venv/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_shared.dll','onnxruntime/capi/'),('venv/Lib/site-packages/onnxruntime/capi/onnxruntime_providers_cuda.dll','onnxruntime/capi/')],hiddenimports=[],hookspath=[],hooksconfig={},runtime_hooks=[],excludes=[],noarchive=False,optimize=0,
)
pyz = PYZ(a.pure)exe = EXE(pyz,a.scripts,[],exclude_binaries=True,name='main',debug=False,bootloader_ignore_signals=False,strip=False,upx=True,console=False,disable_windowed_traceback=False,argv_emulation=False,target_arch=None,codesign_identity=None,entitlements_file=None,icon='1.ico'
)
coll = COLLECT(exe,a.binaries,a.datas,strip=False,upx=True,upx_exclude=[],name='main',
)

在项目目录下放置一个图标命名为1.ico,最好是48*48像素

然后命令行运行

pyinstaller main.spec

然后在venv中找到 onnxruntime_gpu-1.18.1.dist-info 文件夹,复制到 dist/main/_internal 中

同时在cuDNN中找到如下几个动态链接库,复制到 dist/main/_internal 中

cudnn_ops_infer64_8.dll
cudnn_cnn_infer64_8.dll
cudnn_adv_infer64_8.dll
cudnn64_8.dll
cudart64_110.dll
cublasLt64_11.dll
cublas64_11.dll
cufft64_10.dll

然后将model.onnx放到 dist/main/ ,并在该目录创建一个目录temp

最后处理的结果如下

XXXdistmain_internalmain.exemodel.onnxtemp

生成安装包

使用into setup软件,并在网站找到中文的语言包下载为 Chinese.isl 文件,放到intosetup软件安装目录的 Languages 文件夹下

接着如图所示

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

取消立即编译,先进入文件里修改一些东西

修改成下面这样 

点击编译

然后就生成了安装包,就可以在任何win10,win11电脑里用CPU跑了,如果安装的电脑 有显卡和CUDA并把CUDA添加到了环境变量,就可以用GPU跑了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/42454.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

简介时间复杂度

好了&#xff0c;今天我们来了解一下&#xff0c;我们在做练习题中常出现的一个名词。时间复杂度。我相信大家如果有在练习过题目的话。对这个名词应该都不陌生吧。但是可能很少的去思考它是干什么的代表的什么意思。反正我以前练习的时候就是这样。我只知道有这么一个名词在题…

【全面讲解下iPhone新机官网验机流程】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

MybatisPlus实现插入/修改数据自动设置时间

引言 插入数据时自动设置当前时间&#xff0c;更新数据时自动修改日期为修改时的日期。 使用MybatisPlus的扩展接口MetaObjectHandler 步骤 实现接口 实体类加注解 实现接口 package com.example.vueelementson.common;import com.baomidou.mybatisplus.core.handlers.M…

C++ 模版进阶

目录 前言 1. 非类型模版参数 1.1 概念与讲解 1.2 array容器 2. 模版的特化 2.1 概念 2.2 函数模版特化 2.3 类模版特化 2.3.1 全特化 2.3.2 偏特化 3.模版的编译分离 3.1 什么是分离编译 3.2 模版的分离编译 3.3 解决方法 4. 模版总结 总结 前言 本篇文章主要…

包/final/权限修饰符/代码块

包package 1、包的作用 包用来管理不同的类。 2、包名 包名要全部小写&#xff0c;一般是域名反写&#xff0c;如com.liu。在Java中&#xff0c;java解释器会将package中的.解释为目录分隔符/&#xff0c;也就是说该文件的目录结构为&#xff1a;...com/liu/... 3、全类名…

1.pwn的汇编基础(提及第一个溢出:整数溢出)

汇编掌握程度 能看懂就行&#xff0c;绝大多数情况不需要真正的编程(shellcode题除外) 其实有时候也不需要读汇编&#xff0c;ida F5 通常都是分析gadget&#xff0c;知道怎么用&#xff0c; 调试程序也不需要分析每一条汇编指令&#xff0c;单步执行然后查看寄存器状态即可 但…

实现多数相加,但是传的参不固定

一、情景 一般实现的加法和减法等简单的相加减函数的话。一般都是写好固定传的参数。比如&#xff1a; function add(a,b) {return a b;} 这是固定的传入俩个&#xff0c;如果是三个呢&#xff0c;有人说当然好办&#xff01; 这样写不就行了&#xff01; function add(a…

vue中自定义设置多语言(包括使用vue-i18n),并且运行js脚本自动生成多语言文件

在项目中需要进行多个国家语言的切换时&#xff0c;可以用到下面方法其中一个 一、自定义设置多语言 方法一: 可以自己编写一个设置多语言文件 在项目新建js文件&#xff0c;命名为&#xff1a;language.js&#xff0c;代码如下 // language.js 文档 let languagePage {CN…

聊一下Maven打包的问题(jar要发布)

文章目录 一、问题和现象二、解决方法&#xff08;1&#xff09;方法一、maven-jar-pluginmaven-dependency-plugin&#xff08;2&#xff09;方法二、maven-assembly-plugin 一、问题和现象 现在的开发一直都是用spring boot&#xff0c;突然有一天&#xff0c;要自己开发一个…

Django之项目开发(二)

目录 一、安装和使用uWSGI 1.1、安装 1.2、配置文件 1.3、启动与停止uwsgi 二、安装nginx 三、Nginx 配置uWSGI 四、Nginx配置静态文件 五、Nginx配置负载均衡 一、安装和使用uWSGI uWSGI 是一个 Web 服务器,可以用来部署 Python Web 应用。它是一个高性能的通用的 We…

味蕾与理解:应对自闭症儿童挑食的策略与理解

在星贝育园自闭症康复学校&#xff0c;我们深知饮食习惯对孩子们的成长至关重要&#xff0c;而自闭症儿童的挑食问题往往比同龄儿童更为突出&#xff0c;给家长和照顾者带来了额外的挑战。今天&#xff0c;作为这里的老师&#xff0c;我想与大家分享一些应对自闭症儿童挑食的策…

(南京观海微电子)——电阻应用及选取

什么是电阻&#xff1f; 电阻是描述导体导电性能的物理量&#xff0c;用R表示。 电阻由导体两端的电压U与通过导体的电流I的比值来定义&#xff0c;即&#xff1a; 所以&#xff0c;当导体两端的电压一定时&#xff0c;电阻愈大&#xff0c;通过的电流就愈小&#xff1b;反之&…

鸿蒙应用实践:利用扣子API开发起床文案生成器

前言 扣子是一个新一代 AI 应用开发平台&#xff0c;无需编程基础即可快速搭建基于大模型的 Bot&#xff0c;并发布到各个渠道。平台优势包括无限拓展的能力集&#xff08;内置和自定义插件&#xff09;、丰富的数据源&#xff08;支持多种数据格式和上传方式&#xff09;、持…

[Unity入门01] Unity基本操作

参考的傅老师的教程学了一下Unity的基础操作&#xff1a; [傅老師/Unity教學] Unity3D基礎入門 [華梵大學] 遊戲引擎應用基礎(Unity版本) Class#01 移动&#xff1a;鼠标中键旋转&#xff1a;鼠标右键放大&#xff1a;鼠标滚轮飞行模式&#xff1a;右键WASDQEFocus模式&…

算法设计与分析 实验5 并查集法求图论桥问题

目录 一、实验目的 二、问题描述 三、实验要求 四、实验内容 &#xff08;一&#xff09;基准算法 &#xff08;二&#xff09;高效算法 五、实验结论 一、实验目的 1. 掌握图的连通性。 2. 掌握并查集的基本原理和应用。 二、问题描述 在图论中&#xff0c;一条边被称…

基于Android Studio订餐管理项目

目录 项目介绍 图片展示 运行环境 获取方式 项目介绍 能够实现登录&#xff0c;注册、首页、订餐、购物车&#xff0c;我的。 用户注册后&#xff0c;登陆客户端即可完成订餐、浏览菜谱等功能&#xff0c;点餐&#xff0c;加入购物车&#xff0c;结算&#xff0c;以及删减…

【学习笔记】操作系统--万字长文

计算机操作系统 文章目录 计算机操作系统引言 操作系统基本概念第一章 引论目标和作用操作系统发展历程单道批处理系统多道批处理系统分时系统实时系统 基本特征并发共享虚拟异步性&#xff08;不确定性&#xff09; 操作系统主要功能处理机管理内存管理设备管理文件管理 第二章…

工程化:Commitlint / 规范化Git提交消息格式

一、理解Commitlint Commitlint是一个用于规范化Git提交消息格式的工具。它基于Node.js&#xff0c;通过一系列的规则来检查Git提交信息的格式&#xff0c;确保它们遵循预定义的标准。 1.1、Commitlint的核心功能 代码规则检查&#xff1a;Commitlint基于代码规则进行检查&a…

汇聚荣拼多多电商的技巧有哪些?

在电商平台上&#xff0c;汇聚荣拼多多以其独特的商业模式和创新的营销策略吸引了大量消费者。那么&#xff0c;如何在这样一个竞争激烈的平台上脱颖而出&#xff0c;成为销售佼佼者呢?本文将深入探讨汇聚荣拼多多电商的成功技巧。 一、精准定位目标客户群体 首先&#xff0c;…

绝区肆--2024 年AI安全状况

前言 随着人工智能系统变得越来越强大和普及&#xff0c;与之相关的安全问题也越来越多。让我们来看看 2024 年人工智能安全的现状——评估威胁、分析漏洞、审查有前景的防御策略&#xff0c;并推测这一关键领域的未来可能如何。 主要的人工智能安全威胁 人工智能系统和应用程…