dfjs

  • 2024-11-16
  • 发表了主题帖: 嘉楠科K230AI开发板测评8--音频采集、播放、编码与解码、视频采集、播放与编码

    本帖最后由 dfjs 于 2024-11-16 22:32 编辑 嘉楠科K230AI开发板测评8 1、音频采集与播放     K230自带采集与播放接口,接口位置如下左图所示,音频输入为:麦克风咪头,音频输出:3.5mm音频口(双声道),查看音频采集与播放的原理图,可能是考虑到功率问题,没有功率放大电路,而采用了3.5mm的音频口输出而不是直接喇叭外放。           具体代码思路如下:     初始化模块:         导入必要的模块,包括 os、media、pyaudio 和 wave。         初始化媒体管理和音频对象。     异常处理:         定义 exit_check 函数,用于捕获键盘中断(Ctrl+C)并优雅地退出程序。     音频录制:         定义 record_audio 函数,用于录制音频并保存为 WAV 文件。         设置音频参数,如采样率、采样精度、声道数和 chunk 大小。         打开音频输入流,读取音频数据并存储到列表中。         将列表中的音频数据保存到 WAV 文件中。     音频播放:         定义 play_audio 函数,用于播放 WAV 文件。         打开 WAV 文件,读取音频参数。         打开音频输出流,读取 WAV 文件中的音频数据并写入输出流。     实时回放:         定义 loop_audio 函数,用于实时采集音频并立即播放。         设置音频参数,打开音频输入流和输出流。         从输入流中读取音频数据并立即写入输出流。     主程序:         在 __main__ 块中,启用退出点,启动音频示例。         调用 play_audio、record_audio 或 loop_audio 函数来执行相应的音频操作。     参考代码如下: # audio input and output example # # Note: You will need an SD card to run this example. # # You can play wav files or capture audio to save as wav import os from media.media import * #导入media模块,用于初始化vb buffer from media.pyaudio import * #导入pyaudio模块,用于采集和播放音频 import media.wave as wave #导入wav模块,用于保存和加载wav音频文件 def exit_check(): try: os.exitpoint() except KeyboardInterrupt as e: print("user stop: ", e) return True return False def record_audio(filename, duration): CHUNK = int(44100/25) #设置音频chunk值 FORMAT = paInt16 #设置采样精度 CHANNELS = 2 #设置声道数 RATE = 44100 #设置采样率 try: p = PyAudio() p.initialize(CHUNK) #初始化PyAudio对象 MediaManager.init() #vb buffer初始化 #创建音频输入流 stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) frames = [] #采集音频数据并存入列表 for i in range(0, int(RATE / CHUNK * duration)): data = stream.read() frames.append(data) if exit_check(): break #将列表中的数据保存到wav文件中 wf = wave.open(filename, 'wb') #创建wav 文件 wf.set_channels(CHANNELS) #设置wav 声道数 wf.set_sampwidth(p.get_sample_size(FORMAT)) #设置wav 采样精度 wf.set_framerate(RATE) #设置wav 采样率 wf.write_frames(b''.join(frames)) #存储wav音频数据 wf.close() #关闭wav文件 except BaseException as e: print(f"Exception {e}") finally: stream.stop_stream() #停止采集音频数据 stream.close()#关闭音频输入流 p.terminate()#释放音频对象 MediaManager.deinit() #释放vb buffer def play_audio(filename): try: wf = wave.open(filename, 'rb')#打开wav文件 CHUNK = int(wf.get_framerate()/25)#设置音频chunk值 p = PyAudio() p.initialize(CHUNK) #初始化PyAudio对象 MediaManager.init() #vb buffer初始化 #创建音频输出流,设置的音频参数均为wave中获取到的参数 stream = p.open(format=p.get_format_from_width(wf.get_sampwidth()), channels=wf.get_channels(), rate=wf.get_framerate(), output=True,frames_per_buffer=CHUNK) data = wf.read_frames(CHUNK)#从wav文件中读取数一帧数据 while data: stream.write(data) #将帧数据写入到音频输出流中 data = wf.read_frames(CHUNK) #从wav文件中读取数一帧数据 if exit_check(): break except BaseException as e: print(f"Exception {e}") finally: stream.stop_stream() #停止音频输出流 stream.close()#关闭音频输出流 p.terminate()#释放音频对象 wf.close()#关闭wav文件 MediaManager.deinit() #释放vb buffer def loop_audio(duration): CHUNK = int(44100/25)#设置音频chunck FORMAT = paInt16 #设置音频采样精度 CHANNELS = 2 #设置音频声道数 RATE = 44100 #设置音频采样率 try: p = PyAudio() p.initialize(CHUNK)#初始化PyAudio对象 MediaManager.init() #vb buffer初始化 #创建音频输入流 input_stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) #创建音频输出流 output_stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, output=True,frames_per_buffer=CHUNK) #从音频输入流中获取数据写入到音频输出流中 for i in range(0, int(RATE / CHUNK * duration)): output_stream.write(input_stream.read()) if exit_check(): break except BaseException as e: print(f"Exception {e}") finally: input_stream.stop_stream()#停止音频输入流 output_stream.stop_stream()#停止音频输出流 input_stream.close() #关闭音频输入流 output_stream.close() #关闭音频输出流 p.terminate() #释放音频对象 MediaManager.deinit() #释放vb buffer if __name__ == "__main__": os.exitpoint(os.EXITPOINT_ENABLE) print("audio sample start") play_audio('/sdcard/app/output.wav') #播放wav文件 #record_audio('/sdcard/app/output.wav', 15) #录制wav文件 loop_audio(15) #采集音频并输出 print("audio sample done")     实验结果:     注意:需要用一个有线耳机插入到K230的3.5mm音频口,才可听到采集到的音频。     下图1为执行音频采集代码之前“CanMV\sdcard\app”目录的文件情况,图2为执行代码之后的目录文件,可以看到成功采集到“output.wav”的音频文件。         接着注释掉音频录制代码,执行音频播放代码,如下图,可以从有线耳机听到刚刚采集到的音频。   2、音频编码与解码     为了高效存储和传输音频数据,通过压缩技术减少文件大小和带宽需求,同时保持或优化音频质量,确保在不同设备和平台上的兼容性和一致性,因此需要音频编码与解码。G.711 是一种常用的音频编码标准,主要用于电话通信系统中。它通过脉冲编码调制(PCM)技术将模拟音频信号转换为数字信号,并进行量化和编码。G.711 编码的目的是在保持较高音频质量的同时,减少数据传输所需的带宽。K230的音频编码与解码采用G.711标准。     具体代码思路如下:     初始化模块:         导入必要的模块,包括 os、media、pyaudio 和 g711。         初始化媒体管理和音频对象。     异常处理:         定义 exit_check 函数,用于捕获键盘中断(Ctrl+C)并优雅地退出程序。     音频采集与编码:         定义 encode_audio 函数,用于采集音频数据并将其编码为 G.711 格式,然后保存到文件中。         设置音频参数,如采样率、采样精度、声道数和 chunk 大小。         打开音频输入流,读取音频数据并进行 G.711 编码,将编码后的数据保存到文件中。     音频解码与播放:         定义 decode_audio 函数,用于从文件中读取 G.711 编码的音频数据,解码为原始音频数据并播放。         打开 G.711 文件,读取音频参数。         打开音频输出流,读取 G.711 文件中的数据并解码为原始音频数据,然后写入输出流。     实时编码与解码回放:         定义 loop_codec 函数,用于实时采集音频数据,进行 G.711 编码和解码,然后立即播放。         设置音频参数,打开音频输入流和输出流。         从输入流中读取音频数据,进行 G.711 编码和解码,然后写入输出流。     主程序:         在 __main__ 块中,启用退出点,启动音频示例。         调用 encode_audio、decode_audio 或 loop_codec 函数来执行相应的音频操作。     参考代码如下: # g711 encode/decode example # # Note: You will need an SD card to run this example. # # You can collect raw data and encode it into g711 or decode it into raw data output. import os from mpp.payload_struct import * #导入payload模块,用于获取音视频编解码类型 from media.media import * #导入media模块,用于初始化vb buffer from media.pyaudio import * #导入pyaudio模块,用于采集和播放音频 import media.g711 as g711 #导入g711模块,用于g711编解码 def exit_check(): try: os.exitpoint() except KeyboardInterrupt as e: print("user stop: ", e) return True return False def encode_audio(filename, duration): CHUNK = int(44100/25) #设置音频chunk值 FORMAT = paInt16 #设置采样精度 CHANNELS = 2 #设置声道数 RATE = 44100 #设置采样率 try: p = PyAudio() p.initialize(CHUNK) #初始化PyAudio对象 enc = g711.Encoder(K_PT_G711A,CHUNK) #创建g711编码器对象 MediaManager.init() #vb buffer初始化 enc.create() #创建编码器 #创建音频输入流 stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) frames = [] #采集音频数据编码并存入列表 for i in range(0, int(RATE / CHUNK * duration)): frame_data = stream.read() #从音频输入流中读取音频数据 data = enc.encode(frame_data) #编码音频数据为g711 frames.append(data) #将g711编码数据保存到列表中 if exit_check(): break #将g711编码数据存入文件中 with open(filename,mode='w') as wf: wf.write(b''.join(frames)) stream.stop_stream() #停止音频输入流 stream.close() #关闭音频输入流 p.terminate() #释放音频对象 enc.destroy() #销毁g711音频编码器 except BaseException as e: print(f"Exception {e}") finally: MediaManager.deinit() #释放vb buffer def decode_audio(filename): FORMAT = paInt16 #设置音频chunk值 CHANNELS = 2 #设置声道数 RATE = 44100 #设置采样率 CHUNK = int(RATE/25) #设置音频chunk值 try: wf = open(filename,mode='rb') #打开g711文件 p = PyAudio() p.initialize(CHUNK) #初始化PyAudio对象 dec = g711.Decoder(K_PT_G711A,CHUNK) #创建g711解码器对象 MediaManager.init() #vb buffer初始化 dec.create() #创建解码器 #创建音频输出流 stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, output=True, frames_per_buffer=CHUNK) stream_len = CHUNK*CHANNELS*2//2 #设置每次读取的g711数据流长度 stream_data = wf.read(stream_len) #从g711文件中读取数据 #解码g711文件并播放 while stream_data: frame_data = dec.decode(stream_data) #解码g711文件 stream.write(frame_data) #播放raw数据 stream_data = wf.read(stream_len) #从g711文件中读取数据 if exit_check(): break stream.stop_stream() #停止音频输入流 stream.close() #关闭音频输入流 p.terminate() #释放音频对象 dec.destroy() #销毁解码器 wf.close() #关闭g711文件 except BaseException as e: print(f"Exception {e}") finally: MediaManager.deinit() #释放vb buffer def loop_codec(duration): CHUNK = int(44100/25) #设置音频chunk值 FORMAT = paInt16 #设置采样精度 CHANNELS = 2 #设置声道数 RATE = 44100 #设置采样率 try: p = PyAudio() p.initialize(CHUNK) #初始化PyAudio对象 dec = g711.Decoder(K_PT_G711A,CHUNK) #创建g711解码器对象 enc = g711.Encoder(K_PT_G711A,CHUNK) #创建g711编码器对象 MediaManager.init() #vb buffer初始化 dec.create() #创建g711解码器 enc.create() #创建g711编码器 #创建音频输入流 input_stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) #创建音频输出流 output_stream = p.open(format=FORMAT, channels=CHANNELS, rate=RATE, output=True, frames_per_buffer=CHUNK) #从音频输入流中获取数据->编码->解码->写入到音频输出流中 for i in range(0, int(RATE / CHUNK * duration)): frame_data = input_stream.read() #从音频输入流中获取raw音频数据 stream_data = enc.encode(frame_data) #编码音频数据为g711 frame_data = dec.decode(stream_data) #解码g711数据为raw数据 output_stream.write(frame_data) #播放raw数据 if exit_check(): break input_stream.stop_stream() #停止音频输入流 output_stream.stop_stream() #停止音频输出流 input_stream.close() #关闭音频输入流 output_stream.close() #关闭音频输出流 p.terminate() #释放音频对象 dec.destroy() #销毁g711解码器 enc.destroy() #销毁g711编码器 except BaseException as e: print(f"Exception {e}") finally: MediaManager.deinit() #释放vb buffer if __name__ == "__main__": os.exitpoint(os.EXITPOINT_ENABLE) print("audio codec sample start") #encode_audio('/sdcard/app/test.g711a', 5) #采集并编码g711文件 #decode_audio('/sdcard/app/test.g711a') #解码g711文件并输出 loop_codec(15) #采集音频数据->编码g711->解码g711->播放音频 print("audio codec sample done")     实验结果:     音频采集编码结果如下,生成“test.g711a”编码文件。       解码并播放代码如下,可以从耳机听到解码后的音频。   3、视频采集     K230自带三个摄像头接口CSI0、CSI1、CSI2,如下图,可以用来图像识别,拍照,录像等功能,下面用K230录取一段视频保存在内存卡中。       具体代码思路如下:     初始化模块:         导入必要的模块,包括 media.mp4format 和 os。         初始化 MP4 容器和配置对象。     配置 MP4 容器:         设置 MP4 文件的路径、视频编码格式、分辨率和音频编码格式。         创建和启动 MP4 复用器(muxer)。     处理音视频数据:         在一个循环中,调用 MP4 复用器的 Process 方法,将音视频数据写入 MP4 文件。         控制循环次数,达到一定帧数后停止。         停止和销毁 MP4 复用器:         停止 MP4 复用器。         销毁 MP4 复用器,释放资源。     异常处理:         捕获并处理可能发生的异常。     参考代码如下: # Save MP4 file example # # Note: You will need an SD card to run this example. # # You can capture audio and video and save them as MP4.The current version only supports MP4 format, video supports 264/265, and audio supports g711a/g711u. from media.mp4format import * import os def mp4_muxer_test(): print("mp4_muxer_test start") width = 1280 height = 720 # 实例化mp4 container mp4_muxer = Mp4Container() mp4_cfg = Mp4CfgStr(mp4_muxer.MP4_CONFIG_TYPE_MUXER) if mp4_cfg.type == mp4_muxer.MP4_CONFIG_TYPE_MUXER: file_name = "/sdcard/app/tests/test.mp4" mp4_cfg.SetMuxerCfg(file_name, mp4_muxer.MP4_CODEC_ID_H265, width, height, mp4_muxer.MP4_CODEC_ID_G711U) # 创建mp4 muxer mp4_muxer.Create(mp4_cfg) # 启动mp4 muxer mp4_muxer.Start() frame_count = 0 try: while True: os.exitpoint() # 处理音视频数据,按MP4格式写入文件 mp4_muxer.Process() frame_count += 1 print("frame_count = ", frame_count) if frame_count >= 200: break except BaseException as e: print(e) # 停止mp4 muxer mp4_muxer.Stop() # 销毁mp4 muxer mp4_muxer.Destroy() print("mp4_muxer_test stop") if __name__ == "__main__": os.exitpoint(os.EXITPOINT_ENABLE) mp4_muxer_test()     实验结果:     可以看到在“\CanMV\sdcard\app\tests”目录下成功生成一个名为“test.mp4”的文件,可以用电脑带的播放器点开播放,视频带有声音。   4、视频播放     视频播放的思路如下:     初始化模块:         导入必要的模块,包括 media.player 和 os。         定义全局变量 start_play 用于控制播放状态。     定义播放器事件回调函数:         定义 player_event 函数,用于处理播放器事件,特别是播放结束事件。     加载和播放 MP4 文件:         创建播放器对象。         加载 MP4 文件。         设置播放器事件回调函数。         开始播放 MP4 文件。         等待播放结束。     停止播放:         捕获并处理可能发生的异常。         停止播放器,释放资源。     参考代码如下: # play mp4 file example # # Note: You will need an SD card to run this example. # # You can load local files to play. The current version only supports MP4 format, video supports 264/265, and audio supports g711a/g711u. from media.player import * #导入播放器模块,用于播放mp4文件 import os start_play = False #播放结束flag def player_event(event,data): global start_play if(event == K_PLAYER_EVENT_EOF): #播放结束标识 start_play = False #设置播放结束标识 def play_mp4_test(filename): global start_play player=Player() #创建播放器对象 player.load(filename) #加载mp4文件 player.set_event_callback(player_event) #设置播放器事件回调 player.start() #开始播放 start_play = True #等待播放结束 try: while(start_play): time.sleep(0.1) os.exitpoint() except KeyboardInterrupt as e: print("user stop: ", e) except BaseException as e: sys.print_exception(e) player.stop() #停止播放 print("play over") if __name__ == "__main__": os.exitpoint(os.EXITPOINT_ENABLE) play_mp4_test("/sdcard/app/tests/test.mp4")#播放mp4文件     实验结果:   5、视频编码     视频编码与解码是数字视频处理中不可或缺的步骤,通过压缩技术减少视频文件的大小和传输带宽需求,同时保持或优化视频质量,确保高效存储、快速传输和跨平台兼容性。编码将原始视频数据转换为压缩格式,解码则将压缩数据还原为可播放的视频,二者共同保证了视频内容的高效分发和流畅播放。     嘉楠K230内置多个高清视频图像输入处理和智能硬件处理单元,兼顾高性能、低功耗( 采用大小核设计兼顾性能与功耗,提供百毫秒级快速启动软件SDK支持,适合电池类产品开发)和高安全性特点。     使用 Python 和 media 模块来捕获视频数据并将其编码为 H.264 或 H.265 格式的文件。     视频编码的思路如下:     初始化模块:         导入必要的模块,包括 media.vencoder、media.sensor、media.media 和 os。         初始化传感器(相机)和视频编码器。     配置传感器:         重置传感器。         设置传感器的输出分辨率和格式。     实例化和配置视频编码器:         创建视频编码器对象。         设置视频编码器的输出缓冲区。         绑定传感器和视频编码器。     启动传感器和编码器:         初始化媒体管理器。         创建和启动视频编码器。         启动传感器。     处理视频数据:         在一个循环中,从视频编码器获取编码后的码流数据,并将其写入文件。         控制循环次数,达到一定帧数后停止。     停止和销毁编码器:         停止传感器和编码器。         销毁传感器和编码器的绑定。         停止和销毁编码器,释放资源。     异常处理:         捕获并处理可能发生的异常。     参考代码如下: # Video encode example # # Note: You will need an SD card to run this example. # # You can capture videos and encode them into 264 files from media.vencoder import * from media.sensor import * from media.media import * import time, os # NOT WORK NOW!!! def venc_test(): print("venc_test start") width = 1280 height = 720 venc_chn = VENC_CHN_ID_0 width = ALIGN_UP(width, 16) # 初始化sensor sensor = Sensor() sensor.reset() # 设置camera 输出buffer # set chn0 output size sensor.set_framesize(width = width, height = height, alignment=12) # set chn0 output format sensor.set_pixformat(Sensor.YUV420SP) # 实例化video encoder encoder = Encoder() # 设置video encoder 输出buffer encoder.SetOutBufs(venc_chn, 15, width, height) # 绑定camera和venc link = MediaManager.link(sensor.bind_info()['src'], (VIDEO_ENCODE_MOD_ID, VENC_DEV_ID, venc_chn)) # init media manager MediaManager.init() chnAttr = ChnAttrStr(encoder.PAYLOAD_TYPE_H265, encoder.H265_PROFILE_MAIN, width, height) streamData = StreamData() # 创建编码器 encoder.Create(venc_chn, chnAttr) # 开始编码 encoder.Start(venc_chn) # 启动camera sensor.run() frame_count = 0 if chnAttr.payload_type == encoder.PAYLOAD_TYPE_H265: suffix = "265" elif chnAttr.payload_type == encoder.PAYLOAD_TYPE_H264: suffix = "264" else: suffix = "unkown" print("cam_venc_test, venc payload_type unsupport") out_file = f"/sdcard/app/tests/venc_chn_{venc_chn:02d}.{suffix}" print("save stream to file: ", out_file) with open(out_file, "wb") as fo: try: while True: os.exitpoint() encoder.GetStream(venc_chn, streamData) # 获取一帧码流 for pack_idx in range(0, streamData.pack_cnt): stream_data = uctypes.bytearray_at(streamData.data[pack_idx], streamData.data_size[pack_idx]) fo.write(stream_data) # 码流写文件 print("stream size: ", streamData.data_size[pack_idx], "stream type: ", streamData.stream_type[pack_idx]) encoder.ReleaseStream(venc_chn, streamData) # 释放一帧码流 frame_count += 1 if frame_count >= 100: break except KeyboardInterrupt as e: print("user stop: ", e) except BaseException as e: sys.print_exception(e) # 停止camera sensor.stop() # 销毁camera和venc的绑定 del link # 停止编码 encoder.Stop(venc_chn) # 销毁编码器 encoder.Destroy(venc_chn) # 清理buffer MediaManager.deinit() print("venc_test stop") if __name__ == "__main__": os.exitpoint(os.EXITPOINT_ENABLE) venc_test()     实验结果:     在”This PC\CanMV\sdcard\app\tests”目录下生成名为“venc_chnn_00.265”的编码文件。  

  • 2024-11-15
  • 发表了主题帖: 嘉楠K230AI开发板测评8--人脸3D网络、人体关键点、车牌识别、字符识别、物体识别

    本帖最后由 dfjs 于 2024-11-15 22:43 编辑 嘉楠科K230AI开发板测评8--AI视觉篇     观察K230文件系统,AI视觉开发框架主要API接口代码位于“\CanMV\sdcard\app\libs”目录下,如下图:     PineLine :用于采集图像、画图以及结果图片显示的API接口。     Ai2d : 预处理(Preprocess)相关接口。     AIBase : 模型推理主要接口。       同时可以看到官方训练好的模型(后缀为.kmodel)位于“\CanMV\sdcard\app\tests\kmodel”目录下,包括人脸相关模型、人体相关模型、手部相关模型、车牌相关模型、字符识别模型、物体检测模型等,当然,也可以将自己训练的模型放在该目录下调用,如下图:       同时为了下面更好的学习,在这里我搜索了目标检测算法常用的三个重要的概念:置信度阈值(Confidence Threshold)、非极大值抑制阈值(NMS)和锚点数据(Anchors),置信度阈值用于过滤检测结果,NMS阈值用于去除重叠的检测框,而锚点数据则是模型预测目标位置的基础。这三个参数共同作用,使得目标检测算法能够准确地识别和定位目标。它们在检测过程中起到关键作用,在后面的推理过程经常用到。     置信度阈值(Confidence Threshold):     含义:置信度阈值用于过滤模型输出的检测结果。在目标检测模型中,模型会为每个目标输出一个置信度分数,表示模型认为检测到的目标属于某个类别的确定程度。     作用:置信度阈值用于确定一个检测结果是否足够可靠。只有当检测结果的置信度分数高于这个阈值时,该结果才会被认为是有效的检测。低于阈值的结果将被忽略。这有助于减少误检和提高检测的准确性。     非极大值抑制阈值(NMS):     含义:非极大值抑制是一种常用的技术,用于在目标检测中去除重叠的检测框。在实际场景中,同一个目标可能被模型多次检测到,产生多个边界框。NMS通过合并重叠的边界框来解决这个问题。     作用:NMS阈值决定了两个检测框需要有多大的重叠(通常是通过交并比IoU来衡量)才会被认为是同一个目标。如果两个检测框的IoU高于NMS阈值,那么置信度较低的检测框将被抑制(即删除),只保留置信度最高的那个。这有助于减少冗余的检测结果,提高检测的精确度。     锚点数据(Anchors):     含义:锚点(也称为先验框)是目标检测算法中用于预测目标位置的一种技术。锚点是一组预定义的边界框,它们有不同的尺寸和比例,用于覆盖目标可能出现的各种尺寸。     作用:在基于锚点的目标检测算法(如Faster R-CNN、SSD等)中,模型会预测每个锚点的偏移量,以调整锚点的位置和尺寸,使其更准确地匹配目标。锚点数据包含了这些预定义边界框的坐标和尺寸,它们是模型预测的基础。 1、人脸相关     人脸3D网格,在检测到人脸后用多个点描绘整个脸,从而把人脸轮廓像网格一样描绘出来,支持单个和多个人脸。     通过CanMV K230 AI视觉框架开发,用到的模型已经存放在CanMV K230的文件系统,具体编程思路如下:       自定义人脸检测人物类、人脸网络任务类、人脸网络后处理任务类、3D人脸网络类。     人脸检测任务类(FaceDetApp):     作用:         负责执行人脸检测任务。     功能:         加载人脸检测模型。         配置图像预处理操作,如填充(pad)和缩放(resize)。         执行模型推理。         进行后处理,包括置信度过滤和非极大值抑制(NMS)。     人脸网格任务类(FaceMeshApp)     作用:         负责执行人脸网格估计任务。     功能:         加载人脸网格模型。         配置图像预处理操作,如裁剪(crop)和缩放(resize)。         执行模型推理。         后处理,将模型输出的参数映射回人脸网格的参数。     人脸网格后处理任务类(FaceMeshPostApp)     作用:         负责对人脸网格估计的结果进行后处理。     功能:         接收人脸网格模型的输出参数。         将参数转换为最终的人脸网格。         调用aidemo库的接口进行进一步的后处理,如人脸网格的绘制。    3D人脸网格类(FaceMesh)     作用:         整合人脸检测、人脸网格估计和后处理流程。     功能:         初始化人脸检测、人脸网格和人脸网格后处理的实例。         运行整个3D人脸网格流程,包括人脸检测、人脸网格估计和后处理。         绘制最终的3D人脸网格结果。     主函数思路:     初始化显示模式和分辨率:根据平台选择HDMI或LCD模式,并设置相应的分辨率。     设置模型路径和其他参数:指定人脸检测、人脸网格和人脸网格后处理模型的路径,以及其他参数如锚点数据、输入分辨率等。     初始化PipeLine:创建一个图像处理流程的实例,设置传给AI的图像分辨率和显示分辨率。     创建3D人脸网格实例:初始化FaceMesh类,传入所需的模型路径和参数。     主循环:         获取当前帧图像。         调用FaceMesh类的run方法进行人脸检测、人脸网格估计和后处理。         打印检测和网格估计的结果。         调用FaceMesh类的draw_result方法绘制3D人脸网格结果。         显示推理效果。         进行垃圾回收。         打印帧率信息。         异常处理:捕获并打印异常信息。         资源清理:在finally块中释放资源,包括反初始化人脸检测、人脸网格和人脸网格后处理实例,以及销毁PipeLine实例。     参考代码如下: ''' 实验名称:人脸3D网格 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' 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 time import * import nncase_runtime as nn import ulab.numpy as np import time import image import aidemo import random import gc import sys # 自定义人脸检测任务类 class FaceDetApp(AIBase): def __init__(self,kmodel_path,model_input_size,anchors,confidence_threshold=0.25,nms_threshold=0.3,rgb888p_size=[1280,720],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.confidence_threshold=confidence_threshold # nms阈值 self.nms_threshold=nms_threshold # 检测任务锚框 self.anchors=anchors # 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 # 实例化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/app/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 # 设置padding预处理 self.ai2d.pad(self.get_pad_param(), 0, [104,117,123]) # 设置resize预处理 self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 构建预处理流程,参数为预处理输入tensor的shape和预处理输出的tensor的shape 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]]) # 自定义后处理,results是模型推理输出的array列表,这里使用了aidemo库的face_det_post_process接口 def postprocess(self,results): with ScopedTiming("postprocess",self.debug_mode > 0): res = aidemo.face_det_post_process(self.confidence_threshold,self.nms_threshold,self.model_input_size[0],self.anchors,self.rgb888p_size,results) if len(res)==0: return res else: return res[0] # padding参数计算 def get_pad_param(self): dst_w = self.model_input_size[0] dst_h = self.model_input_size[1] # 计算最小的缩放比例,等比例缩放 ratio_w = dst_w / self.rgb888p_size[0] ratio_h = dst_h / self.rgb888p_size[1] if ratio_w < ratio_h: ratio = ratio_w else: ratio = ratio_h new_w = (int)(ratio * self.rgb888p_size[0]) new_h = (int)(ratio * self.rgb888p_size[1]) 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 [0,0,0,0,top, bottom, left, right] # 自定义人脸网格任务类 class FaceMeshApp(AIBase): def __init__(self,kmodel_path,model_input_size,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 # 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 # 人脸mesh参数均值 self.param_mean = np.array([0.0003492636315058917,2.52790130161884e-07,-6.875197868794203e-07,60.1679573059082,-6.295513230725192e-07,0.0005757200415246189,-5.085391239845194e-05,74.2781982421875,5.400917189035681e-07,6.574138387804851e-05,0.0003442012530285865,-66.67157745361328,-346603.6875,-67468.234375,46822.265625,-15262.046875,4350.5888671875,-54261.453125,-18328.033203125,-1584.328857421875,-84566.34375,3835.960693359375,-20811.361328125,38094.9296875,-19967.85546875,-9241.3701171875,-19600.71484375,13168.08984375,-5259.14404296875,1848.6478271484375,-13030.662109375,-2435.55615234375,-2254.20654296875,-14396.5615234375,-6176.3291015625,-25621.919921875,226.39447021484375,-6326.12353515625,-10867.2509765625,868.465087890625,-5831.14794921875,2705.123779296875,-3629.417724609375,2043.9901123046875,-2446.6162109375,3658.697021484375,-7645.98974609375,-6674.45263671875,116.38838958740234,7185.59716796875,-1429.48681640625,2617.366455078125,-1.2070955038070679,0.6690792441368103,-0.17760828137397766,0.056725528091192245,0.03967815637588501,-0.13586315512657166,-0.09223993122577667,-0.1726071834564209,-0.015804484486579895,-0.1416848599910736],dtype=np.float) # 人脸mesh参数方差 self.param_std = np.array([0.00017632152594160289,6.737943476764485e-05,0.00044708489440381527,26.55023193359375,0.0001231376954820007,4.493021697271615e-05,7.923670636955649e-05,6.982563018798828,0.0004350444069132209,0.00012314890045672655,0.00017400001524947584,20.80303955078125,575421.125,277649.0625,258336.84375,255163.125,150994.375,160086.109375,111277.3046875,97311.78125,117198.453125,89317.3671875,88493.5546875,72229.9296875,71080.2109375,50013.953125,55968.58203125,47525.50390625,49515.06640625,38161.48046875,44872.05859375,46273.23828125,38116.76953125,28191.162109375,32191.4375,36006.171875,32559.892578125,25551.1171875,24267.509765625,27521.3984375,23166.53125,21101.576171875,19412.32421875,19452.203125,17454.984375,22537.623046875,16174.28125,14671.640625,15115.6884765625,13870.0732421875,13746.3125,12663.1337890625,1.5870834589004517,1.5077009201049805,0.5881357789039612,0.5889744758605957,0.21327851712703705,0.2630201280117035,0.2796429395675659,0.38030216097831726,0.16162841022014618,0.2559692859649658],dtype=np.float) # 实例化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) # 配置预处理操作,这里使用了crop和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/libs/AI2D.py查看 def config_preprocess(self,det,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 # 计算crop参数,并设置crop预处理 roi = self.parse_roi_box_from_bbox(det) self.ai2d.crop(int(roi[0]),int(roi[1]),int(roi[2]),int(roi[3])) # 设置resize预处理 self.ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 构建预处理流程,参数为预处理输入tensor的shape和预处理输出的tensor的shape 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]]) return roi # 自定义后处理,results是模型输出的array列表 def postprocess(self,results): with ScopedTiming("postprocess",self.debug_mode > 0): param = results[0] * self.param_std + self.param_mean return param def parse_roi_box_from_bbox(self,bbox): # 获取人脸roi x1, y1, w, h = map(lambda x: int(round(x, 0)), bbox[:4]) old_size = (w + h) / 2 center_x = x1 + w / 2 center_y = y1 + h / 2 + old_size * 0.14 size = int(old_size * 1.58) x0 = center_x - float(size) / 2 y0 = center_y - float(size) / 2 x1 = x0 + size y1 = y0 + size x0 = max(0, min(x0, self.rgb888p_size[0])) y0 = max(0, min(y0, self.rgb888p_size[1])) x1 = max(0, min(x1, self.rgb888p_size[0])) y1 = max(0, min(y1, self.rgb888p_size[1])) roi = (x0, y0, x1 - x0, y1 - y0) return roi # 自定义人脸网格后处理任务类 class FaceMeshPostApp(AIBase): def __init__(self,kmodel_path,model_input_size,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 # 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 # 实例化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) # 重写预处理函数preprocess,因为该模型的预处理不是单纯调用一个ai2d能实现的,返回模型输入的tensor列表 def preprocess(self,param): with ScopedTiming("set preprocess config",self.debug_mode > 0): # face mesh post模型预处理,param解析 param = param[0] trans_dim, shape_dim, exp_dim = 12, 40, 10 R_ = param[:trans_dim].copy().reshape((3, -1)) R = R_[:, :3].copy() offset = R_[:, 3].copy() offset = offset.reshape((3, 1)) alpha_shp = param[trans_dim:trans_dim + shape_dim].copy().reshape((-1, 1)) alpha_exp = param[trans_dim + shape_dim:].copy().reshape((-1, 1)) R_tensor = nn.from_numpy(R) offset_tensor = nn.from_numpy(offset) alpha_shp_tensor = nn.from_numpy(alpha_shp) alpha_exp_tensor = nn.from_numpy(alpha_exp) return [R_tensor,offset_tensor,alpha_shp_tensor,alpha_exp_tensor] # 自定义模型后处理,这里调用了aidemo的face_mesh_post_process接口 def postprocess(self,results,roi): with ScopedTiming("postprocess",self.debug_mode > 0): x, y, w, h = map(lambda x: int(round(x, 0)), roi[:4]) x = x * self.display_size[0] // self.rgb888p_size[0] y = y * self.display_size[1] // self.rgb888p_size[1] w = w * self.display_size[0] // self.rgb888p_size[0] h = h * self.display_size[1] // self.rgb888p_size[1] roi_array = np.array([x,y,w,h],dtype=np.float) aidemo.face_mesh_post_process(roi_array,results[0]) return results[0] # 3D人脸网格 class FaceMesh: def __init__(self,face_det_kmodel,face_mesh_kmodel,mesh_post_kmodel,det_input_size,mesh_input_size,anchors,confidence_threshold=0.25,nms_threshold=0.3,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0): # 人脸检测模型路径 self.face_det_kmodel=face_det_kmodel # 人脸3D网格模型路径 self.face_mesh_kmodel=face_mesh_kmodel # 人脸3D网格后处理模型路径 self.mesh_post_kmodel=mesh_post_kmodel # 人脸检测模型输入分辨率 self.det_input_size=det_input_size # 人脸3D网格模型输入分辨率 self.mesh_input_size=mesh_input_size # anchors self.anchors=anchors # 置信度阈值 self.confidence_threshold=confidence_threshold # nms阈值 self.nms_threshold=nms_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.face_det=FaceDetApp(self.face_det_kmodel,model_input_size=self.det_input_size,anchors=self.anchors,confidence_threshold=self.confidence_threshold,nms_threshold=self.nms_threshold,rgb888p_size=self.rgb888p_size,display_size=self.display_size,debug_mode=0) # 人脸网格实例 self.face_mesh=FaceMeshApp(self.face_mesh_kmodel,model_input_size=self.mesh_input_size,rgb888p_size=self.rgb888p_size,display_size=self.display_size) # 人脸网格后处理实例 self.face_mesh_post=FaceMeshPostApp(self.mesh_post_kmodel,model_input_size=self.mesh_input_size,rgb888p_size=self.rgb888p_size,display_size=self.display_size) # 人脸检测预处理配置 self.face_det.config_preprocess() # run函数 def run(self,input_np): # 执行人脸检测 det_boxes=self.face_det.run(input_np) mesh_res=[] for det_box in det_boxes: # 对检测到的每一个人脸配置预处理,执行人脸网格和人脸网格后处理 roi=self.face_mesh.config_preprocess(det_box) param=self.face_mesh.run(input_np) tensors=self.face_mesh_post.preprocess(param) results=self.face_mesh_post.inference(tensors) res=self.face_mesh_post.postprocess(results,roi) mesh_res.append(res) return det_boxes,mesh_res # 绘制人脸解析效果 def draw_result(self,pl,dets,mesh_res): pl.osd_img.clear() if dets: draw_img_np = np.zeros((self.display_size[1],self.display_size[0],4),dtype=np.uint8) draw_img = image.Image(self.display_size[0], self.display_size[1], image.ARGB8888, alloc=image.ALLOC_REF,data = draw_img_np) for vertices in mesh_res: aidemo.face_draw_mesh(draw_img_np, vertices) pl.osd_img.copy_from(draw_img) if __name__=="__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="lcd" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 人脸检测模型路径 face_det_kmodel_path="/sdcard/app/tests/kmodel/face_detection_320.kmodel" # 人脸网格模型路径 face_mesh_kmodel_path="/sdcard/app/tests/kmodel/face_alignment.kmodel" # 人脸网格后处理模型路径 face_mesh_post_kmodel_path="/sdcard/app/tests/kmodel/face_alignment_post.kmodel" # 其他参数 anchors_path="/sdcard/app/tests/utils/prior_data_320.bin" rgb888p_size=[1920,1080] face_det_input_size=[320,320] face_mesh_input_size=[120,120] confidence_threshold=0.5 nms_threshold=0.2 anchor_len=4200 det_dim=4 anchors = np.fromfile(anchors_path, dtype=np.float) anchors = anchors.reshape((anchor_len,det_dim)) # 初始化PipeLine,只关注传给AI的图像分辨率,显示的分辨率 pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode) pl.create() fm=FaceMesh(face_det_kmodel_path,face_mesh_kmodel_path,face_mesh_post_kmodel_path,det_input_size=face_det_input_size,mesh_input_size=face_mesh_input_size,anchors=anchors,confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,rgb888p_size=rgb888p_size,display_size=display_size) clock = time.clock() try: while True: os.exitpoint() clock.tick() img=pl.get_frame() # 获取当前帧 det_boxes,mesh_res=fm.run(img) # 推理当前帧 print(det_boxes,mesh_res) # 打印结果 fm.draw_result(pl,det_boxes,mesh_res) # 绘制推理结果 pl.show_image() # 显示推理效果 gc.collect() print(clock.fps()) #打印帧率 except Exception as e: sys.print_exception(e) finally: fm.face_det.deinit() fm.face_mesh.deinit() fm.face_mesh_post.deinit() pl.destroy()     可以看到使用默认配置后只使用了4行代码便实现了获取当前帧图像、AI推理、绘制结果、显示结果 的识别流程。代码中det_boxes变量为人脸检测结果, mesh_res为网格点数据。 ... while True: os.exitpoint() clock.tick() img=pl.get_frame() # 获取当前帧 det_boxes,mesh_res=fm.run(img) # 推理当前帧 print(det_boxes,mesh_res) # 打印结果 fm.draw_result(pl,det_boxes,mesh_res) # 绘制推理结果 pl.show_image() # 显示推理效果 gc.collect() print(clock.fps()) #打印帧率 ...     实验结果如下:   2、人体相关     人体关键点检测是指标注出人体关节等关键信息,分析人体姿态、运动轨迹、动作角度等。检测摄像头拍摄到的画面中的人体关键点并通过画图提示。     本实验通过CanMV K230 AI视觉框架开发,用到的模型已经存放在CanMV K230的文件系统,无需额外拷贝。     具体编程思路如下:       自定义人体关键点检测类。     人体关键点检测类(PersonKeyPointApp):     初始化(__init__):        设置模型路径、模型输入尺寸、置信度阈值、非极大值抑制(NMS)阈值、RGB图像尺寸、显示尺寸和调试模式。        初始化骨骼信息和关键点颜色,用于后续绘制关键点和骨骼。     预处理配置(config_preprocess):        配置Ai2d实例,用于实现模型预处理,包括填充(pad)和缩放(resize)操作。     后处理(postprocess):        对模型的推理结果进行后处理,使用aidemo库的person_kp_postprocess接口,根据置信度阈值和NMS阈值过滤和处理关键点。     绘制结果(draw_result):       将处理后的关键点和骨骼信息绘制到图像上,使用不同的颜色区分不同的骨骼和关键点。     计算填充参数(get_padding_param):        计算为了将输入图像调整到模型输入尺寸所需的填充参数。     主函数的思路:     设置显示模式和尺寸:         根据选择的显示模式(HDMI或LCD),设置显示尺寸。     初始化模型和参数:        设置模型路径、置信度阈值、NMS阈值和RGB图像尺寸。     创建PipeLine:        初始化PipeLine实例,用于管理图像的获取和显示。     初始化人体关键点检测实例:        创建PersonKeyPointApp类的实例,并配置预处理操作。     主循环:        在一个无限循环中,不断获取当前帧图像,进行推理,绘制结果,并显示。        使用clock对象来计算和打印帧率。     异常处理:        捕获异常,打印异常信息,并在退出前释放相关资源。     参考代码如下: ''' 实验名称:人体关键点检测 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' 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 time import * import nncase_runtime as nn import ulab.numpy as np import time import utime import image import random import gc import sys import aidemo # 自定义人体关键点检测类 class PersonKeyPointApp(AIBase): def __init__(self,kmodel_path,model_input_size,confidence_threshold=0.2,nms_threshold=0.5,rgb888p_size=[1280,720],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.confidence_threshold=confidence_threshold # nms阈值设置 self.nms_threshold=nms_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 #骨骼信息 self.SKELETON = [(16, 14),(14, 12),(17, 15),(15, 13),(12, 13),(6, 12),(7, 13),(6, 7),(6, 8),(7, 9),(8, 10),(9, 11),(2, 3),(1, 2),(1, 3),(2, 4),(3, 5),(4, 6),(5, 7)] #肢体颜色 self.LIMB_COLORS = [(255, 51, 153, 255),(255, 51, 153, 255),(255, 51, 153, 255),(255, 51, 153, 255),(255, 255, 51, 255),(255, 255, 51, 255),(255, 255, 51, 255),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0)] #关键点颜色,共17个 self.KPS_COLORS = [(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 0, 255, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 255, 128, 0),(255, 51, 153, 255),(255, 51, 153, 255),(255, 51, 153, 255),(255, 51, 153, 255),(255, 51, 153, 255),(255, 51, 153, 255)] # 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/app/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): # 这里使用了aidemo库的person_kp_postprocess接口 results = aidemo.person_kp_postprocess(results[0],[self.rgb888p_size[1],self.rgb888p_size[0]],self.model_input_size,self.confidence_threshold,self.nms_threshold) return results #绘制结果,绘制人体关键点 def draw_result(self,pl,res): with ScopedTiming("display_draw",self.debug_mode >0): if res[0]: pl.osd_img.clear() kpses = res[1] for i in range(len(res[0])): for k in range(17+2): if (k < 17): kps_x,kps_y,kps_s = round(kpses[i][k][0]),round(kpses[i][k][1]),kpses[i][k][2] kps_x1 = int(float(kps_x) * self.display_size[0] // self.rgb888p_size[0]) kps_y1 = int(float(kps_y) * self.display_size[1] // self.rgb888p_size[1]) if (kps_s > 0): pl.osd_img.draw_circle(kps_x1,kps_y1,5,self.KPS_COLORS[k],4) ske = self.SKELETON[k] pos1_x,pos1_y= round(kpses[i][ske[0]-1][0]),round(kpses[i][ske[0]-1][1]) pos1_x_ = int(float(pos1_x) * self.display_size[0] // self.rgb888p_size[0]) pos1_y_ = int(float(pos1_y) * self.display_size[1] // self.rgb888p_size[1]) pos2_x,pos2_y = round(kpses[i][(ske[1] -1)][0]),round(kpses[i][(ske[1] -1)][1]) pos2_x_ = int(float(pos2_x) * self.display_size[0] // self.rgb888p_size[0]) pos2_y_ = int(float(pos2_y) * self.display_size[1] // self.rgb888p_size[1]) pos1_s,pos2_s = kpses[i][(ske[0] -1)][2],kpses[i][(ske[1] -1)][2] if (pos1_s > 0.0 and pos2_s >0.0): pl.osd_img.draw_line(pos1_x_,pos1_y_,pos2_x_,pos2_y_,self.LIMB_COLORS[k],4) gc.collect() else: pl.osd_img.clear() # 计算padding参数 def get_padding_param(self): 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(dh - 0.1)) bottom = int(round(dh + 0.1)) left = int(round(dw - 0.1)) right = int(round(dw - 0.1)) return top, bottom, left, right if __name__=="__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="lcd" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 模型路径 kmodel_path="/sdcard/app/tests/kmodel/yolov8n-pose.kmodel" # 其它参数设置 confidence_threshold = 0.2 nms_threshold = 0.5 rgb888p_size=[1920,1080] # 初始化PipeLine pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode) pl.create() # 初始化自定义人体关键点检测实例 person_kp=PersonKeyPointApp(kmodel_path,model_input_size=[320,320],confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,rgb888p_size=rgb888p_size,display_size=display_size,debug_mode=0) person_kp.config_preprocess() clock = time.clock() try: while True: os.exitpoint() clock.tick() img=pl.get_frame() # 获取当前帧数据 res=person_kp.run(img) # 推理当前帧 person_kp.draw_result(pl,res) # 绘制结果到PipeLine的osd图像 print(res) #打印结果 pl.show_image() # 显示当前的绘制结果 gc.collect() print(clock.fps()) #打印帧率 #IDE中断释放相关资源 except Exception as e: sys.print_exception(e) finally: person_kp.deinit() pl.destroy()     实验结果:     3、车牌相关     车牌识别对找出的车牌进行车牌内容识别。识别摄像头拍摄到的画面中的车牌内容并通过写字符和画图指示。     通过CanMV K230 AI视觉框架开发,用到的模型已经存放在CanMV K230的文件系统,无需额外拷贝。     具体的编程思路如下:       自定义车牌检测类,车牌识别任务类(继承于AIBase类),车牌识别任务类。     LicenceDetectionApp类(车牌检测类):        这个类负责车牌的检测工作。        初始化函数中设置了车牌检测应用的参数,如模型路径、模型输入尺寸、置信度阈值、NMS阈值、RGB图像尺寸和显示尺寸。        config_preprocess方法配置了预处理操作,这里使用了resize操作来调整输入图像的尺寸以匹配模型的输入要求。        postprocess方法对检测结果进行后处理,使用aidemo.licence_det_postprocess接口,根据置信度阈值和NMS阈值过滤和处理检测结果。     LicenceRecognitionApp 类(车牌识别类):        这个类负责对检测到的车牌进行识别。        初始化函数中设置了车牌识别应用的参数,包括模型路径、模型输入尺寸、RGB图像尺寸和显示尺寸。        config_preprocess 方法配置了预处理操作,同样使用了resize操作。        postprocess 方法对识别结果进行后处理,将模型输出的数组转换为车牌上的字符序列。     LicenceRec 类(车牌识别任务类):        这个类整合了车牌检测和识别的功能。        初始化函数中接收车牌检测和识别模型的路径、输入尺寸等参数,并初始化了LicenceDetectionApp和LicenceRecognitionApp两个实例。        run 方法执行车牌检测,并将检测到的车牌区域抠出来,然后对每个车牌区域进行识别。       draw_result 方法将检测和识别的结果绘制到图像上,显示车牌的边界框和识别出的车牌号码。     主函数的思路:     设置显示模式和尺寸:        根据选择的显示模式(HDMI或LCD),设置显示尺寸。     初始化模型和参数:        设置车牌检测和识别模型的路径、输入尺寸、置信度阈值和NMS阈值。     创建PipeLine实例:        初始化PipeLine,管理图像的获取和显示。     初始化车牌识别任务实例:        创建LicenceRec类的实例,整合车牌检测和识别的功能,并配置预处理操作。     主循环:    在一个无限循环中,不断获取当前帧图像,进行车牌检测和识别,绘制结果,并显示。    使用clock对象来计算和打印帧率。     异常处理:        捕获异常,打印异常信息,并在退出前释放相关资源。     参考代码如下:     # 车牌字符字典     self.dict_rec = ["挂", "使", "领", "澳", "港", "皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "警", "学", "0", 1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "_", "-"]列出了所有的车牌可能出现的结果。 ''' 实验名称:车牌识别 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' 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 time import * import nncase_runtime as nn import ulab.numpy as np import time import image import aidemo import random import gc import sys # 自定义车牌检测类 class LicenceDetectionApp(AIBase): # 初始化函数,设置车牌检测应用的参数 def __init__(self, kmodel_path, model_input_size, confidence_threshold=0.5, nms_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.confidence_threshold = confidence_threshold self.nms_threshold = nms_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 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 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): # 对检测结果进行后处理 det_res = aidemo.licence_det_postprocess(results, [self.rgb888p_size[1], self.rgb888p_size[0]], self.model_input_size, self.confidence_threshold, self.nms_threshold) return det_res # 自定义车牌识别任务类 class LicenceRecognitionApp(AIBase): def __init__(self,kmodel_path,model_input_size,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 # 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_rec = ["挂", "使", "领", "澳", "港", "皖", "沪", "津", "渝", "冀", "晋", "蒙", "辽", "吉", "黑", "苏", "浙", "京", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "桂", "琼", "川", "贵", "云", "藏", "陕", "甘", "青", "宁", "新", "警", "学", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "_", "-"] self.dict_size = len(self.dict_rec) self.ai2d=Ai2d(debug_mode) self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8) # 配置预处理操作,这里使用了resize,Ai2d支持crop/shift/pad/resize/affine def config_preprocess(self,input_image_size=None): with ScopedTiming("set preprocess config",self.debug_mode > 0): ai2d_input_size=input_image_size if input_image_size else self.rgb888p_size 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]]) # 自定义后处理,results是模型输出的array列表 def postprocess(self,results): with ScopedTiming("postprocess",self.debug_mode > 0): output_data=results[0].reshape((-1,self.dict_size)) max_indices = np.argmax(output_data, axis=1) result_str = "" for i in range(max_indices.shape[0]): index = max_indices[i] if index > 0 and (i == 0 or index != max_indices[i - 1]): result_str += self.dict_rec[index - 1] return result_str # 车牌识别任务类 class LicenceRec: def __init__(self,licence_det_kmodel,licence_rec_kmodel,det_input_size,rec_input_size,confidence_threshold=0.25,nms_threshold=0.3,rgb888p_size=[1920,1080],display_size=[1920,1080],debug_mode=0): # 车牌检测模型路径 self.licence_det_kmodel=licence_det_kmodel # 车牌识别模型路径 self.licence_rec_kmodel=licence_rec_kmodel # 人脸检测模型输入分辨率 self.det_input_size=det_input_size # 人脸姿态模型输入分辨率 self.rec_input_size=rec_input_size # 置信度阈值 self.confidence_threshold=confidence_threshold # nms阈值 self.nms_threshold=nms_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.licence_det=LicenceDetectionApp(self.licence_det_kmodel,model_input_size=self.det_input_size,confidence_threshold=self.confidence_threshold,nms_threshold=self.nms_threshold,rgb888p_size=self.rgb888p_size,display_size=self.display_size,debug_mode=0) self.licence_rec=LicenceRecognitionApp(self.licence_rec_kmodel,model_input_size=self.rec_input_size,rgb888p_size=self.rgb888p_size) self.licence_det.config_preprocess() # run函数 def run(self,input_np): # 执行车牌检测 det_boxes=self.licence_det.run(input_np) # 将车牌部分抠出来 imgs_array_boxes = aidemo.ocr_rec_preprocess(input_np,[self.rgb888p_size[1],self.rgb888p_size[0]],det_boxes) imgs_array = imgs_array_boxes[0] boxes = imgs_array_boxes[1] rec_res = [] for img_array in imgs_array: # 对每一个检测到的车牌进行识别 self.licence_rec.config_preprocess(input_image_size=[img_array.shape[3],img_array.shape[2]]) licence_str=self.licence_rec.run(img_array) rec_res.append(licence_str) gc.collect() return det_boxes,rec_res # 绘制车牌检测识别效果 def draw_result(self,pl,det_res,rec_res): pl.osd_img.clear() if det_res: point_8 = np.zeros((8),dtype=np.int16) for det_index in range(len(det_res)): for i in range(4): x = det_res[det_index][i * 2 + 0]/self.rgb888p_size[0]*self.display_size[0] y = det_res[det_index][i * 2 + 1]/self.rgb888p_size[1]*self.display_size[1] point_8[i * 2 + 0] = int(x) point_8[i * 2 + 1] = int(y) for i in range(4): pl.osd_img.draw_line(point_8[i * 2 + 0],point_8[i * 2 + 1],point_8[(i+1) % 4 * 2 + 0],point_8[(i+1) % 4 * 2 + 1],color=(255, 0, 255, 0),thickness=4) pl.osd_img.draw_string_advanced( point_8[6], point_8[7] + 20, 40,rec_res[det_index] , color=(255,255,153,18)) if __name__=="__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="lcd" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 车牌检测模型路径 licence_det_kmodel_path="/sdcard/app/tests/kmodel/LPD_640.kmodel" # 车牌识别模型路径 licence_rec_kmodel_path="/sdcard/app/tests/kmodel/licence_reco.kmodel" # 其它参数 rgb888p_size=[640,360] licence_det_input_size=[640,640] licence_rec_input_size=[220,32] confidence_threshold=0.2 nms_threshold=0.2 # 初始化PipeLine,只关注传给AI的图像分辨率,显示的分辨率 pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode) pl.create() lr=LicenceRec(licence_det_kmodel_path,licence_rec_kmodel_path,det_input_size=licence_det_input_size,rec_input_size=licence_rec_input_size,confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,rgb888p_size=rgb888p_size,display_size=display_size) clock = time.clock() try: while True: os.exitpoint() clock.tick() img=pl.get_frame() # 获取当前帧 det_res,rec_res=lr.run(img) # 推理当前帧 lr.draw_result(pl,det_res,rec_res) # 绘制当前帧推理结果 print(det_res,rec_res) #打印结果 pl.show_image() # 展示推理结果 gc.collect() print(clock.fps()) #打印帧率 except Exception as e: sys.print_exception(e) finally: lr.licence_det.deinit() lr.licence_rec.deinit() pl.destroy()     实验结果如下:         某次结果[array([456.35, 298.8773, 148.475, 301.1344, 146.225, 207.4359, 453.65, 205.5375], dtype=float32)] ['\u82cfE991N6'],['\u82cfE991N6']为识别结果,”u”为汉字的Unicode编码,82cf为汉字“苏”的编码编码结果,所以车牌为“苏E991N6”。 4、字符识别     OCR (Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相机)检查纸上打印的字符,通过检测暗、亮的模式确定其形状,然后用字符识别方法将形状翻译成计算机文字的过程。关键是从图像中提取有助于区分不同字符的特征,这些特征可能包括形状、边缘、纹理、笔画结构等。然后将提取的特征与已知的字符模板或模型进行匹配,以识别图像中的字符。     编程实现图片中的字符识别(支持中文和英文),通过CanMV K230 AI视觉框架开发,用到的模型已经存放在CanMV K230的文件系统,无需额外拷贝。     具体编程思路如下:       自定义OCR检测类,OCR识别类,OCR检测识别类。     OCRDetectionApp 类(OCR检测类):        这个类负责检测图像中的文本区域。        初始化函数中设置了检测模型的路径、模型输入尺寸、掩码阈值、文本框阈值、RGB图像尺寸和显示尺寸。        config_preprocess 方法配置了预处理操作,包括填充(pad)和缩放(resize)。        postprocess 方法对检测结果进行后处理,使用 aicube.ocr_post_process 接口,返回检测到的文本区域的坐标。        get_padding_param 方法计算填充参数,以确保输入图像的尺寸与模型输入尺寸匹配。        chw2hwc 方法将通道在前的图像数据(CHW)转换为通道在后的图像数据(HWC)。     OCRRecognitionApp 类(OCR识别类):        这个类负责识别检测到的文本区域中的字符。        初始化函数中设置了识别模型的路径、模型输入尺寸、字典路径、RGB图像尺寸和显示尺寸。        config_preprocess 方法配置了预处理操作,包括填充(pad)和缩放(resize)。        postprocess 方法对识别结果进行后处理,将模型输出的数组转换为文本字符串。        get_padding_param 方法计算填充参数,以确保输入图像的尺寸与模型输入尺寸匹配。        read_dict 方法读取OCR字典,该字典用于将识别结果的数字索引转换为对应的字符。     OCRDetRec 类(OCR检测识别类):        这个类整合了OCR检测和识别的功能。        初始化函数中接收OCR检测和识别模型的路径、输入尺寸、字典路径等参数,并初始化了 OCRDetectionApp 和 OCRRecognitionApp 两个实例。        run 方法执行OCR检测,并将检测到的文本区域传递给识别类进行识别。        draw_result 方法将检测和识别的结果绘制到图像上,显示文本区域的边界框和识别出的文本内容。     主函数的思路:     设置显示模式和尺寸:        根据选择的显示模式(HDMI或LCD),设置显示尺寸。     初始化模型和参数:        设置OCR检测和识别模型的路径、字典路径、输入尺寸、掩码阈值、文本框阈值。     创建 PipeLine 实例:        初始化 PipeLine,管理图像的获取和显示。     初始化OCR检测识别实例:        创建 OCRDetRec 类的实例,整合OCR检测和识别的功能,并配置预处理操作。     主循环:        在一个无限循环中,不断获取当前帧图像,进行OCR检测和识别,绘制结果,并显示。        使用 clock 对象来计算和打印帧率。         异常处理:            捕获异常,打印异常信息,并在退出前释放相关资源。     参考代码如下: ''' 实验名称:字符识别(OCR) 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' 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 time import * import nncase_runtime as nn import ulab.numpy as np import time import image import aicube import random import gc import sys # 自定义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/app/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参数 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/app/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__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="lcd" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # OCR检测模型路径 ocr_det_kmodel_path="/sdcard/app/tests/kmodel/ocr_det_int16.kmodel" # OCR识别模型路径 ocr_rec_kmodel_path="/sdcard/app/tests/kmodel/ocr_rec_int16.kmodel" # 其他参数 dict_path="/sdcard/app/tests/utils/dict.txt" rgb888p_size=[640,360] 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) pl.create() 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() try: while True: os.exitpoint() clock.tick() img=pl.get_frame() # 获取当前帧 det_res,rec_res=ocr.run(img) # 推理当前帧 ocr.draw_result(pl,det_res,rec_res) # 绘制当前帧推理结果 print(det_res,rec_res) # 打印结果 pl.show_image() # 展示当前帧推理结果 gc.collect() print(clock.fps()) #打印帧率 except Exception as e: sys.print_exception(e) finally: ocr.ocr_det.deinit() ocr.ocr_rec.deinit() pl.destroy()     实验结果:       某次结果[array([62.24493, 141.8473, 413.3598, 143.8497, 412.755, 275.1527, 61.64014, 273.1503], dtype=float32)] ['01\u79d1\u6280'],['01\u79d1\u6280']为识别结果,“\u79d1\u6280”为“科技”的Unicode编码,识别正确。 5、物体检测     物体检测,是机器视觉里面非常典型的应用。要实现的就是将一幅图片里面的各种物体检测出来,然后跟已知模型做比较从而判断物体是什么。     例程基于YOLOv8n, 支持识别80种物体。     ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"]     [“人”、“自行车”、“汽车”、“摩托车”、“飞机”、“公共汽车”、“火车”、“卡车”、“船”、“交通灯”、“消防栓”、“停车标志”、“停车收费表”、“长凳”、“鸟”、“猫”、“狗”、“马”、“羊”、“牛”、“大象”、“熊”、“斑马”、“长颈鹿”、“背包”、“雨伞”、“手提包”、“领带”、“手提箱”、“飞盘”、“滑雪板”、“滑雪板”、“运动球”、“风筝”、“棒球棒”、“棒球手套”、“滑板”、“冲浪板”、“网球拍”、“瓶子”、“酒杯”、“杯子”、“叉子”、“刀子”、“勺子”、“碗”、“香蕉”、“苹果”、“三明治”、“橙子”、“西兰花”、“胡萝卜”、“热狗”、“披萨”、“甜甜圈”、“蛋糕”、“椅子”、“沙发“盆栽”、“床”、“餐桌”、“马桶”、“电视”、“笔记本电脑”、“鼠标”、“遥控器”、“键盘”、“手机”、“微波炉”、“烤箱”、“烤面包机”、“水槽”、“冰箱”、“书”、“钟表”、“花瓶”、“剪刀”、“泰迪熊”、“吹风机”、“牙刷”]     具体编程思路如下:       自定义YOLOv8检测类     初始化(__init__):        设置模型路径、类别标签、模型输入尺寸、最大检测框数量、置信度阈值、NMS(非极大值抑制)阈值、RGB图像尺寸和显示尺寸。        初始化Ai2d实例,用于实现模型预处理。     预处理配置(config_preprocess):        配置Ai2d的预处理操作,这里使用了缩放(resize)操作来调整输入图像的尺寸以匹配模型的输入要求。     后处理(postprocess):        对模型的推理结果进行后处理,包括转换输出格式、应用置信度阈值、执行NMS以及限制最大检测框数量。     绘制结果(draw_result):        将检测结果绘制到图像上,包括绘制检测框和类别标签。     非极大值抑制(nms):        实现NMS算法,用于去除重叠的检测框,提高检测的准确性。     获取颜色(get_color):        根据检测到的物体类别索引,返回预设的颜色值,用于绘制不同类别的检测框。     主函数的思路:     设置显示模式和尺寸:        根据选择的显示模式(HDMI或LCD),设置显示尺寸。     初始化模型和参数:       设置YOLOv8模型的路径、类别标签、置信度阈值、NMS阈值和最大检测框数量。     创建PipeLine实例:       初始化PipeLine,管理图像的获取和显示。     初始化YOLOv8检测实例:        创建ObjectDetectionApp类的实例,并配置预处理操作。     主循环:        在一个无限循环中,不断获取当前帧图像,进行物体检测,绘制结果,并显示。        使用clock对象来计算和打印帧率。     异常处理:        捕获异常,打印异常信息,并在退出前释放相关资源。 参考代码如下: ''' 实验名称:物体检测(基于yolov8n) 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' 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 time import * import nncase_runtime as nn import ulab.numpy as np import time import utime import image import random import gc import sys import aidemo # 自定义YOLOv8检测类 class ObjectDetectionApp(AIBase): def __init__(self,kmodel_path,labels,model_input_size,max_boxes_num,confidence_threshold=0.5,nms_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.labels=labels # 模型输入分辨率 self.model_input_size=model_input_size # 阈值设置 self.confidence_threshold=confidence_threshold self.nms_threshold=nms_threshold self.max_boxes_num=max_boxes_num # 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 # 检测框预置颜色值 self.color_four=[(255, 220, 20, 60), (255, 119, 11, 32), (255, 0, 0, 142), (255, 0, 0, 230), (255, 106, 0, 228), (255, 0, 60, 100), (255, 0, 80, 100), (255, 0, 0, 70), (255, 0, 0, 192), (255, 250, 170, 30), (255, 100, 170, 30), (255, 220, 220, 0), (255, 175, 116, 175), (255, 250, 0, 30), (255, 165, 42, 42), (255, 255, 77, 255), (255, 0, 226, 252), (255, 182, 182, 255), (255, 0, 82, 0), (255, 120, 166, 157)] # 宽高缩放比例 self.x_factor = float(self.rgb888p_size[0])/self.model_input_size[0] self.y_factor = float(self.rgb888p_size[1])/self.model_input_size[1] # 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) # 配置预处理操作,这里使用了resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/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 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): result=results[0] result = result.reshape((result.shape[0] * result.shape[1], result.shape[2])) output_data = result.transpose() boxes_ori = output_data[:,0:4] scores_ori = output_data[:,4:] confs_ori = np.max(scores_ori,axis=-1) inds_ori = np.argmax(scores_ori,axis=-1) boxes,scores,inds = [],[],[] for i in range(len(boxes_ori)): if confs_ori[i] > confidence_threshold: scores.append(confs_ori[i]) inds.append(inds_ori[i]) x = boxes_ori[i,0] y = boxes_ori[i,1] w = boxes_ori[i,2] h = boxes_ori[i,3] left = int((x - 0.5 * w) * self.x_factor) top = int((y - 0.5 * h) * self.y_factor) right = int((x + 0.5 * w) * self.x_factor) bottom = int((y + 0.5 * h) * self.y_factor) boxes.append([left,top,right,bottom]) if len(boxes)==0: return [] boxes = np.array(boxes) scores = np.array(scores) inds = np.array(inds) # NMS过程 keep = self.nms(boxes,scores,nms_threshold) dets = np.concatenate((boxes, scores.reshape((len(boxes),1)), inds.reshape((len(boxes),1))), axis=1) dets_out = [] for keep_i in keep: dets_out.append(dets[keep_i]) dets_out = np.array(dets_out) dets_out = dets_out[:self.max_boxes_num, :] return dets_out # 绘制结果 def draw_result(self,pl,dets): with ScopedTiming("display_draw",self.debug_mode >0): if dets: pl.osd_img.clear() for det in dets: x1, y1, x2, y2 = map(lambda x: int(round(x, 0)), det[:4]) x= x1*self.display_size[0] // self.rgb888p_size[0] y= y1*self.display_size[1] // self.rgb888p_size[1] w = (x2 - x1) * self.display_size[0] // self.rgb888p_size[0] h = (y2 - y1) * self.display_size[1] // self.rgb888p_size[1] pl.osd_img.draw_rectangle(x,y, w, h, color=self.get_color(int(det[5])),thickness=4) pl.osd_img.draw_string_advanced( x , y-50,32," " + self.labels[int(det[5])] + " " + str(round(det[4],2)) , color=self.get_color(int(det[5]))) else: pl.osd_img.clear() # 多目标检测 非最大值抑制方法实现 def nms(self,boxes,scores,thresh): """Pure Python NMS baseline.""" x1,y1,x2,y2 = boxes[:, 0],boxes[:, 1],boxes[:, 2],boxes[:, 3] areas = (x2 - x1 + 1) * (y2 - y1 + 1) order = np.argsort(scores,axis = 0)[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) new_x1,new_y1,new_x2,new_y2,new_areas = [],[],[],[],[] for order_i in order: new_x1.append(x1[order_i]) new_x2.append(x2[order_i]) new_y1.append(y1[order_i]) new_y2.append(y2[order_i]) new_areas.append(areas[order_i]) new_x1 = np.array(new_x1) new_x2 = np.array(new_x2) new_y1 = np.array(new_y1) new_y2 = np.array(new_y2) xx1 = np.maximum(x1[i], new_x1) yy1 = np.maximum(y1[i], new_y1) xx2 = np.minimum(x2[i], new_x2) yy2 = np.minimum(y2[i], new_y2) w = np.maximum(0.0, xx2 - xx1 + 1) h = np.maximum(0.0, yy2 - yy1 + 1) inter = w * h new_areas = np.array(new_areas) ovr = inter / (areas[i] + new_areas - inter) new_order = [] for ovr_i,ind in enumerate(ovr): if ind < thresh: new_order.append(order[ovr_i]) order = np.array(new_order,dtype=np.uint8) return keep # 根据当前类别索引获取框的颜色 def get_color(self, x): idx=x%len(self.color_four) return self.color_four[idx] if __name__=="__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="lcd" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 模型路径 kmodel_path="/sdcard/app/tests/kmodel/yolov8n_320.kmodel" labels = ["person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush"] # 其它参数设置 confidence_threshold = 0.2 nms_threshold = 0.2 max_boxes_num = 50 rgb888p_size=[320,320] # 初始化PipeLine pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode) pl.create() # 初始化自定义目标检测实例 ob_det=ObjectDetectionApp(kmodel_path,labels=labels,model_input_size=[320,320],max_boxes_num=max_boxes_num,confidence_threshold=confidence_threshold,nms_threshold=nms_threshold,rgb888p_size=rgb888p_size,display_size=display_size,debug_mode=0) ob_det.config_preprocess() clock = time.clock() try: while True: os.exitpoint() clock.tick() img=pl.get_frame() # 获取当前帧数据 res=ob_det.run(img) # 推理当前帧 ob_det.draw_result(pl,res) # 绘制结果到PipeLine的osd图像 print(res) # 打印当前结果 pl.show_image() # 显示当前的绘制结果 gc.collect() print(clock.fps()) #打印帧率 except Exception as e: sys.print_exception(e) finally: ob_det.deinit() pl.destroy()     实验结果:     缓冲区显示各个物体的名称和置信度(可信度),可以看到准确率还是挺高的。  

  • 2024-11-06
  • 发表了主题帖: 嘉楠K230AI开发板测评7--AI Demo开发框架

    本帖最后由 dfjs 于 2024-11-6 21:01 编辑 嘉楠科K230AI开发板测评7--AI视觉篇 1、AI视觉开发框架     更高级的机器视觉(AI视觉)需要使用KPU。可以简单类别比计算机的GPU(显卡),本质是实现高速的图像数据运算。     KPU是K230内部一个神经网络处理器,它可以在低功耗的情况下实现卷积神经网络计算,实时获取被检测目标的大小、坐标和种类,对人脸或者物体进行检测和分类。K230 KPU支持INT8和INT16, 典型网络下实测推理能力可达K210的13.7倍,MAC利用率超70%。     CanMV官方基于K230专门搭建了配套的AI视觉开发框架,框架结构如下图所示:     这个框架简单来说就是Sensor(摄像头)默认输出两路图像,一路格式为YUV420,直接给到Display显示;另一路格式为RGB888,给到AI部分进行处理。AI主要实现任务的前处理、推理和后处理流程,得到后处理结果后将其绘制在osd image实例上,并送给Display叠加,最后在HDMI、LCD或IDE缓冲区显示识别结果。 这套框架的优势是用户可以直接基于处理结果编程实现自己的功能,同时AI主要实现任务的前处理、推理和后处理流程也是通过Python代码实现,方便用户深入二次开发。充分满足不同用户和开发者的需求。     AI视觉开发框架主要API接口有:     PineLine : 将sensor、display封装成固定接口,用于采集图像、画图以及结果图片显示。     Ai2d : 预处理(Preprocess)相关接口。     AIBase : 模型推理主要接口。 2、相关接口     可在该网址查看:https://developer.canaan-creative.com/k230_canmv/main/zh/example/ai/AI_Demo%E8%AF%B4%E6%98%8E%E6%96%87%E6%A1%A3.html 2.1 PipeLine     将Media部分的代码封装在PipeLine类型中,通过固定的接口实现整个流程操作。     PipeLine类提供的接口包括: 初始化参数     rgb888p_size:list类型,预设给到AI部分的图像分辨率;如rgb888p_size=[1920,1080]。     display_size:list类型,显示部分Display的分辨率;如display_size=[1920,1080]。     display_mode:str类型,显示模式,包括”hdmi“和”lcd“;如display_mode=”hdmi“。     debug_mode:int类型,耗时调试模式,如果大于0,打印操作耗时;如debug_mode=0。 creat(sensor=None,hmirror=None,vfilp=None)     sensor:参数为可选参数,类型为Sensor对象,可自主配置现有CanMV、01Studio和k230d zero开发板实现了自动探测,可以默认使用create()实现。     hmirror:默认为None,当主动设置时为bool类型(True/False),表示是否实现水平方向镜像显示。     vflip: 默认为None,当主动设置时为bool类型(True/False),表示是否实现垂直方向翻转。 get_frame     返回一帧ulab.numpy.ndarray类型图像数据,分辨率为rgb888p_size,排布为CHW。 show_image     PipeLine实例中预设一帧OSD图像,该接口将成员变量osd_img显示在屏幕上。 destroy     销毁PipeLine实例。     示例代码: from libs.PipeLine import PipeLine, ScopedTiming from media.media import * import gc import sys,os if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=[1920,1080], display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 print(img.shape) gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: pl.destroy() # 销毁PipeLine实例     通过pl.get_frame()接口获取一帧分辨率为rgb888p_size的图像,类型为ulab.numpy.ndarray,排布为CHW。基于上面的代码得到了一帧图像给AI处理,只关注AI推理部分的操作即可。     图像AI开发过程包括:图像预处理、模型推理、输出后处理的过程,整个过程封装在Ai2d类和AIBase类中。 2.2 Ai2d     对于Ai2d类,我们给出了常见的几种预处理方法,包括crop/shift/pad/resize/affine。该类别提供的接口包括: 初始化参数     debug_mode:int类型,耗时调试模式,如果大于0,打印操作耗时;如debug_mode=0。 Set_ai2d_dtype(input_format,output_format,input_type,output_type)     设置ai2d计算过程中的输入输出数据类型,输入输出数据格式。 Crop(start_x,start_y,width,height)预处理crop函数:     start_x:宽度方向的起始像素,int类型     start_y: 高度方向的起始像素,int类型     width: 宽度方向的crop长度,int类型     height: 高度方向的crop长度,int类型 Shift(shift_va预处理shift函数:     shift_val:右移的比特数,int类型 Pad(paddings,pad_mode,pad_val)预处理padding函数:     paddings:各个维度的padding, size=8,分别表示dim0到dim4的前后padding的个数,其中dim0/dim1固定配置{0, 0},list类型     pad_mode:只支持pad constant,配置0即可,int类型     pad_val:每个channel的padding value,list类型 Resize(interp_method,interp_mode)预处理resize函数:     interp_method:resize插值方法,ai2d_interp_method类型     interp_mode:resize模式,ai2d_interp_mode类型 Affine(interp_method,crop_round,bound_ind,bound_val,bound_smooth,M)预处理affine函数:     interp_method:Affine采用的插值方法,ai2d_interp_method类型     cord_round:整数边界0或者1,uint32_t类型     bound_ind:边界像素模式0或者1,uint32_t类型     bound_val:边界填充值,uint32_t类型     bound_smooth:边界平滑0或者1,uint32_t类型     M:仿射变换矩阵对应的vector,仿射变换为Y=[a_0, a_1; a_2, a_3] \cdot  X + [b_0, b_1] $, 则  M=[a_0,a_1,b_0,a_2,a_3,b_1 ],list类型 Build(ai2d_input_shape,ai2d_output_shape):ai2d构造函数,前面配置的预处理方法起作用。 Run使用ai2d完成预处理     注意:         (1) Affine和Resize功能是互斥的,不能同时开启; (2) Shift功能的输入格式只能是Raw16; (3) Pad value是按通道配置的,对应的list元素个数要与channel数相等; (4) 当配置了多个功能时,执行顺序是Crop->Shift->Resize/Affine->Pad, 配置参数时注意要匹配;如果不符合该顺序,需要初始化多个Ai2d实例实现预处理过程;       示例代码如下: from libs.PipeLine import PipeLine, ScopedTiming from libs.AI2D import Ai2d from media.media import * import nncase_runtime as nn import gc import sys,os if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=[512,512], display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 my_ai2d=Ai2d(debug_mode=0) #初始化Ai2d实例 # 配置resize预处理方法 my_ai2d.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 构建预处理过程 my_ai2d.build([1,3,512,512],[1,3,640,640]) try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 print(img.shape) # 原图shape为[1,3,512,512] ai2d_output_tensor=my_ai2d.run(img) # 执行resize预处理 ai2d_output_np=ai2d_output_tensor.to_numpy() # 类型转换 print(ai2d_output_np.shape) # 预处理后的shape为[1,3,640,640] gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: pl.destroy() # 销毁PipeLine实例 2.3 AIBase                          AIBase部分封装了实现模型推理的主要接口,也是进行AI开发主要关注的部分。用户需要按照自己demo的要求实现前处理和后处理部分。     AIBase提供的接口包括: 初始化参数     kmodel_path:str类型,kmodel路径,用于初始化kpu对象并加载kmodel;     model_input_size:list类型,可选,模型输入分辨率,在单输入时起作用,格式为[width,height],如:model_input_size=[512,512];     ​ rgb888p_size:list类型,可选,AI得到的图像的分辨率,在单输入时起作用,格式为[width,height],如:rgb888p_size=[640,640];     debug_mode:int类型,耗时调试模式,如果大于0,打印操作耗时;如debug_mode=0。 get_kmodel_inputs_num():返回当前模型的输入个数 get_kmodel_outputs_num():返回当前模型的输出个数 preprocess(input_np):使用ai2d对input_np做预处理,,如果不使用单个ai2d实例做预处理,需要在子类重写该函数。 inference(tensors):对预处理后得到的kmodel的输入(类型为tensor)进行推理,得到多个输出(类型为ulab.numpy.ndarray) postprocess(results):模型输出后处理函数,该函数需要用户在任务子类重写,因为不同AI任务的后处理是不同的 run(input_np):模型的前处理、推理、后处理流程,适用于单ai2d实例能解决的前处理的AI任务,其他任务需要用户在子类重写。 deinit():AIBase销毁函数。 2.4 ScopedTiming     ScopedTiming 类在PipeLine.py模块内,是一个用来测量代码块执行时间的上下文管理器。上下文管理器通过定义包含 __enter__ 和 __exit__ 方法的类来创建。当在 with 语句中使用该类的实例时,__enter__ 在进入 with 块时被调用,__exit__ 在离开时被调用。     示例代码: from libs.PipeLine import ScopedTiming def test_time(): with ScopedTiming("test",1): #####代码##### # ... ##############  3、应用方法和示例     用户可根据具体的AI场景自写任务类继承AIBase,可以将任务分为如下四类:单模型任务、多模型任务,自定义预处理任务、无预处理任务。不同任务需要编写不同的代码实现,具体如下图所示:   3.1 单模型任务     该任务只有一个模型,只需要关注该模型的前处理、推理、后处理过程,此类任务的前处理使用Ai2d实现,可能使用一个Ai2d实例,也可能使用多个Ai2d实例,后处理基于场景自定义。 编写自定义任务类,主要关注任务类的config_preprocess、postprocess、以及该任务需要的其他方法如:draw_result等。     如果该任务包含多个Ai2d实例,则需要重写preprocess,按照预处理的顺序设置预处理阶段的计算过程。     单模型任务的伪代码结构如下: from libs.PipeLine import PipeLine, ScopedTiming from libs.AIBase import AIBase from libs.AI2D import Ai2d import os from media.media import * import nncase_runtime as nn import ulab.numpy as np import image import gc import sys # 自定义AI任务类,继承自AIBase基类 class MyAIApp(AIBase): def __init__(self, kmodel_path, model_input_size, 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 # sensor给到AI的图像分辨率,并对宽度进行16的对齐 self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # 显示分辨率,并对宽度进行16的对齐 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) # 配置预处理操作,这里使用了resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/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 # 配置resize预处理方法 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]]) # 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写 def postprocess(self, results): with ScopedTiming("postprocess", self.debug_mode > 0): pass # 绘制结果到画面上,需要根据任务自己写 def draw_result(self, pl, dets): with ScopedTiming("display_draw", self.debug_mode > 0): pass if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 设置模型路径,这里要替换成当前任务模型 kmodel_path = "example_test.kmodel" rgb888p_size = [1920, 1080] ###### 其它参数######## ... ###################### # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 # 初始化自定义AI任务实例 my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0) my_ai.config_preprocess() # 配置预处理 try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 res = my_ai.run(img) # 推理当前帧 my_ai.draw_result(pl, res) # 绘制结果 pl.show_image() # 显示结果 gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: my_ai.deinit() # 反初始化 pl.destroy() # 销毁PipeLine实例     多个Ai2d实例时的伪代码如下: from libs.PipeLine import PipeLine, ScopedTiming from libs.AIBase import AIBase from libs.AI2D import Ai2d import os from media.media import * import nncase_runtime as nn import ulab.numpy as np import image import gc import sys # 自定义AI任务类,继承自AIBase基类 class MyAIApp(AIBase): def __init__(self, kmodel_path, model_input_size, 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 # sensor给到AI的图像分辨率,并对宽度进行16的对齐 self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # 显示分辨率,并对宽度进行16的对齐 self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]] # 是否开启调试模式 self.debug_mode = debug_mode # 实例化Ai2d,用于实现模型预处理 self.ai2d_resize = Ai2d(debug_mode) # 设置Ai2d的输入输出格式和类型 self.ai2d_resize.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8) # 实例化Ai2d,用于实现模型预处理 self.ai2d_resize = Ai2d(debug_mode) # 设置Ai2d的输入输出格式和类型 self.ai2d_resize.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8) # 实例化Ai2d,用于实现模型预处理 self.ai2d_crop = Ai2d(debug_mode) # 设置Ai2d的输入输出格式和类型 self.ai2d_crop.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT, nn.ai2d_format.NCHW_FMT, np.uint8, np.uint8) # 配置预处理操作,这里使用了resize和crop,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/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 # 配置resize预处理方法 self.ai2d_resize.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel) # 构建预处理流程 self.ai2d_resize.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,640,640]) # 配置crop预处理方法 self.ai2d_crop.crop(0,0,320,320) # 构建预处理流程 self.ai2d_crop.build([1,3,640,640],[1,3,320,320]) # 假设该任务需要crop和resize预处理,顺序是先resize再crop,该顺序不符合ai2d的处理顺序,因此需要设置两个Ai2d实例分别处理 def preprocess(self,input_np): resize_tensor=self.ai2d_resize.run(input_np) resize_np=resize_tensor.to_numpy() crop_tensor=self.ai2d_crop.run(resize_np) return [crop_tensor] # 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写 def postprocess(self, results): with ScopedTiming("postprocess", self.debug_mode > 0): pass # 绘制结果到画面上,需要根据任务自己写 def draw_result(self, pl, dets): with ScopedTiming("display_draw", self.debug_mode > 0): pass # 重写deinit,释放多个ai2d资源 def deinit(self): with ScopedTiming("deinit",self.debug_mode > 0): del self.ai2d_resize del self.ai2d_crop super().deinit() if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 设置模型路径,这里要替换成当前任务模型 kmodel_path = "example_test.kmodel" rgb888p_size = [1920, 1080] ###### 其它参数######## ... ###################### # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 # 初始化自定义AI任务实例 my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0) my_ai.config_preprocess() # 配置预处理 try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 res = my_ai.run(img) # 推理当前帧 my_ai.draw_result(pl, res) # 绘制结果 pl.show_image() # 显示结果 gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: my_ai.deinit() # 反初始化 pl.destroy() # 销毁PipeLine实例 3.2 自定义预处理任务     该任务只有一个模型,只需要关注该模型的前处理、推理、后处理过程,此类任务的前处理不使用Ai2d实现,可以使用ulab.numpy自定义,后处理基于场景自定义。     编写自定义任务类,主要关注任务类的preprocess、postprocess、以及该任务需要的其他方法如:draw_result等     对于需要重写前处理(不使用提供的ai2d类,自己手动写预处理)的AI任务伪代码如下: from libs.PipeLine import PipeLine, ScopedTiming from libs.AIBase import AIBase from libs.AI2D import Ai2d import os from media.media import * import nncase_runtime as nn import ulab.numpy as np import image import gc import sys # 自定义AI任务类,继承自AIBase基类 class MyAIApp(AIBase): def __init__(self, kmodel_path, model_input_size, 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 # sensor给到AI的图像分辨率,并对宽度进行16的对齐 self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # 显示分辨率,并对宽度进行16的对齐 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) # 对于不使用ai2d完成预处理的AI任务,使用封装的接口或者ulab.numpy实现预处理,需要在子类中重写该函数 def preprocess(self,input_np): ############# #注意自定义预处理过程 ############# return [tensor] # 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写 def postprocess(self, results): with ScopedTiming("postprocess", self.debug_mode > 0): pass # 绘制结果到画面上,需要根据任务自己写 def draw_result(self, pl, dets): with ScopedTiming("display_draw", self.debug_mode > 0): pass if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 设置模型路径,这里要替换成当前任务模型 kmodel_path = "example_test.kmodel" rgb888p_size = [1920, 1080] ###### 其它参数######## ... ###################### # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 # 初始化自定义AI任务实例 my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0) my_ai.config_preprocess() # 配置预处理 try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 res = my_ai.run(img) # 推理当前帧 my_ai.draw_result(pl, res) # 绘制结果 pl.show_image() # 显示结果 gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: my_ai.deinit() # 反初始化 pl.destroy() # 销毁PipeLine实例 3.3 无预处理任务     该任务只有一个模型且不需要预处理,只需要关注该模型的推理和后处理过程,此类任务一般作为多模型任务的一部分,直接对前一个模型的输出做为输入推理,后处理基于需求自定义。     编写自定义任务类,主要关注任务类的run(模型推理的整个过程,包括preprocess、inference、postprocess中的全部或某一些步骤)、postprocess、以及该任务需要的其他方法如:draw_results等     对于不需要预处理(直接输入推理)的AI任务伪代码如下: from libs.PipeLine import PipeLine, ScopedTiming from libs.AIBase import AIBase from libs.AI2D import Ai2d import os from media.media import * import nncase_runtime as nn import ulab.numpy as np import image import gc import sys # 自定义AI任务类,继承自AIBase基类 class MyAIApp(AIBase): def __init__(self, kmodel_path, model_input_size, 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 # sensor给到AI的图像分辨率,并对宽度进行16的对齐 self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # 显示分辨率,并对宽度进行16的对齐 self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]] # 是否开启调试模式 self.debug_mode = debug_mode # 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写 def postprocess(self, results): with ScopedTiming("postprocess", self.debug_mode > 0): pass # 对于用预处理的AI任务,需要在子类中重写该函数 def run(self,inputs_np): # 先将ulab.numpy.ndarray列表转换成tensor列表 tensors=[] for input_np in inputs_np: tensors.append(nn.from_numpy(input_np)) # 调用AIBase内的inference函数进行模型推理 results=self.inference(tensors) # 调用当前子类的postprocess方法进行自定义后处理 outputs=self.postprocess(results) return outputs # 绘制结果到画面上,需要根据任务自己写 def draw_result(self, pl, dets): with ScopedTiming("display_draw", self.debug_mode > 0): pass if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] # 设置模型路径,这里要替换成当前任务模型 kmodel_path = "example_test.kmodel" rgb888p_size = [1920, 1080] ###### 其它参数######## ... ###################### # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 # 初始化自定义AI任务实例 my_ai = MyAIApp(kmodel_path, model_input_size=[320, 320],rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0) my_ai.config_preprocess() # 配置预处理 try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 res = my_ai.run(img) # 推理当前帧 my_ai.draw_result(pl, res) # 绘制结果 pl.show_image() # 显示结果 gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: my_ai.deinit() # 反初始化 pl.destroy() # 销毁PipeLine实例 3.4 多模型任务     该任务包含多个模型,可能是串联,也可能是其他组合方式。对于每个模型基本上属于前三种模型中的一种,最后通过一个完整的任务类将上述模型子任务统一起来。     编写多个子模型任务类,不同子模型任务参照前三种任务定义。不同任务关注不同的方法。     编写多模型任务类,将子模型任务类统一起来实现整个场景。     以双模型串联推理为例,给出的伪代码如下: from libs.PipeLine import PipeLine, ScopedTiming from libs.AIBase import AIBase from libs.AI2D import Ai2d import os from media.media import * import nncase_runtime as nn import ulab.numpy as np import image import gc import sys # 自定义AI任务类,继承自AIBase基类 class MyAIApp_1(AIBase): def __init__(self, kmodel_path, model_input_size, 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 # sensor给到AI的图像分辨率,并对宽度进行16的对齐 self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # 显示分辨率,并对宽度进行16的对齐 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) # 配置预处理操作,这里使用了resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/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 # 配置resize预处理方法 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]]) # 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写 def postprocess(self, results): with ScopedTiming("postprocess", self.debug_mode > 0): pass # 自定义AI任务类,继承自AIBase基类 class MyAIApp_2(AIBase): def __init__(self, kmodel_path, model_input_size, 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 # sensor给到AI的图像分辨率,并对宽度进行16的对齐 self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # 显示分辨率,并对宽度进行16的对齐 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) # 配置预处理操作,这里使用了resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/app/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 # 配置resize预处理方法 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]]) # 自定义当前任务的后处理,results是模型输出array列表,需要根据实际任务重写 def postprocess(self, results): with ScopedTiming("postprocess", self.debug_mode > 0): pass class MyApp: def __init__(kmodel1_path,kmodel2_path,kmodel1_input_size,kmodel2_input_size,rgb888p_size,display_size,debug_mode): # 创建两个模型推理的实例 self.app_1=MyApp_1(kmodel1_path,kmodel1_input_size,rgb888p_size,display_size,debug_mode) self.app_2=MyApp_2(kmodel2_path,kmodel2_input_size,rgb888p_size,display_size,debug_mode) self.app_1.config_preprocess() # 编写run函数,具体代码根据AI任务的需求编写,此处只是给出一个示例 def run(self,input_np): outputs_1=self.app_1.run(input_np) outputs_2=[] for out in outputs_1: self.app_2.config_preprocess(out) out_2=self.app_2.run(input_np) outputs_2.append(out_2) return outputs_1,outputs_2 # 绘制 def draw_result(self,pl,outputs_1,outputs_2): pass ######其他函数######## # 省略 #################### if __name__ == "__main__": # 显示模式,默认"hdmi",可以选择"hdmi"和"lcd" display_mode="hdmi" if display_mode=="hdmi": display_size=[1920,1080] else: display_size=[800,480] rgb888p_size = [1920, 1080] # 设置模型路径,这里要替换成当前任务模型 kmodel1_path = "test_kmodel1.kmodel" kmdoel1_input_size=[320,320] kmodel2_path = "test_kmodel2.kmodel" kmodel2_input_size=[48,48] ###### 其它参数######## # 省略 ###################### # 初始化PipeLine,用于图像处理流程 pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode) pl.create() # 创建PipeLine实例 # 初始化自定义AI任务实例 my_ai = MyApp(kmodel1_path,kmodel2_path, kmodel1_input_size,kmodel2_input_size,rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0) my_ai.config_preprocess() # 配置预处理 try: while True: os.exitpoint() # 检查是否有退出信号 with ScopedTiming("total",1): img = pl.get_frame() # 获取当前帧数据 outputs_1,outputs_2 = my_ai.run(img) # 推理当前帧 my_ai.draw_result(pl, outputs_1,outputs_2) # 绘制结果 pl.show_image() # 显示结果 gc.collect() # 垃圾回收 except Exception as e: sys.print_exception(e) # 打印异常信息 finally: my_ai.app_1.deinit() # 反初始化 my_ai.app_2.deinit() pl.destroy() # 销毁PipeLine实例  

  • 2024-11-04
  • 发表了主题帖: 嘉楠K230AI开发板测评6--条形码、二维码与AprilTag标签识别

    嘉楠科K230AI开发板测评6--机器视觉篇 1、条形码识别         条形码(barcode)是将宽度不等的多个黑条和空白,按照一定的编码规则排列,用以表达一组信息的图形标识符。常见的条形码是由反射率相差很大的黑条(简称条)和白条(简称空)排成的平行线图案。条形码可以标出物品的生产国、制造厂家、商品名称、生产日期、图书分类号、邮件起止地点、类别、日期等许多信息,因而在商品流通、图书管理、邮政管理、银行系统等许多领域都得到广泛的应用。         编程实现条形码识别,并将识别到的信息通过串口终端打印出来。         对于CanMV K230而言,直接使用MicroPython中的find_barcodes()即可获取摄像头采集图像中条形码的相关信息。         该函数支持所有一维条形码:image.EAN2 image.EAN5 image.EAN8 image.UPCE image.ISBN10 image.UPCA image.EAN13 image.ISBN13 image.I25 image.DATABAR (RSS-14) image.DATABAR_EXP (RSS-Expanded) image.CODABAR image.CODE39 image.PDF417 image.CODE93 image.CODE128 条形码对象是由 image.find_barcodes 返回的。         barcode.corners()返回一个由该对象的四个角组成的四个元组(x,y)的列表。四个角通常是按照从左上角开始沿顺时针顺序返回的。         barcode.rect()返回一个矩形元组(x, y, w, h),用于如数据矩阵的边界框的 image.draw_rectangle 等其他的 image 方法。         barcode.payload()返回条形码的有效载荷的字符串。例:数量。         barcode.type()返回条形码的列举类型 (int)。         barcode.rotation()返回以弧度计的条形码的旋度(浮点数)。         barcode.quality()返回条形码在图像中被检测到的次数(int)。         调用find_barcodes()函数,对得到的结果再进行处理即可,代码编写流程如下:           参考代码如下: ''' 实验名称:条形码识别 实验平台:01Studio CanMV K230 说明:编程实现摄像头识别各类条形码 ''' import time, math, os, gc from media.sensor import * #导入sensor模块,使用摄像头相关接口 from media.display import * #导入display模块,使用display相关接口 from media.media import * #导入media模块,使用meida相关接口 #定义条形码类型 def barcode_name(code): if(code.type() == image.EAN2): return "EAN2" if(code.type() == image.EAN5): return "EAN5" if(code.type() == image.EAN8): return "EAN8" if(code.type() == image.UPCE): return "UPCE" if(code.type() == image.ISBN10): return "ISBN10" if(code.type() == image.UPCA): return "UPCA" if(code.type() == image.EAN13): return "EAN13" if(code.type() == image.ISBN13): return "ISBN13" if(code.type() == image.I25): return "I25" if(code.type() == image.DATABAR): return "DATABAR" if(code.type() == image.DATABAR_EXP): return "DATABAR_EXP" if(code.type() == image.CODABAR): return "CODABAR" if(code.type() == image.CODE39): return "CODE39" if(code.type() == image.PDF417): return "PDF417" if(code.type() == image.CODE93): return "CODE93" if(code.type() == image.CODE128): return "CODE128" try: sensor = Sensor() #构建摄像头对象 sensor.reset() #复位和初始化摄像头 #sensor.set_framesize(Sensor.FHD) #设置帧大小FHD(1920x1080),默认通道0 sensor.set_framesize(width=800, height=480) #设置帧大小VGA,默认通道0 sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0 Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率 #Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像 MediaManager.init() #初始化media资源管理器 sensor.run() #启动sensor clock = time.clock() while True: os.exitpoint() #检测IDE中断 clock.tick() img = sensor.snapshot() #拍摄图片 codes = img.find_barcodes() #查找图像中所有条形码 for code in codes: #对条码画矩形表示 img.draw_rectangle(code.rect(),thickness=2) #打印相关信息 print_args = (barcode_name(code), code.payload(), (180 * code.rotation()) / math.pi, code.quality()) print("Barcode %s, Payload \"%s\", rotation %f (degrees), quality %d" % print_args) img.draw_string_advanced(0, 0, 30, code.payload(), color = (255, 255, 255)) #图像显示条码信息 Display.show_image(img) #显示图片 print(clock.fps()) #打印帧率 ################### # IDE中断释放资源代码 ################### 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()         实验结果如下,为了更好地识别,图像上条形码需比较平展,不能太小;在线生成一个条形码值位“ABC-abc-1234”的Code 128型条形码,运行程序,打开条形码图片。摄像头正对条形码,识别成功后可以看到图片出现方框以及在串口终端打印出条形码信息。   2、二维码识别         二维码又称二维条码,常见的二维码为QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型。         二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的、黑白相间的、记录数据符号信息的图形;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。         MicroPython中的find_qrcodes()即可获取摄像头采集图像中二维码的相关信息。         二维码对象是由 image.find_qrcodes 返回的。         qrcode.corners()返回一个由该对象的四个角组成的四个元组(x,y)的列表。四个角通常是按照从左上角开始沿顺时针顺序返回的。         qrcode.rect()返回一个矩形元组(x, y, w, h),用于如二维码的边界框的 image.draw_rectangle 等其他的 image 方法。         qrcode.payload()返回二维码有效载荷的字符串,例如URL 。         qrcode.version()返回二维码的版本号(int)。         qrcode.ecc_level()返回二维码的ECC水平(int)。         qrcode.mask()返回二维码的掩码(int)。         qrcode.data_type()返回二维码的数据类型。         qrcode.eci()返回二维码的ECI。ECI储存了QR码中存储数据字节的编码。若想要处理包含超过标准ASCII文本的二维码,您需要查看这一数值。         MicroPython编程我们只需要简单地调用find_qrcodes()函数,对得到的结果再进行处理即可,代码编写流程如下:           核心代码如下: '''''''''''' img = sensor.snapshot() #拍摄图片 res = img.find_qrcodes() #寻找所有二维码,返回列表,每个值为一个二维码 if len(res) > 0: #在图片和终端显示二维码信息 img.draw_rectangle(res[0].rect(), thickness=2)#列表不能属性,列表的值可以属性 img.draw_string_advanced(0, 0, 30, res[0].payload(), color = (255, 255, 255)) print(res[0].payload()) #串口终端打印 Display.show_image(img) #显示图片 print(clock.fps()) #打印帧率 ''''''''''''         实验结果,为了更好地识别,图像上二维码需比较平展,不能太小,在线生成一个值为“https://www.bing.com”的QR码,运行程序,打开二维码图片;摄像头正对二维码,识别成功后可以看到图片出现方框以及在串口终端打印出二维码信息。   3、AprilTag标签识别         AprilTag是一种视觉基准系统,可用于多种任务,包括增强现实、机器人和相机校准。可以通过普通打印机创建目标,AprilTag 检测软件可以计算标签相对于相机的精确3D位置、方向和标识。         AprilTag通过特定的标志(与二维码相似,但是降低了复杂度以满足实时性要求),可以快速地检测标志,并计算相对位置。         AprilTag内容主要包含三个步骤:         第一步是如何根据梯度检测出图像中的各种边缘。         第二步即如何在边缘图像中找出需要的四边形图案并进行筛选,AprilTag尽可能的对检测出的边缘检测,首先剔除非直线边缘,在直线边缘进行邻接边缘查找,最终若形成闭环则为检测到一个四边形。         最后一个步便是如何进行二维码编码和二维码解码,编码方式分为三种,其黑边色块长度分别为8,7,6三个色块长度,对于解码内容,要在检测到的四边形内生成点阵列用于计算每色块的值,再根据局部二值模式(Local Binary Patterns)构造简单分类器对四边形内的色块进行分类,将正例色块编码为1将负例色块编码为0,就可以得到该二维码的编码。得到编码以后再与已知库内的编码进行匹配,确定解码出的二维码是否为正确。 可以将AprilTag简单地理解为一个特定信息的二维码,有family和ID两个概念:         TAG16H5 → 0 to 29         TAG25H7 → 0 to 241         TAG25H9 → 0 to 34         TAG36H10 → 0 to 2319         TAG36H11 → 0 to 586 (CanMV K230推荐使用)         ARTOOLKIT → 0 to 511         以【TAG36H11 → 0 to 586】为例,family信息就是:TAG36H11 , ID可以是“0 到 586” ,也就是一共有587种标记码。         不同家族区别:TAG16H5的有效区域是 4x4 的方块,那么它比TAG36H11看的更远(因为他有 6x6 个方块)。 但是内容少,所以TAG16H5的错误率比TAG36H11 高很多,因为TAG36H11的校验信息多。CanMV K210推荐使用TAG36H11家族的标记码。         可以在CanMV IDE生成AprilTag。点击工具--机器视觉--AprilTag生成器--TAG36H11家族:最小输入0 ,最大输入9 ,制作id从0-9共10张标签。点击OK后选择要生成的位置文件夹即可,如下图:           识别apriltag使用find_apriltags对象函数,返回一个 image.apriltag 对象的列表。         与二维码相比,AprilTags可在更远距离、较差光线和更扭曲的图像环境下被检测到。 AprilTags可应对所有种类的图像失真问题,而二维码并不能。也就是说,AprilTags只能将数字ID编码作为其有效载荷。         AprilTags也可用于本地化。每个 image.apriltag 对象都从摄像机返回其三维位置信息和旋转角度。 位置信息由 fx 、 fy 、 cx 和 cy 决定,分别为X和Y方向上图像的焦距和中心点。         tag.rect()返回一个矩形元组(x,y,w,h),二维码的边界。可以通过索引[0-3]来获得单个值。         tag.family()家族信息。         tag.id()ID信息。         tag.rotation()方向。         代码编写流程如下:           参考代码如下: ''' 实验名称:AprilTags标签识别 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc 说明:推荐使用QVGA(320x240)分辨率,分辨率太高帧率会下降。 ''' import time, math, os, gc from media.sensor import * #导入sensor模块,使用摄像头相关接口 from media.display import * #导入display模块,使用display相关接口 from media.media import * #导入media模块,使用meida相关接口 # apriltag代码最多支持可以同时处理6种tag家族。 # 返回的tag标记对象,将有其tag标记家族及其在tag标记家族内的id。 tag_families = 0 tag_families |= image.TAG16H5 # 注释掉,禁用这个家族 tag_families |= image.TAG25H7 # 注释掉,禁用这个家族 tag_families |= image.TAG25H9 # 注释掉,禁用这个家族 tag_families |= image.TAG36H10 # 注释掉,禁用这个家族 tag_families |= image.TAG36H11 # 注释掉以禁用这个家族(默认家族) tag_families |= image.ARTOOLKIT # 注释掉,禁用这个家族 #标签系列有什么区别? 那么,例如,TAG16H5家族实际上是一个4x4的方形标签。 #所以,这意味着可以看到比6x6的TAG36H11标签更长的距离。 #然而,较低的H值(H5对H11),意味着4x4标签的假阳性率远高于6x6标签。 #所以,除非你有理由使用其他标签系列,否则使用默认族TAG36H11。 def family_name(tag): if(tag.family() == image.TAG16H5): return "TAG16H5" if(tag.family() == image.TAG25H7): return "TAG25H7" if(tag.family() == image.TAG25H9): return "TAG25H9" if(tag.family() == image.TAG36H10): return "TAG36H10" if(tag.family() == image.TAG36H11): return "TAG36H11" if(tag.family() == image.ARTOOLKIT): return "ARTOOLKIT" try: sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3 sensor.reset() #复位和初始化摄像头 sensor.set_framesize(width=320, height=240) #设置帧大小为LCD分辨率(800x480),默认通道0 sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0 Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率 #Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像 MediaManager.init() #初始化media资源管理器 sensor.run() #启动sensor clock = time.clock() while True: os.exitpoint() #检测IDE中断 clock.tick() img = sensor.snapshot() #拍摄图片 for tag in img.find_apriltags(families=tag_families): # 如果没有给出家族,默认TAG36H11。 img.draw_rectangle(tag.rect(), color = (255, 0, 0), thickness=4) img.draw_cross(tag.cx(), tag.cy(), color = (0, 255, 0), thickness=2) print_args = (family_name(tag), tag.id(), (180 * tag.rotation()) / math.pi) #打印标签信息 print("Tag Family %s, Tag ID %d, rotation %f (degrees)" % print_args) #img.draw_string_advanced(0, 0, 30, code.payload(), color = (255, 255, 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()) #打印帧率 ################### # IDE中断释放资源代码 ################### 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()         实验结果如下,打开family: TAG36H11 , id: 0的标签图片测试:  

  • 2024-11-03
  • 回复了主题帖: 嘉楠K230AI开发板测评4--图像显示、画图、边缘/线段/圆形/矩形检测、线性回归

    秦天qintian0303 发表于 2024-11-2 23:09 这个学习怎么喂图啊?拍摄好的?还是喂视频? 直接电脑截图,哈哈

  • 回复了主题帖: 嘉楠K230AI开发板测评4--图像显示、画图、边缘/线段/圆形/矩形检测、线性回归

    littleshrimp 发表于 2024-11-2 21:58 这个开发板这么强大吗?如果不用micropython能实现相同的功能吗?或者micropython在商用产品上使用适不 ... micropython都是封装好的函数,比较容易上手,也可以用linux开发,不过比较麻烦,没有micropython的生态好,个人感觉拿来商用的话成本还是太高了  

  • 发表了主题帖: 嘉楠K230AI开发板测评5---颜色识别、摄像头物体计数与巡线

    本帖最后由 dfjs 于 2024-11-3 21:29 编辑 嘉楠科K230AI开发板测评5--机器视觉篇 1.单一颜色识别         预先设定颜色阈值,如红、绿、蓝,这样K230摄像头采集图像后就能自动识别了。         CanMV集成了RGB565颜色块识别find_blobs函数(其位于image模块下),主要是基于LAB(L:亮度,取值0-100,表示从纯黑到纯白的变化;A代表从绿色到红色的范围,取值是-128--127;B代表从蓝色到黄色的范围,取值是-128--127)颜色模型,每个颜色都是用一组LAB阈值表示。         image.find_blobs(thresholds[, invert=False[, roi[, x_stride=2[, y_stride=1[, area_threshold=10 [, pixels_threshold=10[, merge=False[, margin=0[, threshold_cb=None[, merge_cb=None]]]]]]]]]])函数查找图像中指定的色块,返回image.blog对象列表。         thresholds: 必须是元组列表。 [(lo, hi), (lo, hi), ..., (lo, hi)] 定义你想追踪的颜色范围。 对于灰度图像,每个元组需要包含两个值 - 最小灰度值和最大灰度值。 仅考虑落在这些阈值之间的像素区域。 对于RGB565图像,每个元组需要有六个值(l_lo,l_hi,a_lo,a_hi,b_lo,b_hi) - 分别是LAB L,A和B通道的最小值和最大值;         area_threshold: 若色块的边界框区域小于此参数值,则会被过滤掉;         pixels_threshold: 若色块的像素数量小于此参数值,则会被过滤掉;         merge: 若为True,则合并所有没有被过滤的色块;         margin: 调整合并色块的边缘。         blob.rect()函数返回一个矩形元组(x,y,w,h),如色块边界。可以通过索引[0-3]来获得这些值。         blob.cx()返回色块(int)的中心x位置。可以通过索引[5]来获得这个值。         blob.cy()返回色块(int)的中心y位置。可以通过索引[6]来获得这个值。         代码编写流程如下:           参考代码: ''' 实验名称:单一颜色识别 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' import time, os, sys from media.sensor import * #导入sensor模块,使用摄像头相关接口 from media.display import * #导入display模块,使用display相关接口 from media.media import * #导入media模块,使用meida相关接口 # 颜色识别阈值 (L Min, L Max, A Min, A Max, B Min, B Max) LAB模型 # 下面的阈值元组是用来识别 红、绿、蓝三种颜色,当然你也可以调整让识别变得更好。 thresholds = [(30, 100, 15, 127, 15, 127), # 红色阈值 (30, 100, -64, -8, 50, 70), # 绿色阈值 (0, 40, 0, 90, -128, -20)] # 蓝色阈值 try: sensor = Sensor() #构建摄像头对象 sensor.reset() #复位和初始化摄像头 sensor.set_framesize(width=800, height=480) #设置帧大小为LCD分辨率(800x480),默认通道0 sensor.set_pixformat(Sensor.RGB565) #设置输出图像格式,默认通道0 Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率 #Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像 MediaManager.init() #初始化media资源管理器 sensor.run() #启动sensor clock = time.clock() while True: os.exitpoint() #检测IDE中断 ################ ## 这里编写代码 ## ################ clock.tick() img = sensor.snapshot() #拍摄一张图片 blobs = img.find_blobs([thresholds[0]]) # 0,1,2分别表示红,绿,蓝色。 if blobs: for b in blobs: #画矩形和箭头表示 tmp=img.draw_rectangle(b[0:4], thickness = 2) tmp=img.draw_cross(b[5], b[6], thickness = 2) img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps())), color = (255, 255, 255)) 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()         实验结果,如下图1,也可通过阈值编辑器来手动调节LAB的阈值范围,如下图2。     2.多种颜色识别         基于单一颜色识别,加以修改,即可实现多种颜色识别。         代码编写流程如下:           核心代码如下,与单一颜色识别例程相比,修改的代码如下,在颜色识别前中加入了for循环,识别预设的3种颜色: # 颜色识别阈值 (L Min, L Max, A Min, A Max, B Min, B Max) LAB模型 # 下面的阈值元组是用来识别 红、绿、蓝三种颜色,当然你也可以调整让识别变得更好。 thresholds = [(30, 100, 15, 127, 15, 127), # 红色阈值 (30, 100, -64, -8, 50, 70), # 绿色阈值 (0, 40, 0, 90, -128, -20)] # 蓝色阈值 colors1 = [(255,0,0), (0,255,0), (0,0,255)] colors2 = ['RED', 'GREEN', 'BLUE'] .............. img = sensor.snapshot() #拍摄一张图片 for i in range(3): blobs = img.find_blobs([thresholds[i]]) # 0,1,2分别表示红,绿,蓝色。 if blobs: for b in blobs: #画矩形、箭头和字符表示 tmp=img.draw_rectangle(b[0:4], thickness = 4, color = colors1[i]) tmp=img.draw_cross(b[5], b[6], thickness = 2) tmp=img.draw_string_advanced(b[0], b[1]-35, 30, colors2[i],color = colors1[i]) img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps())), color = (255, 255, 255)) Display.show_image(img) #显示图片 print(clock.fps()) #打印FPS         实验结果如下,将每个颜色的圆形用矩形画出并表明颜色,与单一颜色识别例程相比,修改的代码如下,在颜色识别前中加入了for循环,识别预设的3种颜色:   3.物体计数(相同颜色)         基于上一节颜色识别我们看到可以识别出色块的数量,来学习如何识别指定颜色的物体,计算其数量。         针对不同颜色的物体我们如何获取它的阈值呢?         先使用 摄像头代码采集物体图像,在IDE右上角缓冲区点击“禁用”将要识别的物体确认下来;点击 工具—机器视觉—阈值编辑器 。在弹出的对话框选择“帧缓冲区”。通过调整下方6个LAB值,使得物体颜色在右边为白色,其余背景为黑色。记录颜色的LAB值,在后面代码中使用,如下图。           代码编写流程如下图:           核心代码如下: ............. thresholds = [(18, 72, -13, 31, 18, 83)] #黄色跳线帽阈值 ............. img = sensor.snapshot() blobs = img.find_blobs([thresholds[0]]) if blobs: #画框显示 for b in blobs: tmp=img.draw_rectangle(b[0:4]) tmp=img.draw_cross(b[5], b[6]) #显示计算信息 img.draw_string_advanced(0, 0, 30, 'FPS: '+str("%.3f"%(clock.fps()))+' Num: ' +str(len(blobs)), color = (255, 255, 255)) Display.show_image(img) print(clock.fps()) #打印FPS .............         实验结果如下图,在阈值准确的情况下,统计出跳线帽的数量:   4.机器人巡线(实线)         机器人巡线依然基于颜色识别,根据摄像头采集到的图像直线与中心偏离的位置计算出偏离角度。         对画面是有一定要求的,也就是摄像头采集图像一定要出现唯一1条连续的黑色直线。程序通过对画面切割成三部分,计算每个部分黑色线的中心点X坐标,然后采用加权平均算法估算出直线的偏离位置。通常情况下越靠近底部的地方离摄像头越近,顶部表示远方线段。因此底部的图形权重高。         假设摄像头当前画面的像素是例程的QQVGA分辨率:160(宽)X120(高),左上角坐标为(0,0),然后当前出现直线坐标为(80,120)至(160,0)偏右的直线。上中下三个部分的权重分别为0.1、0.3、0.7(底部图像靠近机器人,权重大,权重总和可以不是1),我们来计算一下其中心值,如下图:           上图中Y轴的中点坐标就是60,X坐标加权平均值计算如下: X=(80*0.7+120*0.3+160*0.1)/(0.7+0.3+0.1)=98         那么直线偏离坐标可以认为是(98,60),图中绿色“+”位置。那么利用反正切函数可以求出偏离角度:a = atan((98-80)/60)=16.7°,机器人相当于实线的位置往左偏了,所以加一个负号,即 -16.7°;偏离角度就是这么计算出来的。得到偏离角度后就可以自己编程去调整小车或者机器人的运动状态,直到0°为没有偏离。 代码编写思路如下:           参考代码如下: ''' 实验名称:机器人巡线(实线) 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc # 黑色灰度线巡线跟踪示例 # #做一个跟随机器人的机器人需要很多的努力。这个示例脚本 #演示了如何做机器视觉部分的线跟随机器人。你 #可以使用该脚本的输出来驱动一个差分驱动机器人 #跟着一条线走。这个脚本只生成一个表示的旋转值(偏离角度) #你的机器人向左或向右。 # # 为了让本示例正常工作,你应该将摄像头对准一条直线(实线) #并将摄像头调整到水平面45度位置。请保证画面内只有1条直线。 ''' import time, os, sys, math from media.sensor import * #导入sensor模块,使用摄像头相关接口 from media.display import * #导入display模块,使用display相关接口 from media.media import * #导入media模块,使用meida相关接口 # 追踪黑线。使用 [(128, 255)] 追踪白线. GRAYSCALE_THRESHOLD = [(0, 64)] # 下面是一个roi【区域】元组列表。每个 roi 用 (x, y, w, h)表示的矩形。 ''' #采样图像QQVGA 160*120,列表把roi把图像分成3个矩形,越靠近的摄像头视野(通常为图像下方)的矩形权重越大。 ROIS = [ # [ROI, weight] (0, 100, 160, 20, 0.7), # 可以根据不同机器人情况进行调整。 (0, 50, 160, 20, 0.3), (0, 0, 160, 20, 0.1) ] ''' #采样图像为QVGA 320*240,列表把roi把图像分成3个矩形,越靠近的摄像头视野(通常为图像下方)的矩形权重越大。 ROIS = [ # [ROI, weight] (0, 200, 320, 40, 0.7), # 可以根据不同机器人情况进行调整。 (0, 100, 320, 40, 0.3), (0, 0, 320, 40, 0.1) ] # 计算以上3个矩形的权值【weight】的和,和不需要一定为1. weight_sum = 0 for r in ROIS: weight_sum += r[4] # r[4] 为矩形权重值. try: sensor = Sensor(width=1280, height=960) #构建摄像头对象,将摄像头长宽设置为4:3 sensor.reset() #复位和初始化摄像头 sensor.set_framesize(width=320, height=240) #设置帧大小,默认通道0 sensor.set_pixformat(Sensor.GRAYSCALE) #设置输出图像格式,默认通道0 Display.init(Display.ST7701, to_ide=True) #同时使用3.5寸mipi屏和IDE缓冲区显示图像,800x480分辨率 #Display.init(Display.VIRT, sensor.width(), sensor.height()) #只使用IDE缓冲区显示图像 MediaManager.init() #初始化media资源管理器 sensor.run() #启动sensor clock = time.clock() while True: os.exitpoint() #检测IDE中断 ################ ## 这里编写代码 ## ################ clock.tick() img = sensor.snapshot() #拍摄一张图片 centroid_sum = 0 for r in ROIS: blobs = img.find_blobs(GRAYSCALE_THRESHOLD, roi=r[0:4], merge=True) # r[0:4] 是上面定义的roi元组. if blobs: # Find the blob with the most pixels. largest_blob = max(blobs, key=lambda b: b.pixels()) # Draw a rect around the blob. img.draw_rectangle(largest_blob.rect()) img.draw_cross(largest_blob.cx(), largest_blob.cy()) centroid_sum += largest_blob.cx() * r[4] # r[4] 是每个roi的权重值. center_pos = (centroid_sum / weight_sum) # 确定直线的中心. # 将直线中心位置转换成角度,便于机器人处理. deflection_angle = 0 # 使用反正切函数计算直线中心偏离角度。可以自行画图理解 #权重X坐标落在图像左半部分记作正偏,落在右边部分记为负偏,所以计算结果加负号。 #deflection_angle = -math.atan((center_pos-80)/60) #采用图像为QQVGA 160*120时候使用 deflection_angle = -math.atan((center_pos-160)/120) #采用图像为QVGA 320*240时候使用 # 将偏离值转换成偏离角度. deflection_angle = math.degrees(deflection_angle) # 计算偏离角度后可以控制机器人进行调整. print("Turn Angle: %f" % deflection_angle) # LCD显示偏移角度,scale参数可以改变字体大小 img.draw_string_advanced(2,2,20, str('%.1f' % deflection_angle), color=(255,255,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 ################### # 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()         实验结果如下,以手机做黑色直线用,分别观察摄像头采集到没偏移、左偏和右偏各个直线的实验结果,可以看出效果良好。手机偏移角度分别为接近0°,负数,正数。获取到的偏移角度可以通过串口发送给其他外设或者主控。      

  • 2024-11-02
  • 发表了主题帖: 嘉楠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角度信息:

  • 2024-10-28
  • 发表了主题帖: 嘉楠K230 AI开发板测评3--RTC、ADC、PWM、UART、Thread、看门狗、文件读写

    嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板--测评3 ----基础实验篇 RTC(实时时钟)     MicroPyrhon已经集成了内置时钟函数模块,位于machine的RTC模块中(https://docs.micropython.org/en/latest/library/machine.RTC.html#machine-rtc),实验的原理是读取RTC时钟数据。     首先构造RTC对象,rtc=machine.RTC(),设置RTC日期和时间,rtc.datetime(year,mon, day,hour,min,sec,microsec),其中datetime有8个参数,按顺序分别为年、月、日、星期、时、分、秒、微妙,其中星期使用0-6表示星期一到星期日。同时rtc.datetime返回当前RTC时间元组。     通过代码首次上电如果检测到未设置时间先设置时间,然后周期打印获取的时间信息,代码流程图如图所示      参考代码: ''' 实验名称:RTC实时时钟 说明:实时时钟使用 教程:wiki.01studio.cc ''' # 导入相关模块 from machine import RTC import time # 构建RTC对象 rtc = RTC() # 首次上电设置RTC日期和时间。(2024, 1, 1, 0, 0, 0, 0, 0)按顺序分别表示(年,月,日,星期,时,分,秒,微妙), # 其中星期使用0-6表示星期一到星期日。 if rtc.datetime()[0] != 2024: rtc.datetime((2024, 1, 1, 0, 0, 0, 0, 0)) while True: print(rtc.datetime()) #打印时间 time.sleep(1) #延时1秒      实验结果如下图,运行代码,可以看到终端打印当前RTC时间信息,注:RTC时间是断电丢失的,想要RTC时间连续运行需要持续对开发板供电。 ADC(电压测量)      ADC(analog to digital conversion) 模拟数字转换,意思就是将模拟信号转化成数字信号,由于单片机只能识别二级制数字,所以外界模拟信号常常会通过ADC转换成其可以识别的数字信息,常见的应用就是将变化的电压转成数字信号实现对电压值测量。      K230内部包含一个ADC硬件模块,有6个通道,采样分辨率为12bit(0-4095),采样速率为1M。01Studio CanMV K230开发板引出0~3共4个通道。其中通道0、1量程为0-3.6V(原理图如下左图),通道2、3量程为0-1.8V,引脚定义如下右图。      首先构造函数对象adc=machine.ADC(channel),channel为0-3,接着adc.read_u16()获取ADC值,测量精度是12位,返回0-4095,adc.read_uv()获取ADC电压值,返回0-1.8,通道0、1要乘以2,对应0-3.6V,通道2、3返回值与实际电压值一一对应,代码编写流程如下:     参考代码如下: ''' 实验名称:ADC(电压测量) 版本:v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:ADC共4个通道,其中通道0、1实际量程为0-3.6V,通道2、3量程为0-1.8V。 (请勿超出测量量程, 可能导致主控芯片烧坏!) ''' from machine import ADC import time ''' 构建ADC对象: ADC0(排针32引脚,量程0-3.6V), ADC1(排针36引脚,量程0-3.6V), ADC2(排针38引脚,量程0-1.8V), ADC3(排针40引脚,量程0-1.8V)。 ''' adc = ADC(0) #通道0 while True: print(adc.read_u16()) # 获取ADC通道采样值 # 获取ADC通道电压值,保留2为小数。通道0、1实际量程为0-3.6V,返回值x2。 print('%.2f'%(adc.read_uv()/1000000*2), "V") time.sleep(1) #延时1秒      实验结果如下图,通过杜邦线将CanMV K230 ADC通道0引脚分别与和GND引脚和3.3V引脚短接分别测量,可以看到在一定误差范围内,结果正确。 PWM(蜂鸣器)     PWM(脉冲宽度调制)就是一个特定信号输出,主要用于输出不同频率、占空比(一个周期内高电平出现时间占总时间比例)的方波,以实现固定频率或平均电压输出,如下图占空比为75%的5V方波等效于3.75V,占空比为50%的5V方波等效于2.5V,占空比为20%的5V方波等效于1V。     蜂鸣器分为有源蜂鸣器与无源蜂鸣器,有源蜂鸣器只需要接上电源,蜂鸣器就能发声,断开电源就停止发声。而无源蜂鸣器,需要给指定的频率,才能发声,通过改变频率来改变蜂鸣器的发声音色。    首先构造PWM函数对象,PWM对象位于machine模块下,pwm = machine.PWM(channel, freq, duty, enable=False),channel为通道编号,取值0-3,PWM0对应GPIO42,PWM1对应GPIO43,PWM2对应GPIO46,PWM3对应GPIO47,frep为PWM频率,duty为PWM占空比,enable为PWM输出使能。可以通过pwm.freq([value]),pwm.duty([value]),直接设置频率与占空比。无源蜂鸣器我们可以用特定频率的方波来驱动,方波的原理很简单,就是一定频率的高低电平转换,可以简单理解成占空比为50%的PWM输出。代码流程如下:    参考代码如下 ''' 实验名称:PWM 版本:v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:通过不同频率的PWM信号输出,驱动无源蜂鸣器发出不同频率的声音。 ''' from machine import Pin, PWM from machine import FPIOA import time #配置引脚42为PWM0功能 #通道0:GPIO42,通道1:GPIO43,通道2:GPIO46,通道3:GPIO47, fpioa = FPIOA() fpioa.set_function(42,FPIOA.PWM0) #构建蜂鸣器PWM对象,通道0,频率为200Hz,占空比为50%,默认使能输出 Beep = PWM(0,200, 50, enable=True) # 在同一语句下创建和配置PWM,占空比50% #蜂鸣器发出频率200Hz响声 Beep.freq(200) time.sleep(1) #蜂鸣器发出频率400Hz响声 Beep.freq(400) time.sleep(1) #蜂鸣器发出频率600Hz响声 Beep.freq(600) time.sleep(1) #蜂鸣器发出频率800Hz响声 Beep.freq(800) time.sleep(1) #蜂鸣器发出频率1000Hz响声 Beep.freq(1000) time.sleep(1) #停止 Beep.enable(False)     实验结果如下图,使用引脚GPIO42输出PWM波形,通过示波器测量PWM波形,在图一中,设置初始频率为200Hz,最终频率为800Hz;在图二中,设置默认频率为200Hz,初始占空比为50%,最终占空比为80%。 UART(串口通信)       K230内部包含五个UART硬件模块,其中UART0被小核终端占用,UART3被大核终端占用,剩余UART1,UART2,UART4。 01Studio CanMV K230开发板通过排针引出了UART1和UART2共2个UART供用户使用。       首先构造函数,machine.UART (id, baudrate=115200, bits=UART.EIGHTBITS, parity=UART.PARITY_NONE, stop=UART.STOPBITS_ONE ),id为串口编号,串口1(UART.UART1:TX1(GPIO3), RX1(GPIO4))和串口2(UART.UART2:TX2(GPIO11), RX2(GPIO12))可用,baudrate为波特率,常用的115200、9600,bits为数据位,默认8位,parity为奇偶校验位,默认None,stop为停止位,默认1。通过UART.read(num)和UART.write(buf)分别读取和写入缓冲数据,同时可通过UART.readline(num)整行读取。        需要使用一个USB转TTL工具,然后配合串口助手与K230通信,串口1的引脚与原理图如下图,IO3--TX1,IO4--RX1:     使用3.3V串口转TTL工具,接线示意图与实物图分别如下:      先初始化串口,然后给串口发去一条信息,这样PC机的串口助手就会在接收区显示出来,然后进入循环,当检测到有数据可以接收时候就将数据接收并打印,并通过REPL打印显示。代码编写流程图如下:      参考代码如下: ''' 实验名称:UART(串口通信) 作者:01Studio 实验平台:01Studio CanMV K230 说明:通过编程实现串口通信,跟电脑串口助手实现数据收发。 ''' #导入串口模块 from machine import UART from machine import FPIOA import time fpioa = FPIOA() # UART1代码 fpioa.set_function(3,FPIOA.UART1_TXD) fpioa.set_function(4,FPIOA.UART1_RXD) uart=UART(UART.UART1,115200) #设置串口号1和波特率 ''' # UART2代码 fpioa.set_function(11,FPIOA.UART2_TXD) fpioa.set_function(12,FPIOA.UART2_RXD) uart=UART(UART.UART2,115200) #设置串口号2和波特率 ''' uart.write('Hello 01Studio!')#发送一条数据 while True: text=uart.read(128) #接收128个字符 if text != b'': print(text) #通过REPL打印串口3接收的数据 time.sleep(0.1) #100ms     实验结果如下图,将串口助手调为COM6,波特率为115200,可以看到串口成功输出(如左图),同时使用串口助手向K230发送一串字符“http://www.cmsoft.cn”,也可以看到K230的UART1正确输出(如右图)。 Thread(线程)      当我们需要分时完成不同任务时候,线程编程就派上用场了,这有点像RTOS(实时操作系统),编程实现多线程同时运行任务。      CanMV K230的MicroPython固件已经集成了_thread线程模块。我们直接调用即可。该模块衍生于python3(https://docs.python.org/3.5/library/_thread.html#module-thread),属于低级线程,代码编写流程如下:       参考代码如下: ''' 实验名称:线程 版本: v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:通过编程实现多线程。 ''' import _thread #导入线程模块 import time #线程函数 def func(name): while True: print("hello {}".format(name)) time.sleep(1) _thread.start_new_thread(func,("1",)) #开启线程1,参数必须是元组 _thread.start_new_thread(func,("2",)) #开启线程2,参数必须是元组 while True: time.sleep(0.01) #防止CPU满跑     实验结果,运行代码,可以看到串口终端重复执行2个线程,如下图所示。 看门狗      任何代码在运行过程中都可能出现崩溃的情况,这时候就可以加入看门狗代码。看门狗的用途是在应用程序崩溃并最终进入不可恢复状态时自动重新启动系统。一旦启动,就无法以任何方式停止或重新配置。启用后,应用程序必须定期“喂食”看门狗,以防止其过期并重置系统。      CanMV K230的MicroPython固件已经集成了看门狗WDT模块,我们直接调用即可。      首先构造WDT对象,wdt=WDT(id,timeout)创建看门狗对象,id为看门狗编号,timeout为超时时间,wdt.feed()喂狗,需要在构建看门狗对象时指定的超时时间内执行该指令。代码编写流程如图所示:      参考代码如下: ''' 实验名称:看门狗 版本: v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:看门狗测试。 ''' from machine import WDT #导入线程模块 import time #构建看门狗对象。 wdt = WDT(1,3) #看门狗编号1,超时时间3秒。 #每隔1秒喂一次狗,执行3次。 for i in range(3): time.sleep(1) print(i) wdt.feed() #喂狗 #停止喂狗,系统会重启。 while True: time.sleep(0.01) #防止CPU满跑       实验结果,运行代码,可以看到串口终端打印了3次信息后自动重启,断开了IDE连接。有了看门狗,当开发板死机时候就可以自动重启了。 文件读写       在嵌入式编程中我们经常会遇到需要将某些数据实现掉电保存功能,如传感器数据等。往往会用到EEPROM、flash等一些储存方式。而CanMV K230 MicroPython自带文件系统,我们 只  需要将数据直接用文件方式保存即可。      micropython的文件操作大部分指令兼容CPython。因此我们可以直接使用Python编程来实现文件读写。CanMV K230盘符对应的目录为/sdcard/路径下,代码编写流程如下所示:      参考代码如下: ''' 实验名称:文件读写 版本: v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:文件读写,将字符“01Studio”写入文件后再读取出来。 ''' ########### ## 写文件 ########### f = open('/sdcard/1.txt', 'w') #以写的方式打开一个文件,没有该文件就自动新建 f.write('01Studio') #写入数据 f.close() #每次操作完记得关闭文件 ########### ## 读文件 ########### f = open('/sdcard/1.txt', 'r') #以读方式打开一个文件 text = f.read() print(text) #读取数据并在终端打印 f.close() #每次操作完记得关闭文件     实验结果,运行代码,可以看到串口终端打印了文件内容。打开CanMV盘符,在sdcard目录下可以看到刚刚代码新建的1.txt文件,里面的内容就是我们写入的01Studio。除了txt,你可以写入任何后缀的文件,如json、csv或者无后缀的文件。    

  • 2024-10-13
  • 发表了主题帖: 嘉楠K230 AI开发板测评2--GPIO、LED、按键、定时器

    GPIO介绍         K230开发板总共引出了46个引脚,有常见的通用GPIO口、4路PWM、2路串口、1路SPI、I2C、I2S和4路的ADC,提供5V和3.3V的电源输出引脚对外供电,用常见的type-c引脚对开发板供电,方便简洁,GPIO引脚图如图1所示。                                     图 1 K230 GPIO引脚图 点亮第一个LED       CanMV K230有一个与GPIO直接连接的可控制LED蓝灯,其连接到GPIO52,其原理图如图2所示,从电路图可以看出,当GPIO口为高电平时,蓝灯被点亮。                                           图 2 LED灯原理图       由于K230功能多,在引脚有限的情况下,会复用引脚功能,通过FPIO库(也叫现场可编程IO阵列,库介绍网址2.8 FPIOA 模块API手册 — K230 CanMV (canaan-creative.com))实现对引脚的不同功能的选择。       代码编写流程如下:         首先引入FPIO模块,对GPIO52引脚的功能设置为IO口功能。引入Pin对象设置IO口,通过Pin(id, mode, pull) 设置IO口的引脚,模式与电阻配置,value([X])设置输出电平。         完整代码如下: ''' 实验名称:点亮LED蓝灯 版本:v1.0 作者:01Studio 实验平台:01Studio CanMV K230 教程:wiki.01studio.cc ''' from machine import Pin #导入Pin模块 from machine import FPIOA import time #将GPIO52配置为普通GPIO fpioa = FPIOA() fpioa.set_function(52,FPIOA.GPIO52) LED=Pin(52,Pin.OUT) #构建led对象,GPIO52,输出 LED.value(1) #点亮LED,也可以使用led.on()      实验结果如图3所示:                       图 3 点亮led灯 按键        按键是最常见的输入设备,通过检测按键被按下之后,改变LED灯的亮灭状态。按键对应的IO口引脚原理图如下图4所示,按键的一段接到GPIO21,另一端接到GND,所以按键在没按下的时候输入高电平1,按下的时候输入低电平0。                                 图 4 按键原理图         按键按下时的电平变化如下图5所示,会发生抖动,有可能会造成误判,因此需要延时函数来进行消抖。常见的方法就是当检测到按键值为0时,延时一段时间,大约10ms,再次判断按键引脚至仍然是0,是的话说明按键被按下,延时使用time模块(time – time related functions — MicroPython latest documentation)。                      图 5 按键一次出现的电平变化            首先将IO21配置为GPIO功能(使用FPIO模块),需要将按键的GPIO21引脚配置为输入模式(使用pin模块),当K230检测到按键被按下时led灯点亮,松开时led灯熄灭,代码编写流程图如图6所示。               图 6 按键代码流程图        完整代码如下所示: ''' 实验名称:按键 版本:v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:通过按键改变LED的亮灭状态 ''' from machine import Pin from machine import FPIOA import time #将GPIO52、GPIO21配置为普通GPIO模式 fpioa = FPIOA() fpioa.set_function(52,FPIOA.GPIO52) fpioa.set_function(21,FPIOA.GPIO21) LED=Pin(52,Pin.OUT) #构建LED对象,开始熄灭 KEY=Pin(21,Pin.IN,Pin.PULL_UP) #构建KEY对象 state=0 #LED引脚状态 while True: if KEY.value()==0: #按键被按下 time.sleep_ms(10) #消除抖动 if KEY.value()==0: #确认按键被按下 state=not state #使用not语句而非~语句 LED.value(state) #LED状态翻转 print('KEY') while not KEY.value(): #检测按键是否松开 pass      实验结果如图7所示,当按键KEY每次被按下时,LED灯的亮灭状态发生反转。                    图 7 按键控制LED灯结果图 定时器        定时器常常用来计时,通过定时器来周期性的执行各种任务。       引用machine的Timer模块(2.11 Timer 模块API手册 — K230 CanMV (canaan-creative.com))用定时器让LED灯周期性每秒闪烁一次。       首先构造函数tim = machine.Timer(id),接着初始化tim.init(mode, freq, period, callback)就可以轻松调用定时器模块。       代码编写流程如图8所示,定时器到达预定设计时间后,产生中断,跟外部中断的编程方式类似。                               图 8 定时器调用代码流程图      完整代码如下: ''' 实验名称:定时器 版本:v1.0 作者:01Studio 实验平台:01Studio CanMV K230 说明:通过定时器让LED周期性每秒闪烁1次。 ''' from machine import Pin,Timer import time led=Pin(52,Pin.OUT) Counter = 0 Fun_Num = 0 def fun(tim): global Counter Counter = Counter + 1 print(Counter) led.value(Counter%2) #使用软件定时器,编号-1 tim = Timer(-1) tim.init(period=1000, mode=Timer.PERIODIC,callback=fun) #周期为1000ms while True: time.sleep(0.01) #避免CPU满跑     实验结果:可以看到LED灯每隔一秒闪烁一次

  • 2024-09-26
  • 发表了主题帖: 嘉楠K230 AI开发板测评1--开发板初始篇

    嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板--测评1 1、开发板介绍:         嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板基于Micropython语言(https://docs.micropython.org/en/latest/index.html),内置多个高清视频图像输入处理和智能硬件处理单元,兼顾高性能、低功耗和高安全性特点,实测推理能力可达K210的13.7倍,广泛应用于各类智能产品。   2、开发板照片         开发板配件安装完成之后,正面图片如图1所示,精密的PCB布局到高质量的元器件选型,再到无缝衔接的接口与外壳设计,无不透露出制造商对品质的不懈追求。                                                                                      开发板背面如下图2所示,                                                                                        3、资料介绍         CanMV K230 AI开发板基于嘉楠科技边缘计算芯片K230和CanMV开源项目。有着丰富的学习资料与开发手册,支持在线查看和线下学习。在线网站(https://wiki.01studio.cc/docs/canmv_k230)如下图3所示,                                                                              线下资料(百度网盘链接:https://pan.baidu.com/s/1zVOi2lHbaQhlQEqK-7s-Nw?pwd=01KJ提取码:01KJ)目录如下图4所示:                                                                      有着丰富的社区论坛(https://forum.01studio.cc/),论坛里面包含各个开发者分享的各种AI项目,问题描述和群组供大家学习。   4、环境搭建         根据文档( https://wiki.01studio.cc/docs/category/开发环境搭建-1)介绍,你可以轻松的一直点击下一步,下一步直到环境搭建完成,通过软件安装,镜像烧录固件,代码测试,串口交互,离线运行,你已经完成所有的环境测试,下一步可以进入基础实验。   5、初步测试         拿到开发板的第一件事,当然是了解板子的功能模块与IO口,CanMV K230支持常用的I/O(输入输出引脚)、UART(串口)、I2C、SPI、PWM、ADC等功能, 第二件事就是最令人开心的点亮灯环节了,当你点亮一个LED灯的时候,你已经成功了一半。通过machine模块的Pin类,设置IO口的引脚输入/输出功能和IO口的值,点灯成功,如下5所示。                                                                                我们可以在micropython的官方文档中查看machine模块的具体内容(https://docs.micropython.org/en/latest/library/machine.html#module-machine),在(https://docs.micropython.org/en/latest/library/machine.Pin.html)中查看Pin类的所有函数解释,以此提高阅读英文文档的能力。    

  • 2024-09-19
  • 回复了主题帖: 测评入围名单: 嘉楠科技CanMV 勘智(Kendryte)K230嵌入式AI开发板

    个人信息无误,确认可以完成测评分享计划。

最近访客

< 1/1 >

统计信息

已有18人来访过

  • 芯积分:82
  • 好友:--
  • 主题:9
  • 回复:3

留言

你需要登录后才可以留言 登录 | 注册


现在还没有留言