2025年电赛H题野生动物巡查,做的无人机下挂的机载视觉云台。现在向大家开放,希望有所帮助。由于当年的视觉模型训练数据丢失,就用最简单的RGB颜色识别代替。
硬件:mega2560,庐山派K230,MG996R舵机,多功能舵机支架。
(当然用其他的单片机也可以,主要是学习原理和思想,我这里用mega2560)
软件:CanMV IDE K230,Arduino IDE。
废话不多说,mega2560的代码如下
#include <Servo.h>
// ================= 引脚定义 =================
const int servoXPin = 9;
const int servoYPin = 10;
const int buzzerPin = 11; // 有源蜂鸣器(低电平响)
// ================= 舵机限位(重要!) =================
#define X_MIN_ANGLE 20 // X轴最小角度
#define X_MAX_ANGLE 160 // X轴最大角度
#define Y_MIN_ANGLE 20 // Y轴最小角度
#define Y_MAX_ANGLE 160 // Y轴最大角度
// ================= 舵机对象 =================
Servo servoX;
Servo servoY;
const int initialPos = 90;
int currentAngleX = initialPos;
int currentAngleY = initialPos;
// 追踪阈值
const int xLowThreshold = 350;
const int xHighThreshold = 450;
const int yLowThreshold = 190;
const int yHighThreshold = 290;
const int stepSize = 1;
// 超时
unsigned long lastDataTime = 0;
const unsigned long timeout = 500;
void setup() {
Serial3.begin(115200); // K230 通信串口
// 有源蜂鸣器:低电平触发,初始化=高电平(不响)
pinMode(buzzerPin, OUTPUT);
digitalWrite(buzzerPin, HIGH);
// 舵机初始化
servoX.attach(servoXPin);
servoY.attach(servoYPin);
servoX.write(initialPos);
servoY.write(initialPos);
}
void loop() {
bool hasData = false;
// 读取 K230 数据
if (Serial3.available()) {
String data = Serial3.readStringUntil('\\n');
data.trim();
int xIndex = data.indexOf("X:");
int yIndex = data.indexOf("Y:");
if (xIndex != -1 && yIndex != -1) {
int targetX = data.substring(xIndex + 2, data.indexOf(',', xIndex)).toInt();
int targetY = data.substring(yIndex + 2, data.indexOf(',', yIndex)).toInt();
updateServoPosition(targetX, targetY);
lastDataTime = millis();
hasData = true;
}
}
// ================= 蜂鸣器(低电平触发!) =================
if (hasData) {
digitalWrite(buzzerPin, LOW); // 低电平 = 响(声音洪亮)
} else {
digitalWrite(buzzerPin, HIGH); // 高电平 = 不响
}
// 超时无数据 → 回中
if (millis() – lastDataTime > timeout) {
currentAngleX = initialPos;
currentAngleY = initialPos;
servoX.write(currentAngleX);
servoY.write(currentAngleY);
digitalWrite(buzzerPin, HIGH); // 停止报警
}
delay(20);
}
// ================= 舵机控制 + 硬限位 =================
void updateServoPosition(int x, int y) {
// X 轴
if (x < xLowThreshold) {
currentAngleX += stepSize;
} else if (x > xHighThreshold) {
currentAngleX -= stepSize;
}
// 限位
currentAngleX = constrain(currentAngleX, X_MIN_ANGLE, X_MAX_ANGLE);
servoX.write(currentAngleX);
// Y 轴
if (y < yLowThreshold) {
currentAngleY -= stepSize;
} else if (y > yHighThreshold) {
currentAngleY += stepSize;
}
// 限位
currentAngleY = constrain(currentAngleY, Y_MIN_ANGLE, Y_MAX_ANGLE);
servoY.write(currentAngleY);
}
K230的代码
import time, os, sys
from machine import UART, FPIOA
from media.sensor import *
from media.display import *
from media.media import *
sensor_id = 2
sensor = None
# 配置串口引脚
fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD)
fpioa.set_function(12, FPIOA.UART2_RXD)
# 初始化UART2,波特率115200
uart = UART(UART.UART2, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE)
# 显示模式选择:可以是 "VIRT"、"LCD" 或 "HDMI"
DISPLAY_MODE = "LCD"
# 根据模式设置显示宽高
if DISPLAY_MODE == "LCD":
# 3.1寸屏幕模式
DISPLAY_WIDTH = 800
DISPLAY_HEIGHT = 480
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)
# 根据模式初始化显示器
if DISPLAY_MODE == "LCD":
Display.init(Display.ST7701, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, to_ide=True)
# 初始化媒体管理器
MediaManager.init()
# 启动传感器
sensor.run()
# 指定颜色阈值,格式:[min_L, max_L, min_A, max_A, min_B, max_B]
color_threshold = [(0, 79, 31, 67, 26, 60)]
# 初始化和基本设置完成,开始进行识别
while True:
os.exitpoint()
# 捕获通道0的图像
img = sensor.snapshot(chn=CAM_CHN_ID_0)
# 在屏幕正中间绘制白色交叉(起始坐标x,起始坐标y,十字颜色,大小,厚度)
img.draw_cross(400, 240, color=(255, 255, 255), size=20, thickness=2)
# color_threshold 是要寻找的颜色的阈值,area_threshold 表示过滤掉小于此面积的色块。
blobs = img.find_blobs(color_threshold, area_threshold=10000)
# 如果检测到颜色块
if blobs:
# 找出面积最大的物体
max_blob = max(blobs, key=lambda b: b[4])
# 绘制颜色块的外接矩形,max_blob[0:4] 表示颜色块的矩形框 [x, y, w, h]
img.draw_rectangle(max_blob[0:4])
# 在颜色块的中心绘制一个十字,max_blob[5] 和 max_blob[6] 分别是颜色块的中心坐标 (cx, cy)
img.draw_cross(max_blob[5], max_blob[6])
# 绘制白色字符串
test = "X:{} Y:{} S:{}".format(max_blob[5], max_blob[6], max_blob[4])
img.draw_string_advanced(10, 10, 32, test, color=(255, 255, 255))
# 在控制台输出颜色块的中心坐标
print("Max Blob: X={}, Y={}, Area={}".format(max_blob[5], max_blob[6], max_blob[4]))
# 通过串口发送数据给Arduino (格式: "X:xxx,Y:yyy,S:zzz\\n")
data_str = "X:{},Y:{},S:{}\\n".format(max_blob[5], max_blob[6], max_blob[4])
uart.write(data_str)
# 显示捕获的图像,中心对齐,居中显示
Display.show_image(img)
finally:
# 停止传感器运行
if isinstance(sensor, Sensor):
sensor.stop()
# 反初始化显示模块
Display.deinit()
os.exitpoint(os.EXITPOINT_ENABLE_SLEEP)
time.sleep_ms(100)
# 释放媒体缓冲区
MediaManager.deinit()
原理如下:
K230识别到相应的颜色后,将相应的物体框选出来,将中心的坐标(X,Y)通过串口2发送给mega,mega的串口3接收到后通过对X,Y坐标的数据来控制舵机的转动,舵机有两个,分为控制左右的X舵机,还有控制上下的Y舵机,mega设置了死区的范围,也就是说当X,Y数据在这个范围的时候舵机是不会动的,要不然舵机将会一直抖动,但是这个范围也不能设置的太大,太大了会影响精度,识别到后蜂鸣器会报警,提示识别到了相应的物体,并且两个舵机开始追踪。这个死区的范围是要根据摄像头的分辨率调的,我这个K230是有屏幕的,长800,宽480,取中心线,也就是400,240,上下值增大50,所以X死区范围是190-290,Y的死区范围是350-450。也就是中间的100×100的区域。
当时的应用场景就是无人机下面挂一个这个云台,识别一些小动物的贴纸,然后mega控制舵机将激光笔打到动物身上。说明识别了这个动物。
以下是演示的视频:
视觉追踪云台演示视频
有问题欢迎大家提问




