嘉楠K230AI开发板测评4--图像显示、画图、边缘/线段/圆形/矩形检测、线性回归
[复制链接]
本帖最后由 dfjs 于 2024-11-2 18:13 编辑
嘉楠科K230AI开发板测评4--机器视觉篇
- 摄像头
摄像头是整个机器视觉应用的基础,K230的引出了3路摄像头,接口如下图:
CanMV K230使用camera模块实现摄像头采集图像功能,K230硬件支持3路sensor输入(CSI接口),每个sensor设备均可独立完成图像数据采集捕获处理,并可以同时输出3路图像数据,sensor 0,sensor 1,sensor 2表示三个图像传感器;Camera Device 0,Camera Device 1,Camera Device 2表示三个sensor设备;output channel 0,output channel 1,output channel 2表示sensor设备的三个输出通道。三个图像传感器可以通过软件配置映射到不同的sensor 设备,示意图如下图。
摄像头(sensor)位于media模块下,通过from media.sensor import * #导入sensor模块,使用摄像头相关接口,sensor = Sensor(id,[width, height, fps])构建摄像头对象,id为CSI输入号,默认值为CSI2即开发板上的摄像头,width、height和fps为可选参数,分别表示sensor采集图像宽度,高度和帧率。
sensor.reset()复位和初始化摄像头。sensor.set_framesize(framesize = FRAME_SIZE_INVAILD, [width, height],chn = CAM_CHN_ID_0, alignment=0, **kwargs)设置每个通道的图像输出尺寸,framesize: 通道图像输出尺寸。chn: 通道编号,每个摄像头设备有3个通道。
sensor.set_pixformat(pixformat, chn = CAM_CHN_ID_0)设置图像像素格式。pixformat: 格式。chn: 通道编号,每个摄像头设备有3个通道。
sensor.set_hmirror(enable)设置摄像头画面水平镜像。sensor.set_vflip(enable)设置摄像头画面垂直翻转。
sensor.run()启动摄像头。
sensor.snapshot()使用相机拍摄一张照片,并返回 image 对象。
然后使用计算FPS(每秒帧数)的clock模块。clock=time.clock()构建一个时钟对象。clock.tick()开始追踪运行时间。clock.fps()停止追踪运行时间,并返回当前FPS(每秒帧数)。在调用该函数前始终首先调用 clock.tick(),完整代码编写流程如下。
参考代码如下,摄像头实时拍摄并显示在IDE缓冲区,由于CanMV K230 MicroPython底层基于Linux + RTOS实现,因此可以看到代码中出现一些辅助中断等代码,这些代码相对固定。
'''
实验名称:摄像头使用
实验平台:01Studio CanMV K230
说明:实现摄像头图像采集显示
'''
import time, os, sys
from media.sensor import * #导入sensor模块,使用摄像头相关接口
from media.display import * #导入display模块,使用display相关接口
from media.media import * #导入media模块,使用meida相关接口
try:
sensor = Sensor() #构建摄像头对象
sensor.reset() #复位和初始化摄像头
sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0
sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0
#使用IDE缓冲区输出图像,显示尺寸和sensor配置一致。
Display.init(Display.VIRT, sensor.width(), sensor.height())
MediaManager.init() #初始化media资源管理器
sensor.run() #启动sensor
clock = time.clock()
while True:
os.exitpoint() #检测IDE中断
################
## 这里编写代码 ##
################
clock.tick()
img = sensor.snapshot() #拍摄一张图
Display.show_image(img) #显示图片
print(clock.fps()) #打印FPS
###################
# IDE中断释放资源代码
###################
except KeyboardInterrupt as e:
print("user stop: ", e)
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()
实验结果,点击运行代码,右边显示摄像头实时拍摄情况,下方则显示RGB颜色直方图。
- 图像的3种显示方式
在摄像头拍摄图像后我们需要观察图像,这就涉及如何显示的问题,目前CanMV K230支持3种显示方式。分别是:IDE缓冲区显示、外接HDMI显示器或MIPI显示屏,3种图像显示方式,各有特点:
IDE缓冲区显示:性价比最高,图像质量有一定下降,但能满足大部分场合调试使用。最大支持1920x1080分辨率。
HDMI:外接HDMI显示屏,清晰度最高。最大支持1920x1080分辨率。
MIPI显示屏:外接01Studio 3.5寸MiPi显示屏,可以一体化组装,适合离线部署调试使用。最大支持800x480分辨率。
首先导入Display模块,Display.init(type = None, width = None, height = None, osd_num = 1, to_ide = False, fps = None)初始化Display模块,type: 显示设备类型,VIRT : IDE缓冲区显示;LT9611 : HDMI显示;ST7701 : mipi显示屏。width: 可选参数,显示图像宽度;height: 可选参数,显示图像高度;to_ide: 同时在IDE显示,仅用于设置为HDMI或MIPI屏显示时使用。
Display.show_image(img, x = 0, y = 0, layer = None, alpha = 255, flag = 0),img为显示图像对象,x: 起始横坐标;y: 起始纵坐标。
Display.deinit(),注销Display模块,必须在MediaManager.deinit()之前, 在sensor.stop()之后调用。代码编写流程图如下图:
参考代码如下,只展示与摄像头节不同的地方(核心代码):
#################################
## 图像3种不同显示方式(修改注释实现)
#################################
Display.init(Display.VIRT, sensor.width(), sensor.height()) #通过IDE缓冲区显示图像
#Display.init(Display.LT9611, to_ide=True) #通过HDMI显示图像
#Display.init(Display.ST7701, to_ide=True) #通过01Studio 3.5寸mipi显示屏显示图像
实验结果,分别为IDE缓冲区,HDMI显示器和MIPI屏幕
- 画图
通过摄像头采集到照片后,我们会进行一些处理,而这时候往往需要一些图形来指示,比如在图CanMV已经将图片处理(包含画图)封装成各类模块,我们 只需要熟悉其构造函数和使用方法即可片某个位置标记箭头、人脸识别后用矩形框提示等。
img=sensor.snapshot()通过摄像头拍摄方式返回image对象。
image.draw_line()对图像进行画线段,参数为起始坐标,终点坐标,颜色与线条粗细。
image.draw_rectangle()画矩形,参数为起始坐标,宽度,高度,颜色,边框粗细,是否填充。
image.draw_circle()画圆,参数为圆心,宽度,高度,颜色,线条粗细,是否填充。
image.draw_arrow()画箭头,参数为起始坐标,终点坐标,颜色,箭头位置大小,线条粗细。
image.draw_cross()画十字交叉,参数为交叉中点坐标,颜色,大小,线条粗细。
image.draw_string()写字符,参数为起始坐标,字符内容,颜色,字体大小,强制间隔。
image.draw_string_advanced()写字符,支持中文,参数为起始坐标,字体大小,字符内容,颜色,字体类型。
代码编写思路如下:
核心代码如下:
img = sensor.snapshot()
# 画线段:从 x0, y0 到 x1, y1 坐标的线段,颜色红色,线宽度 2。
img.draw_line(20, 20, 100, 20, color = (255, 0, 0), thickness = 2)
#画矩形:绿色不填充。
img.draw_rectangle(150, 20, 100, 30, color = (0, 255, 0), thickness = 2, fill = False)
#画圆:蓝色不填充。
img.draw_circle(60, 120, 30, color = (0, 0, 255), thickness = 2, fill = False)
#画箭头:白色。
img.draw_arrow(150, 120, 250, 120, color = (255, 255, 255), size = 20, thickness = 2)
#画十字交叉。
img.draw_cross(60, 200, color = (255, 255, 255), size = 20, thickness = 2)
#写字符。
#img.draw_string(150, 200, "Hello 01Studio!", color = (255, 255, 255), scale = 4, mono_space = False)
#写字符,支持中文。
img.draw_string_advanced(150, 180, 30, "Hello 01Studio", color = (255, 255, 255))
img.draw_string_advanced(40, 300, 30, "人生苦短, 我用Python", color = (255, 255, 255))
Display.show_image(img)
实验结果如下,在合适位置依次画出线段、矩形、圆形、箭头、十字交叉和字符:
- 图像检测
- 边缘检测
生活中每个物体都有一个边缘, 简单来说就是轮廓,使用MicroPython 结合 CanMV K230 自带的库来做图像轮廓检测。
CanMV集成了RGB565颜色块识别find_edges函数,位于 image 模块下,因此直接将拍摄到的图片进行处理即可。
直接通过image.find_edges(edge_type[, threshold])即可对图像进行边缘检测,参数edge_type为处理方式,image.EDGE_SIMPLE : 简单的阈值高通滤波算法(其基本原理是设置一个频率阈值,将高于该阈值的频率成分保留或增强,而将低于该阈值的频率成分抑制或去除,从而实现图像的锐化或边缘检测。); image.EDGE_CANNY: Canny 边缘检测算法(核心思想是找寻图像中灰度强度变化最强的位置,这些位置即边缘);threshold: 包含高、低阈值的二元组,默认是(100,200),仅支持灰度图像。
代码编写思路如下:
核心代码如下,对图片对象进行边缘:
img = sensor.snapshot() #拍摄一张图片
#使用 Canny 边缘检测器
img.find_edges(image.EDGE_CANNY, threshold=(50, 80))
# 也可以使用简单快速边缘检测,效果一般,配置如下
#img.find_edges(image.EDGE_SIMPLE, threshold=(100, 255))
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print(clock.fps()) #打印FPS
实验结果如下,对任务画像进行了边缘化:
- 线段检测
CanMV集成了线段识别 find_line_segments 函数,位于 image 模块下,因此我们直接将拍摄到的图片进行处理即可。
image.find_line_segments([roi[,merge_distance=0[,max_theta_difference=15]]])线段识别函数,返回image.line线段对象列表。参数roi: 识别区域(x,y,w,h),未指定则默认整张图片。参数merge_distance: 两条线段间可以相互分开而不被合并的最大像素。参数max_theta_difference: 将少于这个角度值的线段合并。大部分参数使用默认即可,不支持压缩图像和bayer图像。
代码编写流程如下:
核心代码如下:
img = sensor.snapshot() #拍摄一张图片
if enable_lens_corr: img.lens_corr(1.8) # for 2.8mm lens...
# `merge_distance` 控制相近的线段是否合并. 数值 0 (默认值)表示不合并。数值
#为1时候表示相近1像素的线段被合并。因此你可以通过改变这个参数来控制检测到线
#段的数量。
# `max_theta_diff` 控制相差一定角度的线段合并,默认是15度,表示15度内的线
# 段都会合并
for l in img.find_line_segments(merge_distance = 0, max_theta_diff = 5):
img.draw_line(l.line(), color = (255, 0, 0), thickness=2)
print(l)
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print(clock.fps()) #打印FPS
实验结果如下,IDE缓冲区标出图像中的横线。
- 圆形检测
CanMV集成了圆形识别find_circles函数,位于image模块下,因此我们直接将拍摄到的图片进行处理即可。
image.find_circles([roi[, x_stride=2[, y_stride=1[, threshold=2000[, x_margin=10[, y_margin=10[, r_margin=10[, r_min=2[, r_max[, r_step=2]]]]]]]]]])找圆函数。返回一个image.circle圆形对象,该圆形对象有4个值: x, y(圆心), r (半径)和magnitude(量级);量级越大说明识别到的圆可信度越高。
roi: 识别区域(x,y,w,h),未指定则默认整张图片;
threshold: 阈值。返回大于或等于threshold的圆,调整识别可信度;
x_stride y_stride : 霍夫变换时跳过x,y像素的量;
x_margin y_margin r_margin : 控制所检测圆的合并;
r_min r_max: 控制识别圆形的半径范围‘
r_step:控制识别步骤。
代码编写思路如下:
核心代码如下:
img = sensor.snapshot() #拍摄一张图片
# 圆形类有 4 个参数值: 圆心(x, y), r (半径)和 magnitude(量级);
# 量级越大说明识别到的圆可信度越高。
# `threshold` 参数控制找到圆的数量,数值的提升会降低识别圆形的总数。
# `x_margin`, `y_margin`, and `r_margin`控制检测到接近圆的合并调节.
# r_min, r_max, and r_step 用于指定测试圆的半径范围。
for c in img.find_circles(threshold = 2000, x_margin = 10, y_margin= 10,
r_margin = 10,r_min = 2, r_max = 100, r_step = 2):
#画红色圆做指示
img.draw_circle(c.x(), c.y(), c.r(), color = (255, 0, 0),thickness=2)
print(c) #打印圆形的信息
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
实验结果如下,图片检测识别结果如图。
- 矩形检测
CanMV集成了矩形识别find_rects函数,位于image模块下,因此我们直接将拍摄到的图片进行处理即可。
image.find_rects([roi=Auto, threshold=10000])矩形识别函数。返回一个image.rect矩形对象列表。
roi: 识别区域(x,y,w,h),未指定则默认整张图片;
threshold: 阈值。返回大于或等于threshold的矩形,调整识别可信度。
代码编写思路如下:
核心代码如下:
img = sensor.snapshot() #拍摄一张图片
# `threshold` 需要设置一个比价大的值来过滤掉噪声。
#这样在图像中检测到边缘亮度较低的矩形。矩形
#边缘量级越大,对比越强…
for r in img.find_rects(threshold = 10000):
img.draw_rectangle(r.rect(), color = (255, 0, 0),thickness=2) #画矩形显示
for p in r.corners(): img.draw_circle(p[0], p[1], 5, color = (0, 255, 0))#四角画小圆形
print(r)
#Display.show_image(img) #显示图片
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print(clock.fps()) #打印FPS
实验结果,左边矩形识别结果如图。
- 快速线性回归(巡线)
快速线性回归的用途非常广泛,如比赛经常用到的小车、机器人巡线,可以通过线性回归的方式判断虚线和实线的轨迹,从而做出判断和响应。
CanMV集成了快速线性回归get_regression函数,位于image模块下。
mage.get_regression(thresholds[, invert=False[, roi[, x_stride=2[, y_stride=1[, area_threshold=10[, pixels_threshold=10[, robust=False]]]]]]])对图像所有阈值像素进行线性回
归计算。这一计算通过最小二乘法进行,通常速度较快,但不能处理任何异常值。若 robust 为True,则将使用泰尔指数。泰尔指数计算图像中所有阈值像素间的所有斜率的中值。
若在阈值转换后设定太多像素,即使在80x60的图像上,这一N^2操作也可能将您的FPS降到5以下。 但是,只要阈值转换后的进行设置的像素数量较少,即使在超过30%的阈值像素
为异常值的情况下,线性回归也依然有效。
threshold: 必须是元组列表。 (lo, hi) 定义你想追踪的颜色范围。对于灰度图像,每个元组需要包含两个值:最小灰度值和最大灰度值。
代码编写流程如下:
核心代码如下:
#image.binary([THRESHOLD])将灰度值在THRESHOLD范围变成了白色
img = sensor.snapshot().binary([THRESHOLD]) if BINARY_VISIBLE else sensor.snapshot()
# 返回一个类似 find_lines() 和find_line_segments()的对象.
# 有以下函数使用方法: x1(), y1(), x2(), y2(), length(),
# theta() (rotation in degrees), rho(), and magnitude().
#
# magnitude() 代表线性回归的指令,其值为(0, INF]。
# 0表示一个圆,INF数值越大,表示线性拟合的效果越好。
line = img.get_regression([(255,255) if BINARY_VISIBLE else THRESHOLD])
if (line):
img.draw_line(line.line(), color = 127,thickness=4)
print(line) #打印结果
#显示图片,仅用于LCD居中方式显示
Display.show_image(img, x=round((800-sensor.width())/2),y=round((480-sensor.height())/2))
print("FPS %f, mag = %s" % (clock.fps(), str(line.magnitude()) if (line) else "N/A"))
实验结果,为了标明线性的变化趋势,取多组实验结果如下,串口结果包含拟合线段的两个点坐标,长度,以及非常重要的theta角度信息:
|