(开源)DIY一台APSC画幅的CCD相机

Github:https://github.com/BellssGit/ICX453_CCD_Mirrorless_Camera

提示:这不是一篇复刻指南,本项目的出发点并非方便复刻,更像是一个过程记录,存在代码不规范、脑溢血的软件实现、硬件电路设计存在没有指出的修订(例:小修小补不会在原理图上标出)等问题。

但也十分欢迎各位尝试复刻,有问题可以到评论区留言。

本文在emoe.xyz和我的bilibili专栏发布

整体架构

总体架构还是比较简单的,PL端负责CCD时序的产生和ADC读取,通过AXI HP接口直接向内存写入RAW图像缓存。PL端还负责预览图生成(缩图)和直方图统计。

PS端则是利用到了两个核心,一个负责用户交互以及写卡,另一个只负责更新屏幕。

硬件

传感器特点

先从传感器说起吧,传感器型号为ICX453,是尼康向索尼定制的600万像素CCD,无公开手册。453是413的魔改,而后者手册是公开的,可以从那里获取一些有用信息,例如传感器的电压要求。但是不能直接套用413的引脚和读出时序,453在这方面有很大的不同。

幸运的是,前面我们提到的Cam84项目已经对这款传感器进行了全面逆向,包括引脚、驱动代码都是公开的资料,所以大吉大利,今晚吃鸡?

要是有这么简单就好了

理论上人家已经给各种最困难的工作做了,甚至代码都给你了,还有一百多页的论坛讨论帖可以看,但很反直觉的地方就是:我必须先把Cam84项目复刻,挂上示波器,从行为层面去理解他们都干了啥,进行逆向工程,出来才能成功把传感器驱动器起来。

听起来很奇怪对吧?逆向一个开源的项目。但是我们必须考虑到Cam84和Cam86项目没有系统性的文档可以看,加之这两个项目都快10年了,相关讨论基本也就停留在那段时间,发生这种情况是十分正常的。

无论如何,关于ICX453的逆向文章我都发在其他文章里了,感兴趣的朋友可以看看,在这里就不重复了。

ICX453 CCD传感器时序分析

ICX453 CCD传感器,简单的逆向工程手记

453与413最大的不同之处在于,453进行一次垂直转移实际上会同时转移两行像素,水平转移的像素量也会相对应加倍。

反应到实际数据上就是:同样是读出完整的一帧,ICX413需要进行2000次垂直转移,每次垂直转移结束后进行3000次水平转移。而ICX453只需要进行1000次垂直转移,每次垂直转移结束后进行6000次水平转移。两者都会得到600万像素。

我不是很清楚为什么尼康为什么要这样定制,但我个人猜测,是因为水平转移的速度比垂直转移快得多(时钟速度可以快很多)。通过减少垂直转移的次数就能够加快传感器读出速度。

这种设计在RAW解析软件上造成了一定麻烦,从传感器读出的是一幅6000×1000分辨率的图像,需要在软件中将像素重新排列对齐,才能得到正确的3000×2000图像。

ADC采样时钟

因为ICX453输出信号是CDS类型的,需要使用专门为CCD传感器设计的ADC进行采集。这里使用了AD9826的P2P替代HT82V38,原因是它只需要单3.3V电源,能够简化供电系统设计。

为什么要强调ADC采样时钟呢?因为当时没理解这类ADC的工作方式,在写驱动代码时,产生了错误的时钟,导致读出来的数据一直是错的,困扰了我一段时间,当时还以为ADC坏了,实则不然。

这一切要从CDS的工作原理说起。CDS分为两个阶段,第一阶段称为“参考阶段”(书中称“复位部”),第二阶段称为“信号阶段”。每个像素的亮度就由“参考电压减信号电压的差值”表示,差值越大,说明该像素越亮。

那么ADC里面是如何实现这个减法(差分)操作的呢?下面是AD9826的采样部分结构图

原来是电容(电龙)啊。这里我们忽略S4开关,因为它只是个提供偏置电压的。一次完整的CDS采样-转换过程如下:

  1. CDSCLK1为高电平时,S1闭合,S2和S3打开,上面的电容被充电至参考电压。

  2. CDSCLK2为高电平时,S2闭合,S1和S3打开,下面的电容被充电至信号电压。

  3. 当检测到CDSCLK2由高电平转向低电平跳变时,就会断开S2,闭合S3(此时S1也已断开),两个电容连接产生电压差,这就完成了差分操作。

  4. 趁着这个机会,向ADCCLK输入一个下降沿跳变,ADC完成对这个电压差的采样,后面就是常规的ADC转换过程了,此处不再赘述。

从上面的转换过程中,可以看出,给两个电容充电的时机,只能在CDS的对应阶段完成,如果充电时机不对/充电时间过长,差分出来的电压就是错误的,也将导致ADC转换结果错误。

因此手册里给出了如下时序要求(使用CDS模式时)

可以看到,CDSCLK1、CDSCLK2都只在对应阶段为高电平,这也是要在实践中严格遵守的。

水平驱动器延迟

CCD的水平时钟驱动器我选用UCC27524,现在回过头来看并不是最佳选择。根据数据手册,这驱动器的传播延迟17ns,反应是真迟钝吧,一个50MHz时钟的周期都快过去了它才反应过来。

高传播延迟直接关系到CDS采样时机。如果不对驱动器延迟加以补偿,结合前面ADC时序部分,最坏情况下,采样“参考电压”的电容会采样到“复位电压”(书中叫“RG耦合”),采样“信号电压”的电容会采样到“参考电压”,这是我们不希望看到的。

本项目的FPGA部分运行在50MHz频率下,将ADC时钟发生模块的触发信号打一拍,滞后一个时钟周期(20ns),即可补偿水平驱动器延迟。

镜头卡口、手柄

镜头卡口继续沿用了以前的设计,把别人做的E卡口镜头盖模型与底座进行缝合,然后3D打印出来。

这么干的缺点当然很明显,就是没有一个正儿八经的镜头锁紧机构。如果转动镜头控制环稍微用力些就可以给镜头转掉了。另外就是没有电子通信支持,任何需要电力的镜头都无法使用。

不过我也没有啥机械结构设计经验,就这样凑合用吧.jpg

还有一点需要注意的就是法兰距,机身法兰距和镜头法兰距对不上时有两种情况:

  1. 机身法兰距 > 镜头设计法兰距:无限远不可合焦,最近对焦距离缩短

  2. 机身法兰距 < 镜头设计法兰距:无限远可以合焦,最近对焦距离变长

我没有去做很细致的调整,为了能够保证无限远能够合焦,所以就偏保守设计,让机身法兰距偏短(小于E卡口的18mm)。

手柄则是一个简单的半圆柱配合加长板,顶部开个放按钮的小孔。

握起来马马虎虎吧

软件

这ADC还是DDR输出的?

AD9826/HT82V38是16bit ADC,但输出数据线只有8条,在ADCCLK上升沿输出高8位,下降沿输出低8位。一般对于DDR信号,需要调用FPGA内专门的DDR模块进行处理,但是在这里不用这么麻烦。

既然ADCCLK是FPGA内部产生的,而且速度比较慢(约10MHz),并且ADC数据输出延迟已知。只要在发出ADCCLK跳变后进行打拍,估摸着差不多了就把数据锁存进来即可,不需要用到专用DDR电路,也不需要处理跨时钟域问题。

内存规划

因为RAW图像的缓存和预览显示缓存会同时被PL和PS端读/写,所以就需要事先规划好内存区域。

PL端的内存映射

PS端修改链接器脚本:

预览图+直方图

当ADC原始数据被采集进来后,会分别输入到预览缩图模块和直方图统计模块。

缩图模块本质上就是对画面进行跳采,读入数据是6000*1000长宽比,显示屏分辨率为240*240,为了保持3:2比例,缩图结果应为240*160。X方向上每跳过25个像素采集1个,Y方向上每跳过6行采集1行。

有人可能会问,这样缩图,画面比例、预览质量不会受到影响吗?口说无凭,下面是我在Python里模拟以上算法的结果:

结论:画面比例基本没变,至于预览质量问题,我都用240*240的马赛克屏幕了,哪怕缩图算法换成世界上最棒最好的,最终在屏幕上看到的效果恐怕差别不大,所以直接开摆。

直方图的实现就更简单了,弄128个32位累加寄存器,写个巨大无比的case语句,当亮度落入相应区间时,就把对应寄存器加一。这些寄存器都可以通过AXI接口被PS端访问到,每当一帧完整读出后,PS端就会读出一次直方图,绘制在屏幕上。

当然,不得不承认的是,128个超长case语句对时序还是有点影响的,在加入直方图统计前,系统时序裕量为12ns左右,加入直方图只剩8ns左右了。

预览伽马矫正

由于人眼和传感器对光强的响应曲线不同,传感器是线性响应,而人眼是对数响应,如果不对预览图和直方图做伽马矫正,看到的图像亮度就会很暗。

还好这是个简单活,只需要使用查表法即可。

对于预览图,因为是PL直接向显存区域写入图像,所以查表这个过程要在PL端完成(疯狂写Case语句.jpg)

(此处省略剩下20几条case)

对于直方图,因为PL端只负责统计,将统计数据转化为显示屏上的图表是由PS完成的,我让AI帮忙写个简单的查找表计算程序,在绘制直方图时候,对着查找表画就行了。

以上的伽马校正只针对预览画面和直方图,真正存到卡里的RAW是没有经过矫正的,这在后面的章节会介绍。

双核操作

ZYNQ7000有两个A9核心,当然得好好利用下。核心0用来做控制和写卡,核心1用来做刷屏,这样写卡的时候就不会刷不了屏了。

既然用上了双核,那么预览的帧率一定很高罢?

很遗憾不是,因为整个系统的瓶颈都在那块CCD传感器了,读出速度也就1fps。跟前面直接跳采样缩图道理一样,既然瓶颈不在软件这边,我就直接开摆了。代码效率很低就是,但即使是这么低效的代码,速度也还是会比传感器的读出速度快,乐。

这能对上焦?

最后预览效果:

啊,不得不承认这玩意对焦确实困难哈哈。

不过在对上焦时,屏幕上还是能显示出那种强烈反差的。所以我在拍摄时对焦的方法就是古法反差对焦,先拧到焦点外,然后反复寻找反差度最高的时候,配合估焦经验,一般都能对上。

不过这样干肯定不适合快速拍摄就是了,享受慢节奏吧(笑)

我的数据啊啊啊啊

编写软件过程中,出现过比较严重的bug就是写到卡里的图像数据损毁了,原因有两个:

  1. 快门按键按下时,会触发CPU0的中断。如果此时CPU0正在写卡,用户又一次按下快门,写卡被中断就有几率导致数据损坏

  2. 同样是写卡时,由于相机实时预览还开着,实时预览本质上也是对整个传感器进行读取,在写预览图的同时也会对内存写入RAW,所以就出现了下面新图和老图混合在一起的奇特景象:

还好这些都不是啥大问题,解决方法也很简单:

  1. 写卡之前屏蔽外部按钮的中断,这样就不会重复触发快门了,写完卡再打开中断

  2. 加入控制逻辑,只有检测到用户触发快门时才对RAW图像的内存区域进行写操作,其他时间不对这块区域进行写入。

图像处理—像素重排

从相机出来的RAW是真真正正的RAW到不能再RAW的纯二进制文件,文件头文件信息一概没有。所以市面上自然是没有现成软件能够直接打开这野鸡RAW文件的。

并且,如果你还记得前面提到的传感器特点,RAW图像的分辨率是6000*1000的,需要把像素重排成3000*2000的比例。

传感器在真正输出有效像素前,会输出一些校准用的像素,如果我们真的只读取了6000*1000个像素,那么出来的图像就是不完整的。为了保守起见,我最后读取的像素总数量是6248*1024。

好了,现在有一个相当于6248*1024,数据类型为uint16的二维数组文件摆在眼前,我选择用Python进行处理。

为了测试传感器,我给传感器表面放了个三角形小纸片,然后拍了一张。先试试直接给这个数组原封不动的封装到图像文件:

啊哈,从这张图里面可以看出很多信息(?),但是直接用文字描述还是比较抽象,我尝试尽量用大白话说明白。

第一点,和前面提到的一样,ICX453是两行放到一行里进行读出,那么不妨对上面那幅图想象一下:把图像从X方向的中间处分开。然后将左右两边的图像一行行的交织在一起,存入一副新图像。比如第一行放左边的像素,第二行放右边的像素,第三行放左边的像素……。这样就实现了像素重排,最后得到一副3:2比例的图像。

第二点,图像里面的斜黑条是咋回事?这里要分成两个小点

  1. 为啥有黑色像素?这就是从传感器读出的校准像素(光学黑体),以及多读出的无效像素,后期处理时只要跳过就好。
  2. 为啥是斜着的?因为我不知道这个传感器设计时,每一行/列究竟留了多少校准用的像素,所以每一行都会故意多读一些像素,确保能够充分读出,这样就导致了每次读取会人为的添加一些偏移,这些偏移可以在后期进行修正。

下面是像素重排+偏移修正后的图像:

放大看看

这些一个个的就是拜尔阵列了

像素重排的一些问题……

在处理过程中,我注意到了一些奇怪的现象,只有在某些图像放大后才比较明显

为什么这玩意“波浪感”这么重呢,看着跟油画似的,所以上DPreview找了下使用同款传感器的尼康D40样片

看着很正常啊。不过需要指出的是,DPreview的D40样片只有JPEG,不排除是因为压缩给那些“波浪”抹去了,但我相信尼康的工程师应该是不会允许那种奇怪的瑕疵存在的)

后面摆弄了半天重排的算法,发现不同重排方式的“波浪”形状也不一样,基本可以判定是重排的问题。但直到最后也没有找到彻底根除的方法,只好作罢。

图像处理—把色彩带回来!

其实我本来是不想做色彩的,因为色彩科学是一个无尽的大坑,这是一个团队+几十年经验才能调好的东西,哪是我一个人能搞定的?但是既然传感器是彩色的,那还是试试给颜色调出来吧。

以下面这样图为样例,需要用到的工具有:GIMP、Fitswork4、Photoshop。工具间传递的格式可能会有变化,但本质上都是使用16比特无符号类型,尽量避免信息丢失。

RAW图像伽马校正

用GIMP对图像做伽马校正

这一步可以在Python里做的,但当时没想到。

解拜尔

用Fitswork4对图像进行解拜尔

可能需要多试几个排列才行,因为像素重排的时候也会改变拜尔阵列的排序。

调色

用Photoshop对图像调色

感谢@Floyd-Fish帮做的预设,要是没有他的话我也不懂这玩意该咋调

啊,所以色彩科学真的好难,完全搞不出自己想要的那种效果。不过也就这样吧,开摆开摆。

USB版本?

有的兄弟,有的,Cam86/Cam84就是USB接口的了,人家的软件也写的很不错,直接复刻那个就行。

👆🤓欸不过话说回来,我自己也做了个USB版本的,而且还是在便携ZYNQ版本之前做出来的:

大部分代码都是来自于这个版本的复用,但是为什么最后没继续做下去呢?很简单,这个玩意不便携,而且和原版的Cam86/Cam84重叠了,就没什么太大必要做下去。而且从版型设计也能看出,这玩意因为结构冲突导致根本装不上卡口。

但他重要吗?很重要,就是在这个版本上我验证了CCD驱动、读取、存储的代码,没有这个阶段的成果,就没有今天的ZYNQ版本了。

样片

下面放一些样片吧,为了加载速度,我给这些图都狠狠的压了一遍,所以清晰度啥的不会很好还请见谅。

(学校的小林子,这张图颜色有点崩,因为传感器红外污染十分严重)

(学校的小林子2,这张还行)

(自拍照,这应该是清晰度最高的一张了,图像撕裂是因为前面提到的SD卡写入问题,当时还没意识到)

(无描述)

(一条小路)

后记/可以改进的地方

到这里,我个人认为这个项目的完成度可以称作“已完成”了,虽然跟商业上的相机差的还是很远。

可以改进的地方实在是太多太多了,说不完的。但未来我希望能加上几个影响体验的最主要功能:

1. 电池供电,咱就不拖着充电宝出门了
2. 放大对焦+峰值辅助对焦,这样就能精细对焦了
3. 换个大一点的屏幕,这小屏看着难受

参考项目/致谢

整个项目从开始到现在历时两年多一点(之前说三年是记错了,乐),这其中得到了不少人帮助:

@EMZ1 --- 提供了ICX453传感器
@上电冒烟 --- 蹭了他画的ZYNQ核心板
@Dualmono --- 帮我选型电源
@趙火龙 --- 他也在做ICX453相机,跟我交流了很多
@Floyd-Fish --- 帮我调色
@emoe.xyz交流群的群友 --- 和我吹水

也参考了一些项目和教程:

Cam86/84 --- http://astroccd.org/2015/04/cam84/
ST7789屏幕驱动库 --- https://github.com/Jackistang/st7789
ZYNQ AMP教程 --- https://zhuanlan.zhihu.com/p/30336605
E卡口模型 --- https://www.printables.com/model/95626-rear-lens-cap-for-sony-e-mount

发表回复