# 二维追踪炮台 **Repository Path**: laoguaige/tracking-cannon ## Basic Information - **Project Name**: 二维追踪炮台 - **Description**: K230识别目标通过玩具枪开炮 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 8 - **Forks**: 0 - **Created**: 2024-11-26 - **Last Updated**: 2025-11-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 摄像头运用篇 ## IDE显示图像 显示图像在K230的固件中一共支持4种,分别是HDMI的VGA,HDMI的1080P、LCD、IDE等。我们先了解怎么将摄像头采集到的画面显示在IDE的图像缓冲区。 根据K230的[Sensor例程讲解](https://developer.canaan-creative.com/k230_canmv/dev/zh/example/media/sensor.html) ,提取出摄像头的初始化+采集图像的代码。 ```py # Camera 示例 import time import os import sys from media.sensor import * from media.display import * from media.media import * DETECT_WIDTH = 640 DETECT_HEIGHT = 480 sensor = None try: # 配置摄像头的图像大小 sensor = Sensor(width = DETECT_WIDTH, height = DETECT_HEIGHT) # 摄像头传感器复位 sensor.reset() # 设置图像是否镜像 # sensor.set_hmirror(False) # 设置图像是否翻转 # sensor.set_vflip(False) # 设置通道0的图像输出尺寸 sensor.set_framesize(width = DETECT_WIDTH, height = DETECT_HEIGHT) #设置通道0输出格式为RGB565(彩色图像) sensor.set_pixformat(Sensor.RGB565) # Init媒体管理器 MediaManager.init() # 传感器启动运行 sensor.run() while True: sensor.snapshot() #获取一帧图像 except KeyboardInterrupt as e: print(f"user stop") except BaseException as e: print(f"Exception '{e}'") finally: # sensor stop run if isinstance(sensor, Sensor): sensor.stop() os.exitpoint(os.EXITPOINT_ENABLE_SLEEP) time.sleep_ms(100) # release media buffer MediaManager.deinit() ``` 根据[Display例程讲解](https://developer.canaan-creative.com/k230_canmv/dev/zh/example/media/display.html),提取出将图像显示到IDE上的代码。 ```python import time, os, gc, sys, math from media.sensor import * from media.display import * from media.media import * DETECT_WIDTH = 640 DETECT_HEIGHT = 480 try: #配置图像显示方式为IDE显示,显示的宽高为DETECT_WIDTH+DETECT_HEIGHT,帧率100fps Display.init(Display.VIRT, width = DETECT_WIDTH, height = DETECT_HEIGHT, fps = 100) while True: Display.show_image(img) #显示一个图像,这个img是摄像头采集的一帧图像 except KeyboardInterrupt as e: print(f"user stop") except BaseException as e: print(f"Exception '{e}'") finally: # 清除显示缓存 Display.deinit() os.exitpoint(os.EXITPOINT_ENABLE_SLEEP) time.sleep_ms(100) ``` 将这两个代码一整合,就得到了将图像显示到IDE中的代码: ```python import time import os import sys from media.sensor import * from media.display import * from media.media import * DETECT_WIDTH = 640 DETECT_HEIGHT = 480 sensor = None try: # 配置摄像头的图像大小 sensor = Sensor(width = DETECT_WIDTH, height = DETECT_HEIGHT) # 摄像头传感器复位 sensor.reset() # 设置图像是否镜像 # sensor.set_hmirror(False) # 设置图像是否翻转 # sensor.set_vflip(False) # 设置通道0的图像输出尺寸 sensor.set_framesize(width = DETECT_WIDTH, height = DETECT_HEIGHT) #设置通道0输出格式为RGB565(彩色图像) sensor.set_pixformat(Sensor.RGB565) #配置图像显示方式为IDE显示,显示的宽高为DETECT_WIDTH+DETECT_HEIGHT,帧率100fps Display.init(Display.VIRT, width = DETECT_WIDTH, height = DETECT_HEIGHT, fps = 100) # Init媒体管理器 MediaManager.init() # 传感器启动运行 sensor.run() while True: img = sensor.snapshot() #获取一帧图像 Display.show_image(img) #显示摄像头采集的图像 except KeyboardInterrupt as e: print(f"user stop") except BaseException as e: print(f"Exception '{e}'") finally: # sensor stop run if isinstance(sensor, Sensor): sensor.stop() # deinit display Display.deinit() os.exitpoint(os.EXITPOINT_ENABLE_SLEEP) time.sleep_ms(100) # release media buffer MediaManager.deinit() ``` ## 颜色识别篇 根据 `canMV-K230 IDE` 自带的颜色识别例程,整理出关键的代码。 以下是颜色识别例程: ```python # Find Blobs Example # # This example shows off how to find blobs in the image. import time, os, gc, sys from media.sensor import * from media.display import * from media.media import * DETECT_WIDTH = ALIGN_UP(320, 16) DETECT_HEIGHT = 240 sensor = None def camera_init(): global sensor # construct a Sensor object with default configure sensor = Sensor(width=DETECT_WIDTH,height=DETECT_HEIGHT) # sensor reset sensor.reset() # set hmirror # sensor.set_hmirror(False) # sensor vflip # sensor.set_vflip(False) # set chn0 output size sensor.set_framesize(width=DETECT_WIDTH,height=DETECT_HEIGHT) # set chn0 output format sensor.set_pixformat(Sensor.RGB565) # use IDE as display output Display.init(Display.VIRT, width= DETECT_WIDTH, height = DETECT_HEIGHT,fps=100,to_ide = True) # init media manager MediaManager.init() # sensor start run sensor.run() def camera_deinit(): global sensor # sensor stop run sensor.stop() # deinit display Display.deinit() # sleep os.exitpoint(os.EXITPOINT_ENABLE_SLEEP) time.sleep_ms(100) # release media buffer MediaManager.deinit() def capture_picture(): fps = time.clock() while True: fps.tick() try: os.exitpoint() global sensor img = sensor.snapshot()#获取图像 # 设置颜色阈值 thresholds = [[0, 80, 40, 80, 10, 80]] # 红色的阈值,这样代码就只识别红色 # 从图像中查找颜色,根据阈值进行对比,当阈值一致时,将对应颜色的各个参数/位置等保存到blobs变量中 blobs=img.find_blobs(thresholds ,pixels_threshold= 500) # 从blobs中遍历各个被识别到的阈值一样的颜色,将遍历到的颜色参数赋值给blob for blob in blobs: # 调用画矩形API,填入遍历到的颜色4个角的位置,设置矩形框的颜色为RGB的RG全色 img.draw_rectangle(blob[0], blob[1], blob[2], blob[3], color = (255, 255, 0)) # 显示图像 Display.show_image(img) img = None gc.collect() print(fps.fps()) #输出帧率 except KeyboardInterrupt as e: print("user stop: ", e) break except BaseException as e: print(f"Exception {e}") break def main(): os.exitpoint(os.EXITPOINT_ENABLE) camera_is_init = False try: print("camera init") camera_init() camera_is_init = True print("camera capture") capture_picture() except Exception as e: print(f"Exception {e}") finally: if camera_is_init: print("camera deinit") camera_deinit() if __name__ == "__main__": main() ``` 分析:其实主要就是 [find_blobs](https://developer.canaan-creative.com/k230_canmv/dev/zh/api/openmv/image.html#find-blobs) API,它会去图像中查找我们设置的阈值的颜色,然后我们就可以对该阈值颜色为所欲为。 但是这个例程还是有一点问题,也不能说是问题,只是不符合我们的要求,我们的要求是只识别一个颜色。毕竟我们只有一个炮台,识别再多个也只能打一个。那么问题来了,这个例程是会将图像中识别到的颜色都画框并记录,类似以下现象:(识别浅蓝色) ![picture 0](images/e266cbbdf411081a44b4e0e1ac299492949457b7739fa104015d17dcf00c372e.png) 图中的白色矩形框+白色十字就是表示的识别到的颜色。现在识别出了那么多个目标,我们只有一个炮口怎么办?**我们可以优先找到最大的,最近的一个开炮!**,而在图像的世界里,像素越多的,说明离摄像头越近!那就是最大的目标! 以下是在一堆识别到的图像中,找到最大像素的图像代码:(只写关键的了) ```python # 拍摄一张图片 img = sensor.snapshot() # 查找图像中满足红色阈值(red_threshold)的区域 blobs = img.find_blobs([red_threshold], pixels_threshold=200, area_threshold=200, merge=True) # 如果找到了至少一个blob if blobs: # 从blobs中找到最大像素点的blob largest_blob = max(blobs, key=lambda b: b.pixels()) # 只对最大像素点的目标画框,框的颜色是RGB中的R(红色) img.draw_rectangle(largest_blob.rect(), color=(255, 0, 0)) # 在框内画十字,标记中心点 img.draw_cross(largest_blob.cx(), largest_blob.cy(), color=(255, 0, 0)) # 将位置和宽高格式化为字符串 wz = "x={}, y={}, w={}, h={}".format(x_offset, y_offset, largest_blob.w(), largest_blob.h()) # 图像上显示位置和宽高信息的字符串,字符的大小是32 img.draw_string_advanced(0,0,32,wz) ``` > 上面代码中的阈值注释虽然是红色阈值,但是我实际上用的是绿色的阈值 现在我们就已经完成了目标定位的任务了,上面的代码中找到了最大像素的目标,并且输出它对于摄像头图像大小的X轴Y轴位置。 ![picture 2](images/894c9d086d7d717969028a192c8e8b811776ed832db9429ef09b4de2a38f64f2.png) 我还加了一些辅助内容,比如给图像的中心画一个绿色的十字。其他内容具体看源码。 # 舵机控制篇 180度的电机,我们将其分成为-90~90度的范围。我们要知道3个参数: 最大角度90度时我们要设置多少占空比; 最小角度-90度时我们要设置多少占空比; 中间角度0度时我们要设置多少占空比; 经过实测,上下动作的舵机占空比范围是2.5~12.5%的占空比,2.5%占空比时角度最小,12.5%占空比时角度最大,7.5%占空比时角度居中。 这里以Y轴举例,现在Y轴的范围是0 ~ 479,屏幕的中心值就是480/2=240,而Y轴的范围是从0开始的,所以我们得减一,屏幕的中心值就是239。 我们以屏幕中心为参考,当Y轴的值小于239时,我们就判断识别物体是上方; 图 当Y轴的值大于239时,我们就判断识别物体是下方; 图 当Y轴的值等于239时,我们就判断识别的物体是Y轴中心; 图 我个人觉得全部是正数的范围,不方便我们去处理舵机,0 ~ 238是上方,239是中间,240 ~ 379是下方,这样不好带入到我们后面的舵机控制中。为了更加方便我自己,我将其Y轴范围修改如下: | 数值范围 | 说明 | | :---: | :---: | | -239~0 | 上方 | | 0 | 中间 | | 0~239 | 下方 | 这样分布后我们就知道负数是上,正数是下,我们要让舵机尽量往中间(就是0的位置)移动。 实现的代码: ```python # 如果找到了至少一个blob if blobs: # 找到最大的blob largest_blob = max(blobs, key=lambda b: b.pixels()) # 画框 img.draw_rectangle(largest_blob.rect(), color=(255, 0, 0)) # 在框内画十字,标记中心点 img.draw_cross(largest_blob.cx(), largest_blob.cy(), color=(255, 0, 0)) # 计算相对于屏幕中心的X轴和Y轴的偏移量 x_offset = largest_blob.cx() - img.width() // 2 y_offset = largest_blob.cy() - img.height() // 2 # 屏幕显示位置信息和像素大小,包含正负号 wz = "x={}, y={}, w={}, h={}".format(x_offset, y_offset, largest_blob.w(), largest_blob.h()) img.draw_string_advanced(0,0,32,wz) ``` **接下来我们来考虑这个问题:如何将Y轴的数值反馈给舵机让它按照我们设置的方向动作?** 接下来了解一下我的想法,我们要让舵机往Y轴的中心动作。 当识别物体Y轴为-239 ~ 0时,我们让舵机向上,直到识别物体Y轴的数值为0; 当识别物体Y轴为0 ~ 239 时,我们让舵机向下,直到识别物体Y轴的数值为0; **现在这个Y轴就是误差!我们要让误差尽量保持为0!** 首先是考虑如何解决数值不对等问题。舵机的范围是2.5 ~ 12.5,Y轴的范围是 -239 ~ +239 ,我们要将Y轴的数值压缩到2.5 ~ 12.5 的范围。可以通过以下代码实现: ```python # 将数值转换为占空比的函数 def input_to_duty_cycle(input_value): min_input = -max_duty max_input = max_duty min_duty_cycle = min_duty max_duty_cycle = max_duty # 确保输入值在允许的范围内 if input_value < min_input or input_value > max_input: raise ValueError('输入值超出范围,应为{}到{}'.format(min_input,max_input)) # 计算输出占空比 output_value = min_duty_cycle + ((input_value - min_input) / (max_input - min_input)) * (max_duty_cycle - min_duty_cycle) return output_value ``` 现在解决了这个数值不对等问题,舵机的居中角度是7.5,当识别物体在上方时,舵机的数值输出小于7.5; 当识别物体在下方时,舵机的数值输出大于7.5; GIF动图 如果我们将摄像头固定在舵机上,舵机移动的时候,我们的摄像头也移动。那按照想法就是屏幕跟舵机一起动,这样就能够稳定的固定到中心,实现了识别瞄准功能。 将这个代码直接应用到舵机上的效果: GIF动图 ![picture 3](images/1cf1f878f9cab309ea8ff15983ddb58b09588d8404dde28ff1dfaed0eeaaabc0.gif) # PID自动控制篇 - ![图 4](images/cc6f61b04d0e18fd9b1e6b74cdd4b68b4bbce5b18b2b824293c3db0e2cc9a8ad.gif) # 数据滤波篇 -