看上去很牛逼但实际上没什么卵用的网络摄像头制作教程

1,391 阅读11分钟

1

笔者一直信奉这样一句话:

没有什么事是理所当然的。

最近两周的经历再次验证了这句话。故事还得从一张图片说起......

某日,笔者走在街上,看到路边躺着一只猫猫,腿就像灌了铅似的迈不动了,兴奋地搓了搓小手手就想上去撸一番。可小家伙警惕得很,瞅着笔者靠近了,立马翻了个身子撅起屁股,随时准备逃走。

哎呀,今天遇到了只贞洁烈猫啊。

没办法,只好掏出手机,拍了张照片发到朋友圈,悻悻而归。

就在按下【发表】的一瞬间,脑中突然出现一个闪念:这个照片到底是怎么拍出来的啊?(这和撸猫到底有什么关系啊喂!捂脸笑)恰巧单片机又是笔者的业余爱好,那这次就来做个网络摄像头吧。

想法就这样萌生了。

本以为搞起来会很轻松(不就是摄像头拍出画面上传到网络么),没想到拉开了长达两周噩梦的帷幕。打个比方,你以为前面只是一个小水洼,本想上去踩一踩,没想到整个人就下去了。

那么希望这篇文章,能让你瞥见那些,习以为常的表象背后发生的事情,毕竟,没有什么事是理所当然的。

FBI Warning

笔者也只是菜鸡,亦是第一次涉猎这一领域,出现纰漏在所难免。希望读者保留自己的判断,尽信书不如无书。

2

灯光熄灭,聚光灯亮起。

咳咳。

一阵短暂的回啸,观众们安静了下来。

女士们,先生们,欢迎观赏本次演出!下面,有请本期的嘉宾,登~场~(汽笛声X4):

不被人理解,却渴望被人理解的单片机开发板——Arduino UNO R3!

1.jpeg

拥有能穿透人心,直达灵魂深处眼眸的摄像头,OV7670!

2.jpeg

身体虽然变小,但头脑依然灵活的Wifi模块,ESP01!

3.jpeg

最后出场的是,平平无奇的母对公杜邦线!

杜邦线:就我没资格配图是吧!!!(对!哦,对了,这些线越短越好,不为什么!)

今天的嘉宾会为我们带来怎样精彩的演出呢?ARE YOU READYYYYYY?

4.jpeg

3

首先来攻克最困难的部分,图像传感器。

别急,正所谓知其然,知其所以然,这之前,还是先简单聊聊图像传感器的工作原理。

但在这之前,首先抛出两个概念(STOP!禁止套娃):数字信号模拟信号

概念笔者就不抄了,这里只需要知道,计算机不能直接处理模拟信号,只能处理数字信号就行。而图像传感器的作用,正是将模拟信号转换为数字信号。

知道人的眼睛是如何看到颜色的吗?人的视网膜上有两种感官细胞,视秆细胞视锥细胞。视秆细胞能感受明暗,视锥细胞则有三种,分别用来感应红,绿,蓝。看到这儿是不是有一种恍然大悟的感觉?没错,自然界所有颜色都可以由这三种颜色组合形成,也因此,这三种颜色被成为光学三原色,但它有一个更家喻户晓的名字,那就是RGB

有时候,人和机器之间的界限,是相当模糊的。人体器官的工作,大部分机器是可以模仿的,这也意味着大部分人体器官也可以被机器代替。

图像传感器大抵也是模拟眼球的工作方式。

这里简要概括一下转换过程吧(胡诌警告!这里特指CMOS传感器),闭上眼睛想象一下:

在一层正方形的大楼里,整整齐齐地划分为若干小的正方形的工作隔间,像一个正方形的表格。

每个隔间有一名程序员(感光二极管)。

每隔一段时间(机器时钟),有产品经理会一行一行地找到程序员,催促进度(寻址,并接通水平开关)。

又有老板一列一列地找到程序员:饮茶时间饮茶,做工时间做工,今天的代码什么时候交?(接通垂直开关)

哇靠,你们两个合起来搞我?程序员压力山大(由于同时接通了水平,垂直开关,产生了偏压)!

此时,天上降下天使(光线),她张开双臂将程序员拥入怀中,这让程序员感到慰藉,于是开始疯狂提交代码(偏压二极管遇到光子产生电流)。

产品经理和老板满意地点了点头,笑着,带着数据(RGB,光线强弱等)离开了。

每一行每一列依次重复这个过程。

等所有程序员都提交了代码,老板把分支一合并,远远看去,竟凑成了一副《春树秋霜图》!

这一切只发生在一瞬,而瞬间即是永恒。

稍稍把时间放慢一点的话,大概可以比喻成,从左上角向右下角倒塌的多米诺骨牌吧。滴水成河,聚沙成塔,虽然一支感光二极管什么都做不到,但成千上万支二极管,就能组成包含整个世界的图象。

当然,还有很多工作也在同步进行,比如浮动扩散,信号放大,消除噪音等等,展开来说的话,又是另外一篇文章了。

纸上得来终觉浅,绝知此事要躬行。是时候展示真正的技术啦。

笔者使用了一个第三方库来操作OV767(github.com/indrekluuk/…)。这个库定死了引脚连接:

VS - PIN2

XLK - PIN3

PLK - PIN12

SD - A4 还需要用连接3.3V单独供电,请在中间安装一个10K电阻

SC - A5 还需要用连接3.3V单独供电,请在中间安装一个10K电阻

D0 ~ D3 - A0 ~ A3 依次对应

D4 ~ D7 - PIN4 ~ PIN7 同上

3.3V - 3.3V

RESET - 3.3V

GND - GND

PWDN - GND

5.jpeg

还记得OV7670左侧的脚针吗?不记得的请退回查看图片~其中VS和HS就是控制水平,垂直开关的脚针!了解理论还是有用的。

Arduino调用层代码就很简单了。逻辑和上面的步骤一致,这里只给出Fake代码展示过程。

#include <CameraOV7670.h>

CameraOV7670 camera(CameraOV7670::RESOLUTION_QQVGA_160x120, CameraOV7670::PIXEL_RGB565, 35);

void setup() {
  // 摄像头初始化
  camera.init();
  noInterrupts();
}

void loop() {
  // 发送起始帧标识,0x01可随意更换
  UDR0 = 0x01;
  // 空循环,直到上一条数据被发送完毕
  commit();
  // 等待老板合并代码
  camera.waitForVsync();
  // 循环列
  for (uint16_t y = 0; y < COL; y++) {
    // 行为单位发送数据,所以每行开始时清空容器,重置下标
    BUFFER[0] = 0;
    INDEX = 0;
    uint8_t counter = 0;
    // 循环行,产品经理行为
    // READ = ROW * 2 + 1,因为一个像素点需要高光和低光2个数据合成,+1是因为容器头有一个0
    for (uint16_t x = 1; x < READ; x++) {
      // 不必等待一行全部读出才发,边读边发
      if (counter) {
        counter--;
      } else {
        // 发送数据,清除BUFFER
        sendByte();
        counter = 4;
      }
      // 读取数据,存入BUFFER
      camera.waitForPixelClockRisingEdge();
      camera.readPixelByte(BUFFER[x]);
    }
    // SEND = ROW * 2
    while (INDEX < SEND) {
      sendByte();
    }
    // 发送行结束标识,0x02可随意更换
    UDR0 = 0x02;
    commit();
  }
}

快上传代码,打开串口监视器,看看有没有数据在咕噜咕噜地滚动吧。

解释一下为什么没自己写。

在了解传感器工作原理和每个脚针的作用后,其实完全可以自己写代码去寄存器读取数据的。可这需要高超的寻址能力和娴熟的内存管理,笔者能力有限,只能在巨人的肩膀上瑟瑟发抖。

留下了没有技术的泪水.jpg

4

蛤?就这?拜托,我想看的是图片,谁想去看这些二进制数据?

笔者很能理解这种心情,因为笔者自己也忍不了啊(为了掩盖看不懂这个事实)!!!做事做到底,送佛送到西。接着就用Processing来捣鼓了一个视频播放器。

这次真的把笔者压箱底的宝贝都拿出来啦,各位观众姥爷请务必点个赞,谢谢!

Processing代码是为Arduino代码量身定做的,Arduino代码逻辑变更可能会导致Processing不能正常工作哟~ 还是给Fake代码吧,便于理解。

final int row = 160;
final int col = 120;
PImage screen;

boolean start = false;
// 行游标
int x = 0;
// 列游标
int y = 0;
IntList tmper = new IntList();

void setup()
{
  size(160, 120);
  // 波特率要与山的内边,海的内边一致
  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;
  }
  // 旧的一行结束了
  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();
}

嘿,胖虎,看什么呢?看得这么出神?

6.jpeg

看到画面啦,笔者从座位上跳了起来。

但过一会儿发现,图象是一行一行在刷新的,有明显的撕裂感。玩过游戏的都应该知道有一个设置叫垂直同步吧。很遗憾,由于笔者的开发板算力不足(还记得之前笔者说过Arduino在业界被称为玩具吗),并且使用了较长的杜邦线(也不是所有东西都是越长越好),造成数据读取,传输都很慢,无法达到视频帧数的最低标准。只能当做PPT看了。

若想在自制单片机上获得最佳体验,可购买更强算力的主板,盆油你听过树莓派吗?也可购买TFT摄像头一体板,此刻尽丝滑哦。或自行设计PCB板。道路千万条,原理第一条。根因找不到,做工两行泪。嗯,好诗好诗。

现实生活中,只要达到20帧左右一秒,就可以看作视频了。

我们使用的手机摄像头,要么有比较宽的排线(传输量大),要么直接焊接在主板上(传输速度快),再加之现代手机芯片算力都已经很强了,所以不会有这种问题。

5

网络摄像头,网络摄像头,网络呢?!

老婆饼里面有老婆么???

开个玩笑,笔者是从来不忘初心的,最后就来整Wifi模块。

老样子,简单过一下信号送出的过程:

一段无线电波正从ESP01的硬件传出。

笔者:施主从何而来?

电波:方才接了硬件哥哥命令,从东土发射器而来,去往西天传递信息。

笔者:命令?

电波:这你有所不知?似BIOS,ESP01的ROM里也住着一位妖怪(控制程序),ta有一个宝贝(AT命令集),只要将它举过头顶,大喊一声:我叫你一声你敢答应么?!硬件哥哥就会对ta唯命是从!

笔者:那这妖怪又听谁的呢?

电波:天外有天,人外有人。这九霄之上,还有神仙(逻辑代码/第三方库),妖怪们在ta面前那是服服体贴,不敢怠慢。

笔者:阿弥陀佛,善哉善哉,受教受教,一路顺风。

逻辑代码/第三方库调用AT指令集,AT指令集操作硬件。

开始接线:

7.jpeg

3.3V - 3.3V

EN - 3.3V

GND - GNND

RT - 任意PIN

XT - 任意PIN

不像某些传感器,开水烫死猪,接对接错都没反应(图像传感器:就差报身份证号码了是吧!)。当接线无误时,ESP01的电源灯会亮起。

8.jpeg

ESP可用作好几种模式,服务器模式,客户端模式等等,这里只是为了把摄像头的数据传出去,所以选用客户端模式。笔者用的神仙就是WiFiEsp(github.com/bportaluri/…)。

#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}";

// 如果初始化成功,就可以调用这个句柄发送数据啦
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);
  }
}

大功告成了!

啪!现实马上给了笔者左脸一记响亮的耳光。

到目前为止,笔者只是让硬件连上了Wifi,数据要送去哪里,怎么处理都还没着落呢。

做人还是要低调啊。

程序员的逻辑一向是发现问题,解决问题。缺什么就搞什么。

先来冷静分析一波:连上自家的Wifi就能访问公网了,笔者恰好有自己一台服务器(当时怎么就没想到连同一个Wifi也相当于在一个小局域网里呢......)。如果是普通应用,随手写一个接口丢上去接收处理数据就行啦,但这个应用对响应时间要求特别高,思来想去,还真得弄一个websoket服务器来处理,越搞越复杂,笔者还是太年轻了。

Websocket是基于TCP的全双工通信协议。

要快速实现,那肯定用PHP呀,毕竟世界上最好的语言(误)。加上笔者之前用PHP也做过一个IM系统,30分钟用Workerman(github.com/walkor/work…)搭了一个websocket服务器。

代码逻辑没有逻辑,将收到的数据广播给所有已经连接的客户端。

这里要注意,平时大家用websoket几乎都是处理文本格式的数据,这里需要调整参数,原封不动地转发二进制数据。

$connection->websocketType = Websocket::BINARY_TYPE_ARRAYBUFFER;

当然,为了服务器安全,笔者没有直接暴露服务端口,而是用Nginx做了转发。Nginx,YYDS!

服务器有了,这下氵。

啪!现实又给了笔者右脸一记响亮的耳光。

有话不能好好说吗!

因为惯性思维,之前代码发送数据是以HTTP协议发出的,但现在改成WS协议了,旧Arduino代码也就失效了。还是那句老话,一物降一物,别以为神仙就至高无上了,笔者这次还真请来元始天尊了呢!(github.com/arduino-lib…

第三方Websoket库调用Wifi第三方库,Wifi第三方库调用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);
    // 再见了,数据
    ws.print(data);
    ws.endMessage();
  }
}

使用websoket发送数据,不需要每次都新建连接,因此非常快,如果一切正常工作,ESP01的蓝色指示灯会不停地闪烁。

9.jpeg

6

历经九九八十一难,终于来到小雷音寺,心中百感交集。但长征还未结束,同志任需努力。

最后的最后,只需要写一个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是行游标
row.data[{index}] = {R};
row.data[{index} + 1] = {G}; 
row.data[{index} + 2] = {B}; 
row.data[{index} + 3] = 255; // 这个是透明度,可以写死
​
// col是列游标
ctx.putImageData(row, 0, {col});

运行效果如下。

ESP01疯狂地闪烁着蓝灯,芯片也开始滚烫起来。

打开浏览器的开发者工具,在Network的Message一栏,可以看到刷刷刷地下载着推送数据。

10.jpeg

估摸着菩萨掐指一算,哎呀,九九八十一难,还差一难!

之前不是说过在串口通信下只能看PPT吗,在加入Wifi模块后,你猜怎么着?完全加载一行需要1分钟!笔者使用的分辨率是160x120,也就是说,在web页面上想看到一张完整的图片需要约2小时......

想来也是,Wifi发送数据,自家路由器收到,再走公网发送给websocket服务器,服务器再走公网把数据推给web页面。不过核心问题还是在于Arduino板处理得太慢了。

当头一棒,笔者本来还打算再给摄像头下面加一个舵机,远程控制摄像头左右摆动呢,无奈只得放弃了:制作一个可以远程控制,却看不到画面的网络摄像头,无异于买椟还珠。

7

经此种种,产品一句话,开发做十年,还是有道理的。

你看,当初笔者不也仅仅是拍了张照片发送到网上去吗?然而为了复刻这个过程,需要用多少知识?知道得越多,就越发觉得自己知道得越少,这就是,知识的诅咒。

还是那句话,没有什么事是理所当然的,只不过有人替你负重前行罢了。

最后附上涉及的领域,算是抛砖引玉吧。

生理学:视觉细胞。

信息与通信工程:数字信号模拟信号互转。

光学:光学三原色,色图的发展演进(咳咳,正经的),滤波。

电学:电子,光电转换,信号放大,偏压电路。

计算机科学:晶振,二进制,位移运算,内存寻址,寄存器,脉冲信号,数字信号,CMOS阵列,串口通信,无线电通信,网络通信,通信协议,高级语言,系统架构。