K230 视觉方案

撰写人:Keruone
撰写日期 2025.5.26

由于总结分析部分中 k230的篇幅占的有点太大了,所以将视觉方案单独拿出来写。
可以阅读此处下载html

K230 视觉方案

  1. k230 视觉方案

1. k230 视觉方案

视觉好啊,可惜我们探索的不算太多。
注意,如果你使用默认的摄像头,你会发现视野非常狭隘,所以我们使用的是140度的广角镜头

1.1 视觉方案介绍

在本项目中,我们视觉主要是有以下几个目标:

  1. 可以摆脱循迹的限制,自由移动
  2. 可以识别圆柱,防止机械臂防止圆环时存在偏差,导致防止失败
  3. 可以通过视觉的方案,实现抓取对方得分区的圆环和物块

为了轻便快速,我们最终决定使用yolov11作为解决方案。毕竟比较与SAM等视觉方案,yolo实在快的不得了。

1.2 k230 代码实现和分析

1.2.1 k230 摄像头配置

由于它的对于单个摄像头的案例还能用,就参考这里吧。不过这里只是单纯的使用摄像头。
摄像头与yolo(或者其它AI方案)的结合可以使用pipeline来搭建,可以参考这里

但是这有一个问题,它以上的代码针对的都是单摄像头,对于多摄像头的配置并没有给出。如果你直接使用两次pipeline,那就完了。这里我觉得是这板子做的最拉跨的地方。

具体的多摄像头配置可以参考后文视觉代码
不过需要注意!使用了多摄像头后,延迟会大大增加!本来单摄像头一轮的间隔大约为50ms,现在一轮大约为200ms!

1.2.2 k230 yolov11配置

数据很重要,但是非常可惜,我们只能自己拍摄数据集。所以先分享我们获取图片的代码

# 获取图片的代码
import time, os, sys

from machine import Pin
from machine import FPIOA
from media.sensor import *
from media.display import *
from media.media import *

sensor_id = 1
sensor = None

# 创建FPIOA对象,用于初始化引脚功能配置
fpioa = FPIOA()
fpioa.set_function(53,FPIOA.GPIO53)

# 按键引脚为53,按下时为高电平,所以这里设置为下拉并设置为输入模式
button = Pin(53, Pin.IN, Pin.PULL_DOWN)  # 使用下拉电阻

# 显示模式选择:可以是 "VIRT"、"LCD" 或 "HDMI"
DISPLAY_MODE = "LCD"

# 根据模式设置显示宽高
if DISPLAY_MODE == "VIRT":
    # 虚拟显示器模式
    DISPLAY_WIDTH = ALIGN_UP(1920, 16)
    DISPLAY_HEIGHT = 1080
elif DISPLAY_MODE == "LCD":
    # 3.1寸屏幕模式
    DISPLAY_WIDTH = 800
    DISPLAY_HEIGHT = 480
elif DISPLAY_MODE == "HDMI":
    # HDMI扩展板模式
    DISPLAY_WIDTH = 1920
    DISPLAY_HEIGHT = 1080
else:
    raise ValueError("未知的 DISPLAY_MODE,请选择 'VIRT', 'LCD' 或 'HDMI'")

# 定义保存目录
save_dir = "./sdcard/saved_photos"

try:
    # 构造一个具有默认配置的摄像头对象
    sensor = Sensor(id=sensor_id)
    # 重置摄像头sensor
    sensor.reset()

    # 无需进行镜像翻转
    # 设置水平镜像
    # sensor.set_hmirror(False)
    # 设置垂直翻转
    sensor.set_vflip(False)

    # 设置通道0的输出尺寸为显示分辨率
    sensor.set_framesize(width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, chn=CAM_CHN_ID_0)
    # 设置通道0的输出像素格式为RGB565
    sensor.set_pixformat(Sensor.RGB565, chn=CAM_CHN_ID_0)

    sensor.set_framesize(width=1920, height=1080, chn=CAM_CHN_ID_1)
    # 设置通道0的输出像素格式为RGB565
    sensor.set_pixformat(Sensor.RGB565, chn=CAM_CHN_ID_1)

    # 根据模式初始化显示器
    if DISPLAY_MODE == "VIRT":
        Display.init(Display.VIRT, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, fps=60)
    elif DISPLAY_MODE == "LCD":
        Display.init(Display.ST7701, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, to_ide=True)
    elif DISPLAY_MODE == "HDMI":
        Display.init(Display.LT9611, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, to_ide=True)

    # 初始化媒体管理器
    MediaManager.init()
    # 启动传感器
    sensor.run()

    try:
        files = os.listdir(save_dir)
        photo_files = [f for f in files if f.startswith("photo") and f.endswith(".jpg")]
        max_idx = -1
        for f in photo_files:
            try:
                idx = int(f.replace("photo", "").replace(".jpg", ""))
                if idx > max_idx:
                    max_idx = idx
            except:
                pass

        i = max_idx + 1
        print(f"检测到{len(photo_files)}张照片,将从编号{i}开始保存")
    except:
        i = 0


    while True:
        os.exitpoint()

        # 捕获通道0的图像
        img = sensor.snapshot(chn=CAM_CHN_ID_0)

        img1 = sensor.snapshot(chn=CAM_CHN_ID_1)

        # 显示捕获的图像,中心对齐,居中显示
        Display.show_image(img)
        #按键按下保存图片
        if button.value() == 1:
            time.sleep(0.02)
            if button.value() == 1:
                try:
                    save_path = f"./sdcard/saved_photos/photo{i}.jpg"
                    img1.save(save_path)
                    print(f"保存到当前目录成功: {save_path}")
                except Exception as e:
                    print(f"保存到当前目录失败: {e}")

                time.sleep(0.1)
                i += 1


except KeyboardInterrupt as e:
    print("用户停止: ", e)
except BaseException as e:
    print(f"异常: {e}")
finally:
    # 停止传感器运行
    if isinstance(sensor, Sensor):

        sensor.stop()
    # 反初始化显示模块
    Display.deinit()
    os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
    time.sleep_ms(100)
    # 释放媒体缓冲区
    MediaManager.deinit()

此外,为了方便图片的处理,我们还编写了如下几段小代码

yolo训练的yaml文件
train: C:\\Users\\G\\Desktop\\robot-git\\yoloRobot\\data\\images\\train
val: C:\\Users\\G\\Desktop\\robot-git\\yoloRobot\\data\\images\\val
test: C:\\Users\\G\\Desktop\\robot-git\\yoloRobot\\data\\images\\test

nc: 9

# Classes
names: 
  0: platform
  1: column
  2: block
  3: red_score
  4: blue_score
  5: red_circle
  6: blue_circle
  7: platform_red
  8: platform_blue


0 图片批量添加前后缀
import os
import datetime

# ====== 在这里修改配置 ======
FOLDER_PATH = "train_photo0523_2"  # 要处理的文件夹路径
ADD_PREFIX = "0523"             # 要添加的前缀,留空则不添加
ADD_SUFFIX = ""               # 要在扩展名前添加的后缀,留空则不添加
ADD_DATE = False                  # 是否添加日期
DATE_FORMAT = "%Y%m%d"           # 日期格式
DATE_POSITION = "prefix"         # 日期位置:"prefix"添加到开头,"suffix"添加到扩展名前
# ===========================

def batch_rename_files():
    """批量修改文件名"""
    # 获取当前日期
    date_str = datetime.datetime.now().strftime(DATE_FORMAT) if ADD_DATE else ''
    
    # 遍历文件夹
    for filename in os.listdir(FOLDER_PATH):
        # 获取文件完整路径
        old_path = os.path.join(FOLDER_PATH, filename)
        
        # 跳过目录
        if os.path.isdir(old_path):
            continue
            
        # 分离文件名和扩展名
        name, ext = os.path.splitext(filename)
        
        # 构建新文件名
        if DATE_POSITION == 'prefix':
            new_name = f"{ADD_PREFIX}{date_str}{name}{ADD_SUFFIX}{ext}"
        else:  # suffix
            new_name = f"{ADD_PREFIX}{name}{ADD_SUFFIX}{date_str}{ext}"
        
        # 新文件完整路径
        new_path = os.path.join(FOLDER_PATH, new_name)
        
        # 重命名文件
        try:
            os.rename(old_path, new_path)
            print(f"Renamed: {filename} -> {new_name}")
        except Exception as e:
            print(f"Error renaming {filename}: {e}")

if __name__ == '__main__':
    # 确认配置
    print("=== 文件重命名配置 ===")
    print(f"处理文件夹: {FOLDER_PATH}")
    print(f"添加前缀: '{ADD_PREFIX}'")
    print(f"添加后缀: '{ADD_SUFFIX}'")
    print(f"添加日期: {ADD_DATE} ({DATE_FORMAT})")
    print(f"日期位置: {DATE_POSITION}")
    print("====================")
    
    input("按Enter键开始重命名,或Ctrl+C取消...")
    
    # 调用函数
    batch_rename_files()
    print("文件重命名完成!")
1 自己撰写的图片增强(可以不用)(当然你也可以使用Albumentations等现成的包)
"""
本代码用于将Labelme标注的json文件转换为YOLO格式的txt文件,并添加简单的数据增强
执行顺序:1.移动文件夹 2.应用简单增强 3.转换为txt 4.分割
"""
import os
import json
import random
import shutil
import cv2
import numpy as np
from shapely.geometry import Polygon, Point, MultiPolygon
from shapely.validation import make_valid
import re


ori_dic_path = 'train_photo0511_dataAll/';   #原始图片路径
# 目标文件夹路径
target_dic_path = 'yoloRobot/data'; # 内部分为 images和labels两个文件夹

# 1. copy 所有 在path 下有的json文件到另一个文件夹下,image 进入 images文件夹下,json 进入 labels 文件夹下
def copy_json_to_labels(json_path, target_path):
    file_idx = 0
    if not os.path.exists(target_path):
        os.makedirs(target_path)
    # 创建images和labels文件夹
    images_path = os.path.join(target_path, 'images')
    labels_path = os.path.join(target_path, 'labels')
    # 检查文件夹是否存在,如果不存在则创建
    if not os.path.exists(images_path):
        os.makedirs(images_path)
    if not os.path.exists(labels_path):
        os.makedirs(labels_path)
    # 遍历json_path下的所有文件
    for filename in os.listdir(json_path):
        # 复制有json的 json文件,及其标注的图片 到目标文件夹下
        if filename.endswith('.json'):
            
            # # test-used 只保留10%的数据- Start ---------------------------------------------------------
            # if random.random() > 0.05:
            #     continue
            # # test-used 只保留10%的数据- End ---------------------------------------------------------
            # # test-used 只保留 n 份数据- Start ---------------------------------------------------------
            # if file_idx >= 2:
            #     continue
            # file_idx += 1
            # # test-used 只保留 n 份数据- End ---------------------------------------------------------

            label_src = os.path.join(json_path, filename)
            label_dst = os.path.join(labels_path, filename)
            image_src = os.path.join(json_path, filename.replace('.json', '.jpg'))
            image_dst = os.path.join(images_path, filename.replace('.json', '.jpg'))
            # 复制json文件并移动到目标文件夹下
            with open(label_src, 'r') as f:
                data = json.load(f)
            with open(label_dst, 'w') as f:
                json.dump(data, f, indent=4)
            # 复制图片文件
            if os.path.exists(image_src):
                with open(image_src, 'rb') as f:
                    image_data = f.read()
                with open(image_dst, 'wb') as f:
                    f.write(image_data)
            else:
                print(f"Image file {image_src} does not exist.")
    print(f"Copied {len(os.listdir(labels_path))} json files and their corresponding images to {target_path}.")

# 创建有效的多边形(处理自相交等问题)
def create_valid_polygon(points):
    try:
        poly = Polygon(points)
        if not poly.is_valid:
            poly = make_valid(poly)
        return poly
    except Exception as e:
        print(f"Error creating polygon: {e}")
        # 如果创建多边形失败,返回一个简单的凸包
        try:
            from scipy.spatial import ConvexHull
            hull = ConvexHull(points)
            hull_points = [points[i] for i in hull.vertices]
            return Polygon(hull_points)
        except:
            # 如果凸包也失败了,返回None
            return None


# 2. 简单的数据增强函数 - 针对图像进行增强,同时复制对应的json标注文件
def create_valid_polygon(points):
    """创建有效的Shapely多边形"""
    try:
        if len(points) < 3:
            return None
        # 确保多边形是闭合的
        points_list = [(p[0], p[1]) for p in points]
        if points_list[0] != points_list[-1]:
            points_list.append(points_list[0])
        return Polygon(points_list)
    except Exception as e:
        print(f"Error creating polygon: {e}")
        return None

def apply_horizontal_flip(image, json_path, images_path, labels_path, img_filename, json_filename):
    """应用水平翻转并保存图像和标注"""
    
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    h_flipped_img = cv2.flip(image, 1)  # 1表示水平翻转
    h_flip_img_filename = img_filename.replace('.jpg', '_hflip.jpg')
    h_flip_img_path = os.path.join(images_path, h_flip_img_filename)
    cv2.imwrite(h_flip_img_path, h_flipped_img)
    
    # 创建水平翻转的JSON标注
    h_flip_json_filename = json_filename.replace('.json', '_hflip.json')
    h_flip_json_path = os.path.join(labels_path, h_flip_json_filename)
    
    # 复制JSON数据并修改点坐标
    h_flip_json_data = json_data.copy()
    img_width = h_flip_json_data['imageWidth']
    
    # 修改所有形状的坐标
    for shape in h_flip_json_data['shapes']:
        for point in shape['points']:
            # 水平翻转时,x坐标需要变为img_width-x
            point[0] = img_width - point[0]
    
    # 保存修改后的JSON
    with open(h_flip_json_path, 'w') as f:
        json.dump(h_flip_json_data, f, indent=4)
    
    return 1  # 返回增强的图像数量

def apply_vertical_flip(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用垂直翻转并保存图像和标注"""
    v_flipped_img = cv2.flip(image, 0)  # 0表示垂直翻转
    v_flip_img_filename = img_filename.replace('.jpg', '_vflip.jpg')
    v_flip_img_path = os.path.join(images_path, v_flip_img_filename)
    cv2.imwrite(v_flip_img_path, v_flipped_img)
    
    # 创建垂直翻转的JSON标注
    v_flip_json_filename = json_filename.replace('.json', '_vflip.json')
    v_flip_json_path = os.path.join(labels_path, v_flip_json_filename)
    
    # 复制JSON数据并修改点坐标
    v_flip_json_data = json_data.copy()
    img_height = v_flip_json_data['imageHeight']
    
    # 修改所有形状的坐标
    for shape in v_flip_json_data['shapes']:
        for point in shape['points']:
            # 垂直翻转时,y坐标需要变为img_height-y
            point[1] = img_height - point[1]
    
    # 保存修改后的JSON
    with open(v_flip_json_path, 'w') as f:
        json.dump(v_flip_json_data, f, indent=4)
    
    return 1  # 返回增强的图像数量

def apply_both_flips(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用水平和垂直翻转并保存图像和标注"""
    hv_flipped_img = cv2.flip(image, -1)  # -1表示同时水平和垂直翻转
    hv_flip_img_filename = img_filename.replace('.jpg', '_hvflip.jpg')
    hv_flip_img_path = os.path.join(images_path, hv_flip_img_filename)
    cv2.imwrite(hv_flip_img_path, hv_flipped_img)
    
    # 创建同时水平和垂直翻转的JSON标注
    hv_flip_json_filename = json_filename.replace('.json', '_hvflip.json')
    hv_flip_json_path = os.path.join(labels_path, hv_flip_json_filename)
    
    # 复制JSON数据并修改点坐标
    hv_flip_json_data = json_data.copy()
    img_width = hv_flip_json_data['imageWidth']
    img_height = hv_flip_json_data['imageHeight']
    
    # 修改所有形状的坐标
    for shape in hv_flip_json_data['shapes']:
        for point in shape['points']:
            # 同时水平和垂直翻转时,x坐标需要变为img_width-x,y坐标需要变为img_height-y
            point[0] = img_width - point[0]
            point[1] = img_height - point[1]
    
    # 保存修改后的JSON
    with open(hv_flip_json_path, 'w') as f:
        json.dump(hv_flip_json_data, f, indent=4)
    
    return 1  # 返回增强的图像数量

def apply_low_resolution(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)
        
    """应用低分辨率效果并保存图像和标注"""
    h, w = image.shape[:2]
    # 随机降低分辨率的比例,范围在0.3到0.7之间
    scale_factor = random.uniform(0.3, 0.7)
    low_res_w = int(w * scale_factor)
    low_res_h = int(h * scale_factor)
    # 降低分辨率
    low_res_img = cv2.resize(image, (low_res_w, low_res_h), interpolation=cv2.INTER_AREA)
    # 恢复到原始大小
    low_res_img = cv2.resize(low_res_img, (w, h), interpolation=cv2.INTER_NEAREST)
    
    low_res_img_filename = img_filename.replace('.jpg', '_lowres.jpg')
    low_res_img_path = os.path.join(images_path, low_res_img_filename)
    cv2.imwrite(low_res_img_path, low_res_img)
    
    # # 低分辨率图像的标签与原图相同
    # low_res_json_filename = json_filename.replace('.json', '_lowres.json')
    # low_res_json_path = os.path.join(labels_path, low_res_json_filename)
    # json_path = os.path.join(labels_path, json_filename)
    # shutil.copy(json_path, low_res_json_path)
    # 不要copy,会出问题。正解是根据 json_data 进行修改
    low_res_json_filename = json_filename.replace('.json', '_lowres.json')
    low_res_json_path = os.path.join(labels_path, low_res_json_filename)
    with open(low_res_json_path, 'w') as f:
        json.dump(json_data, f, indent=4)
    
    return 1  # 返回增强的图像数量

def apply_blur(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用局部模糊效果并保存图像和标注"""
    blur_img = image.copy()
    h, w = blur_img.shape[:2]
    
    # 随机选择1到3个区域进行模糊处理
    num_blur_regions = random.randint(1, 7)
    
    for _ in range(num_blur_regions):
        # 随机确定模糊区域的大小和位置
        blur_w = random.randint(int(w * 0.1), int(w * 0.4))
        blur_h = random.randint(int(h * 0.1), int(h * 0.4))
        
        # 确保模糊区域不会超出图像边界
        x = random.randint(0, w - blur_w)
        y = random.randint(0, h - blur_h)
        
        # 提取区域
        region = blur_img[y:y+blur_h, x:x+blur_w]
        
        # 应用高斯模糊
        blur_kernel_size = random.choice([5, 7, 9, 11])
        blurred_region = cv2.GaussianBlur(region, (blur_kernel_size, blur_kernel_size), 0)
        
        # 将模糊区域放回原图
        blur_img[y:y+blur_h, x:x+blur_w] = blurred_region
    
    blur_img_filename = img_filename.replace('.jpg', '_blur.jpg')
    blur_img_path = os.path.join(images_path, blur_img_filename)
    cv2.imwrite(blur_img_path, blur_img)
    
    # 局部模糊图像的标签与原图相同
    # blur_json_filename = json_filename.replace('.json', '_blur.json')
    # blur_json_path = os.path.join(labels_path, blur_json_filename)
    # json_path = os.path.join(labels_path, json_filename)
    # shutil.copy(json_path, blur_json_path)
    # 不要copy,会出问题。正解是根据 json_data 进行修改
    blur_json_filename = json_filename.replace('.json', '_blur.json')
    blur_json_path = os.path.join(labels_path, blur_json_filename)
    with open(blur_json_path, 'w') as f:
        json.dump(json_data, f, indent=4)
        return 1 # 返回增强的图像数量

def apply_bulge_effect(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用凸镜效果(哈哈镜)并保存图像和标注,合理处理超出边界的点"""
    try:
        h, w = image.shape[:2]
        
        # 创建网格
        map_x = np.zeros((h, w), dtype=np.float32)
        map_y = np.zeros((h, w), dtype=np.float32)
        
        # 计算图像中心
        center_x, center_y = w // 2, h // 2
        
        # 随机选择扭曲强度
        distortion_strength = random.uniform(0.5, 1.5)
        
        # 创建凸镜效果的映射
        for y in range(h):
            for x in range(w):
                # 计算到中心的距离
                dx = x - center_x
                dy = y - center_y
                r = np.sqrt(dx**2 + dy**2)
                
                # 应用凸镜变换
                if r == 0:
                    theta = 0
                    rho = 0
                else:
                    theta = np.arctan2(dy, dx)
                    rho = r * (1 - distortion_strength * (r / np.sqrt(w**2 + h**2)))
                
                # 计算新坐标
                map_x[y, x] = center_x + rho * np.cos(theta)
                map_y[y, x] = center_y + rho * np.sin(theta)
        
        # 应用重映射
        bulge_img = cv2.remap(image, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
        
        # 保存凸镜效果图像
        bulge_img_filename = img_filename.replace('.jpg', '_bulge.jpg')
        bulge_img_path = os.path.join(images_path, bulge_img_filename)
        cv2.imwrite(bulge_img_path, bulge_img)
        
        # 创建变形后的JSON标注
        bulge_json_filename = json_filename.replace('.json', '_bulge.json')
        bulge_json_path = os.path.join(labels_path, bulge_json_filename)
        
        # 复制JSON数据并修改点坐标
        bulge_json_data = json_data.copy()
        
        # 修改所有形状的坐标,并处理超出边界的情况
        valid_shapes = []
        
        for shape in bulge_json_data['shapes']:
            transformed_points = []
            boundary_points_count = 0  # 计数在边界上的点
            
            for point in shape['points']:
                # 计算到中心的距离
                dx = point[0] - center_x
                dy = point[1] - center_y
                r = np.sqrt(dx**2 + dy**2)
                
                # 应用相同的变换
                if r == 0:
                    theta = 0
                    rho = 0
                else:
                    theta = np.arctan2(dy, dx)
                    rho = r * (1 - distortion_strength * (r / np.sqrt(w**2 + h**2)))
                
                # 计算新坐标
                new_x = center_x + rho * np.cos(theta)
                new_y = center_y + rho * np.sin(theta)
                
                # 处理超出边界的点
                if new_x < 0:
                    new_x = 0
                    boundary_points_count += 1
                elif new_x >= w:
                    new_x = w - 1
                    boundary_points_count += 1
                    
                if new_y < 0:
                    new_y = 0
                    boundary_points_count += 1
                elif new_y >= h:
                    new_y = h - 1
                    boundary_points_count += 1
                
                transformed_points.append([new_x, new_y])
            
            # 检查是否所有点都在边界上(说明形状完全超出了图像)
            if boundary_points_count == len(shape['points']):
                # 所有点都在边界上,跳过这个形状
                continue
            
            # 检查形状是否有足够的点
            min_points = 3 if shape['shape_type'] == 'polygon' else (2 if shape['shape_type'] == 'line' else 1)
            
            if len(transformed_points) >= min_points:
                new_shape = shape.copy()
                new_shape['points'] = transformed_points
                valid_shapes.append(new_shape)
        
        # 更新JSON数据中的shapes,只保留有效的形状
        bulge_json_data['shapes'] = valid_shapes
        
        # 保存修改后的JSON
        with open(bulge_json_path, 'w') as f:
            json.dump(bulge_json_data, f, indent=4)
        
        return 1  # 返回增强的图像数量
    except Exception as e:
        print(f"Error applying bulge effect: {e}")
        return 0

def apply_pinch_effect(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用凹镜效果(哈哈镜)并保存图像和标注"""
    try:
        h, w = image.shape[:2]
        
        # 创建网格
        map_x = np.zeros((h, w), dtype=np.float32)
        map_y = np.zeros((h, w), dtype=np.float32)
        
        # 计算图像中心
        center_x, center_y = w // 2, h // 2
        
        # 随机选择扭曲强度
        distortion_strength = random.uniform(0.5, 1.5)
        
        # 创建凹镜效果的映射
        for y in range(h):
            for x in range(w):
                # 计算到中心的距离
                dx = x - center_x
                dy = y - center_y
                r = np.sqrt(dx**2 + dy**2)
                
                # 应用凹镜变换
                if r == 0:
                    theta = 0
                    rho = 0
                else:
                    theta = np.arctan2(dy, dx)
                    rho = r * (1 + distortion_strength * (r / np.sqrt(w**2 + h**2)))
                
                # 计算新坐标
                map_x[y, x] = center_x + rho * np.cos(theta)
                map_y[y, x] = center_y + rho * np.sin(theta)
        
        # 应用重映射
        pinch_img = cv2.remap(image, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
        
        # 保存凹镜效果图像
        pinch_img_filename = img_filename.replace('.jpg', '_pinch.jpg')
        pinch_img_path = os.path.join(images_path, pinch_img_filename)
        cv2.imwrite(pinch_img_path, pinch_img)
        
        # 创建变形后的JSON标注
        pinch_json_filename = json_filename.replace('.json', '_pinch.json')
        pinch_json_path = os.path.join(labels_path, pinch_json_filename)
        
        # 复制JSON数据并修改点坐标
        pinch_json_data = json_data.copy()
        
        # 修改所有形状的坐标
        for shape in pinch_json_data['shapes']:
            for point in shape['points']:
                # 计算到中心的距离
                dx = point[0] - center_x
                dy = point[1] - center_y
                r = np.sqrt(dx**2 + dy**2)
                
                # 应用相同的变换
                if r == 0:
                    theta = 0
                    rho = 0
                else:
                    theta = np.arctan2(dy, dx)
                    rho = r * (1 + distortion_strength * (r / np.sqrt(w**2 + h**2)))
                
                # 更新坐标
                point[0] = center_x + rho * np.cos(theta)
                point[1] = center_y + rho * np.sin(theta)
        
        # 保存修改后的JSON
        with open(pinch_json_path, 'w') as f:
            json.dump(pinch_json_data, f, indent=4)
        
        return 1  # 返回增强的图像数量
    except Exception as e:
        print(f"Error applying pinch effect: {e}")
        return 0

def apply_wave_effect(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用波浪效果(哈哈镜)并保存图像和标注"""
    try:
        h, w = image.shape[:2]
        
        # 创建网格
        map_x = np.zeros((h, w), dtype=np.float32)
        map_y = np.zeros((h, w), dtype=np.float32)
        
        # 随机选择波浪参数
        x_amplitude = random.uniform(1.0, 2.0)
        y_amplitude = random.uniform(1.0, 2.0)
        x_wavelength = random.uniform(10.0, 20.0)
        y_wavelength = random.uniform(10.0, 20.0)
        
        # 创建波浪效果的映射
        for y in range(h):
            for x in range(w):
                map_x[y, x] = x + x_amplitude * np.sin(y / y_wavelength * 2 * np.pi)
                map_y[y, x] = y + y_amplitude * np.sin(x / x_wavelength * 2 * np.pi)
        
        # 应用重映射
        wave_img = cv2.remap(image, map_x, map_y, interpolation=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT)
        
        # 保存波浪效果图像
        wave_img_filename = img_filename.replace('.jpg', '_wave.jpg')
        wave_img_path = os.path.join(images_path, wave_img_filename)
        cv2.imwrite(wave_img_path, wave_img)
        
        # 创建变形后的JSON标注
        wave_json_filename = json_filename.replace('.json', '_wave.json')
        wave_json_path = os.path.join(labels_path, wave_json_filename)
        
        # 复制JSON数据并修改点坐标
        wave_json_data = json_data.copy()
        
        # 修改所有形状的坐标
        for shape in wave_json_data['shapes']:
            for point in shape['points']:
                x, y = point[0], point[1]
                # 应用相同的波浪变换
                point[0] = x + x_amplitude * np.sin(y / y_wavelength * 2 * np.pi)
                point[1] = y + y_amplitude * np.sin(x / x_wavelength * 2 * np.pi)
        
        # 保存修改后的JSON
        with open(wave_json_path, 'w') as f:
            json.dump(wave_json_data, f, indent=4)
        
        return 1  # 返回增强的图像数量
    except Exception as e:
        print(f"Error applying wave effect: {e}")
        return 0

def apply_occlusion(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)
        
    """应用随机彩色多边形遮挡并保存图像和标注"""
    try:
        # 创建一个新的图像副本
        occluded_img = image.copy()
        h, w = occluded_img.shape[:2]
        
        # 创建一个新的JSON数据副本
        occluded_json_data = json_data.copy()
        
        # 随机生成1到3个彩色多边形
        num_polygons = random.randint(1, 9)
        
        # 创建遮挡多边形列表
        occlusion_polygons = []
        
        for _ in range(num_polygons):
            # 随机生成多边形顶点数量 (3-6个顶点)
            num_vertices = random.randint(3, 6)
            
            # 随机确定多边形的中心位置
            center_x = random.randint(int(w * 0.1), int(w * 0.9))
            center_y = random.randint(int(h * 0.1), int(h * 0.9))
            
            # 随机确定多边形的大小
            max_radius = min(int(w * 0.2), int(h * 0.2))
            min_radius = max(int(max_radius * 0.5), 20)  # 确保多边形不会太小
            
            # 生成多边形顶点
            polygon_points = []
            for i in range(num_vertices):
                angle = 2 * np.pi * i / num_vertices
                radius = random.randint(min_radius, max_radius)
                x = int(center_x + radius * np.cos(angle))
                y = int(center_y + radius * np.sin(angle))
                
                # 确保点在图像范围内
                x = max(0, min(x, w-1))
                y = max(0, min(y, h-1))
                
                polygon_points.append((x, y))
            
            # 随机生成颜色
            color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
            
            # 在图像上绘制多边形
            cv2.fillPoly(occluded_img, [np.array(polygon_points)], color)
            
            # 创建有效的多边形并添加到遮挡多边形列表
            valid_poly = create_valid_polygon(polygon_points)
            if valid_poly:
                occlusion_polygons.append(valid_poly)
        
        # 处理标注,检查是否被遮挡或分割
        new_shapes = []
        label_id_counter = {}  # 用于跟踪每个标签的ID计数
        
        for shape in occluded_json_data['shapes']:
            # 获取形状的标签和点坐标
            label = shape['label']
            points = shape['points']
            
            # 将形状转换为Shapely多边形或点
            if len(points) > 2:
                # 创建有效的多边形
                shape_polygon = create_valid_polygon(points)
                
                if not shape_polygon:
                    # 如果无法创建有效多边形,保留原始形状
                    new_shapes.append(shape)
                    continue
                
                # 检查是否与遮挡多边形相交
                is_intersected = False
                fragments = []
                
                for occlusion_poly in occlusion_polygons:
                    try:
                        if occlusion_poly.intersects(shape_polygon):
                            is_intersected = True
                            
                            # 计算差集,得到未被遮挡的部分
                            try:
                                difference = shape_polygon.difference(occlusion_poly)
                                
                                # 如果差集是MultiPolygon,表示原多边形被分割成多个部分
                                if isinstance(difference, MultiPolygon):
                                    for poly in difference.geoms:
                                        if not poly.is_empty and poly.area > 0.05 * shape_polygon.area:  # 只保留面积大于原始多边形5%的部分
                                            fragments.append(poly)
                                elif not difference.is_empty and difference.area > 0.05 * shape_polygon.area:
                                    fragments.append(difference)
                            except Exception as e:
                                print(f"Error calculating difference: {e}")
                                # 如果计算差集出错,保留原始形状
                                fragments = [shape_polygon]
                                break
                    except Exception as e:
                        print(f"Error checking intersection: {e}")
                        continue
                
                # 如果没有相交,保留原始形状
                if not is_intersected:
                    new_shapes.append(shape)
                else:
                    # 如果有相交,处理分割后的每个部分
                    if fragments:
                        # 为这个标签分配一个唯一ID
                        if label not in label_id_counter:
                            label_id_counter[label] = 0
                        label_id_counter[label] += 1
                        unique_id = label_id_counter[label]
                        
                        # 为每个分割部分创建新的形状
                        for i, fragment in enumerate(fragments):
                            try:
                                # 提取多边形的坐标点
                                if hasattr(fragment.exterior, 'coords'):
                                    fragment_coords = list(fragment.exterior.coords)
                                else:
                                    continue
                                
                                # 创建新的形状
                                new_shape = {
                                    'label': f"{label}_{unique_id}",  # 添加唯一ID
                                    'points': [[p[0], p[1]] for p in fragment_coords],
                                    'group_id': None,
                                    'shape_type': 'polygon',
                                    'flags': {}
                                }
                                new_shapes.append(new_shape)
                            except Exception as e:
                                print(f"Error creating new shape: {e}")
                                continue
            else:
                # 点或线 (简单处理为点)
                try:
                    point = Point(points[0][0], points[0][1])
                    
                    # 检查是否被遮挡
                    is_occluded = False
                    for occlusion_poly in occlusion_polygons:
                        try:
                            if occlusion_poly.contains(point):
                                is_occluded = True
                                break
                        except Exception as e:
                            print(f"Error checking point containment: {e}")
                            continue
                    
                    if not is_occluded:
                        new_shapes.append(shape)
                except Exception as e:
                    print(f"Error processing point: {e}")
                    # 如果处理点出错,保留原始形状
                    new_shapes.append(shape)
        
        # 更新JSON数据中的shapes
        occluded_json_data['shapes'] = new_shapes
        
        # 保存遮挡后的图像
        occluded_img_filename = img_filename.replace('.jpg', '_occluded.jpg')
        occluded_img_path = os.path.join(images_path, occluded_img_filename)
        cv2.imwrite(occluded_img_path, occluded_img)
        
        # 保存更新后的JSON
        occluded_json_filename = json_filename.replace('.json', '_occluded.json')
        occluded_json_path = os.path.join(labels_path, occluded_json_filename)
        with open(occluded_json_path, 'w') as f:
            json.dump(occluded_json_data, f, indent=4)
        
        return 1  # 返回增强的图像数量
    except Exception as e:
        print(f"Error applying occlusion augmentation: {e}")
        return 0

def apply_random_scale_translate(image, json_path, images_path, labels_path, img_filename, json_filename):
    # 读取JSON标注
    with open(json_path, 'r') as f:
        json_data = json.load(f)

    """应用随机缩小和平移,缺失部分用黑色填充"""
    try:
        h, w = image.shape[:2]
        
        # 随机缩小比例(0.5-0.9之间)
        scale_factor = random.uniform(0.5, 0.9)
        
        # 计算缩小后的尺寸
        new_h = int(h * scale_factor)
        new_w = int(w * scale_factor)
        
        # 缩小图像
        resized_img = cv2.resize(image, (new_w, new_h))
        
        # 创建黑色背景图像
        result_img = np.zeros((h, w, 3), dtype=np.uint8)
        
        # 随机平移范围(确保图像不会完全移出边界)
        max_x_shift = w - new_w
        max_y_shift = h - new_h
        
        if max_x_shift <= 0 or max_y_shift <= 0:
            # 如果缩小后的图像大于原图(不应该发生,但以防万一)
            x_shift = 0
            y_shift = 0
        else:
            x_shift = random.randint(0, max_x_shift)
            y_shift = random.randint(0, max_y_shift)
        
        # 将缩小后的图像放在黑色背景上
        result_img[y_shift:y_shift+new_h, x_shift:x_shift+new_w] = resized_img
        
        # 保存处理后的图像
        scaled_img_filename = img_filename.replace('.jpg', '_scaled.jpg')
        scaled_img_path = os.path.join(images_path, scaled_img_filename)
        cv2.imwrite(scaled_img_path, result_img)
        
        # 创建新的JSON标注
        scaled_json_filename = json_filename.replace('.json', '_scaled.json')
        scaled_json_path = os.path.join(labels_path, scaled_json_filename)
        
        # 复制JSON数据并修改点坐标
        scaled_json_data = json_data.copy()
        
        # 修改所有形状的坐标
        new_shapes = []
        for shape in scaled_json_data['shapes']:
            # 创建新的点列表
            new_points = []
            for point in shape['points']:
                # 应用缩放和平移变换
                new_x = point[0] * scale_factor + x_shift
                new_y = point[1] * scale_factor + y_shift
                new_points.append([new_x, new_y])
            
            # 创建新的形状
            new_shape = shape.copy()
            new_shape['points'] = new_points
            new_shapes.append(new_shape)
        
        # 更新JSON数据中的shapes
        scaled_json_data['shapes'] = new_shapes
        
        # 保存修改后的JSON
        with open(scaled_json_path, 'w') as f:
            json.dump(scaled_json_data, f, indent=4)
        
        return 1  # 返回增强的图像数量
    except Exception as e:
        print(f"Error applying random scale and translate: {e}")
        return 0
def apply_hsv_augmentation(image, json_path, images_path, labels_path, img_filename, json_filename):
    """应用HSV色彩空间增强(色相小范围改变,明度和纯度中范围改变)"""
    try:
        # 转换为HSV色彩空间
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
        h, s, v = cv2.split(hsv_img)
        
        # 将h转换为int32类型避免溢出
        h = h.astype(np.int32)
        
        # 色相调整(小范围:-15到+15度)
        hue_shift = random.randint(-15, 15)
        h = (h + hue_shift) % 180  # OpenCV中H范围是0-179
        
        # 转换回uint8
        h = h.astype(np.uint8)
        
        # 饱和度调整(中范围:0.7-1.3倍)
        sat_scale = random.uniform(0.7, 1.3)
        s = np.clip(s * sat_scale, 0, 255).astype(np.uint8)
        
        # 明度调整(中范围:0.7-1.3倍)
        val_scale = random.uniform(0.7, 1.3)
        v = np.clip(v * val_scale, 0, 255).astype(np.uint8)
        
        # 合并通道并转回BGR
        hsv_modified = cv2.merge([h, s, v])
        augmented_img = cv2.cvtColor(hsv_modified, cv2.COLOR_HSV2BGR)
        
        # 保存增强后的图像
        hsv_img_filename = img_filename.replace('.jpg', '_hsv.jpg')
        hsv_img_path = os.path.join(images_path, hsv_img_filename)
        cv2.imwrite(hsv_img_path, augmented_img)
        
        # 标签文件不变(直接复制原json)
        hsv_json_filename = json_filename.replace('.json', '_hsv.json')
        hsv_json_path = os.path.join(labels_path, hsv_json_filename)
        shutil.copy(json_path, hsv_json_path)
        
        return 1
    except Exception as e:
        print(f"Error applying HSV augmentation: {e}")
        return 0
    
def enhance_red_regions(image, variation_range=0.1):
    """
    增强图像中的红色区域,使红色更深更饱和
    
    参数:
    image: 输入图像 (BGR格式)
    variation_range: 波动范围,值越大,波动越明显 (建议0.05-0.15)
    
    返回:
    处理后的图像
    """
    # 复制图像以避免修改原始图像
    enhanced_img = image.copy()
    
    # 转换到HSV色彩空间,便于识别和调整红色区域
    hsv_img = cv2.cvtColor(enhanced_img, cv2.COLOR_BGR2HSV)
    h, s, v = cv2.split(hsv_img)
    
    # 生成随机波动参数
    saturation_boost = 1.3 + random.uniform(-variation_range, variation_range)  # 饱和度增强因子
    value_adjustment = 0.8 + random.uniform(-variation_range, variation_range)  # 亮度调整因子
    
    # 识别红色区域 (HSV中红色的色相值在0-10和160-179之间)
    red_mask = ((h <= 10) | (h >= 160))
    
    # 增强红色区域的饱和度
    s[red_mask] = np.minimum(255, (s[red_mask] * saturation_boost).astype(np.uint8))
    
    # 调整红色区域的亮度,使其更深
    v[red_mask] = np.maximum(0, np.minimum(255, (v[red_mask] * value_adjustment).astype(np.uint8)))
    
    # 合并通道
    hsv_enhanced = cv2.merge([h, s, v])
    
    # 转回BGR色彩空间
    enhanced_img = cv2.cvtColor(hsv_enhanced, cv2.COLOR_HSV2BGR)
    
    return enhanced_img
def apply_red_enhancement(image, json_path, images_path, labels_path, img_filename, json_filename, variation_range=0.1):
    """应用红色区域增强,使红色更深更饱和"""
    try:
        # 调用enhance_red_regions函数进行红色增强
        enhanced_img = enhance_red_regions(image, variation_range)
        
        # 保存增强后的图像
        red_enhanced_img_filename = img_filename.replace('.jpg', '_red_enhanced.jpg')
        red_enhanced_img_path = os.path.join(images_path, red_enhanced_img_filename)
        cv2.imwrite(red_enhanced_img_path, enhanced_img)
        
        # 标签文件不变(直接复制原json)
        red_enhanced_json_filename = json_filename.replace('.json', '_red_enhanced.json')
        red_enhanced_json_path = os.path.join(labels_path, red_enhanced_json_filename)
        shutil.copy(json_path, red_enhanced_json_path)
        
        return 1  # 返回增强的图像数量
    except Exception as e:
        print(f"Error applying red enhancement: {e}")
        return 0

def apply_simple_augmentations(images_path, labels_path, augmentation_probabilities=None):
    """
    主函数:对图像应用多种数据增强方法,每种方法都有可配置的触发概率
    
    参数:
    images_path: 图像文件夹路径
    labels_path: 标签文件夹路径
    augmentation_probabilities: 包含每种增强方法触发概率的字典,如果为None则使用默认概率
    """
    print("Starting simple data augmentation...")
    
    # 默认的增强方法触发概率
    default_probabilities = {
        'horizontal_flip': 0.2,      # 水平翻转概率
        'vertical_flip': 0.3,        # 垂直翻转概率
        'both_flips': 0.3,           # 同时水平和垂直翻转概率
        'low_resolution': 0.1,       # 低分辨率效果概率
        'blur': 0.15,                 # 局部模糊效果概率
        'wave_effect': 0.5,          # 哈哈镜波浪效果概率
        'occlusion': 0.15,            # 随机彩色多边形遮挡概率
        'scale_translate': 0.4,      # 随机缩小和平移概率
        'hsv_augmentation': 0.4,      # 随机HSV色彩空间增强概率
        'red_enhancement': 0.3       # 红色区域增强概率 (新增)
    }
    
    # 如果没有提供概率参数,使用默认值
    if augmentation_probabilities is None:
        augmentation_probabilities = default_probabilities
    else:
        # 合并用户提供的概率和默认概率
        for key in default_probabilities:
            if key not in augmentation_probabilities:
                augmentation_probabilities[key] = default_probabilities[key]
    
    # 获取所有图片文件
    image_files = [f for f in os.listdir(images_path) if f.endswith('.jpg')]
    total_files = len(image_files)
    
    augmented_count = 0
    
    for idx, img_filename in enumerate(image_files):
        img_path = os.path.join(images_path, img_filename)
        json_filename = img_filename.replace('.jpg', '.json')
        json_path = os.path.join(labels_path, json_filename)
        
        # 检查标签文件是否存在
        if not os.path.exists(json_path):
            print(f"Warning: JSON file {json_path} does not exist. Skipping augmentation for {img_filename}")
            continue
        
        # 读取图像
        image = cv2.imread(img_path)
        if image is None:
            print(f"Warning: Could not read image {img_path}. Skipping.")
            continue
        
        # 应用各种数据增强方法,根据概率决定是否触发
        
        # 1. 水平翻转
        if random.random() < augmentation_probabilities['horizontal_flip']:
            augmented_count += apply_horizontal_flip(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 2. 垂直翻转
        if random.random() < augmentation_probabilities['vertical_flip']:
            augmented_count += apply_vertical_flip(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 3. 同时水平和垂直翻转
        if random.random() < augmentation_probabilities['both_flips']:
            augmented_count += apply_both_flips(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 4. 低分辨率效果
        if random.random() < augmentation_probabilities['low_resolution']:
            augmented_count += apply_low_resolution(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 5. 局部模糊效果
        if random.random() < augmentation_probabilities['blur']:
            augmented_count += apply_blur(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 6. 哈哈镜效果 - 波浪
        if random.random() < augmentation_probabilities['wave_effect']:
            augmented_count += apply_wave_effect(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 7. 随机彩色多边形遮挡
        if random.random() < augmentation_probabilities['occlusion']:
            augmented_count += apply_occlusion(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 8. 随机缩小和平移
        if random.random() < augmentation_probabilities['scale_translate']:
            augmented_count += apply_random_scale_translate(image, json_path, images_path, labels_path, img_filename, json_filename)

        # 9. 随机HSV色彩空间增强
        if random.random() < augmentation_probabilities['hsv_augmentation']:
            augmented_count += apply_hsv_augmentation(image, json_path, images_path, labels_path, img_filename, json_filename)
        
        # 10. 红色区域增强 (新增)
        if random.random() < augmentation_probabilities['red_enhancement']:
            # 随机选择波动范围在0.05到0.15之间
            variation = random.uniform(0.05, 0.15)
            augmented_count += apply_red_enhancement(image, json_path, images_path, labels_path, img_filename, json_filename, variation)

        # 显示进度
        if (idx + 1) % 10 == 0 or (idx + 1) == total_files:
            print(f"Processed {idx + 1}/{total_files} images")
    
    print(f"Simple data augmentation completed. Created {augmented_count} augmented images.")


# 3. 将json文件转换为yolo格式的txt文件
# 需要注意的是,考虑到标注可能是 多边形的情况,所以需要提取最大矩形框的坐标
# 同时,对于被分割的同一个对象(有相同的label_id),需要合并处理
lab_dir = {
            'floor': 0,
            'pile': 1,
            'column': 2,
            'platform': 3,
            'red_score': 4,
            'blue_score': 5,
            'red_start': 6,
            'blue_start': 7,
            'block': 8,
            'red_circle': 9,
            'blue_circle': 10,
            'platform_red':11,
            'platform_blue':12,
        }
def convert_json_to_yolo(json_path, yolo_path):
    if not os.path.exists(yolo_path):
        os.makedirs(yolo_path)
    for filename in os.listdir(json_path):
        if filename.endswith('.json'):
            src = os.path.join(json_path, filename)
            dst = os.path.join(yolo_path, filename.replace('.json', '.txt'))
            try:
                with open(src, 'r') as f:
                    data = json.load(f)
                
                # 读取图片的宽高
                img_width = data['imageWidth']
                img_height = data['imageHeight']
                
                # 创建一个字典来存储相同标签ID的形状
                grouped_shapes = {}
                
                # 遍历所有形状,按标签ID分组
                for shape in data['shapes']:
                    label_text = shape['label']
                    
                    # 使用正则表达式检查标签是否包含ID
                    # 格式为:原始标签_数字ID,例如 blue_circle_1
                    match = re.match(r'(.+)_(\d+)$', label_text)
                    if match:
                        # 提取基本标签和ID
                        base_label = match.group(1)  # 这将包含原始标签,如 "blue_circle"
                        label_id = match.group(2)    # 这将是数字ID
                        key = f"{base_label}_{label_id}"
                        
                        # 检查基本标签是否在lab_dir中
                        if base_label not in lab_dir:
                            continue
                        
                        # 将形状添加到对应的组
                        if key not in grouped_shapes:
                            grouped_shapes[key] = []
                        grouped_shapes[key].append(shape)
                    else:
                        # 没有ID的标签直接处理
                        if label_text not in lab_dir:
                            continue
                        
                        # 提取多边形的坐标
                        points = shape['points']
                        x_min = min([point[0] for point in points])
                        y_min = min([point[1] for point in points])
                        x_max = max([point[0] for point in points])
                        y_max = max([point[1] for point in points])
                        
                        # 转换为YOLO格式
                        x_center = (x_min + x_max) / 2 / img_width
                        y_center = (y_min + y_max) / 2 / img_height
                        width = (x_max - x_min) / img_width
                        height = (y_max - y_min) / img_height
                        
                        # 写入txt文件
                        with open(dst, 'a') as f:
                            f.write(f"{lab_dir[label_text]} {x_center} {y_center} {width} {height}\n")
                
                # 处理分组的形状
                for key, shapes in grouped_shapes.items():
                    # 正确提取基本标签(保留原始标签中的所有下划线)
                    # 格式:原始标签_数字ID
                    match = re.match(r'(.+)_(\d+)$', key)
                    if match:
                        base_label = match.group(1)  # 这将保留原始标签中的所有下划线
                    else:
                        continue  # 如果不匹配预期格式,跳过
                    
                    # 合并所有点,找到最大的边界框
                    all_points = []
                    for shape in shapes:
                        all_points.extend(shape['points'])
                    
                    if all_points:
                        x_min = min([point[0] for point in all_points])
                        y_min = min([point[1] for point in all_points])
                        x_max = max([point[0] for point in all_points])
                        y_max = max([point[1] for point in all_points])
                        
                        # 转换为YOLO格式
                        x_center = (x_min + x_max) / 2 / img_width
                        y_center = (y_min + y_max) / 2 / img_height
                        width = (x_max - x_min) / img_width
                        height = (y_max - y_min) / img_height
                        
                        # 写入txt文件
                        with open(dst, 'a') as f:
                            f.write(f"{lab_dir[base_label]} {x_center} {y_center} {width} {height}\n")
            except Exception as e:
                print(f"Error processing {filename}: {e}")

def convert_selected_json_to_yolo(json_path, yolo_path, selected_classes=None, filter_function=None):
    """
    将选定的JSON标注转换为YOLO格式的TXT文件,并重新映射类别编号
    
    参数:
    json_path: JSON文件所在的路径
    yolo_path: 输出YOLO格式TXT文件的路径
    selected_classes: 要转换的类别列表,如果为None则转换所有类别
    filter_function: 自定义过滤函数,接收JSON数据作为参数,返回True/False决定是否转换
    
    返回:
    new_class_mapping: 新的类别映射表,将原始类别映射到新的连续编号
    """
    if not os.path.exists(yolo_path):
        os.makedirs(yolo_path)
    
    # 如果没有指定选择的类别,则使用所有类别
    if selected_classes is None:
        selected_classes = list(lab_dir.keys())
    
    # 创建新的类别映射
    new_class_mapping = {}
    for i, class_name in enumerate(selected_classes):
        if class_name in lab_dir:
            new_class_mapping[class_name] = i
    
    # 打印映射表
    print("类别映射表:")
    print("原始类别名称 -> 原始ID -> 新ID")
    for class_name in new_class_mapping:
        print(f"{class_name} -> {lab_dir[class_name]} -> {new_class_mapping[class_name]}")
    
    # 保存映射表到文件
    with open(os.path.join(os.path.dirname(yolo_path), 'class_mapping.json'), 'w') as f:
        json.dump({
            'original_mapping': {k: v for k, v in lab_dir.items() if k in selected_classes},
            'new_mapping': new_class_mapping,
            'selected_classes': selected_classes
        }, f, indent=4)
    
    for filename in os.listdir(json_path):
        if filename.endswith('.json'):
            src = os.path.join(json_path, filename)
            dst = os.path.join(yolo_path, filename.replace('.json', '.txt'))
            
            try:
                with open(src, 'r') as f:
                    data = json.load(f)
                
                # 如果有自定义过滤函数,先检查整个JSON是否满足条件
                if filter_function and not filter_function(data):
                    continue
                
                # 读取图片的宽高
                img_width = data['imageWidth']
                img_height = data['imageHeight']
                
                # 创建一个字典来存储相同标签ID的形状
                grouped_shapes = {}
                
                # 首先过滤出需要处理的形状
                filtered_shapes = []
                for shape in data['shapes']:
                    label_text = shape['label']
                    
                    # 检查是否在选定的类别中
                    base_label = label_text
                    match = re.match(r'(.+)_(\d+)$', label_text)
                    if match:
                        base_label = match.group(1)
                    
                    if base_label not in selected_classes:
                        continue
                    
                    filtered_shapes.append(shape)
                
                # 如果没有任何形状需要处理,跳过此文件
                if not filtered_shapes:
                    continue
                
                # 处理过滤后的形状
                for shape in filtered_shapes:
                    label_text = shape['label']
                    
                    # 使用正则表达式检查标签是否包含ID
                    match = re.match(r'(.+)_(\d+)$', label_text)
                    if match:
                        # 提取基本标签和ID
                        base_label = match.group(1)
                        label_id = match.group(2)
                        key = f"{base_label}_{label_id}"
                        
                        # 检查基本标签是否在选定的类别中
                        if base_label not in new_class_mapping:
                            continue
                        
                        # 将形状添加到对应的组
                        if key not in grouped_shapes:
                            grouped_shapes[key] = []
                        grouped_shapes[key].append(shape)
                    else:
                        # 没有ID的标签直接处理
                        if label_text not in new_class_mapping:
                            continue
                        
                        # 提取多边形的坐标
                        points = shape['points']
                        x_min = min([point[0] for point in points])
                        y_min = min([point[1] for point in points])
                        x_max = max([point[0] for point in points])
                        y_max = max([point[1] for point in points])
                        
                        # 转换为YOLO格式
                        x_center = (x_min + x_max) / 2 / img_width
                        y_center = (y_min + y_max) / 2 / img_height
                        width = (x_max - x_min) / img_width
                        height = (y_max - y_min) / img_height
                        
                        # 使用新的映射写入txt文件
                        with open(dst, 'a') as f:
                            f.write(f"{new_class_mapping[label_text]} {x_center} {y_center} {width} {height}\n")
                
                # 处理分组的形状
                for key, shapes in grouped_shapes.items():
                    # 正确提取基本标签
                    match = re.match(r'(.+)_(\d+)$', key)
                    if match:
                        base_label = match.group(1)
                    else:
                        continue
                    
                    # 检查基本标签是否在选定的类别中
                    if base_label not in new_class_mapping:
                        continue
                    
                    # 合并所有点,找到最大的边界框
                    all_points = []
                    for shape in shapes:
                        all_points.extend(shape['points'])
                    
                    if all_points:
                        x_min = min([point[0] for point in all_points])
                        y_min = min([point[1] for point in points])
                        x_max = max([point[0] for point in all_points])
                        y_max = max([point[1] for point in all_points])
                        
                        # 转换为YOLO格式
                        x_center = (x_min + x_max) / 2 / img_width
                        y_center = (y_min + y_max) / 2 / img_height
                        width = (x_max - x_min) / img_width
                        height = (y_max - y_min) / img_height
                        
                        # 使用新的映射写入txt文件
                        with open(dst, 'a') as f:
                            f.write(f"{new_class_mapping[base_label]} {x_center} {y_center} {width} {height}\n")
            except Exception as e:
                print(f"Error processing {filename}: {e}")
    
    return new_class_mapping

# 3.5 在图片上叠加yolo标注框,并保存到指定路径
preview_path = 'yoloRobot/data/preview'
def preview_yolo_annotations(images_path, labels_path, preview_path, class_mapping=None):
    if not os.path.exists(preview_path):
        os.makedirs(preview_path)
    
    # 如果提供了类别映射,则使用它来确定颜色
    color_map = {}
    if class_mapping:
        # 为每个类别生成一个唯一的颜色
        for class_name, class_id in class_mapping.items():
            # 使用类别ID来生成一个固定的颜色
            random.seed(class_id * 10)  # 使用固定种子确保颜色一致性
            color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
            color_map[class_id] = color
            random.seed()  # 重置随机种子
    
    # 获取所有图片文件
    image_files = [f for f in os.listdir(images_path) if f.endswith('.jpg')]
    
    for img_filename in image_files:
        img_path = os.path.join(images_path, img_filename)
        label_filename = img_filename.replace('.jpg', '.txt')
        label_path = os.path.join(labels_path, label_filename)
        
        # 检查标签文件是否存在
        if not os.path.exists(label_path):
            print(f"Warning: Label file {label_path} does not exist. Skipping {img_filename}")
            continue
        
        # 读取图像
        image = cv2.imread(img_path)
        if image is None:
            print(f"Warning: Could not read image {img_path}. Skipping.")
            continue
        
        # 读取YOLO标注
        with open(label_path, 'r') as f:
            yolo_data = f.readlines()
        
        # 遍历YOLO标注,绘制边界框
        for line in yolo_data:
            parts = line.strip().split()
            class_id = int(parts[0])
            x_center = float(parts[1]) * image.shape[1]
            y_center = float(parts[2]) * image.shape[0]
            width = float(parts[3]) * image.shape[1]
            height = float(parts[4]) * image.shape[0]
            
            # 计算边界框的坐标
            x_min = int(x_center - width / 2)
            y_min = int(y_center - height / 2)
            x_max = int(x_center + width / 2)
            y_max = int(y_center + height / 2)
            
            # 确定颜色
            if class_id in color_map:
                color = color_map[class_id]
            else:
                # 如果没有为这个类别指定颜色,使用随机颜色
                color = (0, 255, 0)
            
            # 绘制矩形框
            cv2.rectangle(image, (x_min, y_min), (x_max, y_max), color, 2)
            
            # 添加类别标签
            class_name = next((name for name, id in class_mapping.items() if id == class_id), str(class_id)) if class_mapping else str(class_id)
            cv2.putText(image, class_name, (x_min, y_min - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
        
        # 保存预览图像
        preview_img_path = os.path.join(preview_path, img_filename)
        cv2.imwrite(preview_img_path, image)
        print(f"Saved preview image with annotations: {preview_img_path}")

# 删除labels文件夹下的json文件
def delete_json_files(json_path):
    for filename in os.listdir(json_path):
        if filename.endswith('.json'):
            os.remove(os.path.join(json_path, filename))
    print(f"Deleted all json files in {json_path}.")

# 4. 按照 8:1:1 的比例划分数据集
def split_dataset1(images_path, labels_path):
    # 在images_path下获取所有图片文件名
    # 在img与lab的文件夹下都创建 train、val、test三个文件夹
    # 创建img下的三个文件夹
    image_train_path = os.path.join(images_path, 'train')
    image_val_path = os.path.join(images_path, 'val')
    image_test_path = os.path.join(images_path, 'test')
    if not os.path.exists(image_train_path):
        os.makedirs(image_train_path)
    if not os.path.exists(image_val_path):
        os.makedirs(image_val_path)
    if not os.path.exists(image_test_path):
        os.makedirs(image_test_path)

    # 创建lab下的三个文件夹
    label_train_path = os.path.join(labels_path, 'train')
    label_val_path = os.path.join(labels_path, 'val')
    label_test_path = os.path.join(labels_path, 'test')
    if not os.path.exists(label_train_path):
        os.makedirs(label_train_path)
    if not os.path.exists(label_val_path):
        os.makedirs(label_val_path)
    if not os.path.exists(label_test_path):
        os.makedirs(label_test_path)

    # 遍历images_path下的所有文件
    for filename in os.listdir(images_path):
        if filename.endswith('.jpg'):
            # 计算数据集划分比例
            rand_num = random.random()
            if rand_num < 0.8:
                dst_image_path = os.path.join(image_train_path, filename)
                dst_label_path = os.path.join(label_train_path, filename.replace('.jpg', '.txt'))
            elif rand_num < 0.9:
                dst_image_path = os.path.join(image_val_path, filename)
                dst_label_path = os.path.join(label_val_path, filename.replace('.jpg', '.txt'))
            else:
                dst_image_path = os.path.join(image_test_path, filename)
                dst_label_path = os.path.join(label_test_path, filename.replace('.jpg', '.txt'))

            src_image_path = os.path.join(images_path, filename)
            # 移动图片文件到对应的文件夹
            shutil.move(src_image_path, dst_image_path)

            # 移动对应的标签文件
            label_filename = filename.replace('.jpg', '.txt')
            src_label_path = os.path.join(labels_path, label_filename)
            if os.path.exists(src_label_path):
                shutil.move(src_label_path, dst_label_path)
            else:
                print(f"Label file {src_label_path} does not exist.")

    print(f"Split dataset into train, val, and test folders in {images_path} and {labels_path}.")

def split_dataset2(base_path, images_path, labels_path):
    # 创建新的文件夹结构:三个主文件夹train、val、test,每个下面有images和labels子文件夹
    for split in ['train', 'val', 'test']:
        split_path = os.path.join(base_path, split)
        split_images_path = os.path.join(split_path, 'images')
        split_labels_path = os.path.join(split_path, 'labels')
        
        for path in [split_path, split_images_path, split_labels_path]:
            if not os.path.exists(path):
                os.makedirs(path)

    # 遍历images_path下的所有图片文件
    for filename in os.listdir(images_path):
        if filename.endswith('.jpg'):
            # 计算数据集划分比例
            rand_num = random.random()
            if rand_num < 0.8:
                split = 'train'
            elif rand_num < 0.9:
                split = 'val'
            else:
                split = 'test'

            dst_image_path = os.path.join(base_path, split, 'images', filename)
            dst_label_path = os.path.join(base_path, split, 'labels', filename.replace('.jpg', '.txt'))

            src_image_path = os.path.join(images_path, filename)
            # 移动图片文件到对应的文件夹
            shutil.move(src_image_path, dst_image_path)

            # 移动对应的标签文件
            label_filename = filename.replace('.jpg', '.txt')
            src_label_path = os.path.join(labels_path, label_filename)
            if os.path.exists(src_label_path):
                shutil.move(src_label_path, dst_label_path)
            else:
                print(f"Label file {src_label_path} does not exist.")

    # 最后再删除原来的images和labels文件夹
    if os.path.exists(images_path):
        shutil.rmtree(images_path)
    if os.path.exists(labels_path):
        shutil.rmtree(labels_path)
    print(f"Split dataset into train, val, and test folders, each containing images and labels subfolders in {base_path}.")



if __name__ == '__main__':
    # 设置要选择的类别,如果需要所有类别则设为None
    selected_classes = ['platform', 'column', 'block', 'red_score', 'blue_score', 'red_circle', 'blue_circle', 'platform_red', 'platform_blue']  # 这里可以根据需要修改要选择的类别
    
    # 1. copy 所有 在path 下有的json文件到另一个文件夹下,image 进入 images文件夹下,json 进入 labels 文件夹下
    copy_json_to_labels(ori_dic_path, target_dic_path)
    
    # 2. 应用简单的数据增强 - 对json文件进行处理
    apply_simple_augmentations(os.path.join(target_dic_path, 'images'), os.path.join(target_dic_path, 'labels'))
    
    # 3. 将json文件转换为yolo格式的txt文件
    if selected_classes:
        # 使用选择性转换
        class_mapping = convert_selected_json_to_yolo(
            os.path.join(target_dic_path, 'labels'), 
            os.path.join(target_dic_path, 'labels'),
            selected_classes=selected_classes
        )
        
        # 输出类别映射表
        print("\n类别映射表:")
        print("原始类别名称 -> 原始ID -> 新ID")
        for class_name in selected_classes:
            if class_name in lab_dir:
                print(f"{class_name} -> {lab_dir[class_name]} -> {class_mapping[class_name]}")
        
        # 保存映射表到文件
        mapping_file = os.path.join(target_dic_path, 'class_mapping.json')
        with open(mapping_file, 'w') as f:
            json.dump({
                'original_mapping': {k: lab_dir[k] for k in selected_classes if k in lab_dir},
                'new_mapping': class_mapping,
                'selected_classes': selected_classes
            }, f, indent=4)
        print(f"\n类别映射表已保存到: {mapping_file}")
        
        # 生成YOLO训练所需的yaml配置文件
        yaml_path = os.path.join(target_dic_path, 'dataset.yaml')
        with open(yaml_path, 'w') as f:
            f.write(f"path: {os.path.abspath(target_dic_path)}\n")
            f.write("train: images/train\n")
            f.write("val: images/val\n")
            f.write("test: images/test\n\n")
            
            f.write("names:\n")
            # 按照新的映射ID排序类别名称
            sorted_classes = sorted(class_mapping.items(), key=lambda x: x[1])
            for class_name, class_id in sorted_classes:
                f.write(f"  {class_id}: {class_name}\n")
        print(f"已生成YOLO配置文件: {yaml_path}")
    else:
        # 转换所有类别
        convert_json_to_yolo(os.path.join(target_dic_path, 'labels'), os.path.join(target_dic_path, 'labels'))
    
    # 预览yolo标注框
    preview_yolo_annotations(
        os.path.join(target_dic_path, 'images'), 
        os.path.join(target_dic_path, 'labels'), 
        preview_path,
        class_mapping if selected_classes else None
    )

    # 删除labels文件夹下的json文件
    delete_json_files(os.path.join(target_dic_path, 'labels'))
    
    # 4. 按照 8:1:1 的比例划分数据集
    split_dataset1(os.path.join(target_dic_path, 'images'), os.path.join(target_dic_path, 'labels'))
    # 或者使用另一种划分方式
    # split_dataset2(target_dic_path, os.path.join(target_dic_path, 'images'), os.path.join(target_dic_path, 'labels'))

2.yolo训练代码
import os
from ultralytics import YOLO

# 切换当前工作目录到changed_path
current_path = "C:\\Users\\G\\Desktop\\robot-git\\yoloRobot"
imgSavePath = "/ImgDetectResult"
os.chdir(current_path)

# Load a pretrained YOLO11n model
model = YOLO("yolov11\weights\yolo11s.pt")
 

train_results = model.train(
    data="data.yaml",  # Path to dataset configuration file
    epochs=100,  # Number of training epochs
    imgsz=224,  # Image size for training
    device='cpu',  # Device to run on (e.g., 'cpu', 0, [0,1,2,3])
)  
# 如果你要用GPU,那就网上自己了解吧!

3. yolo训练模型转换为k230的kmodel代码,及错误分析
# 将 pt 模型转换为 kmodel 模型
# 参考https://developer.canaan-creative.com/k230_canmv/zh/main/zh/example/ai/YOLO%E5%A4%A7%E4%BD%9C%E6%88%98.html#yolo11

# 按照如下命令,对 runs/classify/exp/weights 下的 pt 模型先导出为 onnx 模型,再转换成 kmodel 模型:

# # 导出onnx,pt模型路径请自行选择
# yolo export model=runs/classify/train/weights/best.pt format=onnx imgsz=224
# cd test_yolo11/classify
# # 转换kmodel,onnx模型路径请自行选择,生成的kmodel在onnx模型同级目录下
# python to_kmodel.py --target k230 --model ../../runs/classify/train/weights/best.onnx --dataset ../test --input_width 224 --input_height 224 --ptq_option 0
# cd ../../
# """
# # Error list 1:
# Traceback (most recent call last):
#   File "C:\Users\G\Desktop\robot-git\yoloRobot\test_yolo11\classify\to_kmodel.py", line 7, in <module>
#     import nncase
#   File "C:\Users\G\.conda\envs\yoloRobot\lib\site-packages\nncase\__init__.py", line 34, in <module>
#     import _nncase
# ImportError: DLL load failed while importing _nncase: 找不到指定的模块。
# 解决方法: https://github.com/kendryte/nncase/issues/451
#   我下载了 相关 dll 文件,并 Place 64-bit DLLs in the '/Windows/System32' directory.
# """

# """
# # Error list 2:
# warn: Nncase.Hosting.PluginLoader[0]
#       NNCASE_PLUGIN_PATH is not set.
# C:\Users\G\Desktop\robot-git\yoloRobot\test_yolo11\classify\to_kmodel.py:24: DeprecationWarning: `mapping.TENSOR_TYPE_TO_NP_TYPE` is now deprecated and will be removed in a future release.To silence this warning, please use `helper.tensor_dtype_to_np_dtype` instead.
#   input_dict['dtype'] = onnx.mapping.TENSOR_TYPE_TO_NP_TYPE[onnx_type.elem_type]
# WARNING: The argument `input_shapes` is deprecated. Please use `overwrite_input_shapes` and/or `test_input_shapes` instead. An 
# error will be raised in the future.
# Unhandled exception. System.Collections.Generic.KeyNotFoundException: The given key 'k230' was not present in the dictionary.
#    at System.Collections.Generic.Dictionary`2.get_Item(TKey key)
#    at Nncase.Targets.TargetProvider.GetTarget(String name)
#    at Nncase.CompilerServicesProvider.GetTarget(String name)
#    at Nncase.Compiler.Interop.CApi.TargetCreate(Byte* targetNamePtr, UIntPtr targetNameLength)
# # 解决方法:
#   1. 解决NNCASE_PLUGIN_PATH is not set.
#       安装<2.0.0版本的nncase
#       参考:https://blog.csdn.net/Lieber_0402/article/details/132021911
#   2. 解决WARNING: The argument `input_shapes` is deprecated. Please use `overwrite_input_shapes` and/or `test_input_shapes` instead. An error will be raised in the future.
#       onnx-simplifier改成0.3.6.(其实我本来似乎都没有这个包)
#       参考:https://github.com/kendryte/nncase/issues/909
# * 最终解决方法:****************************************************************************
#   1. 问题产生原因:nncase_kpu-2.9.0-py2.py3-none-win_amd64.whl 误装为 nncase-2.9.0-cp310-cp310-win_amd64.whl
#   2. 卸载nncase-2.9.0-cp310-cp310-win_amd64.whl
#   3. 安装nncase_kpu-2.9.0-py2.py3-none-win_amd64.whl
# """
import os
import shutil


working_path = "C:\\Users\\G\\Desktop\\robot-git"
pt_model_path = "yoloRobot/runs/detect/train20/weights/best.pt"
tempConvertPath = "yoloRobot/tempForConvert"
test_yolo11_path = "yoloRobot/test_yolo11"
dataset_path = "yoloRobot/data/images/train"
dataset_path = os.path.join(working_path, dataset_path)

os.chdir(working_path)

if __name__ == "__main__":
    ## Step 1: Copy the pt model to the tempConvertPath
    command1 = f"yolo export model={pt_model_path} format=onnx imgsz=224"   # 注意,该命令生成的位置同pt模型同级目录下
    os.system(command1)  # 导出onnx模型
    # 生成的onnx模型路径
    onnx_model_path = os.path.join(working_path, pt_model_path.replace(".pt", ".onnx"))

    # ## Step 2: Change directory to test_yolo11/classify
    # # 切换到 test_yolo11/classify 目录
    # classify_path = os.path.join(test_yolo11_path, "classify")
    # os.chdir(classify_path)
    
    ## Step 2: Change directory to test_yolo11/detect
    # 切换到 test_yolo11/detect 目录
    classify_path = os.path.join(test_yolo11_path, "detect")
    os.chdir(classify_path)

    ## Step 3: Convert the onnx model to kmodel
    command3 = f"python to_kmodel.py --target k230 --model {onnx_model_path} --dataset {dataset_path} --input_width 224 --input_height 224 --ptq_option 0"
    os.system(command3)  # 转换kmodel模型

"""
虽然会有一些警告信息,但最终还是能成功转换为kmodel模型的。
注意:转换后的kmodel模型在onnx模型同级目录下,命名为 best.kmodel。
"""

1.2.3 k230 串口配置

我们是打算使用串口,来将最终的识别结果传输给单片机的,多以串口是必不可少的

fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD)
fpioa.set_function(12, FPIOA.UART2_RXD)
uart = UART(UART.UART2, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)
#---------------- 数据发送配置 ----------------
frame_header = 0xAA
uartDataRead = None
yoloTarget = 10

# 发送数据函数
import ustruct

def k230_yolo_send_data(single_yolo_data, uart, Sensor_Num):
    """本函数专门根据当前代码的yolodata格式发送数据,
    发送格式为:帧头+数据个数+数据+校验和
    发送 [center_x, center_y, bbx_width, bbx_height, 分类号]
    置信度小于0.5的数据不发送

    args:
        single_yolo_data: yolodata数据[center_x, center_y, bbx_width, bbx_height, 置信度, 分类号]
        uart: UART对象,用于发送数据
    """
    # 帧头使用 frame_header
    # 将坐标转换为实际像素值,并确保在u16范围内(0-65535)
    center_x = int(single_yolo_data[0]) & 0xFFFF  # 16位无符号整数
    center_y = int(single_yolo_data[1]) & 0xFFFF  # 16位无符号整数
    bbx_width = int(single_yolo_data[2]) & 0xFFFF  # 16位无符号整数
    bbx_height = int(single_yolo_data[3]) & 0xFFFF  # 16位无符号整数
    # 不发送置信度
    class_id = int(single_yolo_data[5]) & 0xFF  # 8位无符号整数
    Sensor_Num = int(Sensor_Num) & 0xFF  # 8位无符号整数

    # 置信度小于0.5的数据不发送
    if single_yolo_data[4] < 0.5:
        return


    data_num = 10
    # 计算校验和
    checksum = (frame_header +
               (data_num & 0xFF) +
               ((center_x >> 8) & 0xFF) + (center_x & 0xFF) +
               ((center_y >> 8) & 0xFF) + (center_y & 0xFF) +
               ((bbx_width >> 8) & 0xFF) + (bbx_width & 0xFF) +
               ((bbx_height >> 8) & 0xFF) + (bbx_height & 0xFF) +
               class_id + Sensor_Num) & 0xFF


    # 创建数据包
    data = bytearray()
#    data.append(0x00)
    data.append(frame_header)  # 帧头
    data.append(data_num)  # 数据个数

    # 使用ustruct打包u16数据
    data.extend(ustruct.pack('<B', (center_x>>8 & 0XFF)))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', center_x & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', (center_y>>8) & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', center_y & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', (bbx_width>>8) & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', bbx_width & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', (bbx_height>>8) & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', bbx_height & 0XFF))  # 小端序16位无符号整数
    data.append(class_id)  # 分类号(8位)
    data.append(Sensor_Num)  # 分类号(8位)

    # 添加校验和
    data.append(checksum)

    # 发送数据
    uart.write(data)
#----------------------------------------------

但是只有这样还不够,由于一次只能发送一个对象,所以我们需要一个途径,能够让接收方自动选择需要什么样的数据。
因此,我们在代码中,添加一个接收的部分。单片机可以通过串口向k230发送信息,索要指定对象的数据。

       uartDataRead = uart.read()
        if uartDataRead:  # 检查数据是否非空
            try:
                # 确保第一个字符是数字,并转换为整数
#                uartDataRead = int(uartDataRead.decode('ascii')[0])
                uartDataRead = uartDataRead[0]  # 获取第一个字节的整数值
                print("Received: {}".format(uartDataRead))
                yoloTarget = uartDataRead
            except (IndexError, ValueError) as e:
                # 处理可能的错误:
                # - IndexError: uartDataRead 是空字符串(无法取 [0])
                # - ValueError: 首字符不是数字(int() 转换失败)
                print("Error: Invalid data received -", e)
                yoloTarget = None  # 或设置默认值
        else:
            print("Warning: No data received from UART")
#                yoloTarget = None  # 或设置默认值

        print(f"YoloTargetNow:{yoloTarget}")

使用于小车的视觉代码如下:
import time
import os
import sys

from media.sensor import *
from media.display import *
from media.media import *

from libs.YOLO import YOLO11
import os,sys,gc
import image

import ulab.numpy as np
#----------------- 串口导入 -----------------
import time
from machine import UART
from machine import FPIOA
from machine import Pin
#----------------- 串口配置 -----------------
fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD)
fpioa.set_function(12, FPIOA.UART2_RXD)
uart = UART(UART.UART2, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)
#data = b''
#---------------- kmodel 配置 ------------------
kmodel_path1 = "/data/yoloKmodel/best.kmodel"
kmodel_path2 = "/data/yoloKmodel/best2.kmodel"
kmodel_path3 = "/data/yoloKmodel/best3.kmodel"
kmodel_path4 = "/data/yoloKmodel/best0523.kmodel"

labels1 =  ["floor", "pile", "column", "platform", "red_score", "blue_score", "red_start", "blue_start", "block", "red_circle", "blue_circle"]
labels2 =  ["platform", "column", "block", "red_score", "blue_score", "red_circle", "blue_circle", "platform_red", "platform_blue"]

#----------------- LED配置 -----------------
fpioa.set_function(62,FPIOA.GPIO62)
LED = Pin(20, Pin.OUT, pull=Pin.PULL_NONE, drive=7)  # 红灯62 蓝灯63 绿灯20
LED.high()  # 关闭红灯
#---------------- 数据发送配置 ----------------
frame_header = 0xAA
uartDataRead = None
yoloTarget = 10

# 发送数据函数
import ustruct

def k230_yolo_send_data(single_yolo_data, uart, Sensor_Num):
    """本函数专门根据当前代码的yolodata格式发送数据,
    发送格式为:帧头+数据个数+数据+校验和
    发送 [center_x, center_y, bbx_width, bbx_height, 分类号]
    置信度小于0.5的数据不发送

    args:
        single_yolo_data: yolodata数据[center_x, center_y, bbx_width, bbx_height, 置信度, 分类号]
        uart: UART对象,用于发送数据
    """
    # 帧头使用 frame_header
    # 将坐标转换为实际像素值,并确保在u16范围内(0-65535)
    center_x = int(single_yolo_data[0]) & 0xFFFF  # 16位无符号整数
    center_y = int(single_yolo_data[1]) & 0xFFFF  # 16位无符号整数
    bbx_width = int(single_yolo_data[2]) & 0xFFFF  # 16位无符号整数
    bbx_height = int(single_yolo_data[3]) & 0xFFFF  # 16位无符号整数
    # 不发送置信度
    class_id = int(single_yolo_data[5]) & 0xFF  # 8位无符号整数
    Sensor_Num = int(Sensor_Num) & 0xFF  # 8位无符号整数

    # 置信度小于0.5的数据不发送
    if single_yolo_data[4] < 0.5:
        return


    data_num = 10
    # 计算校验和
    checksum = (frame_header +
               (data_num & 0xFF) +
               ((center_x >> 8) & 0xFF) + (center_x & 0xFF) +
               ((center_y >> 8) & 0xFF) + (center_y & 0xFF) +
               ((bbx_width >> 8) & 0xFF) + (bbx_width & 0xFF) +
               ((bbx_height >> 8) & 0xFF) + (bbx_height & 0xFF) +
               class_id + Sensor_Num) & 0xFF


    # 创建数据包
    data = bytearray()
#    data.append(0x00)
    data.append(frame_header)  # 帧头
    data.append(data_num)  # 数据个数

    # 使用ustruct打包u16数据
    data.extend(ustruct.pack('<B', (center_x>>8 & 0XFF)))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', center_x & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', (center_y>>8) & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', center_y & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', (bbx_width>>8) & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', bbx_width & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', (bbx_height>>8) & 0XFF))  # 小端序16位无符号整数
    data.extend(ustruct.pack('<B', bbx_height & 0XFF))  # 小端序16位无符号整数
    data.append(class_id)  # 分类号(8位)
    data.append(Sensor_Num)  # 分类号(8位)

    # 添加校验和
    data.append(checksum)

    # 发送数据
    uart.write(data)
#----------------------------------------------

# sensor0 为爪子摄像头
sensor0 = None
sensor0_id = 0
sensor0_AI_chn = CAM_CHN_ID_2

# sensor1 为圆台摄像头
sensor1 = None
sensor1_id = 1
sensor1_AI_chn = CAM_CHN_ID_1


try:
    print("camera_test")

    # 构建 Sensor 对象 sensor0
    sensor0 = Sensor(id=sensor0_id)
    sensor0.reset()
    sensor0.set_hmirror(1)
    sensor0.set_vflip(1)
    # 绑定通道 0 到显示 VIDEO1 层
    sensor0.set_framesize(width=960, height=540)
    sensor0.set_pixformat(PIXEL_FORMAT_YUV_SEMIPLANAR_420)
    #    # 绑定通道 2 到AI
    sensor0.set_framesize(width=1280, height=720, chn=sensor0_AI_chn)
    sensor0.set_pixformat(PIXEL_FORMAT_RGB_888_PLANAR, chn=sensor0_AI_chn)

    bind_info = sensor0.bind_info(x=0, y=0, chn=CAM_CHN_ID_0)
    Display.bind_layer(**bind_info, layer=Display.LAYER_VIDEO1)

    # 构建 Sensor 对象 sensor1
    sensor1 = Sensor(id=sensor1_id)
    sensor1.reset()
    # 绑定通道 0 到显示 VIDEO1 层
    sensor1.set_framesize(width=960, height=540)
    sensor1.set_pixformat(PIXEL_FORMAT_YUV_SEMIPLANAR_420)
    #    # 绑定通道 2 到AI
    sensor1.set_framesize(width=1280, height=720, chn=sensor1_AI_chn)
    sensor1.set_pixformat(PIXEL_FORMAT_RGB_888_PLANAR, chn=sensor1_AI_chn)

    bind_info = sensor1.bind_info(x=960, y=0, chn=CAM_CHN_ID_0)
    Display.bind_layer(**bind_info, layer=Display.LAYER_VIDEO2)

    # 初始化 HDMI 和 IDE 输出显示,若屏幕无法点亮,请参考 API 文档中的 K230_CanMV_Display 模块 API 手册进行配置
    Display.init(Display.LT9611, to_ide=True)
    # 初始化媒体管理器
    MediaManager.init()

    # 多摄场景仅需执行一次 run
    sensor0.run()


    # ------------------------ YOLO配置 -----------------------------
    display_mode="lcd"
    rgb888p_size=[1280,720]
    if display_mode=="hdmi":
        display_size=[1920,1080]
        screen_width = 1920
        screen_height = 1080
    else:
        display_size=[800,480]
        screen_width = 800
        screen_height = 480
    kmodel_path=kmodel_path4    # path
    labels = labels2
    confidence_threshold = 0.8
    model_input_size=[224,224]
    yolo=YOLO11(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,debug_mode=0)
    yolo.config_preprocess()

    if labels == labels2:
        platform_num = 0
    else:
        platform_num = 3

    rgb888p_img  = None
    img_data = None
#    temptime = time.ticks_ms()
    while True:
        # ========================== 读取数据 ==========================
        uartDataRead = uart.read()
        if uartDataRead:  # 检查数据是否非空
            try:
                # 确保第一个字符是数字,并转换为整数
#                uartDataRead = int(uartDataRead.decode('ascii')[0])
                uartDataRead = uartDataRead[0]  # 获取第一个字节的整数值
                print("Received: {}".format(uartDataRead))
                yoloTarget = uartDataRead
            except (IndexError, ValueError) as e:
                # 处理可能的错误:
                # - IndexError: uartDataRead 是空字符串(无法取 [0])
                # - ValueError: 首字符不是数字(int() 转换失败)
                print("Error: Invalid data received -", e)
                yoloTarget = None  # 或设置默认值
        else:
            print("Warning: No data received from UART")
#                yoloTarget = None  # 或设置默认值

        print(f"YoloTargetNow:{yoloTarget}")
        # ========================== 爪子摄像头 ==========================
        LED.low()   # 点亮当前选择的LED
        rgb888p_img = sensor0.snapshot(chn=sensor0_AI_chn)
        img_data = rgb888p_img.to_numpy_ref()
#        print(f"sen1转换数据时的时间:{time.ticks_ms() - temptime}")
        res = yolo.run(img_data)
#        print(f"sen1 yolo 时间:{time.ticks_ms() - temptime}")
        if len(res)!=0:
            for sub_res in res:
                if sub_res[5]==yoloTarget:    # 此处为测试,实际使用应选择圆台,即编号3
                    k230_yolo_send_data(sub_res, uart, 0)
#                    print("0 Send Successful")
#                    print(f"sen1 yolo 间隔:{time.ticks_ms() - temptime}")
#                    temptime = time.ticks_ms()

        # ========================== 循迹摄像头 ==========================
        LED.high()  # 熄灭当前选择的LED
        rgb888p_img = sensor1.snapshot(chn=sensor1_AI_chn)
        img_data = rgb888p_img.to_numpy_ref()
        res = yolo.run(img_data)
        if len(res)!=0:
            for sub_res in res:
                if sub_res[5]==platform_num:    # 此处为测试,实际使用应选择圆台,即编号3
                    k230_yolo_send_data(sub_res, uart, 1)
#                    print("1 Send Successful")
#                    print(f"sen1 yolo 间隔:{time.ticks_ms() - temptime}")
#                    temptime = time.ticks_ms()

        os.exitpoint()
#        print(f"一轮的时间:{time.ticks_ms() - temptime}")
#        temptime = time.ticks_ms()
#        time.sleep(1)
except KeyboardInterrupt as e:
    print("用户停止")
except BaseException as e:
    print(f"异常: '{e}'")
finally:
    # 每个 sensor 都需要执行 stop
    if isinstance(sensor0, Sensor):
        sensor0.stop()
    if isinstance(sensor1, Sensor):
        sensor1.stop()
    # 销毁显示
    Display.deinit()
    os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
    time.sleep_ms(100)
    # 释放媒体缓冲区
    MediaManager.deinit()


大车同理。

1.3 单片机接收&处理 K230 代码

1.3.1 单片机串口接收数据

首先,我们先进行串口的配置,如下所示,比方我们要使用USART1:

/* USART1 init function */

void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
    HAL_UART_Receive_IT(&huart1,(uint8_t *)&uart1_rx,1);
    // 启动串口接收中断,接收1个字节
    // 注意:这里的uart1_rx是一个全局变量,用于存储接收到的数据
    //       你需要在代码中定义这个变量,如:uint8_t uart1_rx = 0;
}

接着,我们先定义一些type,分别用来表示接收到的数据和处理后的数据

// 相关定义
// 用于接收
typedef struct
{
    uint8_t RxDataPtr;      // 接收数据指针
    uint8_t RxData[DataNum];      // 接收到的数据
    uint8_t ValidData[DataNum];   // 有效的数据
          
}k230_UART_Rx_t;

// 用于处理后的数据
typedef struct
{
    uint16_t x_center;
    uint16_t y_center;
    uint16_t x_width;
    uint16_t y_height;
    uint8_t label_num;
    uint8_t sensor_num;
    
    uint16_t x_left;
    uint16_t y_up;
    uint16_t x_right;
    uint16_t y_down;
    
}k230_Item_Type;

// 相关变量
k230_UART_Rx_t k230_1_Rx;
k230_UART_Rx_t k230_1_Rx_0;
k230_UART_Rx_t k230_1_Rx_1;
k230_UART_Rx_t* k230_1_Rx_x;

k230_Item_Type k230_Item_0;	// 爪子
k230_Item_Type k230_Item_1;	// 圆台
k230_Item_Type k230_Item_2;	// 备用
k230_Item_Type k230_Item_3;	// 备用

实际的接收代码如下

/**
 * @brief: 一个字节中断接收,在 回调函数中调用
 * @param {type}
 * @retval: None
 */
void k230_GetChar(uint8_t pData, k230_UART_Rx_t* k230_UART_Rx) {
    // 串口
    if (k230_UART_Rx->RxDataPtr >= DataNum) {
        k230_UART_Rx->RxDataPtr = 0;
        return;
    }
    // 信息结构:帧头+数据长度+数据+校验和
    if (k230_UART_Rx->RxDataPtr == 0 && pData == k230_FRAME_START) {  // 帧头
        // 如果是帧头
        k230_UART_Rx->RxData[k230_UART_Rx->RxDataPtr++] = pData;
    } else if (k230_UART_Rx->RxDataPtr == 1) {
        // 如果是数据长度
        k230_UART_Rx->RxData[k230_UART_Rx->RxDataPtr++] = pData;  // num
    } else if (k230_UART_Rx->RxDataPtr !=0 && k230_UART_Rx->RxDataPtr < ((k230_UART_Rx->RxData[1] & 0x0f) + 3)) {
        // 如果是数据 ( 判断条件是数据长度+3,3是帧头+数据长度+校验和 )
        k230_UART_Rx->RxData[k230_UART_Rx->RxDataPtr++] = pData;
        
        // 如果是最后一个数据(即校验和)
        if (k230_UART_Rx->RxDataPtr == ((k230_UART_Rx->RxData[1] & 0x0f) + 3)) {  // 接收完成
            // 校验
            if (k230_check_SUM(k230_UART_Rx->RxData, k230_UART_Rx->RxDataPtr)) {
                tickGetData = HAL_GetTick();
                for (uint8_t i = 0; i < ((k230_UART_Rx->RxData[1] & 0x0f) + 3); i++){
                    k230_UART_Rx->ValidData[i] = k230_UART_Rx->RxData[i];  // 数据存储
//                    printf ("%d\n", k230_UART_Rx->ValidData[i]);
                }
                // New Added 2025.5.3
                if(k230_UART_Rx->ValidData[11] == 0){
                    k230_1_Rx_0 = *k230_UART_Rx;
                }
                else{
                    k230_1_Rx_1 = *k230_UART_Rx;
                }
            }
            else{
//                printf("error\n");
            }
            k230_UART_Rx->RxDataPtr = 0;
        }
    } else
        k230_UART_Rx->RxDataPtr = 0;
}

校验和计算如下:

/**
 * @brief: SUM校验,传输校验
 * @param {type}
 * @retval: None
 */
uint8_t k230_check_SUM(uint8_t* pData, uint8_t length) {
    uint16_t sum = 0;
    for (uint8_t i = 0; i < length - 1; i++) {
        sum += pData[i];
    }
    if ((uint8_t)sum == pData[length - 1])
        return 1;
    return 0;
}

其次,记得在hal的中断中调用相关函数,如

  if (huart->Instance == USART1)
  {
        k230_GetChar(uart1_rx,&k230_1_Rx);
      __HAL_UART_CLEAR_OREFLAG(&huart1);
        HAL_UART_Receive_IT(&huart1, (uint8_t *)&uart1_rx, 1);  
  }

接着,对得到的数据进行处理

/**
 * @brief: 用于在程序中调用并处理的函数
 * 
*/
void k230_GetData(k230_Item_Type * k230_Item, uint8_t sensor_num) {
    if (sensor_num == 0)
        k230_1_Rx_x = &k230_1_Rx_0;
    else
        k230_1_Rx_x = &k230_1_Rx_1;
    
    k230_Item->sensor_num = k230_1_Rx_x->ValidData[11];
    if (HAL_GetTick() - tickGetData > NO_TRANSMIT_TIME){	
        k230_ClearData(k230_Item);
    }
    else{
        k230_Item->x_left = (k230_1_Rx_x->ValidData[2]<<8) + k230_1_Rx_x->ValidData[3];
        k230_Item->y_up = (k230_1_Rx_x->ValidData[4]<<8) + k230_1_Rx_x->ValidData[5];
        k230_Item->x_right = (k230_1_Rx_x->ValidData[6]<<8) + k230_1_Rx_x->ValidData[7];
        k230_Item->y_down = (k230_1_Rx_x->ValidData[8]<<8) + k230_1_Rx_x->ValidData[9];
        k230_Item->label_num = k230_1_Rx_x->ValidData[10];
        k230_Item->x_center = (k230_Item->x_left + k230_Item->x_right)/2;
        k230_Item->y_center = (k230_Item->y_up + k230_Item->y_down)/2;
        k230_Item->x_width = k230_Item->x_right - k230_Item->x_left;
        k230_Item->y_height = k230_Item->y_down - k230_Item->y_up;
    }	//更改括号位置从下面移动到上面
    
}

至此,单片机获取数据的代码已经撰写完毕

1.3.2 单片机串口发送需要的数据

首先,我们先进行简单的宏定义,表面我们需要的yolo识别对象

#define LABEL0_PLATFORM		    0
#define LABEL0_COLUMN		    1
#define LABEL0_BLOCK		    2
#define LABEL0_RED_SCORE		3
#define LABEL0_BLUE_SCORE	    4
#define LABEL0_RED_CIRCLE	    5
#define LABEL0_BLUE_CIRCLE	    6
#define LABEL0_PLATFORM_RED	    7
#define LABEL0_PLATFORM_BLUE	8

接着,我们需要一个函数,来发送数据给k230

HAL_StatusTypeDef k230_SendData(uint8_t number)
{
  return HAL_UART_Transmit(&huart1, &number, 1, 100); // 100ms超时
}

1.3.3 一些基于k230传输数据的应用

根据这些数据,其实可以做的处理已经有很多了
实际,我们完成了小车根据左侧摄像头的对于圆台的识别结果,来自动旋转驾驶,即:当圆台图像太大,说明小车太近了,此时小车应该远离;当圆台图像太小,说明小车太远了,此时小车应该靠近。如果圆台靠左,说明小车的车头有点往外偏,应该矫正回来,反之同理。
如:

/**
 * @brief: 根据视觉内容pid矫正
 * @param {None}
 * @retval: Speed Correct
 */
int32_t k230_PID(k230_Item_Type * k230_Item){
    int32_t diff_x_cent, diff_y_cent, diff_width, diff_height;
    int32_t speed_correct = 0;	// outter plus this value, inner sub this value
    
    if(k230_Item->x_left == 0 && k230_Item->x_right == 0)
        return 0;
    
    // postive mean too far down
    // shoule inner increase, outter decrease
    diff_x_cent	=	SCREEN_X_CENTER - 	k230_Item->x_center	;
    // postive mean too close
    // shoule inner increase, outter decrease	
    diff_width 	=	PLATFORM_WIDTH 	-	k230_Item->x_width		;


//	printf("%d %d\n",x_center, x_width);	// 调试接口
    diff_cent_acc 	+= 	diff_x_cent;
    diff_width_acc	+= 	diff_width;
    
    // 计算矫正值
    speed_correct	=	k230_KP_CENT 	* 	diff_x_cent	 	+ \
                        k230_KI_CENT	*	diff_cent_acc	+ \
                        k230_KD_CENT	*	(diff_x_cent - diff_cent_last)	;
    speed_correct	+=	k230_KP_WIDTH 	* 	diff_width	 	+ \
                        k230_KI_WIDTH	*	diff_width_acc	+ \
                        k230_KD_WIDTH	*	(diff_width - diff_width_last)	;
    
    // 更改上一次偏差量
    diff_cent_last 	= 	diff_x_cent;
    diff_width_last	=	diff_width;
    
    return speed_correct;
}

/**
* @brief: 让小车转圈
* @param {int32_t S_Speed} 小车的速度
* @retval: None
*/
uint8_t BreakFlag = 0;
void Go_Circle(int32_t S_Speed)
{
    int32_t PID_Correct;
    int32_t inner_speed;
    int32_t outter_speed;
    uint32_t in_while_Time = 0;
    k230_Item_Type *k230_Item_Ptr = &k230_Item_1;

    while (1)
    {
        k230_GetData(k230_Item_Ptr, SENSOR_Platform);
        if (k230_Item_Ptr->x_center == 0 && k230_Item_Ptr->x_width == 0)
        {
            // 如果没有传输数据
            // 1. 先保持原有速度行进一段时间
            Set_speed(500, 500, 500, 500);
            OSTimeDly(1000);
            // 2. 再次检测
            k230_GetData(k230_Item_Ptr, SENSOR_Platform);
            // 3. 如果仍然检测不到,则原地转圈直到检测到
            if (k230_Item_Ptr->x_center == 0 && k230_Item_Ptr->x_width == 0)
            {
                Set_speed(500, -500, -500, 500);
                in_while_Time = HAL_GetTick();
                while (k230_Item_Ptr->x_center == 0 && k230_Item_Ptr->x_width == 0)
                {
                    if (HAL_GetTick() - in_while_Time > 1500)
                        break;
                    k230_GetData(k230_Item_Ptr, SENSOR_Platform);
                }
            }
        }
        //		else{
        // 如果有数据传输
        PID_Correct = k230_PID(k230_Item_Ptr);
        inner_speed = (OUT_RATIO_IN * S_Speed - PID_Correct);
        outter_speed = (S_Speed + PID_Correct);
        // 左上 右上 右下 左下
        Set_speed(inner_speed, outter_speed, outter_speed, inner_speed);
        //		}
        OSTimeDly(1);
        if(BreakFlag == 1)
            break;
    }
}

爪子跟随同理,以下是爪子跟随的代码

float k230_Arm4_Column_Correct(k230_Item_Type * k230_Item){
    // 如果没有数据,则退出
    if (k230_Item->x_left == 0 && k230_Item->x_right == 0)
        return 0;

    printf("y_center: %d", (int)(k230_Item->y_center));
    printf("y_height: %d", (int)(k230_Item->y_height));
    return (float)(k230_Item->x_center - Column_Target_x_center) * Arm4_Cofficient;	// 计算舵机转动的角度
}
void Arm_See() //机械臂调整角度,k230看
{
    // 更改参数,原来参数见老版代码
    // 观察机械臂横向位置
    SetServoAngle(4,174);	// 177
    SetServoAngle(1,180);
    while(ServoTunnerOK() != 1);	
    SetServoAngle(2,162);	// 原来157
    SetServoAngle(3,111);	// 原来96
    Correct_Arm4_With_K230(174,20);	// 调整云台(舵机4)

    
    // 中间态
    SetServoAngle(3,120);
    while(ServoTunnerOK() != 1);
    SetServoAngle(2,148);
    SetServoAngle(1,87);	// 0522
    while(ServoTunnerOK() != 1);
//	OSTimeDly(500);
    
    // 放置前奏
    float temp1 = 107;
    float temp2 = 175;
    float temp3 = 96;
//	SetServoAngle(1,121);	// 103 
//	while(ServoTunnerOK() != 1);
    SetServoAngle(2,170); 	// 175	173
    while(ServoTunnerOK() != 1);	
    SetServoAngle(3,82); 	// 96	84
    while(ServoTunnerOK() != 1);

    SetServoAngle(1,110); 
    while(ServoTunnerOK() != 1);

//	OSTimeDly(500);
//	for(uint8_t i = 0;i < 20; i ++){
//		temp1--;
//		temp3--;
//		SetServoAngle(1,temp1); 
//		SetServoAngle(3,temp3); 
//	}

    // 等待稳定,放置圆环
    OSTimeDly(200);
    SetServoAngle(0,97); 
    OSTimeDly(500);
    
    
}

这是两者的演示效果

绕圆柱动画 爪子跟随动画

除此之外,还有跟随圆台颜色识别代码:

typedef enum{
    BLUE_TEAM = 0,
    RED_TEAM
}TEAM_COLOR_TYPE;
typedef enum{
    IS_OUR_COLOR = 0,	// 是我方的,则执行放置
    NOT_OUR_COLOR,		// 不是我方,则移动
    NOT_DETECT_COLOR	// 不确定,八成摄像头坏了,那建议先放置
} IS_OURCOLOR_TYPE;

extern TEAM_COLOR_TYPE OUR_COLOR;

IS_OURCOLOR_TYPE ArmCheckPlatformColor(uint8_t maxDetectTimes,uint8_t toward){
    uint8_t i = 0;
    IS_OURCOLOR_TYPE colorType;
    uint8_t isOurColor_Count = 0;
    uint8_t notOurColor_Count = 0;
    uint8_t notDetectColor_Count = 0;
    k230_Item_Type *k230_Item_Ptr_Our = &k230_Item_0;
    k230_Item_Type *k230_Item_Ptr_Other = &k230_Item_2;
    k230_Item_Type *k230_Item_Ptr_Recycle = &k230_Item_3;
        
    // 发送检测对象
    if(OUR_COLOR == BLUE_TEAM)
        k230_SendData(LABEL0_PLATFORM_BLUE);
    else 
        k230_SendData(LABEL0_PLATFORM_RED);	// 发送请求,需要圆柱的坐标
    OSTimeDly(200);
    
    // 比对
    for( i = 0; i < maxDetectTimes; i++){
        if(OUR_COLOR == BLUE_TEAM)
            k230_SendData(LABEL0_PLATFORM_BLUE);
        else 
            k230_SendData(LABEL0_PLATFORM_RED);	// 发送请求,需要圆柱的坐标
        OSTimeDly(200);
        k230_GetDataWithClear(k230_Item_Ptr_Recycle, SENSOR_Gripper);	// 清除可能是前一个请求的返回结果(货不对板)
        OSTimeDly(300);
        k230_GetDataWithClear(k230_Item_Ptr_Our, SENSOR_Gripper);
        printf("A->OurColor:\n");
        printf("x_left: %d, x_right: %d\n",(int)k230_Item_Ptr_Our->x_left,(int)k230_Item_Ptr_Our->x_right);
        printf("y_up: %d, y_down: %d\n",(int)k230_Item_Ptr_Our->y_up,(int)k230_Item_Ptr_Our->y_down);
        printf("x_center: %d, x_width: %d\n",(int)k230_Item_Ptr_Our->x_center,(int)k230_Item_Ptr_Our->x_width);
        printf("y_center: %d, y_height: %d\n",(int)k230_Item_Ptr_Our->y_center,(int)k230_Item_Ptr_Our->y_height);
        OSTimeDly(10);
        
        if(OUR_COLOR == BLUE_TEAM)
            k230_SendData(LABEL0_PLATFORM_RED);
        else 
            k230_SendData(LABEL0_PLATFORM_BLUE);	// 发送请求,需要圆柱的坐标
        OSTimeDly(200);
        k230_GetDataWithClear(k230_Item_Ptr_Recycle, SENSOR_Gripper);	// 清除可能是前一个请求的返回结果(货不对板)
        OSTimeDly(300);
        k230_GetDataWithClear(k230_Item_Ptr_Other, SENSOR_Gripper);
        printf("B->OtherColor:\n");
        printf("x_left: %d, x_right: %d\n",(int)k230_Item_Ptr_Other->x_left,(int)k230_Item_Ptr_Other->x_right);
        printf("y_up: %d, y_down: %d\n",(int)k230_Item_Ptr_Other->y_up,(int)k230_Item_Ptr_Other->y_down);
        printf("x_center: %d, x_width: %d\n",(int)k230_Item_Ptr_Other->x_center,(int)k230_Item_Ptr_Other->x_width);
        printf("y_center: %d, y_height: %d\n",(int)k230_Item_Ptr_Other->y_center,(int)k230_Item_Ptr_Other->y_height);
        OSTimeDly(10);
        
        if(toward == platformInRight)
            colorType = CheckPlatformColor_Right(k230_Item_Ptr_Our,k230_Item_Ptr_Other);
        else
            colorType = CheckPlatformColor_Left(k230_Item_Ptr_Our,k230_Item_Ptr_Other);
        if(colorType == IS_OUR_COLOR)
            isOurColor_Count++;
        else if(colorType == NOT_OUR_COLOR)
            notOurColor_Count++;
        else if(colorType == NOT_DETECT_COLOR)
            notDetectColor_Count++;
    }
    if(isOurColor_Count > notOurColor_Count && isOurColor_Count > notDetectColor_Count){
        printf("colorType:IS_OUR_COLOR\n\n");
        return IS_OUR_COLOR;
    }
    if(notOurColor_Count > isOurColor_Count && notOurColor_Count > notDetectColor_Count){
            printf("colorType:NOT_OUR_COLOR\n\n");
        return NOT_OUR_COLOR;
    }
    if(notDetectColor_Count > isOurColor_Count && notDetectColor_Count > notOurColor_Count){
            printf("colorType:NOT_DETECT_COLOR\n\n");
        return NOT_DETECT_COLOR;
    }
    return colorType;
}

最后,还有环游过程中检测是否是对方得分区,并判断有无五块的代码,不过比较可惜的是代码目前还有一定问题,以后有空再修正吧

void CircleTaskSensor0(void *p_arg){
    OSTimeDly(20);
    
    while(1){
        if(CIRCLE_FLAG == 1){
            Go_Circle(500);
        }
        else{
            BreakFlag = 1;
        }
        OSTimeDly(1);
    }
}
// k230检测是否是敌方得分区
void CheckOtherScoreRegionInCenter(void){
    uint8_t i = 0;
    uint8_t j = 0;
    uint8_t NoneItem_Num = 0;
    int32_t allSpinDelayTime = 0;
    int32_t allForwardDelayTime = 0;
    int32_t tempDelayTime = 0;
    k230_Item_Type *k230_Item_Ptr = &k230_Item_0;
    
    // 寻找敌方得分区位置
    if(OUR_COLOR == BLUE_TEAM){
        k230_SendData(LABEL0_RED_SCORE);
    }
    else if(OUR_COLOR == RED_TEAM){
        k230_SendData(LABEL0_BLUE_SCORE);
    }
    
    // 判断是否是在中心偏差一定范围内部
    k230_GetData(k230_Item_Ptr, SENSOR_Gripper);
    if(k230_Item_Ptr->x_center > 573 && k230_Item_Ptr->x_center < 706){	//判断是否是敌方得分区
        // 关闭绕圈走
        CIRCLE_FLAG = 0;
        Set_speed(0,0,0,0);
        // 开始检测物块
        k230_SendData(LABEL0_BLOCK);
        OSTimeDly(100);
        
        // 防止读取到没用的信息
        k230_GetDataWithClear(k230_Item_Ptr, SENSOR_Gripper);
        OSTimeDly(100);
        
        // 旋转
        spin_right(H_SPEED);
            OSTimeDly(100);
        
        while(j++<5 && k230_Item_Ptr->x_center!=0){
            // 放置机械臂
            k230_SendData(LABEL0_BLOCK);
            Arm_Grab_rectangle();
            OSTimeDly(1000);
            
            // 开始检测物块
            k230_SendData(LABEL0_BLOCK);
            OSTimeDly(100);
            
            // 防止读取到没用的信息
            k230_GetDataWithClear(k230_Item_Ptr, SENSOR_Gripper);
            OSTimeDly(100);
            
            // ***可以考虑在此添加提前退出***
            
            // 使得车头对准物块
            for(i = 0; i < 10; i++){
                // 读取方块位置
                k230_GetDataWithClear(k230_Item_Ptr, SENSOR_Gripper);
                tempDelayTime=k230_CarSpin_Block_Correct(k230_Item_Ptr);
                printf("1:tDT:%d\n", tempDelayTime);
                Circle_ClockType_with_Delay(100,tempDelayTime);
                allSpinDelayTime += tempDelayTime;
                OSTimeDly(tempDelayTime);
                OSTimeDly(100);
            }
            Set_speed(0,0,0,0);
            // 移动到物块
            for(i = 0; i < 10; i++){
                // 读取方块位置
                k230_GetDataWithClear(k230_Item_Ptr, SENSOR_Gripper);
                tempDelayTime=k230_CarForward_Block_Correct(k230_Item_Ptr);
                printf("2:tDT:%d\n", tempDelayTime);
                Circle_Goxx_with_Delay(200,tempDelayTime);
                allForwardDelayTime += tempDelayTime;
                OSTimeDly(tempDelayTime);
                OSTimeDly(100);
            }
            
            Set_speed(0,0,0,0);
            // 存储物块
            Arm_Store_Rectangle();
            
            // 大概恢复初始位置
            Circle_Goxx_with_Delay(200,-allForwardDelayTime);
            Circle_ClockType_with_Delay(100,-allSpinDelayTime);
            Set_speed(0,0,0,0);
        }		
        
        spin_left(H_SPEED);
        CIRCLE_FLAG = 1;
        OSTimeDly(1000);
    }
    OSTimeDly(10);
}
void CircleTaskSensor1(void *p_arg){
    OSTimeDly(20);
    
    while(1){
        if(CIRCLE_FLAG == 1){
            Arm_Set_Circle_See_Side();
            CheckOtherScoreRegionInCenter();
        }
        OSTimeDly(1);
        
    }
}