记录学习k230的过程

25.11.11 准备工作、学习摄像模块

准备工作

  1. 固件烧录
  2. 例程复现
  3. 找到了ocr(文字识别)现成例程
    小声:结合多平台教程文档,如01studio或嘉楠等等,这个没有的例程,另一个说不定有呢

摄像模块

  1. k230摄像头架构
  • 板子最多搭载三个摄像头
  • 每个摄像头可以接入三个不同的处理模块(对输入图像进行加工处理)
  • 每个模块(camera_device)有三个输出通道,可以多输出并行
  1. 摄像头模块编程
  • sensor基础语法

    • 创建处理模块
    1
    2
    
        from media.sensor import *
        sensor = Sensor(id ,width ,height ,fps) # 实例化,对应架构中的处理模块
    
    1. id:摄像头id,默认为2
    2. width ,height ,fps:最大输出图像参数
    • 设置图像输出大小和位置
    1
    2
    3
    4
    
        sensor.reset() # 初始化sensor对象及传感器
    
        sensor.set_framesize(chn=CAM_CHN_ID_0, width=640, height=480)
        sensor.set_framesize(chn=CAM_CHN_ID_3, framesize = Sensor.VGA)
    
    1. framesize参数对应width、height,表示输出图像分辨率,二者作用相同。
    2. framesize = Sensor.VGA即表示640*480分辨率,除此之外,还有Sensor.HD等表示分辨率的代号,这些代号统称为图像帧尺寸
    3. chn:channel_number,表示输出通道,即架构中每个模块的三个输出通道
    • 设置图像怎么输出、输出位置
      sensor.set_pixformat(pix_format, chn=CAM_CHN_ID_0)
    
    1. pix_format:输出图像的像素格式,即每个像素在计算机中如何存储,也就是RGB三个通道数据用多少位存储。
      常用有:
      RGB565:R for 5 bits ; G for 6 bits ; B for 5 bits
      RGB888: R G B for 8 bits respectively
    • 水平与竖直反转
      sensor.set_hmirror(True) # 水平
      sensor.set_vflip(False) # 竖直
    
    • 启动、关闭摄像头
      sensor.run()
      sensor.stop()
    

    多个摄像头只用启动一次,但要分别关闭

    • 指定通道截一帧图片
      sensor.snapshot(chn=CAM_CHN_ID_0)
    

    默认为0设备0通道


25.11.13 GPIO

GPIO

  • 原理明确

    • 硬件之间的通信必须经过通信协议,常用的通信协议有IIC\SPI\UART\CAN\HTTP等等
    • 这些“通信协议”是一种规则,在硬件上的体现为实现信息收发的逻辑电路,可以作为模块嵌入到MPU芯片里。

    当然,不是所有的协议都软硬兼修,HTTP协议就是纯软件协议

    • 通过设置外设的状态,及外设对应的寄存器的值,实现与外界的交互。

    例如将某引脚(GPIOA_1)设置为输入模式,然后访问指定端口 GPIOx 的 输入数据寄存器(IDR)的值,看引脚的值为高电平还是低电平,外界传给中央的信号(斜体的部分由GPIO_ReadInputDataBit函数完成)

  • 名词解释

  1. P-mos N-mos:三极管,作用相当于受电压控制的开关,它俩区别是,P为低于阈值导通,N为超出阈值导通
  2. 串口:特指UART协议的硬件电路部分,信息流向:
    MPU ——> 引脚 ——> 串口(片内外设的一种) ——> 外接设备(片外外设) (一个串口可能会用到许多引脚)
  3. 外设:片内外设:通信协议等硬件的逻辑电路模块,本质上是把数据、奇偶校验、等等一系列的流程集成到一个电路模块上,可以装在芯片内部;片外外设:与芯片连接的模块,在芯片外面
  • GPIO干嘛用的?
    四大功能:输入、输出、模拟、复用

    • 输入
    1. 片内外设向MPU输入
    2. 分类
      • 下拉输入:无外部输入信号时,MPU读到低电平,只有外部输入为高电平时,MPU才能读到高电平
      • 上拉输入:无外部输入信号时,MPU读到高电平,与上面同理
      • 悬空输入:引脚的电平状态完全由外部输入决定
      • 模拟输入:能接受模拟信号,通过ADC(模数转换器)转为数字信号,传递给MPU
    • 输出
    1. MPU向片内外设输出
    2. 分类
      • 推挽输出:P-mos、N-mos均工作(不同时工作),可以主动输出高、低电平
      • 开漏输出:只有N-mos工作,只能主动输出低电平,输出高电平要靠上拉电阻拉上去
      # micropython
      from machine import Pin
      pin = Pin(index, mode, pull=Pin.PULL_NONE, drive=7)
    

    machine.Pin :控制引脚的输入输出状态
    index : 引脚编号
    mode:

    • Pin.IN
    • Pin.OUT

    pull:

    • PULL_UP
    • PULL_DOWN
    • PULL_NONE(默认)

    drive: io驱动能力,默认为7

    • 复用
    1. 此时GPIO像一个容器,可以对应连接到MPU内部的IIC、SPI、UART等协议的硬件模块,所谓通信协议的硬件模块,本质上是把数据、奇偶校验、等等一系列的流程集成到一个电路模块上,也就是通信协议的物理层面实现,随后该引脚与片外外设通过对应的协议通信。
    2. 一个GPIO不能同时连接多个协议模块,故要通过编程选择。
    3. **k230芯片共有63个引脚,开发版上可复用引脚有40个,编写代码时用到的pin_index是芯片上引脚的序号,芯片上引脚与开发板上引脚的GPIO序号通常是一致的,例如,fpioa.set_founction(3, FPIOA.UART0_TXD)这里的3在芯片上为第三个引脚,在开发板上为GPIO3。如果某个芯片引脚的序号在GPIO上没有对应的序号,则说明这个引脚可能被其他外设占用了(比如串口的GH1.25-4P座子)
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
        from machine import FPIOA
        fpioa = FPIOA() # 实例化
    
        # 查看引脚信息
        fpioa.help() # 所有引脚的详细信息
        fpioa.help(1) # 引脚1的详细信息
        fpioa.help(1,func=False) # 同上
        fpioa.help(1,func=True) # 功能1的详细信息(功能编码在 [这里](https://wiki.lckfb.com/zh-hans/lushan-pi-k230/basic/gpio-fpioa.html)
    
    
        # 复用
        fpioa.set_founction(3, FPIOA.UART0_TXD) # 把第3个引脚复用为串口0的TX脚
        fpioa.get_pin_num(FPIOA.UART0_TXD) # 哪些引脚被配置为UART0_TXD
        fpioa.get_pin_func(63) # 查看63号引脚的被配置为什么功能
    
    • 模拟
    1. 通过输入、输出模式配置、电平高低变化模拟通信协议,即把硬件外设的功能通过软件模拟了一遍

    k230的GPIO引脚没有adc,不能模拟

  • 总结

  1. GPIO四大功能:输入、输出、模拟、复用
  2. GPIO可编程,体现为其模拟功能。

25.11.15.

  • GPIO各功能代码示例

25.11.19. UART发送ocr结果

原理明确

  • 不同level的电子设备是不同的圈子,圈子间遵循的通信协议不一样。电脑间遵守USB协议,而嵌入式中一般遵循UART/TTL协议。

  • 信息的本质是电压变化,TTL(Transistor-Transistor Logic)将电压变化转变为一串0-1信号,而UART(异步串行通信协议)将这串0-1信号按规则进行组织,譬如组织为

    起始位–数据位–校验位–终止位

  • 随后将组织好的信号传输出去,对面再按这个规则解密就好啦!当然,这是同一圈子内通信

  • 不同圈子之间通信就要涉及硬件电路了,结合之前片内外设的定义,用硬件电路实现通信并不难理解。比如电脑和单片机间的通信就要通过usb转ttl模块来完成。

k230端发送数据代码

点击展开查看 OCR 源码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381

    # ocr源码
    from libs.PipeLine import PipeLine, ScopedTiming
    from libs.AIBase import AIBase
    from libs.AI2D import Ai2d
    import os
    import ujson
    from media.media import *
    from media.sensor import *
    from time import *
    import nncase_runtime as nn
    import ulab.numpy as np
    import time
    import image
    import aicube
    import random
    import gc
    import sys
    from machine import UART,FPIOA

    # 自定义OCR检测类
    class OCRDetectionApp(AIBase):
        def __init__(self,
                    kmodel_path,
                    model_input_size,
                    mask_threshold=0.5,
                    box_threshold=0.2,
                    rgb888p_size=[224,224],
                    display_size=[1920,1080],
                    debug_mode=0):
            super().__init__(kmodel_path,model_input_size,rgb888p_size,debug_mode)
            self.kmodel_path=kmodel_path
            # 模型输入分辨率
            self.model_input_size=model_input_size
            # 分类阈值
            self.mask_threshold=mask_threshold
            self.box_threshold=box_threshold
            # sensor给到AI的图像分辨率
            self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
            # 显示分辨率
            self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
            self.debug_mode=debug_mode
            # Ai2d实例,用于实现模型预处理
            self.ai2d=Ai2d(debug_mode)
            # 设置Ai2d的输入输出格式和类型
            self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)

        # 配置预处理操作,这里使用了pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/libs/AI2D.py查看
        def config_preprocess(self,input_image_size=None):
            with ScopedTiming("set preprocess config",self.debug_mode > 0):
                # 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,您可以通过设置input_image_size自行修改输入尺寸
                ai2d_input_size=input_image_size if input_image_size else self.rgb888p_size
                top,bottom,left,right=self.get_padding_param()
                self.ai2d.pad([0,0,0,0,top,bottom,left,right], 0, [0,0,0])
                self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
                self.ai2d.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])

        # 自定义当前任务的后处理
        def postprocess(self,results):
            with ScopedTiming("postprocess",self.debug_mode > 0):
                # chw2hwc
                hwc_array=self.chw2hwc(self.cur_img)
                # 这里使用了aicube封装的接口ocr_post_process做后处理,返回的det_boxes结构为[[crop_array_nhwc,[p1_x,p1_y,p2_x,p2_y,p3_x,p3_y,p4_x,p4_y]],...]
                det_boxes = aicube.ocr_post_process(results[0][:,:,:,0].reshape(-1), hwc_array.reshape(-1),self.model_input_size,self.rgb888p_size, self.mask_threshold, self.box_threshold)
                return det_boxes

        # 计算padding参数,在config_preprocess中使用
        def get_padding_param(self):
            # 右padding或下padding
            dst_w = self.model_input_size[0]
            dst_h = self.model_input_size[1]
            input_width = self.rgb888p_size[0]
            input_high = self.rgb888p_size[1]
            ratio_w = dst_w / input_width
            ratio_h = dst_h / input_high
            if ratio_w < ratio_h:
                ratio = ratio_w
            else:
                ratio = ratio_h
            new_w = (int)(ratio * input_width)
            new_h = (int)(ratio * input_high)
            dw = (dst_w - new_w) / 2
            dh = (dst_h - new_h) / 2
            top = (int)(round(0))
            bottom = (int)(round(dh * 2 + 0.1))
            left = (int)(round(0))
            right = (int)(round(dw * 2 - 0.1))
            return  top, bottom, left, right

        # chw2hwc
        def chw2hwc(self,features):
            ori_shape = (features.shape[0], features.shape[1], features.shape[2])
            c_hw_ = features.reshape((ori_shape[0], ori_shape[1] * ori_shape[2]))
            hw_c_ = c_hw_.transpose()
            new_array = hw_c_.copy()
            hwc_array = new_array.reshape((ori_shape[1], ori_shape[2], ori_shape[0]))
            del c_hw_
            del hw_c_
            del new_array
            return hwc_array

    # 自定义OCR识别任务类
    class OCRRecognitionApp(AIBase):
        def __init__(self,kmodel_path,model_input_size,dict_path,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0):
            super().__init__(kmodel_path,model_input_size,rgb888p_size,debug_mode)
            # kmodel路径
            self.kmodel_path=kmodel_path
            # 识别模型输入分辨率
            self.model_input_size=model_input_size
            self.dict_path=dict_path
            # sensor给到AI的图像分辨率,宽16字节对齐
            self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
            # 视频输出VO分辨率,宽16字节对齐
            self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
            # debug模式
            self.debug_mode=debug_mode
            self.dict_word=None
            # 读取OCR的字典
            self.read_dict()
            self.ai2d=Ai2d(debug_mode)
            self.ai2d.set_ai2d_dtype(nn.ai2d_format.RGB_packed,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)

        # 配置预处理操作,这里使用了pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/libs/AI2D.py查看
        def config_preprocess(self,input_image_size=None,input_np=None):
            with ScopedTiming("set preprocess config",self.debug_mode > 0):
                ai2d_input_size=input_image_size if input_image_size else self.rgb888p_size
                top,bottom,left,right=self.get_padding_param(ai2d_input_size,self.model_input_size)
                self.ai2d.pad([0,0,0,0,top,bottom,left,right], 0, [0,0,0])
                self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
                # 如果传入input_np,输入shape为input_np的shape,如果不传入,输入shape为[1,3,ai2d_input_size[1],ai2d_input_size[0]]
                self.ai2d.build([input_np.shape[0],input_np.shape[1],input_np.shape[2],input_np.shape[3]],[1,3,self.model_input_size[1],self.model_input_size[0]])

        # 自定义后处理,results是模型输出的array列表
        def postprocess(self,results):
            with ScopedTiming("postprocess",self.debug_mode > 0):
                preds = np.argmax(results[0], axis=2).reshape((-1))
                output_txt = ""
                for i in range(len(preds)):
                    # 当前识别字符不是字典的最后一个字符并且和前一个字符不重复(去重),加入识别结果字符串
                    if preds[i] != (len(self.dict_word) - 1) and (not (i > 0 and preds[i - 1] == preds[i])):
                        output_txt = output_txt + self.dict_word[preds[i]]
                return output_txt

        # 计算padding参数
        def get_padding_param(self,src_size,dst_size):
            # 右padding或下padding
            dst_w = dst_size[0]
            dst_h = dst_size[1]
            input_width = src_size[0]
            input_high = src_size[1]
            ratio_w = dst_w / input_width
            ratio_h = dst_h / input_high
            if ratio_w < ratio_h:
                ratio = ratio_w
            else:
                ratio = ratio_h
            new_w = (int)(ratio * input_width)
            new_h = (int)(ratio * input_high)
            dw = (dst_w - new_w) / 2
            dh = (dst_h - new_h) / 2
            top = (int)(round(0))
            bottom = (int)(round(dh * 2 + 0.1))
            left = (int)(round(0))
            right = (int)(round(dw * 2 - 0.1))
            return  top, bottom, left, right

        def read_dict(self):
            if self.dict_path!="":
                with open(dict_path, 'r') as file:
                    line_one = file.read(100000)
                    line_list = line_one.split("\r\n")
                self.dict_word = {num: char.replace("\r", "").replace("\n", "") for num, char in enumerate(line_list)}


    class OCRDetRec:
        def __init__(self,
                    ocr_det_kmodel,
                    ocr_rec_kmodel,
                    det_input_size,
                    rec_input_size,
                    dict_path,
                    mask_threshold=0.25,
                    box_threshold=0.3,
                    rgb888p_size=[1920,1080],
                    display_size=[1920,1080],
                    debug_mode=0):
            # OCR检测模型路径
            self.ocr_det_kmodel=ocr_det_kmodel
            # OCR识别模型路径
            self.ocr_rec_kmodel=ocr_rec_kmodel
            # OCR检测模型输入分辨率
            self.det_input_size=det_input_size
            # OCR识别模型输入分辨率
            self.rec_input_size=rec_input_size
            # 字典路径
            self.dict_path=dict_path
            # 置信度阈值
            self.mask_threshold=mask_threshold
            # nms阈值
            self.box_threshold=box_threshold
            # sensor给到AI的图像分辨率,宽16字节对齐
            self.rgb888p_size=[ALIGN_UP(rgb888p_size[0],16),rgb888p_size[1]]
            # 视频输出VO分辨率,宽16字节对齐
            self.display_size=[ALIGN_UP(display_size[0],16),display_size[1]]
            # debug_mode模式
            self.debug_mode=debug_mode
            
            
            self.ocr_det=OCRDetectionApp(self.ocr_det_kmodel,
                                        model_input_size=self.det_input_size,mask_threshold=self.mask_threshold,box_threshold=self.box_threshold,rgb888p_size=self.rgb888p_size,display_size=self.display_size,
                                        debug_mode=0)
            
            self.ocr_rec=OCRRecognitionApp(self.ocr_rec_kmodel,
                                        model_input_size=self.rec_input_size,dict_path=self.dict_path,
                                        rgb888p_size=self.rgb888p_size,display_size=self.display_size)
            self.ocr_det.config_preprocess()

        # run函数
        def run(self,input_np):
            # 先进行OCR检测
            det_res=self.ocr_det.run(input_np)
            boxes=[]
            ocr_res=[]
            for det in det_res:
                # 对得到的每个检测框执行OCR识别
                self.ocr_rec.config_preprocess(input_image_size=[det[0].shape[2],det[0].shape[1]],input_np=det[0])
                ocr_str=self.ocr_rec.run(det[0])
                ocr_res.append(ocr_str)
                boxes.append(det[1])
                gc.collect()
            return boxes,ocr_res

        # 绘制OCR检测识别效果
        def draw_result(self,pl,det_res,rec_res):
            pl.osd_img.clear()
            if det_res:
                # 循环绘制所有检测到的框
                for j in range(len(det_res)):
                    # 将原图的坐标点转换成显示的坐标点,循环绘制四条直线,得到一个矩形框
                    for i in range(4):
                        x1 = det_res[j][(i * 2)] / self.rgb888p_size[0] * self.display_size[0]
                        y1 = det_res[j][(i * 2 + 1)] / self.rgb888p_size[1] * self.display_size[1]
                        x2 = det_res[j][((i + 1) * 2) % 8] / self.rgb888p_size[0] * self.display_size[0]
                        y2 = det_res[j][((i + 1) * 2 + 1) % 8] / self.rgb888p_size[1] * self.display_size[1]
                        pl.osd_img.draw_line((int(x1), int(y1), int(x2), int(y2)), color=(255, 0, 0, 255),thickness=5)

                    # 在检测框位置显示识别的文字
                    pl.osd_img.draw_string_advanced(int(x1),int(y1),32,rec_res[j],color=(0,0,255))


    if __name__=="__main__":


        UART_TX_PIN = 5
        UART_RX_PIN = 6
        UART_ID = 2
        BAUDRATE = 115200

        # 初始化fpioa和uart
        fpioa = FPIOA()
        fpioa.set_function(UART_TX_PIN, FPIOA.UART2_TXD)
        fpioa.set_function(UART_RX_PIN, FPIOA.UART2_RXD)
        uart = UART(UART_ID , BAUDRATE)

        print(fpioa.help(12))
        print(fpioa.help(13))


        # 显示模式,可以选择"hdmi"、"lcd3_5"(3.5寸mipi屏)和"lcd2_4"(2.4寸mipi屏)
        # 配置显示模式
        display="lcd3_5"

        if display=="hdmi":
            display_mode='hdmi'
            display_size=[1920,1080]

        elif display=="lcd3_5":
            display_mode= 'st7701'
            display_size=[800,480]

        elif display=="lcd2_4":
            display_mode= 'st7701'
            display_size=[640,480]

        rgb888p_size=[640,360] #特殊尺寸定义 
        
        

        # OCR检测模型路径
        ocr_det_kmodel_path="/sdcard/examples/kmodel/ocr_det_int16.kmodel"
        # OCR识别模型路径
        ocr_rec_kmodel_path="/sdcard/examples/kmodel/ocr_rec_int16.kmodel"
        # 其他参数
        dict_path="/sdcard/examples/utils/dict.txt"

        # 系统参数 不可改
        ocr_det_input_size=[640,640]
        ocr_rec_input_size=[512,32]
        
        # 可改
        mask_threshold=0.25
        box_threshold=0.3

        # 初始化PipeLine,只关注传给AI的图像分辨率,显示的分辨率
        pl=PipeLine(rgb888p_size=rgb888p_size,
                    display_size=display_size,
                    display_mode=display_mode)
        
        
        if display =="lcd2_4":
            pl.create(Sensor(width=1280, height=960))  # 创建PipeLine实例,画面4:3

        else:
            pl.create(Sensor(width=1920, height=1080))  # 创建PipeLine实例
            
            
        ocr=OCRDetRec(ocr_det_kmodel_path,
                    ocr_rec_kmodel_path,
                    det_input_size=ocr_det_input_size,rec_input_size=ocr_rec_input_size,
                    dict_path=dict_path,
                    mask_threshold=mask_threshold,
                    box_threshold=box_threshold,
                    rgb888p_size=rgb888p_size,
                    display_size=display_size)

        clock = time.clock()


        # 发送数据的细节处理函数
        def send_ocr_data(text, x, y):
            try:
                # 数据清洗:去掉文字里可能存在的逗号或换行,防止破坏协议
                clean_text = text.replace(',', '.').replace('\n', '').strip() # 针对字符串类型数据

                # 协议格式: @文字,X,Y#
                packet = "@{},{},{}#".format(clean_text, x, y)

                # 编码并发送
                uart.write(packet.encode('utf-8'))
                print(f"[UART发送] {packet}") # 调试用
            except Exception as e:
                print(f"发送失败: {e}")

        # 发送时间
        last_send_time = 0
        SEND_INTERVAL_MS = 100 # 发送间隔100ms 每秒最多10张

        while True:

            # clock.tick()
            os.exitpoint()  # 防卡死机制

            img=pl.get_frame()                  # 获取当前帧
            boxes,rec_res=ocr.run(img)        # 推理当前帧
            if boxes:

                ocr.draw_result(pl,boxes,rec_res) # 绘制当前帧推理结果
                print(boxes,rec_res)              # 打印结果
                pl.show_image()                     # 展示当前帧推理结果
                current_time = time.ticks_ms()
                can_send = (time.ticks_diff(current_time, last_send_time) >= SEND_INTERVAL_MS)

                if can_send:
                        # target = det_res[0]  # 只发送第一个检测框
                        raw_text = rec_res[0] # 文本
                        rect = boxes[0] # 检测框四个角的坐标
                        # 计算检测框中心点坐标
                        center_x = (rect[0] + rect[2] + rect[4] + rect[6]) / 4
                        center_y = (rect[1] + rect[3] + rect[5] + rect[7]) / 4

                        # 2. 新增:转换为 STM32 的 240x320 坐标
                        # 原图宽 640 -> 目标宽 240
                        scaled_x = int((center_x / 640) * 1920)
                        # 原图高 360 -> 目标高 320 
                        scaled_y = int((center_y / 360) * 1080)
                        # 发送检测框中心点坐标
                        send_ocr_data(raw_text, scaled_x, scaled_y)
                        last_send_time = current_time


            gc.collect()
  • 更改说明

    1. 代码在0-1studio官方ocr例程上改动,更改了主程序部分,添加工具函数send_ocr_data
    2. 串口定义
    3. 发送逻辑(主函数):

      捕捉当前帧 ——> 推理识别 ——> 满足发送条件(时间间隔)——> 发送

    4. 分辨率变化流:

      Sensor input (1920*1080) —> Pipeline (rgb888p:640*360) —> orc_dec input(640*640) —> orc_rec input(512*32) —> output(640*360) —> send (1920*1080)

    5. stm32接收到一个坐标和对应的字符串,它需要确定这个坐标数字在实际视野中的位置,也就是比例,故需要事先告诉stm32视野的范围为多少,也就是send时发送的坐标所使用的坐标系(也就是分辨率),它可以为任意(即通过主函数中转换坐标部分实现),声明清楚接受到的数字的“地图边界”是几,即可计算比例与转动角度。
  • 代码心得

    • 找到代码逻辑:

      • 从主函数开始看,搞懂每一行的作用,并链接到哪个模块实现了这个功能
      • 对各个模块,也就是各个类、实例、方法总体浏览一遍,搞懂每个模块的作用,一句话写注释
      • 参数传递(重要!) 模块之间层层嵌套,要理清爽数据流(输入数据经过哪些模块、哪些处理,变成怎样的输出)。分清形参和实参,别被名字骗了
    • 有一些逻辑是打包封装好的,譬如Pipeline中的pl.create(),这种背后的工作流就会藏得很深,慢慢修炼吧~

26.1.28. 基于颜色色块识别实现地图识别

1. 语法解释

img = sensor.snapshot()
blobs = img.find_blobs([black_threshold], pixels_threshold=500, merge=True, margin=15)
  • img.find_blobs:寻找色块的函数

    • 参数
    • [black_threshold]:列表,其中每个元素是一个元组,例如black_threshold = (0, 20, -10, 10, -10, 10),用于定义目标颜色的检测阈值,分别描述这个颜色对应的LAB的最大最小值
      可以包含多个颜色,也就是多个元组,寻找时取并集,并自动对每种颜色二进制编码,例如:
      • 匹配了第一个阈值的色块,code = 1 (二进制 $2^0$)。
      • 匹配了第二个阈值的色块,code = 2 (二进制 $2^1$)。
      • 匹配了第三个阈值的色块,code = 4 (二进制 $2^2$)。
        应用时使用CanMV IDE的阈值编辑器确定阈值,达到:目标颜色为白色,其余全部为黑色。滑动阈值调整时会发现,对于六个中的每一个参数,满足目标黑白二值状态的值是一个范围,注意不要取这个范围的端点,因为端点值处于识别变化的临界,易受到外界的干扰,鲁棒性低。要取范围中间的值。
    • pixel_threshold:像素阈值,检测到的总像素小于这个值的色块将会被过滤
    • merge:合并色块,合并所有没有被过滤的色块。margin参数存在时,还要求这两个色块之间最靠近的像素点距离 $\le$ margin此时函数返回的对象就是合并后的大色块
      若一个色块同时包含了两种颜色(比如你把红苹果和绿叶子合并成一个大色块),这个大色块的code是其中颜色的code之和,它的 code 就会变成 $1 + 2 = 3$。你可以用blob.code()查看

    常用的参数就是这些,下面是其他参数 para

    • 输出 函数返回一个list:[(x, y, w, h, pixels, cx, cy, rotation, code, count, area, density),(),()...],这个list的每个元素是一个对象,每个对象有这些属性:
      • rect:blob.rect() 描述颜色块的(x,y,w,h):左上角坐标、width、hight
      • pixels:blob.pixels()blob[4] 色块中的像素数
      • cy cx:x和y的中心点
      • rotation:目标在画面中是横着的( $0$ 或 $\pi$)、竖着的($\pi/2$ 约 1.57)还是斜着的。
      • code:颜色编码
      • count:合并为该色块的多个色块的数量。只有在调用 image.find_blobs 时设置 merge=True,此数字才会大于1,注意和颜色无关
      • area:返回色块周围边框的面积(计算方式为 w * h)
      • density:实心色块检测时,密度越小,检测准确度越小 $$Density = \frac{Pixels(色块实际像素点数)}{Width \times Height(外接矩形的面积)}$$

2. 举例:定位水果位置

map

点击展开查看源码
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
    import sys
    import time, os, sys, gc
    from media.sensor import *
    from media.display import *
    from machine import UART,FPIOA

    # 1. 定义颜色阈值 (LAB 空间)
    # 黑色网格阈值 (需要根据现场光照微调)
    black_threshold = (0, 20, -10, 10, -10, 10)
    # 红色苹果阈值
    #apple_threshold = (30, 70, 40, 80, 10, 60)
    #apple_threshold = (26, 100, 7, 109, 127, -10)
    apple_threshold = (10, 80, 14, 103, 89, -32)

    DETECT_WIDTH = ALIGN_UP(320, 16)
    DETECT_HEIGHT = 240

    sensor = None
    uart = 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 check_roi(roi, img_w=320, img_h=240):
        rx, ry, rw, rh = roi
        # 确保起点不小于 0
        rx = max(0, rx)
        ry = max(0, ry)
        # 确保宽高不为负数或 0
        rw = max(1, rw)
        rh = max(1, rh)
        # 确保终点不超出图像边界
        if rx + rw > img_w: rw = img_w - rx
        if ry + rh > img_h: rh = img_h - ry
        return (rx, ry, rw, rh)

    def find_apples_in_grid():


            while True:
                os.exitpoint()
                img = sensor.snapshot()

                # --- 步骤 A: 寻找黑色网格 (井字线) ---
                # merge=True 将被网格切断的黑色线条合并成一个大的对象
                # margin=15 确保间距小于 15 像素的黑色部分被视为一体
                blobs = img.find_blobs([black_threshold], pixels_threshold=500, merge=True, margin=15)

                if blobs: # blobs是一个列表,每个元素是一个对象,具有rect、pixels等属性
    #                print("type of blobs",type(blobs)) # list 列表不同位置是对象不同的属性值
                    print("blobs",blobs)

                    # 寻找面积最大的黑色物体,通常就是整个网格阵列
                    big_blob = max(blobs, key=lambda x: x.pixels())

                    print("-----------")
                    print("merge number ares",big_blob.count())
                    print("density",big_blob.density())
                    print("rotation",big_blob.rotation())
                    print("-----------")

                    # 绘制网格的外接矩形(绿色)
                    rect = big_blob.rect() # 元组 描述颜色块的(x,y,w,h):左上角坐标、width、hight
                    img.draw_rectangle(rect, color=(0, 255, 0), thickness=2)

                    # --- 步骤 B: 逻辑切片 4x4 ---
                    x, y, w, h = rect
                    w_step = w // 4
                    h_step = h // 4

                    offset_w = w_step // 5
                    offset_h = h_step // 5

                    results = [] # 存储发现水果的坐标索引

                    for r in range(4): # 行号 0, 1, 2, 3
                        for c in range(4): # 列号 0, 1, 2, 3
                            # 计算当前小格子的感兴趣区域 (ROI)
                            # 稍微缩小 ROI (加上 5 像素 offset),避开边缘黑线的干扰
                            # roi = (x + c * w_step + 5, y + r * h_step + 5, w_step - 10, h_step - 10)
                            roi = (x + c * w_step + offset_w, y + r * h_step + offset_h, w_step - 2*offset_w, h_step - 2*offset_h)
                            roi = check_roi(roi)
                            # 在这个 ROI 里寻找红色苹果
                            apple_blobs = img.find_blobs([apple_threshold], roi=roi, pixels_threshold=200)
                            for j in apple_blobs:
                                print("apple density",j.density())
                                if j.density()>=0.7:
                                    # 标记发现水果的格子(红色)
                                    img.draw_rectangle(roi, color=(255, 0, 0), thickness=2)
                                    # 记录坐标 (行列索引从 1 开始)
                                    results.append((r + 1, c + 1))

                    # --- 步骤 C: 打印并发送数据 ---
                    if results:
                        if big_blob.rotation()>=1.0 and big_blob.rotation()<=1.8:
                            print("识别结果 (行, 列):", results)
                            # 此处可添加串口代码:
                            uart.write(str(results) + '\n')

                # 显示处理后的图像
                Display.show_image(img)

                gc.collect()

    # 执行识别

    def main():
        os.exitpoint(os.EXITPOINT_ENABLE)
        camera_is_init = False
        UART_TX_PIN = 5
        UART_RX_PIN = 6
        UART_ID = 2
        BAUDRATE = 115200

        # 初始化fpioa和uart
        global uart
        fpioa = FPIOA()
        fpioa.set_function(UART_TX_PIN, FPIOA.UART2_TXD)
        fpioa.set_function(UART_RX_PIN, FPIOA.UART2_RXD)
        uart = UART(UART_ID , BAUDRATE)

        print("camera init")
        camera_init()
        print("camera capture")
        find_apples_in_grid()


    if __name__ == "__main__":
        main()

结果

detected

  • 原理
  1. 定位黑色外边框:blobs = img.find_blobs([black_threshold], pixels_threshold=500, merge=True, margin=15) 为什么只识别外边框,忽略内部网格? 首先,寻找黑色矩形,在这里是黑色边框,由于pixels_threshold=500的限制,只有最外层大边框内的像素满足条件。
    其次,merge=True,因为网格线间的空白大于15像素,所以合并的是边框上的像素块,但合并后形成的roi覆盖边框及以内的全部区域,而内部小区域被覆盖,故结果仅识别最外层边框