公司动态
MaixPy图像处理入门:从像素、RGB/HSV到颜色追踪实战
1. 从像素到图像理解数字世界的“马赛克”刚拿到MaixPy开发板看着摄像头传回来的画面是不是既兴奋又有点无从下手屏幕上的图像本质上就是一堆数字。我们常说“图像处理”处理的其实就是这些数字。对于新手来说跳过枯燥的理论直接调库跑代码短期内能看到效果但一旦遇到颜色识别不准、边缘检测奇怪、内存不够用等问题就会一头雾水。理解图像的基础知识就像学武功先扎马步是后续玩转人脸识别、物体追踪这些“高级招式”的根基。MaixPy所运行的K210芯片内置了KPU神经网络处理器和专门的图像处理硬件加速单元但它“眼”中的世界和我们用OpenCV在电脑上处理图像底层逻辑是相通的。这篇文章我们就来彻底搞懂几个核心概念图像到底是什么、颜色是怎么表示的、在MaixPy里图像数据如何存放和操作。我会结合MaixPy的实际情况告诉你哪些理论需要重点掌握哪些坑可以提前避开。理解了这些你再看image.find_blobs或者kpu.forward这些函数就会明白它们到底在对那堆数字做什么。2. 图像的基石分辨率、像素与二值图我们首先得把图像拆解到最小单元。你可以把一张数字图像想象成一块巨大的、极其精细的十字绣布。这块布有宽度和高度比如320x240这就是分辨率。分辨率决定了图像的精细程度在MaixPy中它直接关系到处理速度和内存占用。K210的摄像头通常支持多种分辨率如QVGA (320x240)、VGA (640x480)。分辨率越高细节越多但需要处理的像素数据也呈平方级增长对芯片的内存和算力都是考验。这块绣布上的每一个小格子就是一个像素它是图像不可分割的最小单位。每个格子里填充的颜色决定了最终画面的呈现。那么最基础的颜色表达就是黑白。只用“黑”和“白”两种颜色构成的图像就是二值图像。在计算机里我们用0代表黑通常用1或255代表白。在MaixPy的image模块中你可以用Image()创建一张指定大小的二值图import image # 创建一张320x240的黑色二值图 img_binary image.Image(size(320, 240), color(0,), modeimage.BINARY)这里的modeimage.BINARY至关重要它告诉MaixPy这张图每个像素只用一个字节甚至更少的位来表示0或1。二值图虽然简单但用途极广比如通过阈值分割将灰度图转为二值图后可以方便地进行轮廓查找(find_blobs)、计算像素面积这是很多机器视觉检测如寻线、物体有无判断的第一步。注意MaixPy中color参数是一个元组对于二值图(0,)表示黑色(255,)表示白色。虽然二值图理论上只需要0/1但MaixPy内部可能用0-255的灰度值来表示255即为白。3. 灰度图引入亮度的维度只有黑白的世界对比强烈但缺乏层次。于是我们引入了灰度图。灰度图每个像素不再是非黑即白而是用一个数值来表示从黑到白的“亮度”或“灰度”级别。这个数值的范围通常是0到255一个字节8位0代表纯黑255代表纯白中间的数值代表不同程度的灰色。为什么是0-255因为8位二进制数刚好能表示2^8256种状态在存储和计算上非常方便是计算机图形学中的一个标准。灰度图丢弃了色彩信息只保留亮度数据量是彩色图的三分之一处理速度更快。很多复杂的图像处理算法如边缘检测、特征提取都先在灰度图上进行。在MaixPy中创建和处理灰度图非常常见# 从摄像头获取一张灰度图 img_gray sensor.snapshot().to_grayscale() # 或者直接创建一张灰度图 img_gray_new image.Image(size(320, 240), color(128,), modeimage.GRAYSCALE)sensor.snapshot()默认获取的是RGB彩色图.to_grayscale()方法将其转换为灰度图。这个转换过程并非简单地取平均值通常采用加权公式Gray 0.299*R 0.587*G 0.114*B。这个公式源于人眼对绿色最敏感对蓝色最不敏感这样转换得到的灰度图更符合人眼的感知。当你需要对图像进行阈值分割时灰度图是基础# 设定一个阈值将灰度图转为二值图 img_binary img_gray.binary([(100, 255)]) # 灰度值在100-255之间的像素变为白色(255)其余为黑色(0)这里的100就是阈值调整这个值可以控制哪些区域被保留为前景白。这个操作在检测亮度特定的物体如白色小球、发光的LED时非常有效。4. 彩色图像RGB与色彩空间我们的世界是彩色的所以最常用的图像模式是彩色图像。最常见的彩色表示法是RGB模型。它用红、绿、蓝三种基础色光的不同强度来混合出各种颜色。每个像素由三个数值组成(R, G, B)每个数值范围也是0-255。例如(255, 0, 0)是纯红色(0, 255, 0)是纯绿色(255, 255, 0)是黄色红绿(0,0,0)是黑色(255,255,255)是白色。在MaixPy中默认从摄像头获取的就是RGB图像img_color sensor.snapshot() # 默认就是RGB模式此时img_color的mode是image.RGB565。等等为什么不是RGB888这里就是一个关键点内存与速度的权衡。一个RGB888的像素需要3个字节R、G、B各8位共24位。对于一张320x240的图片内存占用为320*240*3 230,400字节约225KB。而K210芯片的可用内存SRAM有限为了节省内存并提高传输处理速度MaixPy默认采用了RGB565格式。RGB565用一个16位2字节的短整数来表示一个像素。这16位中5位用于红色6位用于绿色5位用于蓝色人眼对绿色更敏感所以多给1位。这样每个像素从3字节压缩到2字节内存占用直接减少三分之一。对于刚才的320x240图片现在只需320*240*2 153,600字节约150KB。代价是颜色精度略有下降但从实际观察来看对于大多数机器视觉应用完全足够。当你需要访问某个像素的颜色时# 获取坐标(100, 50)处的像素值 pixel_value img_color.get_pixel(100, 50) # 对于RGB565图像get_pixel返回的是一个16位的整数需要拆解出RGB分量 r (pixel_value 11) 0x1F # 取高5位 g (pixel_value 5) 0x3F # 取中间6位 b pixel_value 0x1F # 取低5位 # 注意这是5-6-5位的值范围是0-31或0-63通常需要转换到0-255范围进行显示或计算 r_8bit (r * 255) // 31 g_8bit (g * 255) // 63 b_8bit (b * 255) // 31实际操作中我们很少需要这样手动拆解。MaixPy的image模块提供了更高级的接口。但了解底层格式能帮助你理解为什么有些颜色操作结果和预期不符以及为什么内存总是不够用。除了RGB还有HSV/HSL色彩空间。这在颜色识别中极其有用。HSV将颜色分为色相、饱和度、明度。其中色相决定了“是什么颜色”红、黄、绿...饱和度决定了颜色的“鲜艳程度”明度决定了“有多亮”。在复杂光照下物体的RGB值变化很大但其色相往往相对稳定。因此在识别红色小球、绿色草坪时转换到HSV空间并针对色相进行阈值过滤鲁棒性会强很多。# 将RGB图像转换为HSV图像 img_hsv img_color.to_hsv() # 在HSV空间定义颜色阈值例如识别红色色相H在0-10或170-180范围 red_threshold [(0, 50, 50, 10, 255, 255), (170, 50, 50, 180, 255, 255)] # 阈值元组格式通常是 (L_H, L_S, L_V, H_H, H_S, H_V)分别是各分量的下限和上限 img_binary_red img_hsv.binary(red_threshold)to_hsv()转换和binary()阈值化是MaixPy颜色识别流水线上的核心操作。理解HSV每个分量的意义是调出精准颜色阈值的关键。5. 图像在内存中的旅程加载、转换与存储理解了图像的表示方式我们来看看在MaixPy中图像数据是如何流动和存储的。这个过程涉及到几个关键对象和概念。1. 图像对象的创建与来源图像数据主要来自两个地方摄像头和文件。import sensor, image, lcd # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) # 设置采集格式可选RGB565或GRAYSCALE sensor.set_framesize(sensor.QVGA) # 设置分辨率 sensor.skip_frames(time 2000) # 等待摄像头稳定 # 来源1从摄像头捕获一帧得到的是一个Image对象 img_from_cam sensor.snapshot() # 来源2从文件系统加载图片支持jpg/bmp/pgm等 img_from_file image.Image(/sd/cat.jpg)sensor.snapshot()返回的Image对象其数据直接存放在芯片的内部内存中访问速度最快。而从文件加载的图片会先被读到内存中。这里要注意K210的内存分为内部高速内存和外部SPI Flash或SD卡。处理大图时必须时刻关注内存占用。2. 图像的复制与转换处理图像时我们经常需要复制或转换它。# 复制一张图像深拷贝操作副本不会影响原图 img_copy img_from_cam.copy() # 转换图像模式 img_gray img_from_cam.to_grayscale() # RGB转灰度 img_hsv img_from_cam.to_hsv() # RGB转HSV img_binary img_gray.binary([(100,255)]) # 灰度转二值to_grayscale(),to_hsv()这些转换方法会生成一个新的Image对象。这意味着内存消耗会增加。在循环中频繁进行图像转换而不释放旧对象是导致内存不足、程序崩溃的常见原因。3. 图像数据的访问与修改有时我们需要直接操作像素数据。# 方法1使用get_pixel和set_pixel速度较慢适合零星操作 color img_from_cam.get_pixel(100, 50) img_from_cam.set_pixel(100, 50, (255, 0, 0)) # 设置为红色 # 方法2使用bytearray访问底层数据速度快适合批量操作 # 首先获取图像数据的bytearray对象 data img_from_cam.bytearray() # data就是一个字节数组其排列方式取决于图像模式RGB565/GRAYSCALE等 # 例如对于RGB565的QVGA图data的长度是 320*240*2 153600字节 # 直接修改data中的字节就会改变图像内容直接操作bytearray是高级用法要求你清楚知道图像数据的存储格式如RGB565的字节序。绝大多数时候使用image模块提供的高级函数如draw_rectangle,find_blobs更为安全和高效。4. 图像的存储与显示# 将图像保存到SD卡 img_from_cam.save(/sd/snapshot.jpg, quality95) # 保存为JPEG # 在LCD屏幕上显示图像 lcd.display(img_from_cam)保存图片会占用Flash写入时间和空间。在调试时可以定期保存图片到SD卡用于分析算法问题。lcd.display()是将图像数据刷到屏幕其性能会影响视觉上的流畅度。6. 实战从理论到代码——一个颜色追踪例子现在我们把所有知识点串起来完成一个简单的红色物体追踪程序。这个例子会清晰地展示图像采集、转换、处理、显示的完整流程。import sensor, image, time, lcd # 1. 初始化硬件 sensor.reset() # 复位摄像头 sensor.set_pixformat(sensor.RGB565) # 设置像素格式为RGB565 sensor.set_framesize(sensor.QVGA) # 设置分辨率为QVGA (320x240) sensor.skip_frames(time 2000) # 等待摄像头设置生效 lcd.init() # 初始化LCD屏幕 # 定义红色在HSV空间的阈值范围。 # 红色在色相环上位于0度和360度附近所以需要两个区间 red_thresholds [ (0, 70, 50, 10, 255, 255), # 区间1: 色相0-10 饱和度70明度50 (160, 70, 50, 180, 255, 255) # 区间2: 色相160-180 ] while(True): # 2. 图像采集 img sensor.snapshot() # 从摄像头获取一帧RGB565图像 # 3. 图像转换与处理 # 将RGB图像转换到HSV色彩空间便于颜色过滤 img_hsv img.to_hsv() # 使用binary()方法根据阈值找出红色区域生成一张二值图 # 在二值图中白色(255)代表符合阈值的像素黑色(0)代表不符合 img_binary img_hsv.binary(red_thresholds) # 4. 特征提取 # 在二值图中寻找所有连通域色块 blobs img_binary.find_blobs([(100, 255)], area_threshold100, mergeTrue) # 参数解释 # [(100,255)]: 在二值图中找白色区域值在100-255之间 # area_threshold100: 忽略面积小于100像素的噪点 # mergeTrue: 合并相邻的色块 # 5. 分析与绘制 max_blob None max_area 0 # 找出面积最大的色块认为它是我们要追踪的主要红色物体 for blob in blobs: if blob.area() max_area: max_area blob.area() max_blob blob # 如果找到了最大色块就在原图上把它框出来并显示其中心坐标 if max_blob: # 在原图img上绘制矩形框 img.draw_rectangle(max_blob.rect(), color(0, 255, 0), thickness2) # 绘制中心点 img.draw_cross(max_blob.cx(), max_blob.cy(), color(0, 255, 0), size5) # 打印中心坐标到串口 print(Red Object Center: ({}, {}).format(max_blob.cx(), max_blob.cy())) # 6. 图像显示 lcd.display(img) # 将处理后的原图显示到LCD # 如果也想看二值图效果可以取消下面这行注释 # lcd.display(img_binary) time.sleep_ms(10) # 短暂延时控制循环频率代码逐段解析与避坑指南初始化部分sensor.skip_frames(2000)非常必要。摄像头启动后自动增益、白平衡等需要时间稳定直接使用前几帧图像会导致颜色和亮度异常。阈值设定red_thresholds的设定是颜色识别的核心难点。不要想当然。最好用MaixPy IDE中的“阈值编辑器”工具。先拍摄一张包含目标物体的图片在IDE中打开工具拖动HSV各分量的滑块观察哪些值能最精准地选中目标物体同时排除背景干扰。饱和度(S)和明度(V)的下限可以过滤掉暗部和灰白的部分。内存管理循环中img_hsv img.to_hsv()和img_binary img_hsv.binary(...)会在每次循环创建新的图像对象。在K210上这可能导致内存碎片和耗尽。对于固定流程可以考虑复用图像对象如果API支持或者定期进行垃圾回收import gc; gc.collect()。find_blobs参数area_threshold面积阈值是滤除噪点的关键。太小会引入大量噪声太大会漏掉小目标。需要根据实际物体在画面中的像素大小来调整。mergeTrue可以帮助将同一个物体上因为反光或阴影造成的断裂部分合并成一个整体。显示选择lcd.display(img)显示的是画了框的原图直观。调试时可以交替显示img_binary来观察你的阈值分割效果是否干净这是排查识别问题的利器。7. 常见问题排查与性能优化掌握了基础遇到问题就不再害怕。以下是几个新手高频问题及其解决思路。问题1颜色识别不稳定时有时无。检查光照这是首要因素。HSV模型对光照变化有一定抵抗力但极端光照强反光、阴影、色温变化仍会影响色相。确保环境光源稳定或考虑使用补光灯。调整阈值使用“阈值编辑器”重新校准。注意不同距离下物体在画面中的饱和度和明度会变化阈值范围可以适当放宽。查看二值图在LCD上显示img_binary观察目标物体是否被完整、连续地提取为白色区域。如果白色区域破碎尝试调整merge参数或对原图进行滤波处理。# 在转换HSV前先对原图进行中值滤波消除细小噪点 img.filter(image.MEDIAN_FILTER, size3)问题2程序跑一会儿就崩溃报内存错误。确认内存杀手循环中不断创建新的Image对象而不释放是主因。确保没有在循环内无节制地to_hsv(),to_grayscale(),copy()。必要时手动删除变量引用del img_hsv并触发垃圾回收gc.collect()。降低分辨率如果不需要VGA(640x480)的细节果断降到QVGA(320x240)。图像像素数减少75%内存压力和计算量大幅下降。使用poolK210的image模块支持pool可以复用内存块适合固定大小的图像处理流水线。问题3处理速度慢画面卡顿。测量帧率在循环开始和结束用time.ticks_ms()计算耗时。start time.ticks_ms() # ...你的处理代码... elapsed time.ticks_diff(time.ticks_ms(), start) print(Frame process time: {} ms.format(elapsed))优化算法find_blobs是比较耗时的操作尤其是画面中白色区域多时。可以通过roi参数限制只在图像特定区域寻找。# 只在中部100x100的区域内找色块 blobs img_binary.find_blobs(..., roi(110,70,100,100))减少操作如果不是每帧都需要可以降低处理频率比如每3帧处理1帧。问题4LCD显示颜色不对。格式匹配确保lcd.init()的初始化格式与sensor.set_pixformat()及Image对象的模式匹配。通常保持默认的RGB565即可。数据搬运lcd.display()本身需要时间。如果处理一帧的时间已经接近或超过屏幕刷新间隔就会感到卡顿。优化处理代码是根本。理解这些基础知识并配合实际的代码调试你就能从“只会跑例程”的新手成长为能解决实际视觉问题的开发者。图像处理没有黑魔法一切效果都源于对数据像素的精准操控。下次当你调阈值、找轮廓时心里想着的应该是HSV空间里那个三维立方体以及二值图里那些白色的连通域你的调试就会更有方向。