1
筆者一直信奉這樣一句話:
沒有什么事是理所當(dāng)然的奠蹬。
最近兩周的經(jīng)歷再次驗證了這句話掺冠。故事還得從一張圖片說起......
某日菇曲,筆者走在街上冠绢,看到路邊躺著一只貓貓,腿就像灌了鉛似的邁不動了常潮,興奮地搓了搓小手手就想上去擼一番弟胀。可小家伙警惕得很喊式,瞅著筆者靠近了孵户,立馬翻了個身子撅起屁股,隨時準(zhǔn)備逃走岔留。
哎呀夏哭,今天遇到了只貞潔烈貓啊。
沒辦法献联,只好掏出手機竖配,拍了張照片發(fā)到朋友圈,悻悻而歸里逆。
就在按下【發(fā)表】的一瞬間进胯,腦中突然出現(xiàn)一個閃念:這個照片到底是怎么拍出來的啊原押?(這和擼貓到底有什么關(guān)系啊喂胁镐!捂臉笑)恰巧單片機又是筆者的業(yè)余愛好,那這次就來做個網(wǎng)絡(luò)攝像頭吧。
想法就這樣萌生了盯漂。
本以為搞起來會很輕松(不就是攝像頭拍出畫面上傳到網(wǎng)絡(luò)么)颇玷,沒想到拉開了長達兩周噩夢的帷幕。打個比方就缆,你以為前面只是一個小水洼帖渠,本想上去踩一踩,沒想到整個人就下去了违崇。
那么希望這篇文章阿弃,能讓你瞥見那些诊霹,習(xí)以為常的表象背后發(fā)生的事情羞延,畢竟,沒有什么事是理所當(dāng)然的脾还。
FBI Warning
筆者也只是菜雞伴箩,亦是第一次涉獵這一領(lǐng)域,出現(xiàn)紕漏在所難免鄙漏。希望讀者保留自己的判斷嗤谚,盡信書不如無書。
2
燈光熄滅怔蚌,聚光燈亮起巩步。
咳咳。
一陣短暫的回嘯桦踊,觀眾們安靜了下來椅野。
女士們,先生們籍胯,歡迎觀賞本次演出竟闪!下面,有請本期的嘉賓杖狼,登~場~(汽笛聲X4):
不被人理解炼蛤,卻渴望被人理解的單片機開發(fā)板——Arduino UNO R3!
擁有能穿透人心蝶涩,直達靈魂深處眼眸的攝像頭理朋,OV7670!
身體雖然變小绿聘,但頭腦依然靈活的Wifi模塊暗挑,ESP01!
最后出場的是斜友,平平無奇的母對公杜邦線炸裆!
杜邦線:就我沒資格配圖是吧!O势痢E肟础(對国拇!哦,對了惯殊,這些線越短越好酱吝,不為什么!)
今天的嘉賓會為我們帶來怎樣精彩的演出呢土思?ARE YOU READYYYYYY?
3
首先來攻克最困難的部分务热,圖像傳感器。
別急己儒,正所謂知其然崎岂,知其所以然,這之前闪湾,還是先簡單聊聊圖像傳感器的工作原理冲甘。
但在這之前,首先拋出兩個概念(STOP途样!禁止套娃):數(shù)字信號江醇,模擬信號。
概念筆者就不抄了何暇,這里只需要知道陶夜,計算機不能直接處理模擬信號,只能處理數(shù)字信號就行裆站。而圖像傳感器的作用条辟,正是將模擬信號轉(zhuǎn)換為數(shù)字信號。
知道人的眼睛是如何看到顏色的嗎遏插?人的視網(wǎng)膜上有兩種感官細胞捂贿,視稈細胞,視錐細胞胳嘲。視稈細胞能感受明暗厂僧,視錐細胞則有三種,分別用來感應(yīng)紅了牛,綠颜屠,藍∮セ觯看到這兒是不是有一種恍然大悟的感覺甫窟?沒錯,自然界所有顏色都可以由這三種顏色組合形成蛙婴,也因此粗井,這三種顏色被成為光學(xué)三原色,但它有一個更家喻戶曉的名字,那就是RGB浇衬!
有時候懒构,人和機器之間的界限,是相當(dāng)模糊的耘擂。人體器官的工作胆剧,大部分機器是可以模仿的,這也意味著大部分人體器官也可以被機器代替醉冤。
圖像傳感器大抵也是模擬眼球的工作方式秩霍。
這里簡要概括一下轉(zhuǎn)換過程吧(胡謅警告!這里特指CMOS傳感器)蚁阳,閉上眼睛想象一下:
在一層正方形的大樓里铃绒,整整齊齊地劃分為若干小的正方形的工作隔間,像一個正方形的表格韵吨。
每個隔間有一名程序員(感光二極管)匿垄。
每隔一段時間(機器時鐘)移宅,有產(chǎn)品經(jīng)理會一行一行地找到程序員归粉,催促進度(尋址,并接通水平開關(guān))漏峰。
又有老板一列一列地找到程序員:飲茶時間飲茶糠悼,做工時間做工,今天的代碼什么時候交浅乔?(接通垂直開關(guān))
哇靠倔喂,你們兩個合起來搞我?程序員壓力山大(由于同時接通了水平靖苇,垂直開關(guān)席噩,產(chǎn)生了偏壓)!
此時贤壁,天上降下天使(光線)悼枢,她張開雙臂將程序員擁入懷中,這讓程序員感到慰藉脾拆,于是開始瘋狂提交代碼(偏壓二極管遇到光子產(chǎn)生電流)馒索。
產(chǎn)品經(jīng)理和老板滿意地點了點頭,笑著名船,帶著數(shù)據(jù)(RGB绰上,光線強弱等)離開了。
每一行每一列依次重復(fù)這個過程渠驼。
等所有程序員都提交了代碼蜈块,老板把分支一合并,遠遠看去,竟湊成了一副《春樹秋霜圖》百揭!
這一切只發(fā)生在一瞬拘哨,而瞬間即是永恒。
稍稍把時間放慢一點的話信峻,大概可以比喻成倦青,從左上角向右下角倒塌的多米諾骨牌吧。滴水成河盹舞,聚沙成塔产镐,雖然一支感光二極管什么都做不到,但成千上萬支二極管踢步,就能組成包含整個世界的圖象癣亚。
當(dāng)然,還有很多工作也在同步進行获印,比如浮動擴散述雾,信號放大,消除噪音等等兼丰,展開來說的話玻孟,又是另外一篇文章了。
紙上得來終覺淺鳍征,絕知此事要躬行黍翎。是時候展示真正的技術(shù)啦。
筆者使用了一個第三方庫來操作OV767(https://github.com/indrekluuk/LiveOV7670)艳丛。這個庫定死了引腳連接:
VS - PIN2
XLK - PIN3
PLK - PIN12
SD - A4 還需要用連接3.3V單獨供電匣掸,請在中間安裝一個10K電阻
SC - A5 還需要用連接3.3V單獨供電,請在中間安裝一個10K電阻
D0 ~ D3 - A0 ~ A3 依次對應(yīng)
D4 ~ D7 - PIN4 ~ PIN7 同上
3.3V - 3.3V
RESET - 3.3V
GND - GND
PWDN - GND
還記得OV7670左側(cè)的腳針嗎氮双?不記得的請退回查看圖片~其中VS和HS就是控制水平碰酝,垂直開關(guān)的腳針!了解理論還是有用的戴差。
Arduino調(diào)用層代碼就很簡單了送爸。邏輯和上面的步驟一致,這里只給出Fake代碼展示過程造挽。
#include <CameraOV7670.h>
CameraOV7670 camera(CameraOV7670::RESOLUTION_QQVGA_160x120, CameraOV7670::PIXEL_RGB565, 35);
void setup() {
// 攝像頭初始化
camera.init();
noInterrupts();
}
void loop() {
// 發(fā)送起始幀標(biāo)識碱璃,0x01可隨意更換
UDR0 = 0x01;
// 空循環(huán),直到上一條數(shù)據(jù)被發(fā)送完畢
commit();
// 等待老板合并代碼
camera.waitForVsync();
// 循環(huán)列
for (uint16_t y = 0; y < COL; y++) {
// 行為單位發(fā)送數(shù)據(jù)饭入,所以每行開始時清空容器嵌器,重置下標(biāo)
BUFFER[0] = 0;
INDEX = 0;
uint8_t counter = 0;
// 循環(huán)行,產(chǎn)品經(jīng)理行為
// READ = ROW * 2 + 1谐丢,因為一個像素點需要高光和低光2個數(shù)據(jù)合成爽航,+1是因為容器頭有一個0
for (uint16_t x = 1; x < READ; x++) {
// 不必等待一行全部讀出才發(fā)蚓让,邊讀邊發(fā)
if (counter) {
counter--;
} else {
// 發(fā)送數(shù)據(jù),清除BUFFER
sendByte();
counter = 4;
}
// 讀取數(shù)據(jù)讥珍,存入BUFFER
camera.waitForPixelClockRisingEdge();
camera.readPixelByte(BUFFER[x]);
}
// SEND = ROW * 2
while (INDEX < SEND) {
sendByte();
}
// 發(fā)送行結(jié)束標(biāo)識历极,0x02可隨意更換
UDR0 = 0x02;
commit();
}
}
快上傳代碼,打開串口監(jiān)視器衷佃,看看有沒有數(shù)據(jù)在咕嚕咕嚕地滾動吧趟卸。
解釋一下為什么沒自己寫。
在了解傳感器工作原理和每個腳針的作用后氏义,其實完全可以自己寫代碼去寄存器讀取數(shù)據(jù)的锄列。可這需要高超的尋址能力和嫻熟的內(nèi)存管理惯悠,筆者能力有限邻邮,只能在巨人的肩膀上瑟瑟發(fā)抖。
留下了沒有技術(shù)的淚水.jpg
4
蛤克婶?就這筒严?拜托,我想看的是圖片情萤,誰想去看這些二進制數(shù)據(jù)鸭蛙?
筆者很難能理解這種心情,因為筆者自己也忍不了白涎摇(為了掩蓋看不懂這個事實)9娑琛2撬泉蝌!做事做到底,送佛送到西揩晴。接著就用Processing來搗鼓了一個視頻播放器勋陪。
這次真的把筆者壓箱底的寶貝都拿出來啦,各位觀眾姥爺請務(wù)必點個贊硫兰,謝謝诅愚!
Processing代碼是為Arduino代碼量身定做的,Arduino代碼邏輯變更可能會導(dǎo)致Processing不能正常工作喲~ 還是給Fake代碼吧劫映,便于理解违孝。
final int row = 160;
final int col = 120;
PImage screen;
boolean start = false;
// 行游標(biāo)
int x = 0;
// 列游標(biāo)
int y = 0;
IntList tmper = new IntList();
void setup()
{
size(160, 120);
// 波特率要與山的內(nèi)邊,海的內(nèi)邊一致
port = new Serial(this, "{your device}", 115200);
// 指定RGB格式
screen = createImage(row, col, RGB);
}
void draw()
{
image(screen, 0, 0);
}
void serialEvent(Serial port) {
if (port.available() < 1) {
return;
}
int input = port.read();
// 新的一幀開始了
if (input == 0x01) {
start = true;
x = 0;
return;
}
// 舊的一行結(jié)束了
if (input == 0x02) {
screen.updatePixels();
return;
}
// 如果是中途開始的泳赋,丟棄這一幀
if (!start) {
return;
}
tmper.append(input);
if (tmper.size() < 2) {
return;
}
int row = ((tmper.get(0) & 0xff) << 8) + (tmper.get(1) & 0xff);
int r = (row >> 8) & 0xf8;
int g = (row >> 3) & 0xfc;
int b = (row << 3) & 0xf8;
// 填充圖片
screen.pixels[x++] = color(r, g, b);
tmper.clear();
}
嘿雌桑,看什么呢?看得這么出神祖今?
看到畫面啦校坑,筆者從座位上跳了起來拣技。
但過一會兒發(fā)現(xiàn),圖象是一行一行在刷新的耍目,有明顯的撕裂感膏斤。玩過游戲的都應(yīng)該知道有一個設(shè)置叫垂直同步吧。很遺憾邪驮,由于筆者的開發(fā)板算力不足(還記得之前筆者說過Arduino在業(yè)界被稱為玩具嗎)莫辨,并且使用了較長的杜邦線(也不是所有東西都是越長越好),造成數(shù)據(jù)讀取毅访,傳輸都很慢衔掸,無法達到視頻幀數(shù)的最低標(biāo)準(zhǔn)。只能當(dāng)做PPT看了俺抽。
若想在自制單片機上獲得最佳體驗敞映,可購買更強算力的主板,盆油你聽過樹莓派嗎磷斧?也可購買TFT攝像頭一體板振愿,此刻盡絲滑哦〕诜梗或自行設(shè)計PCB板冕末。道路千萬條,原理第一條侣颂。根因找不到档桃,做工兩行淚。嗯憔晒,好詩好詩藻肄。
現(xiàn)實生活中,只要達到20幀左右一秒拒担,就可以看作視頻了嘹屯。
我們使用的手機攝像頭,要么有比較寬的排線(傳輸量大)从撼,要么直接焊接在主板上(傳輸速度快)州弟,再加之現(xiàn)代手機芯片算力都已經(jīng)很強了,所以不會有這種問題低零。
5
網(wǎng)絡(luò)攝像頭婆翔,網(wǎng)絡(luò)攝像頭,網(wǎng)絡(luò)呢掏婶?啃奴!
老婆餅里面有老婆么?气堕?纺腊?
開個玩笑畔咧,筆者是從來不忘初心的,最后就來整Wifi模塊揖膜。
老樣子适揉,簡單過一下信號送出的過程:
一段無線電波正從ESP01的硬件傳出叠聋。
筆者:施主從何而來汉矿?
電波:方才接了硬件哥哥命令呻此,從東土發(fā)射器而來,去往西天傳遞信息趁仙。
筆者:命令洪添?
電波:這你有所不知?似BIOS雀费,ESP01的ROM里也住著一位妖怪(控制程序)干奢,ta有一個寶貝(AT命令集),只要將它舉過頭頂盏袄,大喊一聲:我叫你一聲你敢答應(yīng)么忿峻?!硬件哥哥就會對ta唯命是從辕羽!
筆者:那這妖怪又聽誰的呢逛尚?
電波:天外有天,人外有人刁愿。這九霄之上绰寞,還有神仙(邏輯代碼/第三方庫),妖怪們在ta面前那是服服體貼铣口,不敢怠慢滤钱。
筆者:阿彌陀佛,善哉善哉枷踏,受教受教菩暗,一路順風(fēng)。
邏輯代碼/第三方庫調(diào)用AT指令集旭蠕,AT指令集操作硬件。
開始接線:
3.3V - 3.3V
EN - 3.3V
GND - GNND
RT - 任意PIN
XT - 任意PIN
不像某些傳感器旷坦,開水燙死豬掏熬,接對接錯都沒反應(yīng)(圖像傳感器:就差報身份證號碼了是吧!)秒梅。當(dāng)接線無誤時旗芬,ESP01的電源燈會亮起。
ESP可用作好幾種模式捆蜀,服務(wù)器模式疮丛,客戶端模式等等幔嫂,這里只是為了把攝像頭的數(shù)據(jù)傳出去,所以選用客戶端模式誊薄。筆者用的神仙就是WiFiEsp(https://github.com/bportaluri/WiFiEsp)履恩。
#include <WiFiEsp.h>
#ifndef HAVE_HWSERIAL1
#include<SoftwareSerial.h>
SoftwareSerial Serial1({RX PIN}, {TX PIN}); // RX, TX 換成自己插的引腳
#endif
int status = WL_IDLE_STATUS;
const char ssid[] = "{wifi ssid}";
const char pass[] = "{password}";
// 如果初始化成功,就可以調(diào)用這個句柄發(fā)送數(shù)據(jù)啦
WiFiEspClient client;
void setup()
{
Serial1.begin(9600);
WiFi.init(&Serial1);
if (WiFi.status() == WL_NO_SHIELD) {
// 如果連接失敗了就卡在這里
while (true);
}
while (status != WL_CONNECTED) {
status = WiFi.begin(ssid, pass);
}
}
大功告成了呢蔫!
啪切心!現(xiàn)實馬上給了筆者左臉一記響亮的耳光。
到目前為止片吊,筆者只是讓硬件連上了Wifi绽昏,數(shù)據(jù)要送去哪里,怎么處理都還沒著落呢俏脊。
做人還是要低調(diào)啊全谤。
程序員的邏輯一向是發(fā)現(xiàn)問題,解決問題爷贫。缺什么就搞什么啼县。
先來冷靜分析一波:連上自家的Wifi就能訪問公網(wǎng)了,筆者恰好有自己一臺服務(wù)器(當(dāng)時怎么就沒想到連同一個Wifi也相當(dāng)于在一個小局域網(wǎng)里呢......)沸久。如果是普通應(yīng)用季眷,隨手寫一個接口丟上去接收處理數(shù)據(jù)就行啦,但這個應(yīng)用對響應(yīng)時間要求特別高卷胯,思來想去子刮,還真得弄一個websoket服務(wù)器來處理,越搞越復(fù)雜窑睁,筆者還是太年輕了挺峡。
Websocket是基于TCP的全雙工通信協(xié)議。
要快速實現(xiàn)担钮,那肯定用PHP呀橱赠,畢竟世界上最好的語言(誤)。加上筆者之前用PHP也做過一個IM系統(tǒng)箫津,30分鐘用Workerman(https://github.com/walkor/workerman)搭了一個websocket服務(wù)器狭姨。
代碼邏輯沒有邏輯,將收到的數(shù)據(jù)廣播給所有已經(jīng)連接的客戶端苏遥。
這里要注意饼拍,平時大家用websoket幾乎都是處理文本格式的數(shù)據(jù),這里需要調(diào)整參數(shù)田炭,原封不動地轉(zhuǎn)發(fā)二進制數(shù)據(jù)师抄。
$connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER;
當(dāng)然,為了服務(wù)器安全教硫,筆者沒有直接暴露服務(wù)端口叨吮,而是用Nginx做了轉(zhuǎn)發(fā)辆布。Nginx,YYDS茶鉴!
服務(wù)器有了锋玲,這下氵。
啪蛤铜!現(xiàn)實又給了筆者右臉一記響亮的耳光嫩絮。
有話不能好好說嗎!
因為慣性思維围肥,之前代碼發(fā)送數(shù)據(jù)是以HTTP協(xié)議發(fā)出的剿干,但現(xiàn)在改成WS協(xié)議了,舊Arduino代碼也就失效了穆刻。還是那句老話置尔,一物降一物,別以為神仙就至高無上了氢伟,筆者這次還真請來元始天尊了呢0窠巍(https://github.com/arduino-libraries/ArduinoHttpClient)
第三方Websoket庫調(diào)用Wifi第三方庫,Wifi第三方庫調(diào)用AT指令集朵锣,AT指令集操作硬件谬盐。
#include <ArduinoHttpClient.h>
const char host[] = "{your host}";
// 這里的client就是上邊的client,呃诚些,總覺得這句話說了等于沒說
WebSocketClient ws = WebSocketClient(client, host, 80);
void loop()
{
ws.begin();
while (ws.connected()) {
ws.beginMessage(TYPE_BINARY);
// 再見了飞傀,數(shù)據(jù)
ws.print(data);
ws.endMessage();
}
}
使用websoket發(fā)送數(shù)據(jù),不需要每次都新建連接诬烹,因此非吃曳常快,如果一切正常工作绞吁,ESP01的藍色指示燈會不停地閃爍幢痘。
6
歷經(jīng)九九八十一難,終于來到小雷音寺家破,心中百感交集颜说。但長征還未結(jié)束,同志任需努力员舵。
最后的最后脑沿,只需要寫一個web頁面,接收websoket推送马僻,顯示畫面就行啦。代碼邏輯可以參考Processing注服。
猶豫了一下韭邓,還是簡單說說canvas吧措近。別的筆者也就不班門弄斧了,這次只用到了幾個API女淑,如果讀者感興趣瞭郑,可自行查閱谷歌。
let camera = Document.getElementById("{your element}");
// 分辨率
camera.width = {width};
camera.height = {height};
let ctx = camera.getContext("2d");
// 定義一個長條鸭你,因為是一行一行繪制的
let row = ctx.createImageData({width}, 1);
// index是行游標(biāo)
row.data[{index}] = {R};
row.data[{index} + 1] = {G};
row.data[{index} + 2] = {B};
row.data[{index} + 3] = 255; // 這個是透明度屈张,可以寫死
// col是列游標(biāo)
ctx.putImageData(row, 0, {col});
運行效果如下。
ESP01瘋狂地閃爍著藍燈袱巨,芯片也開始滾燙起來阁谆。
打開瀏覽器的開發(fā)者工具,在Network的Message一欄愉老,可以看到刷刷刷地下載著推送數(shù)據(jù)场绿。
估摸著菩薩掐指一算,哎呀嫉入,九九八十一難焰盗,還差一難!
之前不是說過在串口通信下只能看PPT嗎咒林,在加入Wifi模塊后熬拒,你猜怎么著?完全加載一行需要1分鐘垫竞!筆者使用的分辨率是160x120澎粟,也就是說,在web頁面上想看到一張完整的圖片需要約2小時......
想來也是件甥,Wifi發(fā)送數(shù)據(jù)捌议,自家路由器收到,再走公網(wǎng)發(fā)送給websocket服務(wù)器引有,服務(wù)器再走公網(wǎng)把數(shù)據(jù)推給web頁面瓣颅。不過核心問題還是在于Arduino板處理得太慢了。
當(dāng)頭一棒譬正,筆者本來還打算再給攝像頭下面加一個舵機宫补,遠程控制攝像頭左右擺動呢,無奈只得放棄了:制作一個可以遠程控制曾我,卻看不到畫面的網(wǎng)絡(luò)攝像頭粉怕,無異于買櫝還珠。
7
經(jīng)此種種抒巢,產(chǎn)品一句話贫贝,開發(fā)做十年,還是有道理的。
你看稚晚,當(dāng)初筆者不也僅僅是拍了張照片發(fā)送到網(wǎng)上去嗎崇堵?然而為了復(fù)刻這個過程,需要用多少知識客燕?知道得越多鸳劳,就越發(fā)覺得自己知道得越少,這就是也搓,知識的詛咒赏廓。
還是那句話,沒有什么事是理所當(dāng)然的傍妒,只不過有人替你負重前行罷了幔摸。
最后附上涉及的領(lǐng)域,算是拋磚引玉吧拍顷。
生理學(xué):視覺細胞抚太。
信息與通信工程:數(shù)字信號模擬信號互轉(zhuǎn)。
光學(xué):光學(xué)三原色昔案,色圖的發(fā)展演進(咳咳尿贫,正經(jīng)的),濾波踏揣。
電學(xué):電子庆亡,光電轉(zhuǎn)換,信號放大捞稿,偏壓電路又谋。
計算機科學(xué):晶振,二進制娱局,位移運算彰亥,內(nèi)存尋址,寄存器衰齐,脈沖信號任斋,數(shù)字信號,CMOS陣列耻涛,串口通信废酷,無線電通信,網(wǎng)絡(luò)通信抹缕,通信協(xié)議澈蟆,高級語言,系統(tǒng)架構(gòu)卓研。