Encoder Parameter
前言
在使用线激光对物体扫描时,为了获得物体的3D点云,有两种扫描方式:一种是线激光静止,物体随传送带或导轨移动获得整个物体的3D点云图;另一种是物体静止,线激光传感器随高精度导轨移动,获得整个物体的3D点云图。由于线激光是一帧一帧的获取图像,为了获取整个物体的3D点云图,它需要把很多帧的图像最终汇总到一个点云图中,也就是说,线激光在扫描时,其实是对物体进行了一个平均等分的切片处理。切片的精度会直接影响3D点云图的真实度。通常,我们为了提高精度,会在导轨或传送带上安装一个编码器,由于编码器的精度极高,这样,当导轨或传送带移动时,编码器会根据移动的固定距离,准确的发出采集图像的脉冲信号。在设置线激光编码器采集脉冲信号参数时,有两种方式可以获取该参数:一种是让导轨或传送带移动固定的距离,例如1m,然后查看线激光传感器所接收到的编码器AB相脉冲信号数量,例如采集到10000个脉冲信号,然后根据项目所需要达到的扫描精度,例如0.5mm,那么我们就可以估算出每5(=10000/(1/0.005))个脉冲采集一个图像的参数;还有一种方式就是通过编码器,减速机,导轨齿轮参数的数据,准确计算出每一幅图像的编码器脉冲信号数。通常推荐第二种,因为第二种更精确。本文将阐述第二种计算脉冲数的方式,我们通过机器人安装在导轨上,然后机器人手持线激光固定不动,导轨移动扫描物体为例:
设备参数
首先,如果需要通过物理参数计算出编码器与导轨移动距离的关系,我们需要通过资料查阅获取以下物理量:
- 减速机减速比例(编码器安装在减速箱导轨齿轮侧),iR (Ratio) = 8:1
- 传动比,RTGR (Robot Transmission Gear Ratio) = 314 (rad/m)
- 编码器每转一圈的脉冲数,EPC (Incremental rotary encoder Cycle Count) = 2048
计算公式
通过以上几个参数,我们就可以开始计算了:
- 导轨每米齿轮转动弧度数:RPM (Radian per Meter) = RTGR/iR
- 导轨每米脉冲数:CPM (Cycles per Meter) = EPC(RPM/(2π)) = EPC((RTGR/iR)/(2π)) = 2048((314/8)/(23.14))= 12800
- 编码器每脉冲导轨移动距离:DPC (Distance per full cycle) = 1000/CPM = 1000/12800 = 0.078125
以上是设备的固有理论参数,当用于实际项目时,我们还需要针对项目设备的负载和精度做一个优化,线激光的扫描频率不是越快越好,扫描过快,有可能导致传感器超频,图像丢失。通常情况下,线激光扫描的精度保证在0.5mm就可以了。所以我们可以设置如下参数:
- 线激光触发扫描脉冲频率(考虑到AB相4倍脉冲模式):SPL (Steps per line) = 0.5/DPC*4 = 25.6 ≈ 25
- 线激光扫描精度:Accuracy = DPC/4* SPL = 0.078125/4*25=0.48828125
另外线激光传感器的内存也是有限的,对于较长工件,可能需要分多幅图连续采集,如果一幅图采集的帧数超出线激光传感器的容量,也会导致图像丢失。例如我们可以把一幅图设置为由500帧组成,那么:
- 每幅图的线扫帧数:PPF (Profiles per Frame) = 500
- 每幅图的导轨移动的距离:DPF (Distance per Frame) = Accuracy * PPF = 0.48828125*500=244.140625
- 对于一个10m长的工件,需要扫描图像数量为:10000/244.140625 = 40.96 ≈ 42
工具
为了测试方便,我这边写了一个Python小程序,可以自动计算以上参数值:
整个程序较为简单,代码如下:
from tkinter import *
import math
class Application(Frame):
def __init__(self, master=None):
super().__init__()
self.master = master
self.master.title("Encoder Parameter Utility")
self.master.geometry('500x500')
self.create_widgets()
def create_widgets(self):
row = -1
self.pi = DoubleVar()
self.pi.set(round(math.pi,2))
self.expected_accuracy = DoubleVar()
self.expected_accuracy.set(0.5)
self.gearbox_ratio = StringVar()
self.gearbox_ratio.set("8")
self.transmission_gear_ratio = StringVar()
self.transmission_gear_ratio.set("314")
self.encoder_cycles = StringVar()
self.encoder_cycles.set("2048")
self.profiles_per_frame = IntVar()
self.profiles_per_frame.set(500)
self.scan_length = DoubleVar()
self.scan_length.set(10000)
self.encoder_type = IntVar()
self.encoder_type.set(4)
self.radian_per_meter = DoubleVar()
self.cycles_per_meter = IntVar()
self.distance_per_cycle = DoubleVar()
self.steps_per_line = IntVar()
self.accuracy = DoubleVar()
self.distance_per_frame = DoubleVar()
self.number_frames = IntVar()
row += 1
self.label_expected_accuracy = Label(text="Expected Accuracy (mm)")
self.label_expected_accuracy.grid(row=row, column=0)
self.text_expected_accuracy = Entry(width=30, textvariable=self.expected_accuracy)
self.text_expected_accuracy.grid(row=row, column=1)
row += 1
self.label_pi = Label(text="PI")
self.label_pi.grid(row=row, column=0)
self.text_pi = Entry(width=30, textvariable=self.pi)
self.text_pi.grid(row=row, column=1)
row += 1
self.label_gearbox_ratio = Label(text="Gearbox Ratio")
self.label_gearbox_ratio.grid(row=row, column=0)
self.text_gearbox_ratio = Entry(width=30, textvariable=self.gearbox_ratio)
self.text_gearbox_ratio.grid(row=row, column=1)
row += 1
self.label_transmission_gear_ratio = Label(text="Transmission Gear Ratio")
self.label_transmission_gear_ratio.grid(row=row, column=0)
self.text_transmission_gear_ratio = Entry(width=30, textvariable=self.transmission_gear_ratio)
self.text_transmission_gear_ratio.grid(row=row, column=1)
row += 1
self.radio_encoder_type_single = Radiobutton(width=30, text="Single Channel", variable=self.encoder_type, value=1)
self.radio_encoder_type_single.grid(row=row, column=0)
self.radio_encoder_type_dual = Radiobutton(width=30, text="Dual Channel", variable=self.encoder_type, value=4)
self.radio_encoder_type_dual.grid(row=row, column=1)
row += 1
self.label_encoder_cycles = Label(text="Encoder Cycle Count")
self.label_encoder_cycles.grid(row=row, column=0)
self.text_encoder_cycles = Entry(width=30, textvariable=self.encoder_cycles)
self.text_encoder_cycles.grid(row=row, column=1)
row += 1
self.label_profiles_per_frame = Label(text="Profiles Per Frame")
self.label_profiles_per_frame.grid(row=row, column=0)
self.text_profiles_per_frame = Entry(width=30, textvariable=self.profiles_per_frame)
self.text_profiles_per_frame.grid(row=row, column=1)
row += 1
self.label_scan_length = Label(text="Scan Length (mm)")
self.label_scan_length.grid(row=row, column=0)
self.text_scan_length = Entry(width=30, textvariable=self.scan_length)
self.text_scan_length.grid(row=row, column=1)
row += 1
self.button_compute = Button(text="Compute", command=self.compute)
self.button_compute.grid(row=row, column=0)
row += 1
self.label_radian_per_meter = Label(text="Radian Per Meter")
self.label_radian_per_meter.grid(row=row, column=0)
self.text_radian_per_meter = Entry(width=30, state="readonly", textvariable=self.radian_per_meter)
self.text_radian_per_meter.grid(row=row, column=1)
row += 1
self.label_cycles_per_meter = Label(text="Cycles Per Meter")
self.label_cycles_per_meter.grid(row=row, column=0)
self.text_cycles_per_meter = Entry(width=30, state="readonly", textvariable=self.cycles_per_meter)
self.text_cycles_per_meter.grid(row=row, column=1)
row += 1
self.label_distance_per_cycle = Label(text="Distance Per Cycle")
self.label_distance_per_cycle.grid(row=row, column=0)
self.text_distance_per_cycle = Entry(width=30, state="readonly", textvariable=self.distance_per_cycle)
self.text_distance_per_cycle.grid(row=row, column=1)
row += 1
self.label_steps_per_line = Label(text="Steps Per Line (Dual Channel)")
self.label_steps_per_line.grid(row=row, column=0)
self.text_steps_per_line = Entry(width=30, state="readonly", textvariable=self.steps_per_line)
self.text_steps_per_line.grid(row=row, column=1)
row += 1
self.label_accuracy = Label(text="Accuracy")
self.label_accuracy.grid(row=row, column=0)
self.text_accuracy = Entry(width=30, state="readonly", textvariable=self.accuracy)
self.text_accuracy.grid(row=row, column=1)
row += 1
self.label_distance_per_frame = Label(text="Distance Per Frame")
self.label_distance_per_frame.grid(row=row, column=0)
self.text_distance_per_frame = Entry(width=30, state="readonly", textvariable=self.distance_per_frame)
self.text_distance_per_frame.grid(row=row, column=1)
row += 1
self.label_number_frames = Label(text="Number Frames")
self.label_number_frames.grid(row=row, column=0)
self.text_number_frames = Entry(width=30, state="readonly", textvariable=self.number_frames)
self.text_number_frames.grid(row=row, column=1)
def compute(self):
rtgr = float(self.transmission_gear_ratio.get().strip())
ir = float(self.gearbox_ratio.get().strip())
epc = int(self.encoder_cycles.get().strip())
rpm = rtgr/ir
cpm = epc*(rpm/(self.pi.get()*2))
dpc = 1000/cpm
spl = int(self.expected_accuracy.get()/dpc*self.encoder_type.get())
accuracy = dpc / self.encoder_type.get() * spl
dpf = accuracy * self.profiles_per_frame.get()
nf = round(self.scan_length.get()/dpf) + 1
self.radian_per_meter.set(rpm)
self.cycles_per_meter.set(round(cpm))
self.distance_per_cycle.set(dpc)
self.steps_per_line.set(spl)
self.accuracy.set(round(accuracy, 2))
self.distance_per_frame.set(round(dpf, 2))
self.number_frames.set(nf)
root = Tk()
app = Application(master=root)
app.mainloop()
注意
在设置线激光触发扫描脉冲频率SPL时(Y向分辨率),除了需要参考项目的精度要求,还需要参考X方向的分辨率,一般推荐X向和Y向的精度保持相当,这样可以使采集的图像长宽比更符合实际工件,后期使用视觉算法获取特征时也更容易。