2025电赛视觉——K230自训练模型串口发送问题解决(Base亚博智能)
K230自训练模型串口发送问题解决(Base亚博智能)
基础问题的解决
之前本来想直接基于亚博智能的yolov8目标检测魔改,后来技术客服说不能直接魔改还是得基于佳楠科技的去使用……因此搞了整整一下午,最后还是在ai的帮助下完成了相关内容的调试。
作者自己想自己训练一个识别stm32单片机的yolo目标检测算法,一开始基于佳楠科技在线训练后可以得到一个压缩包,nncase版本的多少就看固件烧的是哪一个,名字里就能够看出来;此外,为保证识别精度,建议一个目标能采样100张以上。
得到的压缩包如下,就把mp_deployment_source文件夹放到sdcard目录里面就行,随后打开det_vedio_1_3.py
,直接运行就会发现已经能正确识别单片机了,但是却不能像官方例程用yolov8一样在终端输出对应信息并通过串口发送,根本问题就在于没有打开串口及时反馈相应信息,因此需要魔改。
观察官方例程(部分可以发现)完成串口发送代码主要是自定义函数draw_results
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def draw_result (self,pl,dets ): """ 绘制检测结果 Draw detection results pl: PipeLine实例 PipeLine instance dets: 检测结果 Detection results """ with ScopedTiming("display_draw" ,self .debug_mode >0 ): if dets: pl.osd_img.clear() for i in range (len (dets[0 ])): x, y, w, h = map (lambda x: int (round (x, 0 )), dets[0 ][i]) pl.osd_img.draw_rectangle(x,y, w, h, color=self .color_four[dets[1 ][i]],thickness=4 ) pl.osd_img.draw_string_advanced( x , y-50 ,32 , " " + self .labels[dets[1 ][i]] + " " + str (round (dets[2 ][i],2 )) , color=self .color_four[dets[1 ][i]]) pto_data = pto.get_object_detect_data(x, y, w, h, self .labels[dets[1 ][i]]) uart.send(pto_data) print (pto_data) else : pl.osd_img.clear()
在主函数里可以发现传入的就是res
1 2 3 4 res = ob_det.run(img) ob_det.draw_result(pl,res)
因此要想实现diy串口接发就得搞清楚res
是啥
这时候可以进入libs文件夹逐个查阅引用的文件并用Ctrl+F
查找res,就在PlatTasks.py
里看到了关于res
的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class DetectionApp (AIBase ):(注意是Detection这个类因为是目标检测)...... def draw_result (self,draw_img,res ): with ScopedTiming("draw osd" ,self .debug_mode > 0 ): if self .mode=="video" : draw_img.clear() if res["boxes" ]: for i in range (len (res["boxes" ])): x=int (res["boxes" ][i][0 ] * self .display_size[0 ] // self .rgb888p_size[0 ]) y=int (res["boxes" ][i][1 ] * self .display_size[1 ] // self .rgb888p_size[1 ]) w = int (float (res["boxes" ][i][2 ] - res["boxes" ][i][0 ]) * self .display_size[0 ] // self .rgb888p_size[0 ]) h = int (float (res["boxes" ][i][3 ] - res["boxes" ][i][1 ]) * self .display_size[1 ] // self .rgb888p_size[1 ]) draw_img.draw_rectangle(x , y , w , h , color=self .color_four[res["idx" ][i]]) draw_img.draw_string_advanced(x, y-50 ,24 , self .labels[res["idx" ][i]] + " " + str (round (res["scores" ][i],2 )) , color=self .color_four[res["idx" ][i]])
由此可以得知
res["boxes"]
: 是一个列表,其中每个元素 res["boxes"][i]
是一个包含4个整数的数组,格式为 [x_min, y_min, x_max, y_max]
。
res["idx"]
: 是一个列表,其中每个元素 res["idx"][i]
是一个整数,代表类别索引。
res["scores"]
: 是一个列表,其中每个元素 res["scores"][i]
是一个浮点数,代表置信度。
也就是说
dets[0]
: 包含所有最终边界框的列表,每个边界框是 [x, y, w, h]
。
dets[1]
: 包含每个边界框对应类别索引的列表。
dets[2]
: 包含每个边界框对应置信度分数的列表。
因此,关键在于:基于``res["boxes"][i]中计算出
x, y, w, h`
就可以得到下面的代码用于替换while
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 while True : with ScopedTiming("total" , 1 ): img = pl.get_frame() res = det_app.run(img) if res and res["boxes" ]: for i in range (len (res["boxes" ])): box = res["boxes" ][i] x_min, y_min, x_max, y_max = box[0 ], box[1 ], box[2 ], box[3 ] x = x_min y = y_min w = x_max - x_min h = y_max - y_min class_index = res["idx" ][i] label_name = labels[class_index] pto_data = pto.get_object_detect_data(x, y, w, h, label_name) uart.send(pto_data) print ("发送串口数据:" , pto_data) det_app.draw_result(pl.osd_img, res) pl.show_image() gc.collect()
就可以得到和官方例程一样的输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 total took 35.36 ms 发送串口数据: $27,14,090,051,052,032,stm# total took 35.34 ms 发送串口数据: $27,14,092,048,051,037,stm# total took 35.63 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 33.39 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 35.47 ms 发送串口数据: $27,14,090,046,052,041,stm# total took 37.54 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 34.81 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 34.00 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 35.18 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 34.84 ms 发送串口数据: $27,14,090,051,052,033,stm# total took 36.54 ms 发送串口数据: $27,14,090,048,052,037,stm# total took 34.52 ms 发送串口数据: $27,14,090,047,052,037,stm#
更进一步地,也可以尝试直接封装函数(不过受权限控制好像改不了)
在 DetectionApp
类中添加 send_serial_data
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class DetectionApp (AIBase ): def draw_result (self,draw_img,res ): def send_serial_data (self, res, pto_instance, uart_instance, labels_list ): if res and res["boxes" ]: for i in range (len (res["boxes" ])): box = res["boxes" ][i] x_min, y_min, x_max, y_max = box[0 ], box[1 ], box[2 ], box[3 ] w = x_max - x_min h = y_max - y_min class_index = res["idx" ][i] label_name = labels_list[class_index] pto_data = pto_instance.get_object_detect_data(x_min, y_min, w, h, label_name) uart_instance.send(pto_data) print ("发送串口数据:" , pto_data) def get_cur_result (self ): return self .cur_result
随后while
就可以用更简洁的形式编写:
1 2 3 4 5 6 7 8 9 10 11 while True : with ScopedTiming("total" , 1 ): img = pl.get_frame() res = det_app.run(img) det_app.send_serial_data(res, pto, uart, labels) det_app.draw_result(pl.osd_img, res) pl.show_image() gc.collect()
DIY一个串口输出函数
亚博官方的串口发送函数会把例程对应序号和长度也输出出去,个人感觉没啥必要因此就想diy一个。
只需要用fstring
替换原来的代码即可:
1 pto_data = pto_instance.get_object_detect_data(x_min, y_min, w, h, label_name) -> pto_data = f"${x_min} , {y_min} , {w} , {h} , {label_name} #"
更进一步地,要是想实现只输出识别物体的中心点等其他需求,可以这样操作
首先需要在获取到物体的 x, y, w, h
后,计算出它的中心点坐标:
x
, y
是左上角的坐标
w
, h
是物体的宽度和高度
中心点坐标的计算公式是:
center_x = x + w / 2
center_y = y + h / 2
在发送数据前,最好先定义一个简单的格式,这样接收方(比如STM32、Arduino)才能准确地解析数据。一个好的协议通常包含:
起始符 :告诉接收方一条新数据的开始,比如 $
或 *
。
数据体 :想发送的实际内容,数据之间用分隔符 (如逗号 ,
)隔开。
结束符 :告诉接收方这条数据已经结束,比如 #
或换行符 \n
。
根据发送中心点坐标的需求可以定义这样一个格式:$center_x,center_y#
。 例如,如果中心点是 (120, 240)
,那么发送的字符串就是 $120,240#
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 while True : with ScopedTiming("total" , 1 ): img = pl.get_frame() res = det_app.run(img) if res and res["boxes" ]: for i in range (len (res["boxes" ])): box = res["boxes" ][i] x_min, y_min, x_max, y_max = box[0 ], box[1 ], box[2 ], box[3 ] w = x_max - x_min h = y_max - y_min center_x = x_min + w // 2 center_y = y_min + h // 2 custom_pto_data = f"${center_x} ,{center_y} #" uart.send(custom_pto_data) print ("发送自定义串口数据:" , custom_pto_data) det_app.draw_result(pl.osd_img, res) pl.show_image() gc.collect()
最后,给出我的完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 ''' Script: deploy_det_video.py 脚本名称:deploy_det_video.py Description: This script runs a real-time object detection application on an embedded device. It uses a pipeline to capture video frames, performs inference using a pre-trained Kmodel, and displays the detection results (bounding boxes, class labels) on screen. The model configuration is loaded from the Canaan online training platform via a JSON config file. 脚本说明: 本脚本在嵌入式设备上运行实时目标检测应用。它通过捕获视频帧,使用预训练的 Kmodel 进行推理,并在屏幕上显示检测结果(边界框、类别标签)。 模型配置文件通过 Canaan 在线训练平台从 JSON 文件加载。 Author: Canaan Developer 作者:Canaan 开发者 ''' import os, gcfrom libs.PlatTasks import DetectionAppfrom libs.PipeLine import PipeLinefrom libs.Utils import *from libs.YbProtocol import YbProtocolfrom ybUtils.YbUart import YbUartuart = YbUart(baudrate=115200 ) pto = YbProtocol() display_mode="lcd" rgb888p_size=[224 ,224 ] if display_mode=="hdmi" : display_size=[1920 ,1080 ] else : display_size=[640 ,480 ] root_path = "/sdcard/mp_deployment_source/" deploy_conf = read_json(root_path + "/deploy_config.json" ) kmodel_path = root_path + deploy_conf["kmodel_path" ] labels = deploy_conf["categories" ] confidence_threshold = deploy_conf["confidence_threshold" ] nms_threshold = deploy_conf["nms_threshold" ] model_input_size = deploy_conf["img_size" ] nms_option = deploy_conf["nms_option" ] model_type = deploy_conf["model_type" ] anchors = [] if model_type == "AnchorBaseDet" : anchors = deploy_conf["anchors" ][0 ] + deploy_conf["anchors" ][1 ] + deploy_conf["anchors" ][2 ] inference_mode = "video" debug_mode = 0 pl = PipeLine(rgb888p_size=rgb888p_size, display_mode=display_mode) pl.create() display_size = pl.get_display_size() det_app = DetectionApp(inference_mode,kmodel_path,labels,model_input_size,anchors,model_type,confidence_threshold,nms_threshold,rgb888p_size,display_size,debug_mode=debug_mode) det_app.config_preprocess() while True : with ScopedTiming("total" , 1 ): img = pl.get_frame() res = det_app.run(img) if res and res["boxes" ]: for i in range (len (res["boxes" ])): box = res["boxes" ][i] x_min, y_min, x_max, y_max = box[0 ], box[1 ], box[2 ], box[3 ] x = x_min y = y_min w = x_max - x_min h = y_max - y_min center_x = x_min + w // 2 center_y = y_min + h // 2 class_index = res["idx" ][i] label_name = labels[class_index] pto_data = f"${center_x} ,{center_y} ,{label_name} #" uart.send(pto_data) print ("发送串口数据:" , pto_data) det_app.draw_result(pl.osd_img, res) pl.show_image() gc.collect() det_app.deinit() pl.destroy()
基于本地训练的模型部署及串口通信
在此之前还是先把本人可行的训练流程贴一下(主要基于yolo大作战):
博主手边就有一个说stm32的F103C8T6,因此就想把stm32识别出来。
1 git clone https://github.com/ultralytics/yolov5.git
参考的是国内源安装torch2.1.2+cu121、torch2.3.1+cu121_torch==2.1.2-CSDN博客 ,也就是阿里云源提供了镜像,清华源好像是没有提供
1 pip install torch==2.3.1+cu121 torchvision torchaudio -f https://mirrors.aliyun.com/pytorch-wheels/cu121/
1 2 cd yolov5 pip install -r requirements.txt
1 python train.py --weight yolov5n.pt --cfg models/yolov5n.yaml --data datasets/try.yaml --epochs 100 --batch-size 16 --imgsz 320 --device '0'
代码需要yaml
提供参考,告诉代码文件夹路径和类别名称,因此需要提前准备好相关内容。具体流程就是按照合理比例划分images
和labels
的train
、test
、val
文件夹,然后借助在线打标工具makesense.ai
打就行,这个软件比较简单不懂的话可以查看其他帖子。然后选择Action
的Export
进行导出,如果是用佳楠训练就导出xml
,本地训练就导出txt
,然后也将其放到labels里的train
、test
、val
文件夹(注意要和images名字、内容都要对的上)。
try.yaml
就是直接模仿的水果数据集的yaml。
fruits_yolo.yaml
1 2 3 4 5 6 7 8 9 10 11 path: datasets/fruits_yolo train: images/train val: images/val test: images/test names: 0: apple 1: banana 2: orange
try.yaml
1 2 3 4 5 6 7 8 9 path: datasets/try train: images/train val: images/val test: images/test names: 0: stm
⑥转换为onno
文件(博主这里已经训练过五次因此是exp5
,要根据实际情况调整内容)
首先要先配置环境,建议使用wsl Ubuntu 22.04
或者配个虚拟机VMware
,博主嫌麻烦就用了wsl
1 2 3 4 5 # linux平台:nncase和nncase-kpu可以在线安装,nncase-2.x 需要安装 dotnet-7 sudo apt-get install -y dotnet-sdk-7.0 pip install --upgrade pip pip install nncase==2.9.0 pip install nncase-kpu==2.9.0
其中这个==nncase
版本和上文一样要根据软件版本号确定==
1 2 3 4 # 除nncase和nncase-kpu外,脚本还用到的其他库包括: pip install onnx pip install onnxruntime pip install onnxsim
随后可以转换为onnx
格式了
1 2 # 导出onnx,pt模型路径请自行选择 python export.py --weight runs/train/exp5/weights/best.pt --imgsz 320 --batch 1 --include onnx
还是先配置环境,下佳楠提供的脚本工具,将模型转换脚本工具 test_yolov5.zip 解压到 yolov5 目录下;(注意此时路径在yolov5下)
1 2 wget https://kendryte-download.canaan-creative.com/developer/k230/yolo_files/test_yolov5.zip unzip test_yolov5.zip
然后就可以转换kmodel
1 2 3 4 5 cd test_yolov5/detect # 转换kmodel,onnx模型路径请自定义,生成的kmodel在onnx模型同级目录下 python to_kmodel.py --target k230 --model ../../runs/train/exp5/weights/best.onnx --dataset ../test --input_width 320 --input_height 320 --ptq_option 0 # 要是想回到yolov5文件夹目录下把下面的命令行也运行了 cd ../../
环境及相关权重转换好就可以改写代码了。
本地训练的源代码是基于佳楠科技的yolo大作战改写而成。本地部署和佳楠部署的最大区别就是没有json文件和用的类不一样,本地部署是直接基于yolo.py
来创建,因此会导致res
的结果也不一样。首先可以print(res)
看看res
的内容:示例为array([[74.0, 105.0, 151.0, 143.0, 0.5786896, 0.0]]
因此这与佳楠的res
是不一致的{'boxes': [[...], [...]], 'scores': [...], 'idx': [...]}
就需要改动才可以使用。主要差别就在于获取坐标的过程,其他部分的参数可以直接抄佳楠的json
文件。
具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 from libs.PipeLine import PipeLine, ScopedTimingfrom libs.YOLO import YOLOv5import os,sys,gcimport ulab.numpy as npimport imagefrom libs.YbProtocol import YbProtocolfrom ybUtils.YbUart import YbUartuart = YbUart(baudrate=115200 ) display_mode="lcd" rgb888p_size=[224 ,224 ] display_size=[640 ,480 ] kmodel_path="/data/best.kmodel" labels = ["stm" ] confidence_threshold = 0.3 nms_threshold=0.5 model_input_size=[320 ,320 ] pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode) pl.create() yolo=YOLOv5(task_type="detect" ,mode="video" ,kmodel_path=kmodel_path,labels=labels,rgb888p_size=rgb888p_size,model_input_size=model_input_size,display_size=display_size,conf_thresh=confidence_threshold,nms_thresh=nms_threshold,max_boxes_num=50 ,debug_mode=0 ) yolo.config_preprocess() while True : with ScopedTiming("total" ,1 ): img=pl.get_frame() res=yolo.run(img) if res is not None and len (res) > 0 : for box_data in res: x_min = int (box_data[0 ]) y_min = int (box_data[1 ]) x_max = int (box_data[2 ]) y_max = int (box_data[3 ]) confidence = box_data[4 ] class_index = int (box_data[5 ]) x = x_min y = y_min w = x_max - x_min h = y_max - y_min center_x = x_min + w // 2 center_y = y_min + h // 2 if class_index < len (labels): label_name = labels[class_index] else : label_name = "unknown" pto_data = f"${x_min} ,{y_min} ,{x_max} ,{y_max} ,{center_x} ,{center_y} ,{label_name} #" print ("发送串口数据:" , pto_data) uart.send(pto_data) yolo.draw_result(res,pl.osd_img) pl.show_image() gc.collect() yolo.deinit() pl.destroy()