这篇文章来源于deviceplus.com英语网站的翻译稿。
大多数人听到“jpeg解码”时,通常会觉得这是很困难的事,需要很强的处理能力以及复杂的数学运算,并认为在相对便宜且速度较慢的8位处理器平台(比如arduino)上是不可能实现的,或者说至少是不切实际的。在本文中,我们将学习如何使用基于arduino控制的相机拍摄jpeg照片,以及如何将照片转换成像素点矩阵,并将所有像素通过串行端口传输到我们的pc端或者任何我们想要的平台上!
硬件
• arduino mega
• vc0706 串口摄像头
• 带spi接口的sd卡模块
软件
• arduino ide
• processing (3.3.2 或更高版本)
• adafruit vc0706 库 (可从 github上获取)
• bodmer 的 jpegdecoder 库 (同样可从 github上获取)
虽然说上面描述的内容是完全可以实现的,但是仍然有必要解释一下为什么我们在解码jpeg照片时会遇到麻烦。毕竟,在上面的硬件要求中列有一个sd模块,您会问:“我们直接把照片以photo.jpeg 的格式存储到sd卡里不就行了吗?”当然,这确实是整个过程中的重要一步,但是现在请从不同的角度来考虑这个问题:如果我们想通过速度慢、有些不稳定的连接来发送照片怎么办?如果我们只是把jpeg照片分割成不同的包并通过慢速连接发送,那么就有部分数据损坏或丢失的风险。发生这种情况时,我们很可能无法用损坏的数据还原原始数据。
但是,当我们将jpeg解码为位图,然后发送实际像素时,不会有任何风险。如果某些数据在传输的过程中损坏或丢失,我们仍然可以获取整张图像,只有数据损坏的地方会出现失色,错位或像素丢失的情况。当然,它与我们的原始图像并不相同,但是仍然包含了大多数原始信息,并且仍然是“可读的”。既然已经知道了为什么要这样做,接下来让我们看一下如何实施这种方法。
拍摄照片
在开始解码jpeg照片之前,首先我们需要拍摄照片。我们最终的目标是拍摄一张照片,将照片存储到sd卡中,然后发送到某个地方。那我们按照这个思路先从一个简单的设置开始吧。
图1:可以使用arduino拍摄和存储照片的设置
因为我们需要大量的ram来对照片进行解码,所以我们将使用arduino mega。此外,mega上还有一个额外的有利设计:有四个单独的硬件串行端口,这样我们就可以使用serial1端口与相机进行通信,并使用serial端口与pc进行通信。
您可能已经注意到了,相机rx线上有一个简单的电阻分压器。这是因为vc0706芯片的逻辑电平为3.3v(即使电源电压为5v),但arduino mega的逻辑电平为5v。所以在这里有个善意忠告:当将5v的arduino和3.3v模块进行接合时,在rx线上始终至少使用一个分压器。这比换一个新的模块要快得多。sd卡读卡器通过spi接口直接连接。
既然硬件已经设置好了,那我们就需要开始解决代码部分了。标准arduino ide安装已经包含了用于sd卡的库,因此我们从列表中对sd卡进行查看即可。
我们需要控制的另一个设备是vc0706摄像头。控制过程相对简单,我们只需要使用串行线发送一些指令,然后通过同一条线接收jpeg照片即可。我们可以编写一个库来执行此操作,但是因为这一步我们不需要考虑整体草图的大小,所以我们将使用adafruit开发的一个vc0706库。为了拍摄照片并保存到sd卡上,我们将使用以下代码,代码是该库随附的经过轻微修改的snapshot示例。
// include all the libraries#include #include #include // define slave select pin#define sd_cs 53// create an instance of adafruit_vc0706 class// we will use serial1 for communication with the cameraadafruit_vc0706 cam = adafruit_vc0706(&serial1);void setup() { // begin serial port for communication with pc serial.begin(115200); // start the sd if(!sd.begin(sd_cs)) { // if the sd can't be started, loop forever serial.println(sd failed or not present!); while(1); } // start the camera if(!cam.begin()) { // if the camera can't be started, loop forever serial.println(camera failed or not present!); while(1); } // set the image size to 640x480 cam.setimagesize(vc0706_640x480);}void loop() { serial.print(taking picture in 3 seconds ... ); delay(3000); // take a picture if(cam.takepicture()) { serial.println(done!); } else { serial.println(failed!); } // create a name for the new file in the format imagexy.jpg char filename[13]; strcpy(filename, image00.jpg); for(int i = 0; i 0) { // load the jpeg-encoded image data from the camera into a buffer uint8_t *buff; uint8_t bytestoread = min(32, jpglen); buff = cam.readpicture(bytestoread); // write the image data to the file imgfile.write(buff, bytestoread); jpglen -= bytestoread; } // safely close the file imgfile.close(); serial.println(done!); delay(3000);}
现在,arduino将每10秒左右拍摄一张照片,直到sd卡上的空间用完为止。但是,由于照片通常约为48kb,并且我目前使用的是2gb的sd卡,因此足够容纳超过43000张的照片。理论上来说我们不需要那么多的照片。但是既然已经拍摄了一些照片,我们现在可以继续进行下一个有趣环节了:将它们从jpeg压缩后的难以管理的杂乱数据变成简单的像素阵列!
解码和发送照片
在开始解码前,让我们快速地看一下图片数据在jpeg文件中究竟是如何存储的。如果您对这部分不太感兴趣,可以跳过下面三段内容。如果您确切地对图形和压缩方面的知识了解一二(不像我这样),您也可以跳过这一部分。以下内容进行了一定程度的简化。
对任何类型的图片数据进行存储时,有两种基本方法:无损和有损压缩。两者的区别很明显:当使用无损压缩(例如png)对图像进行编码时,处理之后图像的每个像素都与开始时完全相同。这非常适合于诸如计算机图形学之类的工作,但是不幸的是,这是以增加文件大小为代价的。另一方面,对于像jpeg这样的有损压缩,我们丢失了一些细节,但是生成的文件大小要小得多。
jpeg压缩方式在理解上可能会有点困难,因为会涉及到一些“离散余弦变换”,不过主要原理实际上是非常简单的。首先,将图片从rgb颜色空间转换为ycbcr。我们都知道rgb颜色空间—它存储了红色(r)、绿色(g)和蓝色(b)的颜色值。ycbcr有很大的不同—它使用亮度(y—基本是原始图像的灰度图),蓝色差分量(cb—图片中的“蓝色”)和红色差分量(cr—图片中的“红色”)。
图2:jpeg照片以及其分离出的色差分量。左上角为原始图像,左下角为y分量,右上角为cb分量,右下角为cr分量
jpeg减小文件大小的方法实际上与人眼处理颜色的方式密切相关。看一下上图中的y、cb和cr分量图。哪一个看起来更像是原始图片?是的,灰度图!这是因为人眼对亮度的敏感度要比对其它两个分量的敏感度高得多。jpeg压缩就非常聪明地利用了这一点,在保留原始y分量的同时减少cb和cr分量中的信息量。如此一来,生成的图片就比原始文件小得多,并且由于大多数压缩信息都位于人眼不太敏感的分量中,因此与未压缩的图片相比,您几乎看不到压缩图片的区别。
现在,让我们开始运行真正实现将jpeg转换为像素阵列的代码吧。幸运的是,有一个库可以做到这一点—bodmer的jpegdecoder(可在github上获得),该库基于rich geldreich(也可在github)上获取)提供的出色的picojpeg库。虽然最初编写jpegdecoder的目的是在tft显示器上显示图像,但是将其进行一些细微调整后就可以用于我们的工作了。
该库的使用非常简单:我们输入jpeg文件,然后该库就会开始产生像素阵列—所谓的最小编码单位,或简称为mcu。mcu是一个16×8的像素块。库中的函数将以16位颜色值的形式返回每个像素点的颜色值。高5位是红色值,中6位是绿色值,低5位是蓝色值。现在,我们可以通过任何通信通道来发送这些值。我将使用串行端口,以便之后可以更容易地接收数据。下面的arduino草图对一张图像进行了解码,然后发送了mcu中每个像素点的16位rgb值,并对图像文件中的所有mcu重复该操作。
// include the library#include // define slave select pin#define sd_cs 53void setup() { // set pin 13 to output, otherwise spi might hang pinmode(13, output); // begin serial port for communication with pc serial.begin(115200); // start the sd if(!sd.begin(sd_cs)) { // if the sd can't be started, loop forever serial.println(sd failed or not present!); while(1); } // open the root directory file root = sd.open(/); // wait for the pc to signal while(!serial.available()); // send all files on the sd card while(true) { // open the next file file jpgfile = root.opennextfile(); // we have sent all files if(!jpgfile) { break; } // decode the jpeg file jpegdec.decodesdfile(jpgfile); // create a buffer for the packet char databuff[240]; // fill the buffer with zeros initbuff(databuff); // create a header packet with info about the image string header = $ithdr,; header += jpegdec.width; header += ,; header += jpegdec.height; header += ,; header += jpegdec.mcusperrow; header += ,; header += jpegdec.mcuspercol; header += ,; header += jpgfile.name(); header += ,; header.tochararray(databuff, 240); // send the header packet for(int j=0; j> 8; databuff[i+1] = color; i += 2; // if the packet is full, send it if(i == 240) { for(int j=0; j<240; j++) { serial.write(databuff[j]); } i = 6; } // if we reach the end of the image, send a packet if((mcuxcoord == jpegdec.mcusperrow - 1) && (mcuycoord == jpegdec.mcuspercol - 1) && (mcupixels == 1)) { // send the pixel values for(int j=0; j
注释中已经对大多数代码进行了解释,但是我还是需要对代码结构中的“包”进行一些说明。为了使数据传输更加有序,所有内容都以包的形式传输,最大长度为240字节。包有两种可能的类型:
1.头包:此包以字符串“$ithdr”开头,并且包含我们将要发送的图片的基本信息:以像素为单位的高度和宽度,行和列前的mcu数量,最后是原始文件名。对于我们要发送的每个图像,都会相应发送一个头包。
2.数据包:该包以“$itdat”开头,并包含所有颜色数据。该数据包中的每两个字节代表一个16位像素值。
乍一看,包的长度似乎是随机的。但是为什么恰好是240个字节?为什么不是256个,使我们可以在每个包中发送两个mcu呢?这是另一个我们日后将会解决的谜团,但是我们可以保证, 数字240不会有任何随机性。这里有个小提示:如果包中有256个字节的数据,我们要在哪里存储源地址和目标地址呢?
现在,我们有了一个可以解码和发送图片文件的代码,但是仍然缺少一个核心功能:目前为止,并没有可以响应这些数据的另一端口。这意味着是时候再次启用processing了!
接收图片
我在arduino六足机器人第三部分:远程控制中曾介绍过一些有关processing的内容,用其编写了一个应用程序,通过该应用程序我们能够轻松控制六足机器人。简单回顾一下:processing是一种基于java的语言,主要用于绘图工作。因此它非常适用于我们现在要做的像素显示的工作!该程序就是用processing实现的。
// import the libraryimport processing.serial.*;serial port;void setup() { // set the default window size to 200 by 200 pixels size(200, 200); // set the background to grey background(#888888); // set as high framerate as we can framerate(1000000); // start the com port communication // you will have to replace com30 with the arduino com port number port = new serial(this, com30, 115200); // read 240 bytes at a time port.buffer(240);}// string to save the trimmed inputstring trimmed;// buffer to save data incoming from serial portbyte[] bytebuffer = new byte[240];// the coordinate variablesint x, y, mcux, mcuy;// a variable to measure how long it takes to receive the imagelong starttime;// a variable to save the current timelong currenttime;// flag to signal end of transmissionboolean received = false;// flag to signal reception of header packetboolean headerread = false;// the color of the current pixelint incolor, r, g, b;// image information variablesint jpegwidth, jpegheight, jpegmcusperrow, jpegmcuspercol, mcuwidth, mcuheight, mcupixels;// this function will be called every time any key is pressedvoid keypressed() { // send something to arduino to signal the start port.write('s');}// this function will be called every time the serial port receives 240 bytesvoid serialevent(serial port) { // read the data into buffer port.readbytes(bytebuffer); // make a string out of the buffer string instring = new string(bytebuffer); // detect the packet type if(instring.indexof($ithdr) == 0) { // header packet // remove all whitespace characters trimmed = instring.trim(); // split the header by comma string[] list = split(trimmed, ','); // check for completeness if(list.length != 7) { println(incomplete header, terminated); while(true); } else { // parse the image information jpegwidth = integer.parseint(list[1]); jpegheight = integer.parseint(list[2]); jpegmcusperrow = integer.parseint(list[3]); jpegmcuspercol = integer.parseint(list[4]); // print the info to console println(filename: + list[5]); println(parsed jpeg width: + jpegwidth); println(parsed jpeg height: + jpegheight); println(parsed jpeg mcus/row: + jpegmcusperrow); println(parsed jpeg mcus/column: + jpegmcuspercol); // start the timer starttime = millis(); } // set the window size according to the received information surface.setsize(jpegwidth, jpegheight); // get the mcu information mcuwidth = jpegwidth / jpegmcusperrow; mcuheight = jpegheight / jpegmcuspercol; mcupixels = mcuwidth * mcuheight; } else if(instring.indexof($itdat) == 0) { // data packet // repeat for every two bytes received for(int i = 6; i < 240; i += 2) { // combine two 8-bit values into a single 16-bit color incolor = ((bytebuffer[i] & 0xff) 11) * 8; g = ((incolor & 0x07e0) >> 5) * 4; b = ((incolor & 0x001f) >> 0) * 8; // paint the current pixel with that color set(x + mcuwidth*mcux, y + mcuheight*mcuy, color(r, g, b)); // move onto the next pixel x++; if(x == mcuwidth) { // mcu row is complete, move onto the next one x = 0; y++; } if(y == mcuheight) { // mcu is complete, move onto the next one x = 0; y = 0; mcux++; } if(mcux == jpegmcusperrow) { // line of mcus is complete, move onto the next one x = 0; y = 0; mcux = 0; mcuy++; } if(mcuy == jpegmcuspercol) { // the entire image is complete received = true; } } }}void draw() { // if we received a full image, start the whole process again if(received) { // reset coordinates x = 0; y = 0; mcux = 0; mcuy = 0; // reset the flag received = false; // measure how long the whole thing took long timetook = millis() - starttime; println(image receiving took: + timetook + ms); println(); }}
当您在连接arduino之后运行该程序,然后按下键盘上的任意键时,您(希望)会看到暗淡、单一的灰色背景逐渐被最初存储在sd卡上的图像所取代。由于替换是逐像素进行的,因此整个过程具有一种老式拨号调制解调器的加载图像风格!
图3:使用processing应用程序将照片从arduino加载到pc
虽然我们以相当高的波特率(准确值为115200)运行串行端口,接收一张图像也需要大约60秒。我们可以用它来计算实际的传输速度。
原始图像宽640像素,高480像素,总计307200像素。每个像素都由2字节的颜色值来表示,总共要传输614400个字节(即600kb)。那么我们的最终速度约为10kb/s。对于我们制定的“协议”来说,这并不算很糟糕,不是吗?此外,它还向您展示了为什么图像压缩如此有用。原始jpeg文件只有48kb左右,而解码后的位图则需要600kb。如果我们要传输jpeg文件,即使使用非常简单的“协议”,也可以在5秒之内完成传输。当然,万一传输失败,我们将可能无法追回任何数据—这种情况现在已经不会发生了。
结论
最后,我们证实了本文开头所说的:在arduino上处理图像是可能的,并且在某些情况下可能会更有优势。现在,我们可以使用串行相机拍摄照片,对其进行解码,通过串行端口发送,然后在另一端接收了!可以将本文作为您在arduino上进行图像处理的入门简介。
像往常一样,有很多方面都可以进一步改善。一个需要添加的主要功能可能是使用aes对我们的消息进行加密,这一点很容易实现(即使在arduino上)。在arduino上,安全性通常会被忽视,这是很危险的,因此在下一个项目中我们可能会将重点更多地放在安全性上。
感谢您阅读本文!请继续关注我们的其他有趣项目!也许有些项目将会使用到我们在本项目中所学到的所有内容!
历寒冬酷暑,仍逐光而行!
超40万吨产能落地 电池回收项目迎来密集扩产
正弦波振荡电路图
想入手移动固态硬盘?它的优缺点一定要知道
研华工控机应用领域分享
Arduino上的JPEG解码教程
华为Mate 60 Pro卫星通话功能仅支持电信卡,卫星通话功能设置教程
利用DSP56F805 的PWM模块输出高频正弦波设计方案
8月增21.7%!海尔智家厨电“卖场景”持续增长
仔细研究欧盟 ADS 立法草案中的合规评估
电容器的DF值计算方法
小米手机调整 MIUI 12 等开发版内测公测发布时间
!!销售收购! R3765CH 网络分析仪 R3765CH
ST推出全新麦克风接口芯片 提升音质性能
峰值检测器在硬件与软件的设计上如何权衡
Qualcomm XR企业计划成员已在发展元年颠覆多个行业
小米MIUI9发布会在即:新功能汇总,MIUI9开始内测,第二批适配机型曝光,内部代号:闪电!
怎样设置麦克风支架
SAM-PT:点几下鼠标,视频目标就分割出来了!
KRYTAR 2功率分配器的适用范围