本文參考資料:
[1] (Strongly Recommend!) Fundamentals and Experiments of Line Scan Camera: http://www.elm-chan.org/works/lcam/report.html
[2] 線陣 CCD 的使用方法(以 TCD1304 為例): https://zzi.io/?p=1091

工程地址:https://github.com/divertingPan/Line_Scan_Camera

原文地址:https://divertingpan.github.io/post/line_scan_camera

前言

Overview

這篇是接續【硬核攝影】給火車拍個全身照光流法應用——自適應檢測視頻火車速度的內容。但是實則整個工程和前作關系又不是那么密切,只能算是精神續作。

實際上,這篇2.0的內容是之前做軟件層面的視頻掃描代碼的精神鼻祖。這個掃描相機的原項目[1]是2011年左右設計的,老潘在大概2018年看到的這個並且嘗試復現(失敗),但一直對這個項目留有念想。因為已經大致了解了原理,所以就用視頻錄像做了這個相機的模擬版。結果在2021年的時候突然得知,國內PCB廠商居然開始免費打樣,於是老潘決定重啟這個項目,告別繁雜的飛線,拆解原來的洞洞板,直接上PCB。

老潘在原來的設計基礎上做了一些小改動。本篇主要是為了記錄復現過程中趟過的無數大小坑,以及對本項目的改進的一些指南。

本項目完全開源在github上,包括電路,PCB,硬件代碼,各種資料等。

老潘不是專業搞硬件開發的,所以肯定很多地方說不明白,希望各位能給予指導或者糾正。

一些效果展示:

這個是手持平移掃過桌上的靜物,因為手抖所以會有變形。

這是架在路邊拍攝,因為光圈開到最大,對焦在中央車道,近景就會失焦模糊。

一個完整的掃描結果,沒有調整長寬比例的原始圖像。

火車雖遲但到,可惜這個相機想要准確取景對焦十分考驗手感,導致出片率不高。

線性 vs 二維

這種一維相機也能拍照的原理在前面篇章里已經介紹了,如果理解了線性掃描的原理,這個相機的原理是一模一樣的。只是利用線性的CCD直接從拍攝(或者說錄制視頻)這個地方就已經做好了固定位置-連續取幀-逐幀拼接成圖這么個過程。

這時候會有人問:既然能用錄像機直接錄像之后用軟件拼圖,那毫無必要用線性CCD來做這個?這時候我們應該考慮一下兩種方法的優劣,來選擇到底用哪種方法。

  • 線性CCD器件的分辨率可以輕松做到10000×n,即CCD的單幀覆蓋像素可以做到很高,這點在當前的二維傳感器上很難實現(即使強如GFX 100可以做到最長邊11648×8736分辨率,但是成本爆炸,而且還會帶來第二點問題)
  • 線性CCD只有1維的數據,在高速采集中對外圍電路要求更低(GFX 100和一個線性CCD,同時設計每秒1000幀的采樣速度,難度差異顯而易見)。有人會說:二維器件的采樣率限制可以利用二維平面彌補(即,11648×8736×1幀,采集圖像范圍等同於11648×1×8736幀),但是前文已經實驗證明,利用視頻的窄窗來模擬線性掃描的前提是,開窗的尺寸不能太大,否則會出現透視效應。因此,二維CMOS或者CCD器件會有大量數據浪費。
  • 承接上條,利用線性CCD可以節約存儲空間和IO開銷。
  • 使用CCD的缺點是,由於存在采樣率上限,因此速度超過幀速率上限的物體會發生形變,且丟失的幀細節無法彌補,通過二維器件的錄像和后處理,可以利用窄窗彌補。(這里的速度-幀率-窗尺寸關系在前作也有推導,線性CCD設定窗寬度固定為1即可)
  • 盡管如此,窄窗仍然會帶來以下問題,且很難通過簡單的后期手法修正:
  1. 開窗的窗寬度和目標運動速度緊密相關,並且分正負。
  2. 帶有透視的物體,無法統一開窗寬度。
  3. 對於變速的物體,對開窗寬度非常敏感。
  4. 物體受到固定位置的光影反射會產生條紋干擾。

如圖所示:

而以上問題在窗寬度等於1像素,即直接使用線性CCD捕捉圖像時,可以消除這些缺陷。例如下圖所示,拍攝的汽車是屬於雙向車道的,被白色橫線擋住的大卡車是自右向左行駛的,白線前面的車是自左向右行駛的。但是由於每幀像素寬度為1,因此幀排列順序不會出現上圖問題1的效應,只會影響到物體的鏡像翻轉與否(注意卡車上的字)。

至於物體發生的拉伸形變,可以通過ps縮放簡單修正。將運動速度慢而造成影像拉長的物體可以壓回正常尺寸,沒有信息損失;而運動過快造成的影像縮短,僅使用插值法拉回原本比例則會帶來信息損失。

傳感器簡介

基本原理

整個項目最重要的部分就是傳感器了,這里使用的是TCD32D線性單色傳感器,具有1024個像素單元,最高捕捉速度可以達到每秒大約2000幀。其實目前的科技已經有最多一萬個像元,可以達到更高的分辨率,還支持RGB彩色模式,不過這些東西的原理基本都相通。想要TCD132D輸出東西,首先需要給它一些信號,如圖所示:

這里SH是控制CCD采集光信號,控制積累由光強度轉化成的電信號(即積分)所用的時間長度,遇到一個SH下降沿就使得CCD開始把目前積累的電信號往外搬運。因此可以發現,幀率越大,給每個像元的積分時間就越短,相當於感光度變低。在此同時,\phiϕCCD控制搬運的節奏,這個信號變一次,就讓下一個像素的信號出來,直到走完所有的像元。但是這上面並不是所有像元都能捕捉到光信號,只有中間部分的1024個可以,其他的只會打醬油。\phiϕM是總時鍾,根據圖里的比例可以看到,\phiϕCCD變一次,就要對應\phiϕM變4次,就是說\phiϕM的頻率需要是\phiϕCCD的4倍。

而這個傳感器能接受的這些信號的頻率范圍如下表前三行所示。可以看到\phiϕM的頻率確實是\phiϕCCD的4倍。而\phiϕCCD每變一次就會輸出一個數據,即一個\phiϕCCD周期會有2個數據輸出,所以數據速率是\phiϕCCD的兩倍。一幀有1024個數據,每秒2M個數據即每秒2k個幀,所以這個傳感器的極速就是每秒2千幀左右。

這個TCD132D輸出的是模擬信號(一個連續區間的電壓值),所以需要一個ADC來把電壓值轉化到0-255之間的數碼值。這也就是說,我們需要一個能夠支持每秒轉化2M個數據的ADC才行。ADC1173可以達到15MHz。而驅動他的方法也很簡單,使用一個時鍾,在每次時鍾下降沿的時候就會把當前的數據采集轉化。

Arduino相關實驗

老潘早些時候用arduino嘗試着去驅動TCD132D以及ADC1173,奈何沒有示波器,沒法查看輸出信號是不是符合期望值,arduino mega上面又不帶DMC,數據來不及依次捕捉下來。而且對於如何同步列與列之間的數據,我也沒什么頭緒。這里示范一下使用arduino+ADC1173來把這個CCD當做光線傳感器使用的一個例子吧(無奈)。

好在mega上面有很多定時器可以使用,這些定時器被我拿來當做各個時鍾了。配置定時器又涉及到了寄存器操作,我的淺見是寄存器就是一堆功能按鈕,按下他就會產生相應的功能,這些功能排列組合出來就成了神奇或者詭異的運行姿態。。。使用對應的寄存器的方法就是通過設置某個變量的名字(一般用到的單片機都會把每個寄存器做好名稱和底層地址的對應文件給大家)等於一個二進制的數字,這個二進制數字的每一位都對應了這個寄存器里的一個按鈕,1就是按下,0就是不按。通過紛雜迷人眼的來回切換這些按鈕,這個機器就運轉起來了。

但是一般來說,直接設置某個寄存器就等於某一個數字可能不太妥當。因為有時候我們只想改變這里面好幾個按鈕中的一個按鈕,不想動其他的按鈕,如果每次都這樣手動的一次設置一大排按鈕的狀態,容易搞錯。所以有些時候可以利用邏輯符號來指定對某一個按鈕做操作。

// 讓DDRB的第0位和第1位變成1 這里|是按位或
DDRB |= (00000001 | 00000010);

// 讓ADCSRA的ADPS0:2變成0 這里的&是按位與
ADCSRA &= ~((1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0));

// Arduino里面_BV()的用法等同於設置某個位為1 DDRB和DDB7已經預定義過
DDRB |= _BV(DDB7)

 

因此arduino里面對於各部分的時鍾設置,結合說明書里的指示和網上的例程,可以寫出以下:

void setup() {
    // read one frame (line)
    // Init the port to output mode
    DDRB |= _BV(DDB7) | _BV(DDB5) | _BV(DDB4);
    DDRE |= _BV(DDE3);
    DDRH |= _BV(DDH3);

    // LEDCLK (pin 13)(PB7)
    TCCR0A = _BV(COM0A1) | _BV(WGM01) | _BV(WGM00);
    TCCR0B = _BV(CS00);
    OCR0A = 255;

    // MCLK (pin 11)(PB5)
    TCCR1A = _BV(COM1A0);
    TCCR1B = _BV(WGM12) | _BV(CS10);
    OCR1A = 1;             // 4MHz
    PORTB |= _BV(PORTB5);  // start from HIGH

    // CCD (pin 10)(PB4)
    TCCR2A = _BV(COM2A0) | _BV(WGM21);
    TCCR2B = _BV(CS20);
    OCR2A = 7;  // 1MHz

    // ADCCLK (pin 5)(PE3)
    TCCR3A = _BV(COM3A0);
    TCCR3B = _BV(WGM32) | _BV(CS30);
    OCR3A = 3;  // 2MHz
    TCNT3 = 3;

    // SH (pin 6)(PH3) total: 1092x500ns = 546us
    TCCR4A = _BV(COM4A1) | _BV(WGM41);
    TCCR4B = _BV(WGM42) | _BV(WGM43) | _BV(CS40);
    OCR4A = 7;
    ICR4 = 8735;
    TCNT4 = 18;
}

 

接線如下(懶得畫詳細的面包板圖了,就大概看看圖一樂吧)

時序圖如下,不知道為什么ADC時鍾總是無法對齊,不過只要ADC下降沿在CCD變化的附近即可,這個范圍內的CCD輸出信號仍然是穩定的。(有待通過示波器考證)

最后通過Arduino采集ADC輸出的值,可以看出一定的響應規律。實驗中發現傳感器在亮光時輸出低電位,無光時輸出高電位,和一般數字圖像里的情況剛好反過來。

另外還寫了個用Arduino做呼吸燈的無聊代碼,就是板上13號口自帶的那個燈。但是這個呼吸燈是用的定時器,以及變化規律是正弦的(感覺還是毫無用處呵)。

void led_blink() {
  for (float i = 0; i < 5000; i++) {
    int t = 255 * 0.5 * (1 + sin(i / 5000 * 2 * PI));
    OCR0A = t;
  }
}

 

CCD的老化

如果在很暗的環境下捕獲圖像,之后通過調整曲線或者色階將圖片提亮后,有可能會看到這種條紋。根據參考鏈接[2],這個現象是因為CCD傳感器內部的兩個放大器的微小誤差導致的。

事實上,大多數線陣 CCD 為了提高輸出頻率,都具有多個 Shift Register 結構的設計,在這一點上理論與實際的差異可以用於解釋在線陣 CCD 壽命快要結束時,往往得到的信號會出現奇怪的周期性(偽信號)的現象。比如說 TCD1304 有兩套 Shift Register,因此在使用很長時間之后,兩個 Shift Register 對應的模擬放大器的老化情況不一致,因此輸出的信號中,每個偶數像素的信號比相鄰的奇數像素的信號總是高一些,或者總是低一些)

線性相機實施細節

這一節按照順序講述一下在抄作業的時候可能遇到的各種坑,以免抄作業都抄不好。

說在最前面,這里的元件用貼片還是接插件都影響不大。(我自己為了制作方便,能用直插件都用了,實際試驗沒發現太大問題)

顯示部分替換為SSD1306模組

原作者所用的各種元件,我在當時基本都能集齊,並且花費不是太多。唯獨他所用的OLED顯示屏完全找不到。所以這部分干脆就直接用淘寶白菜價的OLED模塊就行。經過一番研究,這里是用了4線的SPI和OLED通信,所以要買那種7個針腳的OLED模塊。這樣一來,原來設計里面的顯示屏供電部分也可以去掉了,因為模塊上面就帶有供電管理。但是這樣還不夠,因為原作的OLED驅動芯片和淘寶常見的不一樣,所以要改代碼。

好在店家當時給了SSD1306的例程,並且改的地方比較簡單,只是改一下初始化參數。在原作的disp.c里面的233行是這樣的:

static const BYTE ini[] = {    /* Initialization parameters for UG-2832ASWAG or UG-2864ASWAG */
        0xDB, 0x3F,        /* Vcom level */
        0xD9, 0x1F,        /* Pre/Dis-charge period */
        0xA1,            /* Column direction (L/R inverted) */
        0xC8,            /* COM direction (U/D inverted) */
        0xDA, 0x12,        /* COM scan alt mode */
        0xA8, 0x3F,        /* Mux ratio (2832:1F, 2864:3F) */
        0xD5, 0xF0,        /* Clock */
        0x81, 0x64,        /* Contrast (2832:0x14, 2864:0x64) */
        0xD3, 0x00,        /* Display offset (0) */
        0xAD, 0x8A,        /* Internal DC-DC (off) */
        0xA6,            /* Display invert mode (normal) */
        0xA4,            /* Entire display (0) */
        0x40            /* Display start line (0) */
    };

 

如果把這里的初始化參數改成SSD1306的,並且按照我修改的電路圖中的連線方式,直接插上模塊就可以正常使用了:

static const BYTE ini[] = {    /* Initialization parameters for SSD1306 */
        0xAE,//--turn off oled panel
        0x00,//--set low column address
        0x10,//--set high column address
        0x40,//--set start line address  Set Mapping RAM Display Start Line (0x00~0x3F)
        0x81,//--set contrast control register
        0xCF,//--Set SEG Output Current Brightness
        0xA1,//--Set SEG/Column Mapping      0xa0: horizonal reverse  0xa1: none
        0xC8,//--Set COM/Row Scan Direction  0xc0: vertical reverse   0xc8: none
        0xA6,//--set normal display
        0xA8,//--set multiplex ratio(1 to 64)
        0x3f,//--1/64 duty
        0xD3,//--set display offset    Shift Mapping RAM Counter (0x00~0x3F)
        0x00,//--not offset
        0xd5,//--set display clock divide ratio/oscillator frequency
        0x80,//--set divide ratio, Set Clock as 100 Frames/Sec
        0xD9,//--set pre-charge period
        0xF1,//--Set Pre-Charge as 15 Clocks & Discharge as 1 Clock
        0xDA,//--set com pins hardware configuration
        0x12,
        0xDB,//--set vcomh
        0x40,//--Set VCOM Deselect Level
        0x20,//--Set Page Addressing Mode (0x00/0x01/0x02)
        0x02,//
        0x8D,//--set Charge Pump enable/disable
        0x14,//--set(0x10) disable
        0xA4,//--Disable Entire Display On (0xa4/0xa5)
        0xA6,//--Disable Inverse Display On (0xa6/a7) 
        0xAF //--turn on oled panel
    };

 

但是這里仍然有一些小問題,用這個方法屏幕雖然能顯示,但是有2像素的偏移。注意看左上角的一個],其實那是右上角的電池標志的最右邊的框。雖然這個無傷大雅,但是我不知道如何修正這個bug。

代碼編譯

作者給了一個Makefile文件,因此只要配置好編譯環境,進入源文件路徑直接執行make命令即可。但是有一個小細節是,Linux環境里必須先配置好arm-linux-eabi-gcc編譯器(可以直接使用sudo apt-get install gcc-arm-none-eabi,和apt-get install lsb-core)才可以順利的make。最終編譯成功會在路徑下生成一個obj文件夾,里面會有一個hex文件,終端里會顯示這樣。

hex下載進主控

這里出了大問題,原本我死活連不上這個單片機,一度以為是焊接的時候燒壞了芯片(好巧不巧的是我從原來的電路拆這個芯片,以及往新板上裝這個芯片的時候都搞了半天才弄好)。結果看了一下多年前自己留下的記錄才知道,下載的時候需要給芯片復位並且拉低某個引腳,進入ISP模式才行。具體操作是:將P2.10(ISP)置為低,同時讓reset為低,然后先放開RST,再放開ISP,以加載BootLoader,不然flash magic識別不了芯片(LOW on this pin while RESET is LOW forces on-chip bootloader to take over control of the part after a reset.)。判斷方法是:使用flash magic,如果點擊ISP-Read Device Signature能夠出設備的ID,那就是成功連上了。

對於兩個引腳的操作,需要自己手動用插線去接插這兩個針腳到任意的GND上。當時畫PCB的時候沒有留意這部分的操作,當然你也可以拿去圖紙,自己把這部分加兩個按鈕上去。下載器的TX口和RX口要接在主控板上面的RX和TX腳上。

另外,在下載程序時,盡量把另外兩塊板拿掉,我發現在Control Board上插着Analog Board的時候,在下載程序途中總是失敗斷開。有可能是供電不足的問題?

LC4256V

這部分原作者只給了一個abl文件,需要用ispLEVER classic操作一下,但是我在當年已經做過了操作,並且得到了直接往器件里面寫的二進制文件。這部分代碼確實是觸及到了我的知識盲區,我實在改不動什么,所以就沒有再仔細研究。至於下載的方法,首先用lattice下載線,接好線之后需要單獨給主控板通電,下載線是不能供電的。之后利用軟件操作。軟件部分的編譯下載等操作根據說明書操作即可。說明書也在github里面了。

調零

這里需要調整變阻器分壓來控制傳感器在最暗的環境下輸出的信號強度,或者理解為零點校准。我們期望在完全無光的環境時圖像值為0。如果需要調整曝光,希望在很暗的環境下也輸出有一定亮度的圖片也可以在這里調整。通過改變這個變阻器的阻值,遮住鏡頭,觀察屏幕上的亮度強度到自己希望的位置即可(正常狀態下,黑暗時屏幕上的光線曲線也在中央虛線位置)。但是要注意先設置算法層面的增益調為0再調整變阻器。

FS ERROR

開機是必需要插入內存卡的,不然會報錯。但是如果你插了內存卡還是報錯,檢查你的內存卡格式,需要為FAT32才可以。現在的新內存卡都比較大,一般都是exFAT格式並且windows系統自帶的右鍵格式化沒法格式化為FAT32,可以用DiskGenius格式化,注意格式化前檢查好數據情況。

最大記錄長度

在lcam.h的第一行就是設定最大記錄長度的參數,當相機的任意按鈕被按下,或者達到最大記錄長度時,記錄停止。原始設定是100000,但是你可以改更大。不過注意,FAT32系統支持的最大文件大小是4GB。同時,BMP格式文件頭里面通過bfSize定義文件大小,bfSize占4個字節,因此支持的最大文件存儲也是4GB。

BMP圖像轉存PNG

當圖像超過一定大小后,用普通的照片查看器甚至是Photoshop都可能無法正常打開文件,但是使用Windows自帶的畫圖就可以查看。不能用PS編輯簡直震怒,但是既然有些軟件能查看有些沒法查看,而且PS本身編輯操作很大的圖片都沒有問題,於是猜測可能是因為BMP編碼的問題。因為這個BMP里面的顏色表信息是自定義的,也許PS對這方面的支持不是很好。

所以可以把原數據用python先讀進內存,然后再轉存成更加通用的PNG格式,既能壓縮體積還能讓PS編輯。代碼非常簡單。總共就幾行,如下。懶得復制粘貼的話github里也有。

import cv2

image_path = 'Y0023.BMP'
image = cv2.imread(image_path)
cv2.imwrite('{}_modified.png'.format(image_path[:-4]), image)

 

光路與鏡頭

首先要確保鏡頭的像場能夠覆蓋CCD的長度。其次要根據所用鏡頭的卡口對應法蘭距來設計機身法蘭盤的外平面到傳感器的距離。這里有一個靈魂手繪尺寸圖,僅供參考。

機身我直接搞了個紙盒來裝電路以及卡鏡頭,但是機身需要密閉不漏光,如果盒子上有漏光的窟窿(尤其是離傳感器近的位置)最好補住。關於如何獲得法蘭盤,可以通過低價收廢舊相機拆法蘭盤,或者買最便宜的卡口轉接環來獲得。但是挑轉接環時要注意法蘭盤的公母之分。

圖像撕裂問題

如果你用比較舊/雜牌/便宜的內存卡,會發現經常出現下面這種情況,

車尾部的斷層說明這里的數據有卡頓,采集到的數據沒有及時被存下來造成了丟幀。老潘使用過一個撿來的寫速度相當慢的內存卡,經常出現這種情況。使用了一個新買的U1速度的內存卡,偶爾會出現這種情況。使用一個U3的內存卡,極少出現這種情況。

相機操作方法

在原作者的博客里面詳細介紹了。此處無必要再次復制粘貼。

黑白拍照心得

老潘順便還參悟了一些關於黑白攝影的體驗。很多人說攝影是用光的藝術,尤其在黑白攝影里面,沒有顏色加持,光線這一點就相當重要。例如這里的火車,因為是背光所以整個背光區域就顯得非常平,沒有質感,車體的棱條完全沒感覺。但是車頂的布受光很好,顯得起伏明顯,光影變化很豐富。