- 2025-03-23
-
发表了主题帖:
【嘉楠K230开发板】目标跟踪
本帖最后由 dirty 于 2025-3-23 12:35 编辑
本篇讲述K230实现目标跟踪功能。
一.原理流程
通过CanMV K230 AI视觉框架开发,使用CanMV K230的文件系统里的目标跟踪模型,实现采集特定目标并追踪该物体功能。使用到的模型有跟踪模板模型cropped_test127.kmodel、跟踪实时模型nanotrack_backbone_sim.kmodel、跟踪模型nanotracker_head_calib_k230.kmodel。编程思路流程如下:
二.代码实现与测验
1.自定义跟踪模版任务类,里面定义了类初始化、配置预处理操作、重写预处理函数、自定义后处理、计算padding和crop参数、重写deinit
# 自定义跟踪模版任务类
class TrackCropApp(AIBase):
def __init__(self,kmodel_path,model_input_size,ratio_src_crop,center_xy_wh,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
# 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.CONTEXT_AMOUNT = 0.5
#src模型和crop模型输入比值
self.ratio_src_crop = ratio_src_crop
self.center_xy_wh=center_xy_wh
# padding和crop参数
self.pad_crop_params=[]
# 注意:ai2d设置多个预处理时执行的顺序为:crop->shift->resize/affine->pad,如果不符合该顺序,需要配置多个ai2d对象;
# 如下模型预处理要先做resize+padding再做resize+crop,因此要配置两个Ai2d对象
self.ai2d_pad=Ai2d(debug_mode)
self.ai2d_pad.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
self.ai2d_crop=Ai2d(debug_mode)
self.ai2d_crop.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
self.need_pad=False
# 配置预处理操作,这里使用了crop、pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/libs/AI2D.py查看
def config_preprocess(self,input_image_size=None):
with ScopedTiming("set preprocess config",self.debug_mode > 0):
# 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size
# 计算padding参数并应用pad操作,以确保输入图像尺寸与模型输入尺寸匹配
self.pad_crop_params= self.get_padding_crop_param()
# 如果需要padding,配置padding部分,否则只走crop
if (self.pad_crop_params[0] != 0 or self.pad_crop_params[1] != 0 or self.pad_crop_params[2] != 0 or self.pad_crop_params[3] != 0):
self.need_pad=True
self.ai2d_pad.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d_pad.pad([0, 0, 0, 0, self.pad_crop_params[0], self.pad_crop_params[1], self.pad_crop_params[2], self.pad_crop_params[3]], 0, [114, 114, 114])
output_size=[self.rgb888p_size[0]+self.pad_crop_params[2]+self.pad_crop_params[3],self.rgb888p_size[1]+self.pad_crop_params[0]+self.pad_crop_params[1]]
self.ai2d_pad.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,output_size[1],output_size[0]])
self.ai2d_crop.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d_crop.crop(int(self.pad_crop_params[4]),int(self.pad_crop_params[6]),int(self.pad_crop_params[5]-self.pad_crop_params[4]+1),int(self.pad_crop_params[7]-self.pad_crop_params[6]+1))
self.ai2d_crop.build([1,3,output_size[1],output_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])
else:
self.need_pad=False
self.ai2d_crop.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d_crop.crop(int(self.center_xy_wh[0]-self.pad_crop_params[8]/2.0),int(self.center_xy_wh[1]-self.pad_crop_params[8]/2.0),int(self.pad_crop_params[8]),int(self.pad_crop_params[8]))
self.ai2d_crop.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])
# 重写预处理函数preprocess,因为该部分不是单纯的走一个ai2d做预处理,所以该函数需要重写
def preprocess(self,input_np):
if self.need_pad:
pad_output=self.ai2d_pad.run(input_np).to_numpy()
return [self.ai2d_crop.run(pad_output)]
else:
return [self.ai2d_crop.run(input_np)]
# 自定义后处理,results是模型输出array的列表
def postprocess(self,results):
with ScopedTiming("postprocess",self.debug_mode > 0):
return results[0]
# 计算padding和crop参数
def get_padding_crop_param(self):
s_z = round(np.sqrt((self.center_xy_wh[2] + self.CONTEXT_AMOUNT * (self.center_xy_wh[2] + self.center_xy_wh[3])) * (self.center_xy_wh[3] + self.CONTEXT_AMOUNT * (self.center_xy_wh[2] + self.center_xy_wh[3]))))
c = (s_z + 1) / 2
context_xmin = np.floor(self.center_xy_wh[0] - c + 0.5)
context_xmax = int(context_xmin + s_z - 1)
context_ymin = np.floor(self.center_xy_wh[1] - c + 0.5)
context_ymax = int(context_ymin + s_z - 1)
left_pad = int(max(0, -context_xmin))
top_pad = int(max(0, -context_ymin))
right_pad = int(max(0, int(context_xmax - self.rgb888p_size[0] + 1)))
bottom_pad = int(max(0, int(context_ymax - self.rgb888p_size[1] + 1)))
context_xmin = context_xmin + left_pad
context_xmax = context_xmax + left_pad
context_ymin = context_ymin + top_pad
context_ymax = context_ymax + top_pad
return [top_pad,bottom_pad,left_pad,right_pad,context_xmin,context_xmax,context_ymin,context_ymax,s_z]
#重写deinit
def deinit(self):
with ScopedTiming("deinit",self.debug_mode > 0):
del self.ai2d_pad
del self.ai2d_crop
super().deinit()
2.自定义跟踪实时任务类。其自定义函数类别与上类似。
class TrackSrcApp(AIBase):
def __init__(self,kmodel_path,model_input_size,ratio_src_crop,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
# 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]]
# padding和crop参数列表
self.pad_crop_params=[]
# 跟踪框宽、高调整系数
self.CONTEXT_AMOUNT = 0.5
# src和crop模型的输入尺寸比例
self.ratio_src_crop = ratio_src_crop
# debug模式
self.debug_mode=debug_mode
# 注意:ai2d设置多个预处理时执行的顺序为:crop->shift->resize/affine->pad,如果不符合该顺序,需要配置多个ai2d对象;
# 如下模型预处理要先做resize+padding再做resize+crop,因此要配置两个Ai2d对象
self.ai2d_pad=Ai2d(debug_mode)
self.ai2d_pad.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
self.ai2d_crop=Ai2d(debug_mode)
self.ai2d_crop.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
self.need_pad=False
# 配置预处理操作,这里使用了crop、pad和resize,Ai2d支持crop/shift/pad/resize/affine,具体代码请打开/sdcard/libs/AI2D.py查看
def config_preprocess(self,center_xy_wh,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参数并应用pad操作,以确保输入图像尺寸与模型输入尺寸匹配
self.pad_crop_params= self.get_padding_crop_param(center_xy_wh)
# 如果需要padding,配置padding部分,否则只走crop
if (self.pad_crop_params[0] != 0 or self.pad_crop_params[1] != 0 or self.pad_crop_params[2] != 0 or self.pad_crop_params[3] != 0):
self.need_pad=True
self.ai2d_pad.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d_pad.pad([0, 0, 0, 0, self.pad_crop_params[0], self.pad_crop_params[1], self.pad_crop_params[2], self.pad_crop_params[3]], 0, [114, 114, 114])
output_size=[self.rgb888p_size[0]+self.pad_crop_params[2]+self.pad_crop_params[3],self.rgb888p_size[1]+self.pad_crop_params[0]+self.pad_crop_params[1]]
self.ai2d_pad.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,output_size[1],output_size[0]])
self.ai2d_crop.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d_crop.crop(int(self.pad_crop_params[4]),int(self.pad_crop_params[6]),int(self.pad_crop_params[5]-self.pad_crop_params[4]+1),int(self.pad_crop_params[7]-self.pad_crop_params[6]+1))
self.ai2d_crop.build([1,3,output_size[1],output_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])
else:
self.need_pad=False
self.ai2d_crop.resize(nn.interp_method.tf_bilinear, nn.interp_mode.half_pixel)
self.ai2d_crop.crop(int(center_xy_wh[0]-self.pad_crop_params[8]/2.0),int(center_xy_wh[1]-self.pad_crop_params[8]/2.0),int(self.pad_crop_params[8]),int(self.pad_crop_params[8]))
self.ai2d_crop.build([1,3,ai2d_input_size[1],ai2d_input_size[0]],[1,3,self.model_input_size[1],self.model_input_size[0]])
# 重写预处理函数preprocess,因为该部分不是单纯的走一个ai2d做预处理,所以该函数需要重写
def preprocess(self,input_np):
with ScopedTiming("preprocess",self.debug_mode>0):
if self.need_pad:
pad_output=self.ai2d_pad.run(input_np).to_numpy()
return [self.ai2d_crop.run(pad_output)]
else:
return [self.ai2d_crop.run(input_np)]
# 自定义后处理,results是模型输出array的列表
def postprocess(self,results):
with ScopedTiming("postprocess",self.debug_mode > 0):
return results[0]
# 计算padding和crop参数
def get_padding_crop_param(self,center_xy_wh):
s_z = round(np.sqrt((center_xy_wh[2] + self.CONTEXT_AMOUNT * (center_xy_wh[2] + center_xy_wh[3])) * (center_xy_wh[3] + self.CONTEXT_AMOUNT * (center_xy_wh[2] + center_xy_wh[3])))) * self.ratio_src_crop
c = (s_z + 1) / 2
context_xmin = np.floor(center_xy_wh[0] - c + 0.5)
context_xmax = int(context_xmin + s_z - 1)
context_ymin = np.floor(center_xy_wh[1] - c + 0.5)
context_ymax = int(context_ymin + s_z - 1)
left_pad = int(max(0, -context_xmin))
top_pad = int(max(0, -context_ymin))
right_pad = int(max(0, int(context_xmax - self.rgb888p_size[0] + 1)))
bottom_pad = int(max(0, int(context_ymax - self.rgb888p_size[1] + 1)))
context_xmin = context_xmin + left_pad
context_xmax = context_xmax + left_pad
context_ymin = context_ymin + top_pad
context_ymax = context_ymax + top_pad
return [top_pad,bottom_pad,left_pad,right_pad,context_xmin,context_xmax,context_ymin,context_ymax,s_z]
# 重写deinit
def deinit(self):
with ScopedTiming("deinit",self.debug_mode > 0):
del self.ai2d_pad
del self.ai2d_crop
super().deinit()
3.追踪应用类。
class TrackerApp(AIBase):
def __init__(self,kmodel_path,crop_input_size,thresh,rgb888p_size=[1280,720],display_size=[1920,1080],debug_mode=0):
super().__init__(kmodel_path,rgb888p_size,debug_mode)
# kmodel路径
self.kmodel_path=kmodel_path
# crop模型的输入尺寸
self.crop_input_size=crop_input_size
# 跟踪框阈值
self.thresh=thresh
# 跟踪框宽、高调整系数
self.CONTEXT_AMOUNT = 0.5
# 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.ai2d=Ai2d(debug_mode)
self.ai2d.set_ai2d_dtype(nn.ai2d_format.NCHW_FMT,nn.ai2d_format.NCHW_FMT,np.uint8, np.uint8)
def config_preprocess(self,input_image_size=None):
with ScopedTiming("set preprocess config",self.debug_mode > 0):
pass
# 重写run函数,因为没有预处理过程,所以原来run操作中包含的preprocess->inference->postprocess不合适,这里只包含inference->postprocess
def run(self,input_np_1,input_np_2,center_xy_wh):
input_tensors=[]
input_tensors.append(nn.from_numpy(input_np_1))
input_tensors.append(nn.from_numpy(input_np_2))
results=self.inference(input_tensors)
return self.postprocess(results,center_xy_wh)
# 自定义后处理,results是模型输出array的列表,这里使用了aidemo的nanotracker_postprocess列表
def postprocess(self,results,center_xy_wh):
with ScopedTiming("postprocess",self.debug_mode > 0):
det = aidemo.nanotracker_postprocess(results[0],results[1],[self.rgb888p_size[1],self.rgb888p_size[0]],self.thresh,center_xy_wh,self.crop_input_size[0],self.CONTEXT_AMOUNT)
return det
class NanoTracker:
def __init__(self,track_crop_kmodel,track_src_kmodel,tracker_kmodel,crop_input_size,src_input_size,threshold=0.25,rgb888p_size=[1280,720],display_size=[1920,1080],debug_mode=0):
# 跟踪模版模型路径
self.track_crop_kmodel=track_crop_kmodel
# 跟踪实时模型路径
self.track_src_kmodel=track_src_kmodel
# 跟踪模型路径
self.tracker_kmodel=tracker_kmodel
# 跟踪模版模型输入分辨率
self.crop_input_size=crop_input_size
# 跟踪实时模型输入分辨率
self.src_input_size=src_input_size
self.threshold=threshold
self.CONTEXT_AMOUNT=0.5 # 跟踪框宽、高调整系数
self.ratio_src_crop = 0.0 # src模型和crop模型输入比值
self.track_x1 = float(600) # 起始跟踪目标框左上角点x
self.track_y1 = float(300) # 起始跟踪目标框左上角点y
self.track_w = float(100) # 起始跟踪目标框w
self.track_h = float(100) # 起始跟踪目标框h
self.draw_mean=[] # 初始目标框位置列表
self.center_xy_wh = []
self.track_boxes = []
self.center_xy_wh_tmp = []
self.track_boxes_tmp=[]
self.crop_output=None
self.src_output=None
# 跟踪框初始化时间
self.seconds = 8
self.endtime = time.time() + self.seconds
self.enter_init = True
# 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]]
self.init_param()
self.track_crop=TrackCropApp(self.track_crop_kmodel,model_input_size=self.crop_input_size,ratio_src_crop=self.ratio_src_crop,center_xy_wh=self.center_xy_wh,rgb888p_size=self.rgb888p_size,display_size=self.display_size,debug_mode=0)
self.track_src=TrackSrcApp(self.track_src_kmodel,model_input_size=self.src_input_size,ratio_src_crop=self.ratio_src_crop,rgb888p_size=self.rgb888p_size,display_size=self.display_size,debug_mode=0)
self.tracker=TrackerApp(self.tracker_kmodel,crop_input_size=self.crop_input_size,thresh=self.threshold,rgb888p_size=self.rgb888p_size,display_size=self.display_size)
self.track_crop.config_preprocess()
# run函数
def run(self,input_np):
# 在初始化时间内,crop模版部分的到跟踪模版特征,否则,对当前帧进行src推理得到特征并使用tracker对两个特征推理,得到跟踪框的坐标
nowtime = time.time()
if (self.enter_init and nowtime <= self.endtime):
print("倒计时: " + str(self.endtime - nowtime) + " 秒")
self.crop_output=self.track_crop.run(input_np)
time.sleep(1)
return self.draw_mean
else:
self.track_src.config_preprocess(self.center_xy_wh)
self.src_output=self.track_src.run(input_np)
det=self.tracker.run(self.crop_output,self.src_output,self.center_xy_wh)
return det
# 绘制效果,绘制跟踪框位置
def draw_result(self,pl,box):
pl.osd_img.clear()
if self.enter_init:
pl.osd_img.draw_rectangle(box[0],box[1],box[2],box[3],color=(255, 0, 255, 0),thickness = 4)
if (time.time() > self.endtime):
self.enter_init = False
else:
self.track_boxes = box[0]
self.center_xy_wh = box[1]
track_bool = True
if (len(self.track_boxes) != 0):
track_bool = self.track_boxes[0] > 10 and self.track_boxes[1] > 10 and self.track_boxes[0] + self.track_boxes[2] < self.rgb888p_size[0] - 10 and self.track_boxes[1] + self.track_boxes[3] < self.rgb888p_size[1] - 10
else:
track_bool = False
if (len(self.center_xy_wh) != 0):
track_bool = track_bool and self.center_xy_wh[2] * self.center_xy_wh[3] < 40000
else:
track_bool = False
if (track_bool):
self.center_xy_wh_tmp = self.center_xy_wh
self.track_boxes_tmp = self.track_boxes
x1 = int(float(self.track_boxes[0]) * self.display_size[0] / self.rgb888p_size[0])
y1 = int(float(self.track_boxes[1]) * self.display_size[1] / self.rgb888p_size[1])
w = int(float(self.track_boxes[2]) * self.display_size[0] / self.rgb888p_size[0])
h = int(float(self.track_boxes[3]) * self.display_size[1] / self.rgb888p_size[1])
pl.osd_img.draw_rectangle(x1, y1, w, h, color=(255, 255, 0, 0),thickness = 4)
else:
self.center_xy_wh = self.center_xy_wh_tmp
self.track_boxes = self.track_boxes_tmp
x1 = int(float(self.track_boxes[0]) * self.display_size[0] / self.rgb888p_size[0])
y1 = int(float(self.track_boxes[1]) * self.display_size[1] / self.rgb888p_size[1])
w = int(float(self.track_boxes[2]) * self.display_size[0] / self.rgb888p_size[0])
h = int(float(self.track_boxes[3]) * self.display_size[1] / self.rgb888p_size[1])
pl.osd_img.draw_rectangle(x1, y1, w, h, color=(255, 255, 0, 0),thickness = 4)
pl.osd_img.draw_string_advanced( x1 , y1-50,32, "请远离摄像头,保持跟踪物体大小基本一致!" , color=(255, 255 ,0 , 0))
pl.osd_img.draw_string_advanced( x1 , y1-100,32, "请靠近中心!" , color=(255, 255 ,0 , 0))
# crop参数初始化
def init_param(self):
self.ratio_src_crop = float(self.src_input_size[0])/float(self.crop_input_size[0])
print(self.ratio_src_crop)
if (self.track_x1 < 50 or self.track_y1 < 50 or self.track_x1+self.track_w >= self.rgb888p_size[0]-50 or self.track_y1+self.track_h >= self.rgb888p_size[1]-50):
print("**剪切范围超出图像范围**")
else:
track_mean_x = self.track_x1 + self.track_w / 2.0
track_mean_y = self.track_y1 + self.track_h / 2.0
draw_mean_w = int(self.track_w / self.rgb888p_size[0] * self.display_size[0])
draw_mean_h = int(self.track_h / self.rgb888p_size[1] * self.display_size[1])
draw_mean_x = int(track_mean_x / self.rgb888p_size[0] * self.display_size[0] - draw_mean_w / 2.0)
draw_mean_y = int(track_mean_y / self.rgb888p_size[1] * self.display_size[1] - draw_mean_h / 2.0)
self.draw_mean=[draw_mean_x,draw_mean_y,draw_mean_w,draw_mean_h]
self.center_xy_wh = [track_mean_x,track_mean_y,self.track_w,self.track_h]
self.center_xy_wh_tmp=[track_mean_x,track_mean_y,self.track_w,self.track_h]
self.track_boxes = [self.track_x1,self.track_y1,self.track_w,self.track_h,1]
self.track_boxes_tmp=np.array([self.track_x1,self.track_y1,self.track_w,self.track_h,1])
4.main函数。其引用了模型文件路径,初始化初始化PipeLine,建立目标追踪任务,在主循环获取、推理当前帧,绘制当前帧推理结果、打印结果、展示推理结果。
if __name__=="__main__":
# 显示模式,默认"hdmi",可以选择"hdmi"和"lcd"
display_mode="lcd"
if display_mode=="hdmi":
display_size=[1920,1080]
else:
display_size=[800,480]
# 跟踪模板模型路径
track_crop_kmodel_path="/sdcard/examples/kmodel/cropped_test127.kmodel"
# 跟踪实时模型路径
track_src_kmodel_path="/sdcard/examples/kmodel/nanotrack_backbone_sim.kmodel"
# 跟踪模型路径
tracker_kmodel_path="/sdcard/examples/kmodel/nanotracker_head_calib_k230.kmodel"
# 其他参数
rgb888p_size=[1280,720]
track_crop_input_size=[127,127]
track_src_input_size=[255,255]
threshold=0.1
# 初始化PipeLine,只关注传给AI的图像分辨率,显示的分辨率
pl=PipeLine(rgb888p_size=rgb888p_size,display_size=display_size,display_mode=display_mode)
pl.create()
track=NanoTracker(track_crop_kmodel_path,track_src_kmodel_path,tracker_kmodel_path,crop_input_size=track_crop_input_size,src_input_size=track_src_input_size,threshold=threshold,rgb888p_size=rgb888p_size,display_size=display_size)
clock = time.clock()
while True:
clock.tick()
img=pl.get_frame() # 获取当前帧
output=track.run(img) # 推理当前帧
track.draw_result(pl,output) # 绘制当前帧推理结果
print(output) # 打印当前结果
pl.show_image() # 展示推理结果
gc.collect()
print(clock.fps()) #打印帧率
6.测验。
运行起来后将绿框对准跟踪目标(这里选择的是苹果),等待绿框变红后说明学习成功并开始追踪,这时移动摄像头,在视距范围内,始终能跟踪找到目标。效果如下
至此实现K230目标跟踪功能。
- 2025-03-22
-
回复了主题帖:
《ROS2智能机器人开发实践》--7.ROS 2机器人仿真与仿真到实物
秦天qintian0303 发表于 2025-3-22 09:58
机器人模型可视化这个不错,直观
是的,深入可学的也挺多
- 2025-03-21
-
发表了主题帖:
《ROS2智能机器人开发实践》--7.ROS 2机器人仿真与仿真到实物
本帖最后由 dirty 于 2025-3-21 23:28 编辑
前面学习了ROS2原理和常用工具,是开发的很重要基础。本篇讲述ROS 2机器人仿真。
一.机器人仿真
①URDF机器人建模
1.代码display.launch.py
from ament_index_python.packages import get_package_share_path
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.conditions import IfCondition, UnlessCondition
from launch.substitutions import Command, LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
def generate_launch_description():
urdf_tutorial_path = get_package_share_path('learning_urdf')
default_model_path = urdf_tutorial_path / 'urdf/mbot_base.urdf'
default_rviz_config_path = urdf_tutorial_path / 'rviz/urdf.rviz'
gui_arg = DeclareLaunchArgument(name='gui', default_value='false', choices=['true', 'false'],
description='Flag to enable joint_state_publisher_gui')
model_arg = DeclareLaunchArgument(name='model', default_value=str(default_model_path),
description='Absolute path to robot urdf file')
rviz_arg = DeclareLaunchArgument(name='rvizconfig', default_value=str(default_rviz_config_path),
description='Absolute path to rviz config file')
robot_description = ParameterValue(Command(['xacro ', LaunchConfiguration('model')]),
value_type=str)
robot_state_publisher_node = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[{'robot_description': robot_description}]
)
# Depending on gui parameter, either launch joint_state_publisher or joint_state_publisher_gui
joint_state_publisher_node = Node(
package='joint_state_publisher',
executable='joint_state_publisher',
condition=UnlessCondition(LaunchConfiguration('gui'))
)
joint_state_publisher_gui_node = Node(
package='joint_state_publisher_gui',
executable='joint_state_publisher_gui',
condition=IfCondition(LaunchConfiguration('gui'))
)
rviz_node = Node(
package='rviz2',
executable='rviz2',
name='rviz2',
output='screen',
arguments=['-d', LaunchConfiguration('rvizconfig')],
)
return LaunchDescription([
gui_arg,
model_arg,
rviz_arg,
joint_state_publisher_node,
joint_state_publisher_gui_node,
robot_state_publisher_node,
rviz_node
])
2.机器人模型可视化。命令如下
ros2 launch learning_urdf display.launch.py
urdf_to_graphviz mbot_base.urdf # 在模型所在的文件夹下运行
效果如下图
②Gazebo机器人仿真
1.代码load_urdf_into_gazebo_harmonic.launch.py
import os
import xacro
from ament_index_python.packages import get_package_share_directory
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from launch_ros.actions import Node
def generate_launch_description():
# Include the robot_state_publisher launch file, provided by our own package. Force sim time to be enabled
# !!! MAKE SURE YOU SET THE PACKAGE NAME CORRECTLY !!!
package_name='learning_gazebo_harmonic' #<--- CHANGE ME
pkg_path = os.path.join(get_package_share_directory(package_name))
xacro_file = os.path.join(pkg_path,'urdf','mbot_gazebo_harmonic.xacro')
world_file = os.path.join(pkg_path,'worlds','empty.sdf')
robot_description_config = xacro.process_file(xacro_file)
# Pose where we want to spawn the robot
spawn_x_val = '0.0'
spawn_y_val = '0.0'
spawn_z_val = '0.3'
spawn_yaw_val = '0.0'
# Gazebo Sim
pkg_ros_gz_sim = get_package_share_directory('ros_gz_sim')
gazebo = IncludeLaunchDescription(
PythonLaunchDescriptionSource(
os.path.join(pkg_ros_gz_sim, 'launch', 'gz_sim.launch.py')),
launch_arguments={'gz_args': '-r ' + world_file}.items(),
)
# Run the spawner node from the gazebo_ros package. The entity name doesn't really matter if you only have a single robot.
spawn_entity = Node(package='ros_gz_sim', executable='create',
arguments=['-topic', 'robot_description',
'-name', 'mbot',
'-x', spawn_x_val,
'-y', spawn_y_val,
'-z', spawn_z_val,
'-Y', spawn_yaw_val],
output='screen')
# Create a robot_state_publisher node
params = {'robot_description': robot_description_config.toxml(), 'use_sim_time': True}
node_robot_state_publisher = Node(
package='robot_state_publisher',
executable='robot_state_publisher',
output='screen',
parameters=[params]
)
# ros gz bridge
ros_gz_bridge = Node(
package='ros_gz_bridge',
executable='parameter_bridge',
parameters=[{
'config_file': os.path.join(get_package_share_directory(package_name), 'config', 'ros_gz_bridge_mbot.yaml'),
'qos_overrides./tf_static.publisher.durability': 'transient_local',
}],
output='screen'
)
# Launch them all!
return LaunchDescription([
gazebo,
spawn_entity,
ros_gz_bridge,
node_robot_state_publisher,
])
2.在Gazebo中加载机器人模型
ros2 launch learning_gazebo_harmonic load_urdf_into_gazebo_harmonic.launch.py
运行效果如下:
二.机器人仿真到实物
1.下载originbot_desktop 功能包集合
cd ~/dev_ws/src
git clone https://gitee.com/guyuehome/originbot_desktop.git
功能包集合如下图所示
2.安装功能包依赖
cd ~/dev_ws/src/originbot_desktop
./install_prereq.sh
3.编译工作空间
cd ~/dev_ws
colcon build
4.设置环境变量
echo "~/dev_ws/install/setup.sh" >> ~/.bashrc
5.机器人仿真测试
ros2 launch originbot_gazebo_harmonic load_originbot_into_gazebo.launch.py
关于机器人仿真的代码部分还需要细细去梳理学习理解,通过实践可以看到仿真界面。从这里开始,为机器人开发打开了一扇大门,在ROS2机器人应用方面,还有很大的探索空间。
- 2025-03-15
-
回复了主题帖:
【B-G431B-ESC1-无刷电机板】3-开环VF强拖
电机是有感还是无感,购买链接有没?参考看下
- 2025-03-12
-
回复了主题帖:
嵌入式Rust修炼营入围名单来啦,一起度过嵌入式Rust学习时光的小伙伴集合啦~
个人信息无误
-
回复了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--6.人脸检测
Eric_Tseng 发表于 2025-3-11 22:36
网上为数不多靠谱的资料。如果这块开发板连视觉都可以处理,那在工业或者电网行业,能不能通过采集一些数据 ...
AI模型文件tflite是通过数据集训练获得,具体ST 边缘AI工具也是有迭代更新,最新Edge Impulse可以了解下https://edgeimpulse.com/
-
回复了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--2.开发环境搭建与点灯
Eric_Tseng 发表于 2025-3-11 22:30
我尝试过插上stlink,但是板载Led没有反应。但是用typec接口就会量蓝灯和绿灯,不知道是什么原因。而且创建 ...
STLINK接线检查下,电源用3.3V。芯片Pack包可以先安装,Keil版本会有要求,在STM32CubeMx工程配置里有选项留意下版本方面要求。再有的STLINK驱动是CMSIS-DAP在stm32cubeide不可直接下载烧录。
- 2025-03-02
-
回复了主题帖:
《ROS2智能机器人开发实践》--6.ROS2常用工具
Jacktang 发表于 2025-3-2 09:34
理解好机器人中的几个坐标系之间的关系看来主要的
是的,前面这些是基础
- 2025-03-01
-
发表了主题帖:
《ROS2智能机器人开发实践》--6.ROS2常用工具
机器人开发设计系统非常复杂,ROS2中有很多好用的工具,可以让开发事半功倍。这些“神器”包含Launch、TF、URDF、Gazebo、Rviz、Rqt,下面做讲解与学习。
一.Launch:多节点启动与配置脚本
前面我们讲到运行一个ROS节点,都需要打开一个新的终端运行一个命令。一次性启动所有节点,这就是Launch启动文件,它是ROS系统中多节点启动与配置的一种脚本。ROS2中的Launch文件就是基于Python描述的。
1.多节点启动
启动终端,使用ros2中的launch命令来启动第一个launch文件。
ros2 launch learning_launch simple.launch.py
2.代码与运行结果如下,可以看到不需要打开两个终端分别命令运行。
3.ROS系统参数设置
ROS系统中的参数,也可以在Launch文件中设置。
4.Launch文件包含
在复杂的机器人系统中,launch文件也会有很多,此时我们可以使用类似编程中的include机制,让launch文件互相包含。
二.TF:机器人坐标系管理神器
ROS给我们提供了一个坐标系的管理神器——TF。
1.机器人中的坐标系
在机械臂形态的机器人中,机器人安装的位置叫做基坐标系Base Frame,机器人安装位置在外部环境下的参考系叫做世界坐标系World Frame,机器人末端夹爪的位置叫做工具坐标系,外部被操作物体的位置叫做工件坐标系,在机械臂抓取外部物体的过程中,这些坐标系之间的关系也在跟随变化。
关于坐标系变换关系的基本理论,可以分解为平移和旋转两个部分,通过一个四乘四的矩阵进行描述,在空间中画出坐标系,那两者之间的变换关系,其实就是向量的数学描述。
ROS中TF功能的底层原理,就是对这些数学变换进行了封装。
2.TF命令行操作
●查看TF树
ros2 run tf2_tools view_frames
运行结果如下,默认在当前终端路径下生成了一个frames.pdf文件,打开之后,就可以看到系统中各个坐标系的关系了。
●查询坐标信息变换
ros2 run tf2_ros tf2_echo turtle2 turtle1
●坐标可视化
ros2 run rviz2 rviz2 -d $(ros2 pkg prefix --share turtle_tf2_py)/rviz/turtle_rviz.rviz
三.URDF:机器人建模方法
机器人建模方法——URDF,用来描述机器人外观、性能等各方面属性。
1.机器人组成
机器人一般是由硬件结构、驱动系统、传感器系统、控制系统四大部分组成。
机器人建模的过程,其实就是按照类似的思路,通过建模语言,把机器人每一个部分都描述清楚,再组合起来的过程。
2.URDF
ROS中的建模方法叫做URDF,全称是统一机器人描述格式,不仅可以清晰描述机器人自身的模型,还可以描述机器人的外部环境,比如这里的桌子,也可以算作一个模型。
URDF模型文件使用的是XML格式,由一系列尖括号包围的标签和其中的属性组合而成。
在建模中,大臂和小臂就类似机器人的这些独立的刚体部分,称为连杆Link,手肘就类似于机器人电机驱动部分,称为关节joint。
所以在URDF建模过程中,关键任务就是通过这里的<link>和<joint>,理清楚每一个连杆和关节的描述信息。
四.Gazebo:三维物理仿真平台
1.介绍
Gazebo是ROS系统中最为常用的三维物理仿真平台,支持动力学引擎,可以实现高质量的图形渲染,不仅可以模拟机器人及周边环境,还可以加入摩擦力、弹性系数等物理属性。
2.安装
sudo apt install ros-jazzy-gazebo-*
3.运行
ros2 launch gazebo_ros gazebo.launch.py
4.XACRO机器人模型优化
URDF文件格式的升级版本——XACRO文件。同样也是对机器人URDF模型的创建,XACRO文件加入了更多编程化的实现方法,可以让模型创建更友好。安装如下,文件语法可以了解一下。
sudo apt install ros-jazzy-xacro
5.Ignition:下一代Gazebo
随着技术的进步,Gazebo仿真平台也在不断迭代,新一代的Gazebo命名为Ignition,从渲染效果和仿真流畅度上都有较大的变化
五.Rviz:三维可视化显示平台
1.Rviz介绍
Rviz的核心框架是基于Qt可视化工具打造的一个开放式平台,官方出厂就自带了很多机器人常用的可视化显示插件,只要我们按照ROS中的消息发布对应的话题,就可以看到图形化的效果了。
2.运行方法
开启终端,运行以下命令
ros2 run rviz2 rviz2
六.RQT:模块化可视化工具
rqt和Rviz一样,也是基于QT可视化工具开发而来,在使用前安装与运行命令如下
sudo apt install ros-jazzy-rqt
rqt
界面一样,里边可以加载很多小模块,每个模块都可以实现一个具体的小功能,一些常用的功能有:日志显示、图像显示、发布话题数据/调用服务请求、绘制数据曲线、数据包管理、节点可视化等。
本篇将ROS2中常用工具梳理了一遍,实际应用中可以作为一个参考。
- 2025-02-26
-
回复了主题帖:
突发!上海一家AI公司爆雷了!
拉投资资本的,看了下成立时间也不长,收益与投入不能接续上就很容易爆,感觉最近AI炒得有些过热了
- 2025-02-25
-
回复了主题帖:
嵌入式Rust修炼营:动手写串口烧录工具和MCU例程,Rust达人Hunter直播带你入门Rust
-参与理由&个人编程基础:
参与理由:Rust作为一门新兴编程语言,有其优势特点与普及应用。个人作为嵌入式软件开发者,也是机选巧合和一些见闻,对Rust也是看好也很感兴趣。希望可以借此机会,理论与工程实践结合,掌握Rust。
个人编程基础:C、C++、Python
-查看修炼任务和活动时间表,预估可以跟着完成几级任务(初级、中级、高级):
看了实践活动表及开发板附的资料,预估可以完成初级、中级、高级任务。
-如探索过Rust,请说明Rust学习过程遇到难点,希望在参与活动中收获什么?
EEWorld之前评测出过《Rust实战》这本书,有所关注。希望通过这次参与活动,掌握Rust编程语言,可以熟练运用。
- 2025-02-23
-
发表了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--6.人脸检测
本帖最后由 dirty 于 2025-2-23 21:54 编辑
本篇讲述使用ST生态资源X-CUBE-AI实现人脸检测功能。
一.AI模型了解
X-CUBE-AI最新也称STM32Cube.AI,可用于在任意STM32微控制器上优化并部署由主流AI框架训练的神经网络模型。该工具可通过STM32CubeMX环境中的图形界面以及命令行方式使用。该工具现在也可以通过ST Edge AI Developer Cloud在线使用。官方相关文档【1】可以根据需要详细阅读了解,也可以到ST中文论坛搜集资料提问等【2】。
二.配置部署
1.安装X-CUBE-AI 6.0.0
在STM32CubeMX下,按如下选择安装
2.加载AI模型。这里使用到人脸检测模型文件person_detect.tflite。
三.代码
1.生成工程目录下会有x-CUBE-AI的应用API代码函数。
2.关于灰度转换。用到的函数STM32Ipl_Convert。
3.关于转换图像尺寸.用到的函数STM32Ipl_Resize。
4.关于AI的图像处理,用到aiRun。
/*
相对路径:X-CUBE-AI\App\ai_inference.c
*/
void aiRun(ai_u8 *pIn, ai_u8 *pOut)
{
ai_i32 batch;
ai_error err;
/* 1 - Create the AI buffer IO handlers with the default definition */
ai_buffer ai_input[AI_NETWORK_IN_NUM] = AI_NETWORK_IN;
ai_buffer ai_output[AI_NETWORK_OUT_NUM] = AI_NETWORK_OUT;
/* 2 - Update IO handlers with the data payload */
ai_input[0].n_batches = 1;
ai_input[0].data = AI_HANDLE_PTR(pIn);
ai_output[0].n_batches = 1;
ai_output[0].data = AI_HANDLE_PTR(pOut);
batch = ai_network_run(network, ai_input, ai_output);
if (batch != 1) {
err = ai_network_get_error(network);
printf("AI ai_network_run error - type=%d code=%d\r\n", err.type, err.code);
Error_Handler();
}
}
5.模型处理。用到函数postprocess。可以看到在output_labels标记人脸检测结果,ai_output_buffer为置信度,并将检测信息拷贝到了msg.
/**
* @brief Run post-processing operation
* @param ai_output_buffer pointer to NN output
*/
void postprocess(void* ai_output_buffer)
{
int ranking[NN_OUTPUT_CLASS_NUMBER];
/**NN ouput dequantization if required**/
for(int32_t i = AI_NET_OUTPUT_SIZE - 1; i >= 0; i--)
{
int32_t q_value = (int32_t) *(((uint8_t *) ai_output_buffer) + i);
*(((float *)ai_output_buffer) + i) = 0.01627892255783081f * ((float) q_value - 113.0f);
}
/* Add missing softmax layer to get a normalized probability distribution */
AI_Softmax((float *)ai_output_buffer);
/**Perform ranking**/
for (int i = 0; i < NN_OUTPUT_CLASS_NUMBER; i++)
{
ranking[i] = i;
}
UTILS_Bubblesort((float*)(ai_output_buffer), ranking, NN_OUTPUT_CLASS_NUMBER);
sprintf(msg, "%s %.0f%%", output_labels[ranking[0]], 100* (*(float*)(ai_output_buffer)));
//sprintf(msg, "%.3f, %.3f, %.3f", ((float*)(ai_output_buffer))[0], ((float*)(ai_output_buffer))[1], ((float*)(ai_output_buffer))[2]);
}
6.在DCMI帧回调HAL_DCMI_FrameEventCallback里,可以看到AI人脸检测结果在LCD上显示。这里加了一个Title 标题。
/*
路径:\Drivers\BSP\OV2640\OV2640.c
*/
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
char *title_ste={" Wio Lite AI STM32H725AE Board "};
LCD_Frame_Draw_Image(0, 0, 320, 240, (uint16_t *)_OV2640->frame->buffer, ROTATION_LEFT);
LCD_Frame_Draw_String(200, 220, 300, 16, 16, msg, ROTATION_LEFT);
LCD_Frame_Draw_String(44, 8, 300, 16, 16, title_ste, ROTATION_LEFT);
LCD_Frame_Show();
OV2640_DMA_Config(_OV2640->frame->buffer, (_OV2640->frame->length)/4);
}
7.main函数
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART3_UART_Init();
MX_LTDC_Init();
MX_I2C4_Init();
MX_DMA_Init();
MX_DCMI_Init();
MX_TIM2_Init();
MX_OCTOSPI1_Init();
MX_CRC_Init();
/* USER CODE BEGIN 2 */
LOG("here");
Psram_Init();
/*OV2640 Init*/
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); //XCLK
OV2640_Init(&OV2640);
OV2640_ReadID(&(OV2640.ID));
LOG("ID: %02X %02X \r\n", OV2640.ID.PIDH, OV2640.ID.PIDL);
OV2640_UXGAConfig();
//flip after all the camera setting ,or camera setting will change REG04 value.
OV2640_Flip();
OV2640_Start();
//initial IPL and AI
STM32Ipl_InitLib(buffer_ipl, IPL_BUFFER_SIZE);
/* The STM32 CRC IP clock should be enabled to use the network runtime library */
__HAL_RCC_CRC_CLK_ENABLE();
aiInit();
frame_rgb565.data = frame.buffer;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_0, GPIO_PIN_SET);
/*
* Here we use IPL to do image pre-processing, and then use the pre-processed results as input to the model, and finally do post-processing.
*/
// rgb565 320*240*2 to grayscale 320*240
if (STM32Ipl_Convert(&frame_rgb565, &frame_rgb565_grayscale) != stm32ipl_err_Ok) {
while(1);
}
// grayscale 320*240 to grayscale 96x96x2
if (STM32Ipl_Resize(&frame_rgb565_grayscale, &frame_grayscale_resized, &roi) != stm32ipl_err_Ok) {
while(1);
}
//AI
aiRun(buffer_grayscale_resized, network_output);
/*
* for debug
*sprintf(msg, "%d, %d, %d, %d", network_output[0], network_output[1], network_output[2], network_output[0]+network_output[1]+network_output[2]);
*/
postprocess(network_output);
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_0, GPIO_PIN_RESET);
}
/* USER CODE END 3 */
}
四.测验
编译烧录后,摄像头不对准人,LCD显示如下。
人脸检测视频
[localvideo]48e4a911205b927709bc68236169c058[/localvideo]
至此实现开发板人脸检测。
【1】STM32Cube.AI简介
https://stm32ai.st.com/zh/stm32-cube-ai/
【2】ST中文论坛
https://shequ.stmicroelectronics.cn/portal.php
PS:文末附工程代码.
-
发表了主题帖:
《ROS2智能机器人开发实践》--5.服务、通信接口、参数、动作、分布式通信、DDS
本帖最后由 dirty 于 2025-2-23 16:04 编辑
本篇将梳理学习ROS2应用开发方法之服务、通信接口、参数、动作、分布式通信、DDS。
一.服务
1.通信模型
发送一个请求,反馈一个应答的形式,这种通信机制在ROS中成为服务,Service。
2.客户端服务端模型
从服务的实现机制上来看,这种你问我答的形式叫做客户端/服务器模型,简称为CS模型,客户端在需要某些数据的时候,针对某个具体的服务,发送请求信息,服务器端收到请求之后,就会进行处理并反馈应答信息。
3.同步通讯
在服务通信中,客户端可以通过接收到的应答信息,判断服务器端的状态,我们也称之为同步通信。
4.一对多通信
服务通信模型中,服务器端唯一,但客户端可以不唯一。
5.服务接口
服务通信的核心还是要传递数据,数据变成了两个部分,一个请求的数据,还有一个反馈的数据,这些数据和话题消息一样,在ROS中也是要标准定义的,话题使用.msg文件定义,服务使用的是.srv文件定义。
6.案例:加法求解器
(1)启动两个终端,并运行如下节点,第一个节点是服务端,等待请求数据并提供求和功能,第二个节点是客户端,发送传入的两个加数并等待求和结果。
ros2 run learning_service service_adder_server
ros2 run learning_service service_adder_client 2 3
运行结果
(2)代码
(3)流程总结
客户端流程
●编程接口初始化
●创建节点并初始化
●创建客户端对象
●创建并发送请求数据
●等待服务器端应答数据
●销毁节点并关闭接口
服务端流程
●编程接口初始化
●创建节点并初始化
●创建服务器端对象
●通过回调函数处进行服务
●向客户端反馈应答结果
●销毁节点并关闭接口
二.通信接口
1.接口定义
ROS的通信系统,它的主要目的就是传输数据,那就得让大家高效的建立连接,并且准确包装和解析传输的数据内容,话题、服务等机制也就诞生了,他们传输的数据,都要符合通信接口的标准定义。
2.ROS通信接口
接口可以让程序之间的依赖降低,便于我们使用别人的代码,减少重复造轮子。
ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。
3.语言无关
为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的。
●话题通信接口的定义使用的是msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行。
●服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的“---”区分。
●动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,此时接口的定义分成了三个部分,分别是动作的目标、运动的结果、周期反馈。
4.标准接口
在ROS安装路径中的share文件夹中找到,涵盖众多标准定义。
三.动作
动作是一种一种ROS通信机制。目的就是便于对机器人某一完整行为的流程进行管理。
1.通信模型
一个需要执行一段时间的行为,使用动作的通信机制就更为合适,就像装了一个进度条,我们可以随时把控进度,如果运动过程当中,我们还可以随时发送一个取消运动的命令。
客户端/服务器模型
动作和服务类似,使用的也是客户端和服务器模型,客户端发送动作的目标,想让机器人干什么,服务器端执行动作过程, 控制机器人达到运动的目标,同时周期反馈动作执行过程中的状态。
客户端发送一个运动的目标,想让机器人动起来,服务器端收到之后,就开始控制机器人运动,一边运动,一边反馈当前的状态,如果是一个导航动作,这个反馈可能是当前所处的坐标,如果是机械臂抓取,这个反馈可能又是机械臂的实时姿态。当运动执行结束后,服务器再反馈一个动作结束的信息。整个通信过程就此结束。
2.一对多通信
和服务一样,动作通信中的客户端可以有多个,大家都可以发送运动命令,但是服务器端只能有一个,一个机器人,先执行完成一个动作,才能执行下一个动作。
3.同步通信
有反馈,动作也是一种同步通信机制,之前我们也介绍过,动作过程中的数据通信接口,使用.action文件进行定义。
4.由服务和话题合成
上面的动图,动作的三个通信模块,有两个是服务,一个是话题。当客户端发送运动目标时,使用的是服务的请求调用,服务器端也会反馈一个应带,表示收到命令。动作的反馈过程,其实就是一个话题的周期发布,服务器端是发布者,客户端是订阅者。
动作是一种应用层的通信机制,其底层就是基于话题和服务来实现的。
四.参数
参数是ROS机器人系统中的全局字典,可以运行多个节点中共享数据。
1.全局字典
在ROS系统中,参数是以全局字典的形态存在的。字典由名称和数值组成,也叫做键和值,合成键值。
2.可动态监控
在ROS2中,参数的特性非常丰富,比如某一个节点共享了一个参数,其他节点都可以访问,如果某一个节点对参数进行了修改,其他节点也有办法立刻知道,从而获取最新的数值。这在参数的高级编程中,大家都可能会用到。
五.分布式通信
在ROS系统中,机器人功能是由各种节点组成的,这些节点可能位于不同的计算机中,这种结构可以将原本资源消耗较多的任务,分配到不同的平台上,减轻计算压力,这就是分布式通信框架的典型应用之一。
分布式网络搭建包括装系统、安装ROS2、远程桌面网络控制。分布式数据传输确保设备与电脑连在同一个局域网络中,打通两个计算平台的通信能力,包括话题、服务、动作等通信。
分布式网络分组
ROS2提供了一个DOMAIN的机制,就类似分组一样,处于同一个DOMAIN中的计算机才能通信,我们可以在电脑和树莓派端的.bashrc中加入这样一句配置,即可将两者分配到一个小组中。
export ROS_DOMAIN_ID=<your_domain_id>
六.DDS
ROS2中最为重大的变化--DDS,前面学习的话题、服务、动作,他们底层通信的具体实现过程,都是靠DDS来完成的,它相当于是ROS机器人系统中的神经网络。
1.通信模型
DDS的核心是通信,能够实现通信的模型和软件框架非常多,这里我们列出常用的四种模型:
●点对点模型,许多客户端连接到一个服务端,每次通信时,通信双方必须建立一条连接。当通信节点增多时,连接数也会增多。而且每个客户端都需要知道服务器的具体地址和所提供的服务,一旦服务器地址发生变化,所有客户端都会受到影响。
●Broker模型,针对点对点模型进行了优化,由Broker集中处理所有人的请求,并进一步找到真正能响应该服务的角色。优点:客户端就不用关心服务器的具体地址了。缺点:Broker作为核心,它的处理速度会影响所有节点的效率,当系统规模增长到一定程度,Broker就会成为整个系统的性能瓶颈。如果Broker发生异常,可能导致整个系统都无法正常运转。之前的ROS1系统,使用的就是类似这样的架构。
●广播模型,所有节点都可以在通道上广播消息,并且节点都可以收到消息。这个模型解决了服务器地址的问题,而且通信双方也不用单独建立连接,但是广播通道上的消息太多了,所有节点都必须关心每条消息,其实很多是和自己没有关系的。
●以数据为中心的DDS模型了,这种模型与广播模型有些类似,所有节点都可以在DataBus上发布和订阅消息。但它的先进之处在于,通信中包含了很多并行的通路,每个节点可以只关心自己感兴趣的消息,忽略不感兴趣的消息,有点像是一个旋转火锅,各种好吃的都在这个DataBus传送,我们只需要拿自己想吃的就行,其他的和我们没有关系。
在这几种通信模型中,DDS的优势更加明显。
2.DDS
DDS的全称是Data Distribution Service,也就是数据分发服务,是一套专门为实时系统设计的数据分发/订阅标准.
DDS强调以数据为中心,可以提供丰富的服务质量策略,以保障数据进行实时、高效、灵活地分发,可满足各种分布式实时通信应用需求。
3.DDS在ROS2中的应用
DDS在ROS2系统中的位置至关重要,所有上层建设都建立在DDS之上。在这个ROS2的架构图中,蓝色和红色部分就是DDS。
DDS是一种通信的标准,DDS是一种通信的标准,能够按照DDS标准实现的通信系统很多,这里每一个红色模块,就是某一企业或组织实现的一种DDS系统。
为了实现对多个DDS的兼容,ROS设计了一个Middleware中间件,也就是一个统一的标准,不管我们用那个DDS,保证上层编程使用的函数接口都是一样的。
DDS通讯模型如下
DDS中的基本结构是Domain,Domain将各个应用程序绑定在一起进行通信。DDS中另外一个重要特性就是质量服务策略,QoS。
QoS是一种网络传输策略,应用程序指定所需要的网络传输质量行为,QoS服务实现这种行为要求,尽可能地满足客户对通信质量的需求,可以理解为数据提供者和接收者之间的合约。具体策略如下:
●DEADLINE策略,表示通信数据必须要在每次截止时间内完成一次通信;
●HISTORY策略,表示针对历史数据的一个缓存大小;
●RELIABILITY策略,表示数据通信的模式,配置成BEST_EFFORT,就是尽力传输模式,网络情况不好的时候,也要保证数据流畅,此时可能会导致数据丢失,配置成RELIABLE,就是可信赖模式,可以在通信中尽量保证图像的完整性,我们可以根据应用功能场景选择合适的通信模式;
●DURABILITY策略,可以配置针对晚加入的节点,也保证有一定的历史数据发送过去,可以让新节点快速适应系统。
DDS的加入,让ROS2的通信系统焕然一新,多众多样的通信配置,可以更好的满足不同场景下的机器人应用。
4.案例:DDS编程
(1)代码配置DDS,以Hello World话题通信为例。
执行以下指令运行
ros2 run learning_qos qos_helloworld_pub
ros2 run learning_qos qos_helloworld_sub
运行结果如下:
(2)代码解析
发布者代码learning_qos/qos_helloworld_pub.py,订阅者代码learning_qos/qos_helloworld_sub.py
至此,ROS2的应用开发方法基本梳理学习实践了一遍,为后面进一步学习与深入奠定一个好的基础。
- 2025-02-22
-
发表了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--5.摄像头屏显
本帖最后由 dirty 于 2025-2-22 19:19 编辑
本篇实现开发板摄像头驱动屏显。
一.原理
开发板使用了一颗200W像素 DVP接口 OV2640摄像头。其DCMI(数字摄像头接口)原理如下:
引脚定义
DCMI_D0--D7 8位数据线引脚
DCMI_HSYSN 行同步信号输入引脚,由摄像头输入。表示一行有效数据的传输开始或结束
DCMI_VSYSN 帧同步信号输入引脚,和行同步信号差不多,而该信号表示一帧有效数据的传输开始或结束
DCMI_PCLK 像素同步时钟输入,由摄像头提供,每个时钟周期数据会从D[0:7]传输8bit的像素过来。
DCMI_RST 复位信号(低电平有效).
DCMI_PWDN 掉电模式使能(高电平有效)
IIC4_SDA SCCB接口数据线
IIC4_SCL SCCB接口时钟线
二.DCMI接口配置
1.在STM32CubeMX 配置模式及引脚定义
2.参数设置。这里有两项:模式配置(包含像素时钟极性、垂直同步极性、水平同步极性、帧抓取频率、JPEG图像模式)和图像抓取接口配置(包含点选择模式和行选择模式)。
3.DAM配置
4.中断配置
5.SCCBs数据时钟引脚SDA/SCL配置,即I2C4配置。
6.复位引脚PH12、掉电模式使能引脚PE7 GPIO配置
至此,摄像头接口引脚配置基本完成,点击生成工程代码。
三.代码
1.OV2640初始化
在生成代码中,可以看到MX_I2C4_Init对I2C4进行了初始化。定义OV2640结构体变量如下
typedef struct
{
DCMI_HandleTypeDef *dcmi;
DMA_HandleTypeDef *dma;
I2C_HandleTypeDef *i2c;
Frame_TypeDef *frame;
OV2640_IDTypeDef ID;
uint32_t fps;
uint8_t time_show;
}OV2640_TypeDef;
static OV2640_TypeDef OV2640 ={ .dcmi = &hdcmi,
.dma = &hdma_dcmi_pssi,
.i2c = &hi2c4,
.frame = &frame
};
使用I2C配置摄像头寄存器。BSP包OV2640.c、OV2640.h源文件见附件。
/*OV2640 Init*/
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); //XCLK
OV2640_Init(&OV2640);
OV2640_ReadID(&(OV2640.ID));
LOG("ID: %02X %02X \r\n", OV2640.ID.PIDH, OV2640.ID.PIDL);
OV2640_UXGAConfig();
//flip after all the camera setting ,or camera setting will change REG04 value.
OV2640_Flip();
OV2640_Start();
2.LCD初始化函数MX_LTDC_Init里,将图层配置使能打开,帧缓冲LCD_BUFFER定义,代码如下
#define EXTMEM __attribute__((section(".externalram")))
#define DTCMMEM __attribute__((section(".dtcmram")))
#define ITCMMEM __attribute__((section(".itcmram")))
EXTMEM uint8_t LCD_BUFFER[320 * 240 * 2];
EXTMEM static uint8_t LCD_FRAME_BUFFER[320 * 240 * 2];
/* LTDC init function */
void MX_LTDC_Init(void)
{
/* USER CODE BEGIN LTDC_Init 0 */
/*点亮背光*/
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET); //使能的LCD背光
HAL_Delay(100);
/* USER CODE END LTDC_Init 0 */
LTDC_LayerCfgTypeDef pLayerCfg = {0};
/* USER CODE BEGIN LTDC_Init 1 */
/* USER CODE END LTDC_Init 1 */
hltdc.Instance = LTDC;
hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL;
hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL;
hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL;
hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;
hltdc.Init.HorizontalSync = 9;
hltdc.Init.VerticalSync = 3;
hltdc.Init.AccumulatedHBP = 19;
hltdc.Init.AccumulatedVBP = 5;
hltdc.Init.AccumulatedActiveW = 259;
hltdc.Init.AccumulatedActiveH = 325;
hltdc.Init.TotalWidth = 297;
hltdc.Init.TotalHeigh = 327;
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 0;
if (HAL_LTDC_Init(&hltdc) != HAL_OK)
{
Error_Handler();
}
#if 1
pLayerCfg.WindowX0 = 0;
pLayerCfg.WindowX1 = 240;
pLayerCfg.WindowY0 = 0;
pLayerCfg.WindowY1 = 320;
pLayerCfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
pLayerCfg.Alpha = 255;
pLayerCfg.Alpha0 = 0;
pLayerCfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA;
pLayerCfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA;
pLayerCfg.FBStartAdress = LCD_BUFFER;
pLayerCfg.ImageWidth = 240;
pLayerCfg.ImageHeight = 320;
pLayerCfg.Backcolor.Blue = 0;
pLayerCfg.Backcolor.Green = 0;
pLayerCfg.Backcolor.Red = 0;
if (HAL_LTDC_ConfigLayer(&hltdc, &pLayerCfg, 0) != HAL_OK)
{
Error_Handler();
}
#endif
/* USER CODE BEGIN LTDC_Init 2 */
/* USER CODE END LTDC_Init 2 */
}
3.LCD显示库文件
LCD.c/LCD.h主要实现帧缓冲LCD_BUFFER数据赋值做LCD显示。源文件见附件。
4.在摄像头帧数据事件回调函数,将摄像头俘获图像映射到LCD显示如下,主要LCD_Frame_Show复制转换。
void HAL_DCMI_FrameEventCallback(DCMI_HandleTypeDef *hdcmi)
{
LCD_Frame_Draw_Image(0, 0, 320, 240, (uint16_t *)_OV2640->frame->buffer, ROTATION_LEFT);
LCD_Frame_Draw_String(200, 220, 300, 16, 16, msg, ROTATION_LEFT);
LCD_Frame_Show();
OV2640_DMA_Config(_OV2640->frame->buffer, (_OV2640->frame->length)/4);
}
5.主函数如下
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART3_UART_Init();
MX_LTDC_Init();
MX_I2C4_Init();
MX_DMA_Init();
MX_DCMI_Init();
MX_TIM2_Init();
MX_OCTOSPI1_Init();
MX_CRC_Init();
/* USER CODE BEGIN 2 */
LOG("here");
Psram_Init();
/*OV2640 Init*/
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3); //XCLK
OV2640_Init(&OV2640);
OV2640_ReadID(&(OV2640.ID));
LOG("ID: %02X %02X \r\n", OV2640.ID.PIDH, OV2640.ID.PIDL);
OV2640_UXGAConfig();
//flip after all the camera setting ,or camera setting will change REG04 value.
OV2640_Flip();
OV2640_Start();
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
HAL_Delay(1000);
}
/* USER CODE END 3 */
}
四.测试
烧录后,可以看到LCD显示摄像头画面,如下
[localvideo]c233a44ecd62f82acacd4f2f99950fcc[/localvideo]
至此,实现摄像头屏显功能。
-
发表了主题帖:
【Wio Lite AI STM32H725AE视觉开发板】--4.LCD刷屏
本帖最后由 dirty 于 2025-2-22 12:41 编辑
本篇讲述点亮LCD刷屏。
一.硬件原理
开发板使用RGB565触摸屏,分辨率240*320,硬件原理图如下:
二.配置
打开STM32CubeMX,选择好芯片型号,Start Project,进入配置。在Pinout&Configuration选项卡下,选中LTDC。
1.LCD显示屏型号与引脚配置。这里根据原理图逐一选择配置成如下。
2.参数设置。这里主要有宽度同步、高度同步、信号极性、图层默认颜色。
3.图层设置。主要有窗口位置、像素参数、混合、图层默认颜色、帧缓冲、图层数量。其中帧缓冲LCD_BUFFER是一个指向SDRAM起始地址的宏定义。
4.背光引脚PF5配置。
配置到此基本完成,这里选择STM32CubeIDE,点击GEBERATE CoDE,生成工程代码。
三.代码
1.背光引脚使能打开,这个手动添加在MX_LTDC_Init里如下,注意由低置高再稳定。
/*点亮背光*/
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET); //使能的LCD背光
HAL_Delay(100);
2.将Layer层函数注释。MX_LTDC_Init完整代码如下:
/* LTDC init function */
void MX_LTDC_Init(void)
{
/* USER CODE BEGIN LTDC_Init 0 */
/*点亮背光*/
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOG, GPIO_PIN_5, GPIO_PIN_SET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET); //使能的LCD背光
HAL_Delay(100);
/* USER CODE END LTDC_Init 0 */
LTDC_LayerCfgTypeDef pLayerCfg = {0};
/* USER CODE BEGIN LTDC_Init 1 */
/* USER CODE END LTDC_Init 1 */
hltdc.Instance = LTDC;
hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL;
hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL;
hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL;
hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC;
hltdc.Init.HorizontalSync = 9;
hltdc.Init.VerticalSync = 3;
hltdc.Init.AccumulatedHBP = 19;
hltdc.Init.AccumulatedVBP = 5;
hltdc.Init.AccumulatedActiveW = 259;
hltdc.Init.AccumulatedActiveH = 325;
hltdc.Init.TotalWidth = 297;
hltdc.Init.TotalHeigh = 327;
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 0;
if (HAL_LTDC_Init(&hltdc) != HAL_OK)
{
Error_Handler();
}
#if 0
pLayerCfg.WindowX0 = 0;
pLayerCfg.WindowX1 = 240;
pLayerCfg.WindowY0 = 0;
pLayerCfg.WindowY1 = 320;
pLayerCfg.PixelFormat = LTDC_PIXEL_FORMAT_RGB565;
pLayerCfg.Alpha = 255;
pLayerCfg.Alpha0 = 0;
pLayerCfg.BlendingFactor1 = LTDC_BLENDING_FACTOR1_PAxCA;
pLayerCfg.BlendingFactor2 = LTDC_BLENDING_FACTOR2_PAxCA;
pLayerCfg.FBStartAdress = LCD_BUFFER;
pLayerCfg.ImageWidth = 240;
pLayerCfg.ImageHeight = 320;
pLayerCfg.Backcolor.Blue = 0;
pLayerCfg.Backcolor.Green = 0;
pLayerCfg.Backcolor.Red = 0;
if (HAL_LTDC_ConfigLayer(&hltdc, &pLayerCfg, 0) != HAL_OK)
{
Error_Handler();
}
#endif
/* USER CODE BEGIN LTDC_Init 2 */
/* USER CODE END LTDC_Init 2 */
}
3.在main函数主循环添加刷屏代码
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MPU Configuration--------------------------------------------------------*/
MPU_Config();
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART3_UART_Init();
MX_LTDC_Init();
/* USER CODE BEGIN WHILE */
while (1)
{
//红
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 255;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
//绿
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 255;
hltdc.Init.Backcolor.Red = 0;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
//蓝
hltdc.Init.Backcolor.Blue = 255;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 0;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
//黄色
hltdc.Init.Backcolor.Blue = 0;
hltdc.Init.Backcolor.Green = 255;
hltdc.Init.Backcolor.Red = 255;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
//紫色
hltdc.Init.Backcolor.Blue = 255;
hltdc.Init.Backcolor.Green = 0;
hltdc.Init.Backcolor.Red = 255;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
//青色
hltdc.Init.Backcolor.Blue = 255;
hltdc.Init.Backcolor.Green = 255;
hltdc.Init.Backcolor.Red = 0;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
//白色
hltdc.Init.Backcolor.Blue = 255;
hltdc.Init.Backcolor.Green = 255;
hltdc.Init.Backcolor.Red = 255;
HAL_LTDC_Init(&hltdc);
HAL_Delay(1000);
}
/* USER CODE END 3 */
}
四.测验
编译烧录,这里在STM32CubeMX IDE里在线烧录需要调试烧录器驱动是严格ST-LINK,之前准备的创新工坊PWLINK2设备驱动CMSIS-DAP,这里将编译的elf固件用脱机烧录工具即可。显示屏依次循环刷屏不同颜色。效果见视频。
[localvideo]6c6814ca014ea8a00fd33d6983b0fc48[/localvideo]
至此,实现LCD驱动刷屏。
- 2025-02-21
-
回复了主题帖:
恭喜LitchiCheng走马上任机器人开发版块版主!!
加油加油
- 2025-02-20
-
回复了主题帖:
【匠芯创D133CBS】--6.LVGL界面移植
jjppo 发表于 2025-1-16 23:03
大佬,请问这个错误怎么解决
这宏定义提示没定义,看下有没定义以及头文件包含
-
回复了主题帖:
【嘉楠K230开发板】跌倒检测
Jacktang 发表于 2025-2-17 07:28
这个项目可以养老、护理方面大有用处,好好整理一下做成实物
养老、护理方面确实有防跌倒/跌倒报警需求,方案可行是实物落地前提,有需要也可以搞
-
回复了主题帖:
【嘉楠K230开发板】跌倒检测
lugl4313820 发表于 2025-2-17 08:59
可以计算身体变化的时间吗?如果他是慢慢的自己倒下,会不会检测得出来?
检测锚定,状态变化计算时间,多目标动态侦别管理,往深处研究应该可以。 慢慢倒下可以的,可以一张图片镜头旋转检测。
- 2025-02-16
-
发表了主题帖:
【嘉楠K230开发板】跌倒检测
本篇讲述使用K230进行跌倒检测。
一.原理流程
跌倒检测主要根据人体姿态来判断,可以用于老人、小孩跌倒监护。CanMV AI视觉框架和MicroPython编程能快速实现人体关键点检测。
编程思路流程如下:
二.代码实现与测验
迭代检测代码如下:
'''
跌倒检测
'''
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 aicube
# 自定义跌倒检测类,继承自AIBase基类
class FallDetectionApp(AIBase):
def __init__(self, kmodel_path, model_input_size, labels, anchors, confidence_threshold=0.2, nms_threshold=0.5, nms_option=False, strides=[8,16,32], 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.labels = labels # 分类标签
self.anchors = anchors # 锚点数据,用于跌倒检测
self.strides = strides # 步长设置
self.confidence_threshold = confidence_threshold # 置信度阈值
self.nms_threshold = nms_threshold # NMS(非极大值抑制)阈值
self.nms_option = nms_option # NMS选项
self.rgb888p_size = [ALIGN_UP(rgb888p_size[0], 16), rgb888p_size[1]] # sensor给到AI的图像分辨率,并对宽度进行16的对齐
self.display_size = [ALIGN_UP(display_size[0], 16), display_size[1]] # 显示分辨率,并对宽度进行16的对齐
self.debug_mode = debug_mode # 是否开启调试模式
self.color = [(255,0, 0, 255), (255,0, 255, 0), (255,255,0, 0), (255,255,0, 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/libs/AI2D.py查看
def config_preprocess(self, input_image_size=None):
with ScopedTiming("set preprocess config", self.debug_mode > 0): # 计时器,如果debug_mode大于0则开启
ai2d_input_size = input_image_size if input_image_size else self.rgb888p_size # 初始化ai2d预处理配置,默认为sensor给到AI的尺寸,可以通过设置input_image_size自行修改输入尺寸
top, bottom, left, right = self.get_padding_param() # 获取padding参数
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]]) # 构建预处理流程
# 自定义当前任务的后处理,results是模型输出array的列表,这里使用了aicube库的anchorbasedet_post_process接口
def postprocess(self, results):
with ScopedTiming("postprocess", self.debug_mode > 0):
dets = aicube.anchorbasedet_post_process(results[0], results[1], results[2], self.model_input_size, self.rgb888p_size, self.strides, len(self.labels), self.confidence_threshold, self.nms_threshold, self.anchors, self.nms_option)
return dets
# 绘制检测结果到画面上
def draw_result(self, pl, dets):
with ScopedTiming("display_draw", self.debug_mode > 0):
if dets:
pl.osd_img.clear() # 清除OSD图像
for det_box in dets:
# 计算显示分辨率下的坐标
x1, y1, x2, y2 = det_box[2], det_box[3], det_box[4], det_box[5]
w = (x2 - x1) * self.display_size[0] // self.rgb888p_size[0]
h = (y2 - y1) * self.display_size[1] // self.rgb888p_size[1]
x1 = int(x1 * self.display_size[0] // self.rgb888p_size[0])
y1 = int(y1 * self.display_size[1] // self.rgb888p_size[1])
x2 = int(x2 * self.display_size[0] // self.rgb888p_size[0])
y2 = int(y2 * self.display_size[1] // self.rgb888p_size[1])
# 绘制矩形框和类别标签
pl.osd_img.draw_rectangle(x1, y1, int(w), int(h), color=self.color[det_box[0]], thickness=2)
pl.osd_img.draw_string_advanced(x1, y1-50, 32," " + self.labels[det_box[0]] + " " + str(round(det_box[1],2)), color=self.color[det_box[0]])
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/examples/kmodel/yolov5n-falldown.kmodel"
confidence_threshold = 0.3
nms_threshold = 0.45
rgb888p_size = [1920, 1080]
labels = ["Fall","NoFall"] # 模型输出类别名称
anchors = [10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326] # anchor设置
# 初始化PipeLine,用于图像处理流程
pl = PipeLine(rgb888p_size=rgb888p_size, display_size=display_size, display_mode=display_mode)
pl.create()
# 初始化自定义跌倒检测实例
fall_det = FallDetectionApp(kmodel_path, model_input_size=[640, 640], labels=labels, anchors=anchors, confidence_threshold=confidence_threshold, nms_threshold=nms_threshold, nms_option=False, strides=[8,16,32], rgb888p_size=rgb888p_size, display_size=display_size, debug_mode=0)
fall_det.config_preprocess()
clock = time.clock()
while True:
clock.tick()
img = pl.get_frame() # 获取当前帧数据
res = fall_det.run(img) # 推理当前帧
fall_det.draw_result(pl, res) # 绘制结果到PipeLine的osd图像
print(res) # 打印结果
pl.show_image() # 显示当前的绘制结果
gc.collect() # 垃圾回收
print(clock.fps()) #打印帧率
从代码我们可以看到用到/sdcard/examples/kmodel/yolov5n-falldown.kmodel 模型。FallDetectionApp初始化自定义跌倒检测实例。config_preprocess配置预处理。postprocess自定义当前任务的后处理。draw_result绘制检测结果到画面上。main函数中的res为识别结果。运行识别结果如下
这个对特定人群,在养老、护理方面等还是挺实用的。
至此,实现K230跌倒检测功能。