李忠明,余梓唐,黃秀常
(義烏工商職業(yè)技術(shù)學(xué)院 機(jī)電信息學(xué)院,浙江 義烏 322000)
圖像的瀏覽通常是通過本地專門的圖像瀏覽軟件實現(xiàn)的。但對于超大圖像,如超高清晰度的醫(yī)學(xué)切片圖像,圖像文件本身的空間體積往往達(dá)到數(shù)百甚至數(shù)千MB,采用本地圖像瀏覽軟件瀏覽此類圖像,會導(dǎo)致圖像加載、縮放顯示速度變的極慢,甚至發(fā)生系統(tǒng)崩潰。同時,在多數(shù)情況下,這些超大圖像往往是商業(yè)化的,不能下載到本地進(jìn)行瀏覽,只能通過web應(yīng)用在線瀏覽。
越來越多的web應(yīng)用涉及超大圖像的在線瀏覽問題,針對不同應(yīng)用場景下的超大圖像在線瀏覽問題,目前有各種解決方案。一種基于四叉樹虛擬顯示的超大容量圖像的瀏覽方法,[1]該方法采用金字塔數(shù)據(jù)結(jié)構(gòu)布局和四叉樹索引,實現(xiàn)了全切片數(shù)字病理掃描圖像的平移、縮放等操作;一種通過網(wǎng)絡(luò)即時瀏覽超大圖像的方法,[2]該方法將超大圖像切割成若干小塊圖像上傳到服務(wù)器端,用戶通過客戶端瀏覽圖像某個區(qū)域,從圖像存儲服務(wù)器獲取組成該區(qū)域的小塊圖像數(shù)據(jù),實現(xiàn)網(wǎng)絡(luò)即時瀏覽超大圖像;一種使用WebGL技術(shù)的超大圖像的切割加載顯示方法,[3]該方法將超大圖像切割后轉(zhuǎn)換為M個WebGL Texture對象,并分別顯示在M個對應(yīng)的顯示區(qū)域,實現(xiàn)了超大圖像在顯示終端的快速加載、縮略圖顯示和局部放大。針對圖像數(shù)據(jù)的實時傳輸問題,有學(xué)者從2D、3D圖像的繪制與動態(tài)效果實現(xiàn)方面對Canvas、Web?GL技術(shù)進(jìn)行了研究實踐,并提出采用WebSocket技術(shù)解決圖像數(shù)據(jù)的實時傳輸問題。[4]以上方法都需要針對特定的圖像對象進(jìn)行像素級別的大量基礎(chǔ)性編程,工作量非常大,不適合一般的web應(yīng)用場景。
本文提出一種基于PHP GD2和HTML5 Canvas的超大圖像處理與顯示方法,利用PHP GD2實現(xiàn)超大圖像的切割并構(gòu)建不同分辨率的圖像金字塔,利用HTML5 Canvas加載、拼接、顯示不同層級圖像塊,利用PHP和JS編程實現(xiàn)超大圖像的移動和縮放,超大圖像的處理和在線瀏覽無須像素級編程。
圖像金字塔用于圖像編碼和漸進(jìn)式圖像傳輸,是一系列分辨率逐漸降低的圖像集合,如圖1所示,其底層是圖像的高分辨率表示,對應(yīng)原始圖像,頂層是圖像的低分辨率的近似表示,對應(yīng)圖像縮略圖。在超大圖像在線瀏覽應(yīng)用場景下,采樣比例擬設(shè)計為4比1,既可以保證采樣時方便計算,也可以方便上下層之間建立父子關(guān)系;分塊大小取2的冪次方,具體可依據(jù)顯示終端操作系統(tǒng)數(shù)據(jù)存儲格式、內(nèi)存管理最小單位以及終端顯示區(qū)域大小來確定,一般采用64×64、128×128、256×256、512×512等正方形分塊。
圖1 圖像金字塔
GD2是PHP的圖像擴(kuò)展庫,包含了大量圖像處理 API,[5]目前支持GIF、JPEG、PNG、XBM、XPM、WBMP、WebP、BMP等格式的圖像文件。GD2的Imagecopyresampled函數(shù)通過重新采樣可以將源圖像src_image中左上角坐標(biāo)為(src_x,src_y)的src_w×src_h區(qū)域拷貝到目標(biāo)圖像dst_image的(dst_x,dst_y)開始的dst_w×dst_h區(qū)域,實現(xiàn)圖像的切割:
Imagecopyresampled(dst_image,src_image,dst_x,dst_y,src_x,src_y,dst_w,dst_h,src_w,src_h)
在超大圖像在線瀏覽應(yīng)用場景下,dst_x=dst_y=0,dst_w=dst_h∈ (64,128,256,512)都取為固定值;切割第0層圖像時,src_w=dst_w,src_h=dst_h,以(src_x,src_y)為左上角坐標(biāo)、src_w×src_h為正方形切塊大小,遍歷整幅源圖像即可得到全部切塊。對于任意第k層圖像的切割,src_w=2k×dst_w,src_h=2k×dst_h,通過與第0層切割相同的遍歷方法得到第k層全部切塊;或者利用Image?copyresampled函數(shù)將源圖像按縮放比1/2k先縮小為第k層縮略大圖,再將第k層縮略大圖當(dāng)作第0層圖像切割,保持各層挖取區(qū)域大小始終相同。兩種算法實現(xiàn)的切塊圖像質(zhì)量相同。
給定超大圖像fileImage、客戶端顯示區(qū)域?qū)挕粮?wCanvas×hCanvas和切塊寬×高=wCut×hCut,圖像金字塔各層級每行切塊數(shù)量m可通過wImage/wCut計算獲得,每列切塊數(shù)量n可通過hImage/hCut計算獲得,其中wImage和hImage分別是圖像fileImage的寬和高,可通過GD2的getimagesize函數(shù)獲取。實際應(yīng)用中,并不能保證wImage/wCut和hImage/hCut的計算結(jié)果為整數(shù),應(yīng)向上取整,且每行最后一個切塊(第m列切塊)和每列最后一個切塊(第n行切塊)一般都不足給定的切塊面積大小,其實際切塊大小應(yīng)取為剩余大小,即wCuti,m=wCanvas-xCuti,m,hCutn,j=hCanvas-yCutn,j,其中i∈[0,n],j∈ [0,m],xCuti,m是第m列切塊左上角x坐標(biāo),yCutn,j是第n行切塊左上角y坐標(biāo)。
圖像金字塔最大層級數(shù)max依據(jù)客戶端顯示區(qū)域的大小確定,原則是保證該層級縮略大圖(該層級所有切塊拼接的圖像)剛好能全景顯示于顯示區(qū)域。具體算法是:在從第0層向上遍歷金字塔層級過程中,第一個滿足以下條件的層級數(shù)即為金字塔最大層級數(shù):
以jpg圖像格式為例,超大圖像的切割和金字塔構(gòu)建基本算法表示如下:
(1)讀入超大圖像fileImage到image對象
image=imagecreatefromjpeg(fileImage);
(2)獲取圖像寬wImage、高h(yuǎn)Image和mime類型
list(wImage,hImage,mime)=getimagesize(fileIm?age);
(3)遍歷金字塔全部層級
for(k=0,wThumb >=wCanvas||hThumb>=hCan?vas,k++){
wThumb=wImage/2k;//第k層縮略圖寬
hThumb=hImage/2k;//第k層縮略圖高
thumb=imagecreatetruecolor(thumb_width,thumb_height);//第k層縮略圖
imagecopyresampled(thumb,image,0,0,0,0,wThumb,hThumb,wThumb,hThumb);
轉(zhuǎn)第(4)步執(zhí)行切割分塊;}
(4)遍歷所有切塊(xCut和yCut為切塊左上角坐標(biāo))
for(xCut=0,xCut<wThumb,xCut+=wCut){
for(yCut=0,yCut<hThumb,yCut+=hCut){
w=(wThumb-xCut<wCut)?wThumb-xCut:wCut;//切塊寬
h=(hThumb-yCut<hCut)?hThumb–yCut:hCut;//切塊高
img=imagecreatetruecolor(w,h);//實施切塊
imagecopyresampled(img,thumb,0,0,xCut,yCut,w,h,w,h);
fileImg=k_xCut_yCut.jpg;//切塊保存為文件并輸出
header("Content-type:mime");
imagejpeg($newimg,$dstimg);}}
圖像金字塔各切塊圖像文件以k_xCut_yCut形式命名,k表示金字塔層級,xCut和yCut表示該切塊在縮略大圖中的位置坐標(biāo)。實際開發(fā)中,圖像金字塔數(shù)據(jù)同時保存在數(shù)據(jù)庫中,至少包括切塊的層級、大小、位置坐標(biāo)、文件路徑等。
客戶端顯示區(qū)域需要進(jìn)行圖像的平移和縮放操作時,可以直接利用PHP的文件系統(tǒng)函數(shù)獲得該圖像金字塔所有切塊的層級及位置信息,通過這些信息實現(xiàn)必要切塊的識別、查找、加載和拼接。還可以將圖像金字塔信息寫入數(shù)據(jù)庫,寫入的信息至少包括切塊的層級、大小、在縮略大圖中的位置、磁盤路徑等,需要時從數(shù)據(jù)庫中讀出,實現(xiàn)圖像塊更快速識別、查找、加載和拼接,以保證超大圖像在線瀏覽的流暢性,增強(qiáng)用戶體驗。
Canvas是HTML5的畫布,包含大量圖形圖像處理API,配合以JS來繪制2D圖形圖像并逐像素渲染,實現(xiàn)圖像塊的動態(tài)加載和顯示。Canvas對象支持腳本化客戶端繪圖操作,使用其getContext方法可以創(chuàng)建CanvasRenderingContext2D對象(CTX),通過CTX就可以實現(xiàn)對Canvas對象的像素數(shù)據(jù)操縱。CTX對象的drawImage方法可以將圖像切塊寫入Canvas對象,實現(xiàn)切塊圖像的顯示;CTX對象的getImageData方法可以獲取Canvas畫布上已顯示的圖像數(shù)據(jù)對象ImageData,通過ImageData對象的putImageData方法,可以將圖像數(shù)據(jù)寫回Can?vas畫布。
HTML頁面上Canvas畫布構(gòu)成了客戶端顯示區(qū)域,圖像塊的加載拼接就是將服務(wù)器端的金字塔圖像切塊寫入到Canvas畫布的適當(dāng)位置,呈現(xiàn)出超大圖像的部分或者全部圖像以供用戶瀏覽?;舅惴ㄉ婕皠?chuàng)建畫布Canvas對象,創(chuàng)建CanvasRendering?Context2D對象ctx,創(chuàng)建ImageData對象Image,加載圖像切塊ImageDataURL,將圖像切塊寫入Can?vas畫布,實現(xiàn)圖像塊的加載和拼接:
canvas=$("#canvas");//創(chuàng)建Canvas對象
ctx=canvas.getContext("2d");//創(chuàng)建ctx對象
Image=new Image();//創(chuàng)建圖像對象
Image.src=ImageDataURL;//加載圖像切塊
Image.onload=function(){ //寫入圖像切塊
ctx.drawImage(Image,x,y,w,h);}
其中(x,y)為圖像塊拼接點位置(圖像塊左上角在Canvas畫布上的坐標(biāo)),w和h為圖像塊實際呈現(xiàn)的寬度和高度,可以取w=wCut,h=hCut,按1比1比例寫入,無須縮放。實際應(yīng)用中,為了增強(qiáng)用戶體驗,可以針對不同的超大圖像設(shè)定不同的縮放比 r,取 w=r×wCut,h=r×hCut,對同一圖像,r為常量。
圖像塊加載和拼接的核心是拼接點位置計算和拼接哪些圖像塊。只需確定位于Canvas畫布左上的第一個拼接點(x,y),其余各拼接點可通過遞增確定:x=x+w,y=y+h。在超大圖像在線瀏覽應(yīng)用場景下,初始加載拼接到顯示區(qū)域Canvas上的是金字塔最高層級切塊,該層級全部切塊都被加載且全部呈現(xiàn)于Canvas內(nèi)部,拼接后的圖像在Canvas中居中顯示,其第1個拼接點的算法如下:
隨著后續(xù)用戶的平移和縮放操作,第1個拼接點可能位于Canvas外部,實際位置需要疊加每次平移和縮放操作的影響,具體在平移和縮放算法中描述。需要說明的是,拼接到Canvas邊界附近的圖像塊會橫跨Canvas畫布的邊界,有一部分位于邊界之外,邊界外部分并不需要呈現(xiàn)。橫跨Canvas邊界的情形可以歸納為8種:橫跨左邊界,橫跨右邊界,橫跨上邊界,橫跨下邊界,同時橫跨左邊界和上邊界,同時橫跨上邊界和右邊界,同時橫跨右邊界和下邊界,同時橫跨下邊界和左邊界。傳統(tǒng)的處理方法是裁切掉圖像塊位于邊界外的部分,重新計算拼接點,這種方法增加了編程的復(fù)雜性并影響拼接速度。更好的解決方案是只對橫跨左邊界和上邊界的圖像塊,將其拼接點置于顯示區(qū)域外部,其余橫跨邊界的情形無需任何處理,Canvas會自動裁切掉位于顯示區(qū)域外的圖像。
需要加載拼接的圖像塊總是對應(yīng)圖像金字塔的某一層級,放大操作對應(yīng)當(dāng)前層級減1,直到第0層級Level0;縮小操作對應(yīng)當(dāng)前層級增1,直到最高層級LevelMax;平移操作時層級不變,保持為當(dāng)前層級。由于Canvas中初呈現(xiàn)的是超大圖像的全景縮略圖,對應(yīng)圖像金字塔最高層級LevelMax,需要加載該層級的全部圖像塊。隨著用戶不斷的平移和縮放動作,所需加載拼接的圖像塊需要重新計算,具體在平移和縮放操作中描述。
基本思想是利用CTX的getImageData方法先獲取當(dāng)前Canvas畫布上的圖像對象ImageData,利用ImageData對象的putImageData方法,或者CTX的drawImage方法,將ImageData重新寫入Canvas畫布;Canvas畫布上空缺部分調(diào)用圖像加載拼接程序予以補(bǔ)足。
平移算法。主要包括獲取Canvas對象,創(chuàng)建CanvasRenderingContext2D對象CTX、創(chuàng)建Image?Data對象、將ImageData重新寫回Canvas畫布。重新寫入時的水平偏移量dx、垂直偏移量dy取用戶平移操作動作的實際偏移值。算法列示如下:
canvas=$("#canvas");
ctx=canvas.getContext("2d");
ImageData= ctx.getImageData(0,0,wCanvas,hCanvas);
ctx.putImageData(ImageData,dx,dy);
假定平移前Canvas第1個拼接點為(x,y),對應(yīng)的金字塔切塊為k_xCut_yCut,平移偏移量為dx和dy,則平移后,第1個拼接點x=x-ceil((x+dx)/w)*w,y=y-ceil((y+dy)/h)*h,對應(yīng)的第1個金字塔切塊為 k=k,xCut=xCut-ceil((x+dx)/wCut)*wCut,yCut=yCut-ceil((y+dy)/hCut)*hCut。
縮小算法。與圖像金字塔構(gòu)建方法相匹配,采用中心縮放方法,按縮放比2比1縮小,ImageData取Canvas上全部圖像數(shù)據(jù);重新寫入ImageData時保持寫入的圖像在Canvas中居中顯示。算法列示如下:
canvas=$("#canvas");
ctx=canvas.getContext("2d");
ImageData= ctx.getImageData(0,0,wCanvas,hCanvas);
ctx.drawImage(wCanvas/4,hCanvas/4,wCanvas/2,hCanvas/2);
ctx.drawImage(wCanvas/4,hCanvas/4,wCanvas/2,hCanvas/2);
放大算法。與圖像金字塔構(gòu)建方法相匹配,仍然采用中心縮放法,按縮放比1比2放大,以Canvas中心點為中心,ImageData取Canvas中寬×高=wCan?vas/2×hCanvas/2的矩形區(qū)域圖像數(shù)據(jù),其左上角坐標(biāo)為(wCanvas/4,hCanvas/2)。算法列示如下:
canvas=$("#canvas");
ctx=canvas.getContext("2d");
ImageData=ctx.getImageData(wCanvas/4,hCanvas/4,(wCanvas/2,hCanvas/2);
ctx.drawImage(0,0,wCanvas,hCanvas);
縮放(縮小或放大)后,仍然需要調(diào)用圖像塊加載拼接程序完成局部圖像刷新,其第1個拼接點(x,y)以及對應(yīng)的金字塔切塊k_xCut_yCut的計算方法與平移時相類似。實際應(yīng)用中,獲取Canvas畫布上當(dāng)前圖像數(shù)據(jù),還可利用Canvas對象的toData?URL方法實現(xiàn),其中MIME type是形如image/png格式的字符串。
function convertCanvasToImage(canvas){
DataURL=new Image();
DataURL.src=canvas.toDataURL(MIME type);
return DataURL;}
封裝函數(shù)中DataURL是對應(yīng)其MIME格式的一串Base64編碼的URL,可使用CTX的drawImage實現(xiàn)重寫;而ImageData中存儲的是Canvas對象的像素數(shù)據(jù),采用8位無符號整型固定數(shù)組Uint8C-lampedArray結(jié)構(gòu)。實際應(yīng)用中,可根據(jù)應(yīng)用場景的不同選擇使用。
采用本文所述方法,在PHP+MySQL+Apache環(huán)境下開發(fā)了超大圖像在線瀏覽系統(tǒng)。系統(tǒng)能自動對上傳的超大圖像進(jìn)行切割分塊、構(gòu)建圖像金字塔并將金字塔切塊信息保存在數(shù)據(jù)庫中。圖像的切割使用了PHP的GD2圖像擴(kuò)展,金字塔切塊的傳送采用AJAX技術(shù)實現(xiàn),客戶端圖像呈現(xiàn)使用了HTML5 Canvas畫布。最初在客戶端呈現(xiàn)的是超大圖像的全景縮略圖,用戶的任何平移和縮放操作,都會自動觸發(fā)圖像塊的重新加載和拼接。由于無須進(jìn)行像素級編程處理圖像,有效提高了開發(fā)效率。
為追求超大圖像在線瀏覽的平滑性,圖像金字塔需要在瀏覽請求前預(yù)先構(gòu)建。一幅超大圖像的金字塔文件集合所占用的空間體積,一般是源圖像的5-10倍。為減少服務(wù)器空間資源開銷,可適當(dāng)減少金字塔層級數(shù),也可以考慮在有瀏覽請求時實時構(gòu)建圖像金字塔,但客戶端會出現(xiàn)明顯的等待,影響用戶體驗。實際開發(fā)中,切塊大小最好采用2m×2m,m∈[6,9],具體根據(jù)源圖像大小和層級數(shù)多少進(jìn)行選擇,對同一幅圖像各層級切塊大小應(yīng)保持相同。第0層最右邊和最下邊切塊,不足一個切塊大小時,切塊實際大小仍然要遵循2的冪次方的原則,可降低冪次,按就近原則舍棄或者補(bǔ)足像素。