Freescale安徽赛结束,感触颇多。
这次就线性CCD,特别是曝光的问题想做些总结。
Freescale光电组指定的传感器是线性CCD——TSL1401CL。
下面是官方说明中对此线性CCD的简单描述:
所以,TSL1401CL的核心是128个光电二极管组成的感光阵列,阵列后面有一排积分电容,光电二极管在光能量冲击下产生光电流,构成有源积分电路,那么积分电容就是用来存储光能转化后的电荷。积分电容存储的电荷越多,说明前方对应的那个感光二极管采集的光强越大。反映在像素点上就是,像素灰度低。光强接近饱和,像素点灰度趋近于全白,则呈白电平。
TSL1401CL的功能框图与引脚配置如下:
我个人认为, 对于做车来说,不需要懂太多关于线性CCD的硬性知识,因为太底层的架构与原理也比较复杂,对小车所要涉及的图像处理也没有太紧密的联系。但是,对于线性CCD的工作基本原理,特别是时序,要有清晰的概念。
在代码编写与CCD运用中,我的实际经验告诉我,积分时间与曝光时间最最重要。
先就时序谈谈我自己的见解。现附上官方文档的时序图:
对于线性CCD的时序操作,我觉得基本要知道的首先是TSL1401CL的关键引脚(见上表):除了必备的VCC,GND以外,还有与单片机IO口连接的CLK和SI脚,与单片机AD采集联通的AO脚。首先,对于CCD来讲,前18个时钟周期是像素复位时间,不进行积分与曝光。而且,第一个逻辑时钟SI必须出现在下一个时钟信号CLK上升沿之前。从时序图可清晰的看出CCD的操作过程,SI信号相当于一个标志,当它变为高电平后,我们就可以在每个CLK信号高电平到来后进行数据的AD采样。对于这样的整个时钟的时序操作,包括对CLK和SI的操作,就可以写成曝光函数。具体如下(代码具体参照了蓝宙的资料):
//======曝光函数======//
void StartIntegration(void)
{
unsigned char i;
TSL1401_SI = 1; /* SI = 1 */
SamplingDelay(); /*合理延时100ns*/
TSL1401_CLK = 1; /* CLK = 1 */
SamplingDelay();
TSL1401_SI = 0; /* SI = 0 */
SamplingDelay();
TSL1401_CLK = 0; /* CLK = 0 */
for(i=0; i<127; i++)
{
SamplingDelay();
SamplingDelay();
TSL1401_CLK = 1; /* CLK = 1 */
SamplingDelay();
SamplingDelay();
TSL1401_CLK = 0; /* CLK = 0 */
}
}
这仅仅是对CCD做的时序操作,通过对与CLK于SI脚连接的端口做电平变换,使TSL1401CL进入正常的工作状态。完了以后我们才能对CCD上AO口采集到的数据进行AD采样。采样函数如下:
void ImageCapture(unsigned char * ImageData,unsigned char * ImageData2)
{
unsigned char i;
unsigned int temp_int;
unsigned int temp2_int;
TSL1401_SI = 1; //SI = 1
SamplingDelay();
TSL1401_CLK = 1; // CLK = 1
SamplingDelay();
TSL1401_SI = 0; // SI = 0
SamplingDelay();
//Delay 20us for sample the first pixel
for(i = 0; i < 20; i++) //更改25,让CCD的图像看上去比较平滑。
{
Cpu_Delay1us(); //把该值改大或者改小达到自己满意的结果。
}
temp_int = AD_Measure12(0); //读取AD0口采集到的数据。
*ImageData++ =(byte)(temp_int>>4);
TSL1401_CLK = 0; // CLK = 0
for(i=0; i<127; i++)
{
SamplingDelay();
SamplingDelay();
TSL1401_CLK = 1; //CLK = 1
SamplingDelay();
SamplingDelay();
temp_int = AD_Measure12(0);
*ImageData++ =(byte)(temp_int>>4);
TSL1401_CLK = 0; // CLK = 0
}
SamplingDelay();
SamplingDelay();
TSL1401_CLK = 1; // CLK = 1
SamplingDelay();
SamplingDelay();
TSL1401_CLK = 0; // CLK = 0
}
从上述代码中可看出我们的代码完全按照时序进行,只是在先前的曝光函数中的适当位置插入了AD采样的语句。其中:
for(i = 0; i < 20; i++) //更改25,让CCD的图像看上去比较平滑。
{
Cpu_Delay1us(); //把该值改大或者改小达到自己满意的结果。
}
这段代码做了一个合理延时,大约20us,当然i值可调,延时时间可调。针对其作用,蓝宙的说法是,在于控制AD采集的时间点,让采集数据更稳定,出来的图像更平滑。我也尝试过删除这个延时,在我的上位机上看到的图像采集也没有受到什么影响,也很稳定。小车跑出来的效果也正常,所以我个人认为这个延时或许必要性没那么强,只是相当于一个补充与完善,大家可以自己试试看。
解决了时序与采集,需要将ImageCapture()函数写在StartIntegration()函数后。
下面一个关键问题就是对积分时间与曝光时间的掌控。何时进行曝光,曝光的周期设置是整个图像处理部分的先头和关键。积分其实是对像素进行放电的时间。其中,积分时间的计算公式是:
tint(min)=(128-18)*clock period+20us
其中,128是线性CCD一次采集的像素数;18是所需的逻辑设置时钟,也就是之前提到的复位时间;20us是像素转移时间,顾名思义是将积分电容储存的电荷经过运算电路转移到AO数据输出的时间。
其实我感觉此公式大概理解一下就行,更加清晰的体现了一下工作原理。公式本身对于我们的代码与对CCD的操作提供不了什么太直接地帮助。
个人的经验总结是,光线越强,积分时间就越长,我们进行曝光的周期就该越短。这样采集的周期相应缩短,采到的数据更加稳定。
所以自然引出曝光的另一个重要内容:选择自适应曝光或是固定曝光。自适应曝光的写法蓝宙电子的附送资料很详细,大家可以参考。但本人觉得自适应更适合初学者,比较好上手。但是自适应的做法本身很浪费,也没必要。它适合光线很不均匀的环境,但实际上一般的实验室与实际比赛场地不存在这样的问题,而且对于自适应算法,二值化后在小车过急弯CCD可能看到全黑图像时,二值化的阈值不好处理,容易误判,我后面会提到。所以,我认为固定曝光才是真正的出路。自适应的特点是曝光周期可变,所以他能适应多种复杂的光线;而固定曝光的优点就是曝光周期固定,在光线正常情况下采集更加稳定,图像抖动减小且更加清晰,更有利于黑线的提取与二值化。
当然,平常在实验室,光线均匀,我用10ms的曝光周期正好。但是实际到了省赛的大体育馆,那种光线强到和太阳直射几乎无差,建议大家将曝光缩短到5ms,而且务必加上偏振片(旋到合适角度),这样出来的图像才会明暗差距清晰,凹槽明显。
至于固定曝光与采集的代码位置,大家可以用PIT定时器中断,将CCD基础处理部分写在中断里,设置标志位什么的,以下是本人写的中断(10ms固定曝光),包含了速度采集于二值化等内容,不吝赐教:
#pragma CODE_SEG __NEAR_SEG NON_BANKED
__interrupt void PITCh0IntISR(void)
{
static unsigned char TimerCnt10ms = 0;
static unsigned char TimerCnt2s = 0;
static unsigned char u=0;
static unsigned char b=0;
PITTF_PTF0 = 1;
TimerCnt10ms++;
TimerCnt2s++;
if(TimerCnt10ms >= 50)
{
TimerCnt10ms = 0;
TimerFlag10ms = 1;
LED1 = ~LED1;
speed_back = PACNT;//返回速度值
PACNT = 0;
ImageCapture(Pixel,Pixel2);//CCD采样
//SendImageData(Pixel);
Get_Dyn_Th(); //获得动态阈值
Bi_conversion(); //二值化处理
Filter_Pixel_Two(); //数据滤波(均值)
//SendImageData(Pixel);
Get_Flag();
Get_Flag2();
CCD1_mid_point();
CCD2_mid_point();
} else if(TimerCnt10ms==10)
{
StartIntegration(); //开始曝光
}
}
#pragma CODE_SEG DEFAULT
解决了曝光与采集,这时相应的写出SCI串口发送程序,就可在上位机上观察到线性CCD采集到的原始图像了。关于图像的进一步处理,包括二值化与动态阈值我会在后面的文章谈到。
谢谢大家!