Arduino上的JPEG解碼教學

arduino jpeg

大多數人聽到“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卡中,然後發送到某個地方。那我們按照這個思路先從一個簡單的設置開始吧。

arduino jpeg

圖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示例。

 

在,Arduino將每10秒左右拍攝一張照片,直到SD卡上的空間用完為止。但是,由於照片通常約為48kB,並且我目前使用的是2GB的SD卡,因此足夠容納超過43000張的照片。理論上來說我們不需要那麼多的照片。但是既然已經拍攝了一些照片,我們現在可以繼續進行下一個有趣環節了:將它們從JPEG壓縮後的難以管理的雜亂資料變成簡單的圖元陣列!

 

解碼和發送照片

在開始解碼前,讓我們快速地看一下圖片資料在JPEG檔中究竟是如何存儲的。如果您對這部分不太感興趣,可以跳過下面三段內容。如果您確切地對圖形和壓縮方面的知識瞭解一二(不像我這樣),您也可以跳過這一部分。以下內容進行了一定程度的簡化。

對任何類型的圖片資料進行存儲時,有兩種基本方法:無損和失真壓縮。兩者的區別很明顯:當使用無失真壓縮(例如PNG)對圖像進行編碼時,處理之後圖像的每個圖元都與開始時完全相同。這非常適合於諸如電腦圖形學之類的工作,但是不幸的是,這是以增加檔大小為代價的。另一方面,對於像JPEG這樣的失真壓縮,我們丟失了一些細節,但是生成的檔大小要小得多。

JPEG壓縮方式在理解上可能會有點困難,因為會涉及到一些“離散余弦變換”,不過主要原理實際上是非常簡單的。首先,將圖片從RGB顏色空間轉換為YCbCr。我們都知道RGB顏色空間—它存儲了紅色(R)、綠色(G)和藍色(B)的顏色值。YCbCr有很大的不同—它使用亮度(Y—基本是原始圖像的灰度圖),藍色差分量(Cb—圖片中的“藍色”)和紅色差分量(Cr—圖片中的“紅色”)。

arduino jpeg

圖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重複該操作。

 

注釋中已經對大多數代碼進行了解釋,但是我還是需要對代碼結構中的“包”進行一些說明。為了使資料傳輸更加有序,所有內容都以包的形式傳輸,最大長度為240位元組。包有兩種可能的類型:

  1. 頭包:此包以字串“$ITHDR”開頭,並且包含我們將要發送的圖片的基本資訊:以圖元為單位的高度和寬度,行和列前的MCU數量,最後是原始檔案名。對於我們要發送的每個圖像,都會相應發送一個頭包。
  2. 數據包:該包以“$ITDAT”開頭,並包含所有顏色資料。該資料包中的每兩個位元組代表一個16位元圖元值。

乍一看,包的長度似乎是隨機的。但是為什麼恰好是240個位元組?為什麼不是256個,使我們可以在每個包中發送兩個MCU呢?這是另一個我們日後將會解決的謎團,但是我們可以保證, 數字240不會有任何隨機性。這裡有個小提示:如果包中有256個位元組的資料,我們要在哪裡存儲源位址和目標位址呢?

現在,我們有了一個可以解碼和發送圖片檔的代碼,但是仍然缺少一個核心功能:目前為止,並沒有可以回應這些資料的另一埠。這意味著是時候再次啟用Processing了!

 

接收圖片

我在Arduino六足機器人第三部分:遠程控制 中曾介紹過一些有關Processing的內容,用其編寫了一個應用程式,通過該應用程式我們能夠輕鬆控制六足機器人。簡單回顧一下:Processing是一種基於Java的語言,主要用於繪圖工作。因此它非常適用於我們現在要做的圖元顯示的工作!該程式就是用Processing實現的。

 

當您在連接Arduino之後運行該程式,然後按下鍵盤上的任意鍵時,您(希望)會看到暗淡、單一的灰色背景逐漸被最初存儲在SD卡上的圖像所取代。由於替換是逐圖元進行的,因此整個過程具有一種老式撥號數據機的載入圖像風格!

arduino jpeg

圖3:使用Processing應用程式將照片從Arduino載入到PC

雖然我們以相當高的串列傳輸速率(準確值為115200)運行序列埠,接收一張圖像也需要大約60秒。我們可以用它來計算實際的傳送速率。

原始圖像寬640圖元,高480圖元,總計307200圖元。每個圖元都由2位元組的顏色值來表示,總共要傳輸614400個位元組(即600KB)。那麼我們的最終速度約為10kB/s。對於我們制定的“協議”來說,這並不算很糟糕,不是嗎?此外,它還向您展示了為什麼圖像壓縮如此有用。原始JPEG檔只有48kB左右,而解碼後的點陣圖則需要600kB。如果我們要傳輸JPEG檔,即使使用非常簡單的“協議”,也可以在5秒之內完成傳輸。當然,萬一傳輸失敗,我們將可能無法追回任何資料—這種情況現在已經不會發生了。

 

結論

最後,我們證實了本文開頭所說的:在Arduino上處理圖像是可能的,並且在某些情況下可能會更有優勢。現在,我們可以使用串列相機拍攝照片,對其進行解碼,通過序列埠發送,然後在另一端接收了!可以將本文作為您在Arduino上進行影像處理的入門簡介。

像往常一樣,有很多方面都可以進一步改善。一個需要添加的主要功能可能是使用AES對我們的消息進行加密,這一點很容易實現(即使在Arduino上)。在Arduino上,安全性通常會被忽視,這是很危險的,因此在下一個項目中我們可能會將重點更多地放在安全性上。

感謝您閱讀本文!請繼續關注我們的其他有趣項目!也許有些項目將會使用到我們在本項目中所學到的所有內容!