Arduino上的JPEG解碼教學– DevicePlus
Maybe your like

大多數人聽到“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 <Adafruit_VC0706.h> #include <SPI.h> #include <SD.h> // Define Slave Select pin #define SD_CS 53 // Create an instance of Adafruit_VC0706 class // We will use Serial1 for communication with the camera Adafruit_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 < 100; i++) { filename[5] = '0' + i/10; filename[6] = '0' + i%10; if(!SD.exists(filename)) { break; } } // Create a file with the name we created above and open it File imgFile = SD.open(filename, FILE_WRITE); // Get the size of the image uint16_t jpglen = cam.frameLength(); Serial.print("Writing "); Serial.print(jpglen, DEC); Serial.print(" bytes into "); Serial.print(filename); Serial.print(" ... "); // Read all the image data while(jpglen > 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); }| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485 | // Include all the libraries#include <Adafruit_VC0706.h>#include <SPI.h>#include <SD.h> // 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); voidsetup(){// Begin Serial port for communication with PCSerial.begin(115200); // Start the SDif(!SD.begin(SD_CS)){// If the SD can't be started, loop foreverSerial.println("SD failed or not present!");while(1);} // Start the cameraif(!cam.begin()){// If the camera can't be started, loop foreverSerial.println("Camera failed or not present!");while(1);} // Set the image size to 640x480cam.setImageSize(VC0706_640x480);} voidloop(){Serial.print("Taking picture in 3 seconds ... ");delay(3000);// Take a pictureif(cam.takePicture()){Serial.println("done!");}else{Serial.println("failed!");} // Create a name for the new file in the format IMAGExy.JPGcharfilename[13];strcpy(filename,"IMAGE00.JPG");for(inti=0;i<100;i++){filename[5]='0'+i/10;filename[6]='0'+i%10;if(!SD.exists(filename)){break;}} // Create a file with the name we created above and open itFile imgFile=SD.open(filename,FILE_WRITE); // Get the size of the imageuint16_t jpglen=cam.frameLength();Serial.print("Writing ");Serial.print(jpglen,DEC);Serial.print(" bytes into ");Serial.print(filename);Serial.print(" ... "); // Read all the image datawhile(jpglen>0){// Load the JPEG-encoded image data from the camera into a bufferuint8_t *buff;uint8_t bytesToRead=min(32,jpglen);buff=cam.readPicture(bytesToRead); // Write the image data to the fileimgFile.write(buff,bytesToRead);jpglen-=bytesToRead;} // Safely close the fileimgFile.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 <JPEGDecoder.h> // Define Slave Select pin #define SD_CS 53 void 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<240; j++) { Serial.write(dataBuff[j]); } // Pointer to the current pixel uint16_t *pImg; // Color of the current pixel uint16_t color; // Create a data packet with the actual pixel colors strcpy(dataBuff, "$ITDAT"); uint8_t i = 6; // Repeat for all MCUs in the image while(JpegDec.read()) { // Save pointer the current pixel pImg = JpegDec.pImage; // Get the coordinates of the MCU we are currently processing int mcuXCoord = JpegDec.MCUx; int mcuYCoord = JpegDec.MCUy; // Get the number of pixels in the current MCU uint32_t mcuPixels = JpegDec.MCUWidth * JpegDec.MCUHeight; // Repeat for all pixels in the current MCU while(mcuPixels--) { // Read the color of the pixel as 16-bit integer color = *pImg++; // Split it into two 8-bit integers dataBuff[i] = color >> 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<i; j++) { Serial.write(dataBuff[j]); } // Fill the rest of the packet with zeros for(int k=i; k<240; k++) { Serial.write(0); } } } } } // Safely close the root directory root.close(); } // Function to fill the packet buffer with zeros void initBuff(char* buff) { for(int i = 0; i < 240; i++) { buff[i] = 0; } } void loop() { // Nothing here // We don't need to send the same images over and over again }| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 | // Include the library#include <JPEGDecoder.h> // Define Slave Select pin#define SD_CS 53 voidsetup(){// Set pin 13 to output, otherwise SPI might hangpinMode(13,OUTPUT); // Begin Serial port for communication with PCSerial.begin(115200); // Start the SDif(!SD.begin(SD_CS)){// If the SD can't be started, loop foreverSerial.println("SD failed or not present!");while(1);} // Open the root directoryFile root=SD.open("/");// Wait for the PC to signalwhile(!Serial.available()); // Send all files on the SD cardwhile(true){// Open the next fileFile jpgFile=root.openNextFile(); // We have sent all filesif(!jpgFile){break;} // Decode the JPEG fileJpegDec.decodeSdFile(jpgFile); // Create a buffer for the packetchardataBuff[240]; // Fill the buffer with zerosinitBuff(dataBuff); // Create a header packet with info about the imageStringheader="$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 packetfor(intj=0;j<240;j++){Serial.write(dataBuff[j]);} // Pointer to the current pixeluint16_t *pImg; // Color of the current pixeluint16_t color; // Create a data packet with the actual pixel colorsstrcpy(dataBuff,"$ITDAT");uint8_ti=6; // Repeat for all MCUs in the imagewhile(JpegDec.read()){// Save pointer the current pixelpImg=JpegDec.pImage; // Get the coordinates of the MCU we are currently processingintmcuXCoord=JpegDec.MCUx;intmcuYCoord=JpegDec.MCUy; // Get the number of pixels in the current MCUuint32_t mcuPixels=JpegDec.MCUWidth *JpegDec.MCUHeight; // Repeat for all pixels in the current MCUwhile(mcuPixels--){// Read the color of the pixel as 16-bit integercolor=*pImg++;// Split it into two 8-bit integersdataBuff[i]=color>>8;dataBuff[i+1]=color;i+=2; // If the packet is full, send itif(i==240){for(intj=0;j<240;j++){Serial.write(dataBuff[j]);}i=6;} // If we reach the end of the image, send a packetif((mcuXCoord==JpegDec.MCUSPerRow-1)&&(mcuYCoord==JpegDec.MCUSPerCol-1)&&(mcuPixels==1)){// Send the pixel valuesfor(intj=0;j<i;j++){Serial.write(dataBuff[j]);}// Fill the rest of the packet with zerosfor(intk=i;k<240;k++){Serial.write(0);}}}}} // Safely close the root directoryroot.close();} // Function to fill the packet buffer with zerosvoidinitBuff(char*buff){for(inti=0;i<240;i++){buff[i]=0;}} voidloop(){// Nothing here// We don't need to send the same images over and over again} |
注釋中已經對大多數代碼進行了解釋,但是我還是需要對代碼結構中的“包”進行一些說明。為了使資料傳輸更加有序,所有內容都以包的形式傳輸,最大長度為240位元組。包有兩種可能的類型:
- 頭包:此包以字串“$ITHDR”開頭,並且包含我們將要發送的圖片的基本資訊:以圖元為單位的高度和寬度,行和列前的MCU數量,最後是原始檔案名。對於我們要發送的每個圖像,都會相應發送一個頭包。
- 數據包:該包以“$ITDAT”開頭,並包含所有顏色資料。該資料包中的每兩個位元組代表一個16位元圖元值。
乍一看,包的長度似乎是隨機的。但是為什麼恰好是240個位元組?為什麼不是256個,使我們可以在每個包中發送兩個MCU呢?這是另一個我們日後將會解決的謎團,但是我們可以保證, 數字240不會有任何隨機性。這裡有個小提示:如果包中有256個位元組的資料,我們要在哪裡存儲源位址和目標位址呢?
現在,我們有了一個可以解碼和發送圖片檔的代碼,但是仍然缺少一個核心功能:目前為止,並沒有可以回應這些資料的另一埠。這意味著是時候再次啟用Processing了!
接收圖片
我在Arduino六足機器人第三部分:遠程控制 中曾介紹過一些有關Processing的內容,用其編寫了一個應用程式,通過該應用程式我們能夠輕鬆控制六足機器人。簡單回顧一下:Processing是一種基於Java的語言,主要用於繪圖工作。因此它非常適用於我們現在要做的圖元顯示的工作!該程式就是用Processing實現的。
// Import the library import 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 input String trimmed; // Buffer to save data incoming from Serial port byte[] byteBuffer = new byte[240]; // The coordinate variables int x, y, mcuX, mcuY; // A variable to measure how long it takes to receive the image long startTime; // A variable to save the current time long currentTime; // Flag to signal end of transmission boolean received = false; // Flag to signal reception of header packet boolean headerRead = false; // The color of the current pixel int inColor, r, g, b; // Image information variables int jpegWidth, jpegHeight, jpegMCUSPerRow, jpegMCUSPerCol, mcuWidth, mcuHeight, mcuPixels; // This function will be called every time any key is pressed void keyPressed() { // Send something to Arduino to signal the start port.write('s'); } // This function will be called every time the Serial port receives 240 bytes void 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) << 8) | (byteBuffer[i+1] & 0xFF); // Convert 16-bit color into RGB values r = ((inColor & 0xF800) >> 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(); } }| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 | // Import the libraryimport processing.serial.*; Serial port; voidsetup(){// Set the default window size to 200 by 200 pixelssize(200,200);// Set the background to greybackground(#888888);// Set as high framerate as we canframeRate(1000000);// Start the COM port communication// You will have to replace "COM30" with the Arduino COM port numberport=newSerial(this,"COM30",115200);// Read 240 bytes at a timeport.buffer(240);} // String to save the trimmed inputStringtrimmed; // Buffer to save data incoming from Serial portbyte[]byteBuffer=newbyte[240]; // The coordinate variablesintx,y,mcuX,mcuY; // A variable to measure how long it takes to receive the imagelongstartTime; // A variable to save the current timelongcurrentTime; // Flag to signal end of transmissionbooleanreceived=false; // Flag to signal reception of header packetbooleanheaderRead=false; // The color of the current pixelintinColor,r,g,b; // Image information variablesintjpegWidth,jpegHeight,jpegMCUSPerRow,jpegMCUSPerCol,mcuWidth,mcuHeight,mcuPixels; // This function will be called every time any key is pressedvoidkeyPressed(){// Send something to Arduino to signal the startport.write('s');} // This function will be called every time the Serial port receives 240 bytesvoidserialEvent(Serial port){// Read the data into bufferport.readBytes(byteBuffer);// Make a String out of the bufferStringinString=newString(byteBuffer);// Detect the packet typeif(inString.indexOf("$ITHDR")==0){// Header packet// Remove all whitespace characterstrimmed=inString.trim();// Split the header by commaString[]list=split(trimmed,',');// Check for completenessif(list.length!=7){println("Incomplete header, terminated");while(true);}else{// Parse the image informationjpegWidth=Integer.parseInt(list[1]);jpegHeight=Integer.parseInt(list[2]);jpegMCUSPerRow=Integer.parseInt(list[3]);jpegMCUSPerCol=Integer.parseInt(list[4]);// Print the info to consoleprintln("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 timerstartTime=millis();}// Set the window size according to the received informationsurface.setSize(jpegWidth,jpegHeight);// Get the MCU informationmcuWidth=jpegWidth/jpegMCUSPerRow;mcuHeight=jpegHeight/jpegMCUSPerCol;mcuPixels=mcuWidth *mcuHeight;}elseif(inString.indexOf("$ITDAT")==0){// Data packet// Repeat for every two bytes receivedfor(inti=6;i<240;i+=2){// Combine two 8-bit values into a single 16-bit colorinColor=((byteBuffer[i]&0xFF)<<8)|(byteBuffer[i+1]&0xFF);// Convert 16-bit color into RGB valuesr=((inColor&0xF800)>>11)*8;g=((inColor&0x07E0)>>5)*4;b=((inColor&0x001F)>>0)*8;// Paint the current pixel with that colorset(x+mcuWidth*mcuX,y+mcuHeight*mcuY,color(r,g,b));// Move onto the next pixelx++;if(x==mcuWidth){// MCU row is complete, move onto the next onex=0;y++;}if(y==mcuHeight){// MCU is complete, move onto the next onex=0;y=0;mcuX++;}if(mcuX==jpegMCUSPerRow){// Line of MCUs is complete, move onto the next onex=0;y=0;mcuX=0;mcuY++;}if(mcuY==jpegMCUSPerCol){// The entire image is completereceived=true;}}}} voiddraw(){// If we received a full image, start the whole process againif(received){// Reset coordinatesx=0;y=0;mcuX=0;mcuY=0;// Reset the flagreceived=false;// Measure how long the whole thing tooklongtimeTook=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上,安全性通常會被忽視,這是很危險的,因此在下一個項目中我們可能會將重點更多地放在安全性上。
感謝您閱讀本文!請繼續關注我們的其他有趣項目!也許有些項目將會使用到我們在本項目中所學到的所有內容!
Tag » Arduino Jpegdecoder.h
-
JPEGDecoder - Arduino Reference
-
Bodmer/JPEGDecoder: A JPEG Decoder Library - GitHub
-
JPEGDecoder/JPEGDecoder.h At Master · Bodmer/JPEGDecoder
-
JPEGDecoder - Arduino Library List
-
JPEG Decoding On Arduino Tutorial - Device Plus
-
JPEGDEC Is A Faster JPEG Arduino Library Designed For 32-bit MCUs
-
ESP32 - JPEGDecoder From SPIFFS - Arduino Stack Exchange
-
TensorFlow Lite – Person Detection - Realtek Ameba
-
ESP32 - Store RGB From Decoded JPEG MCUs In Buffer
-
Jpeg Decoding - Spark Logic
-
JPEGDecoder.h哪里有,谢谢-Arduino中文社区- 手机版
-
Jpegdec
-
Running TensorFlow Lite "Person Detection" On RTL8722DM-MINI
-
《Arduino》开发TFT_eSPI-master 库之用ESP32 读取SD卡上的图片 ...