Java關於超大圖片的處理,完整實現方案思路(.tiff.svs.mxrs等一系列格式)
前言
這篇文章其實早就想寫了,最近事情比較多,耽擱了,詳細的程式碼可能不會提供多少,但我會盡量把
開發的過程和思路寫清楚,其實整體邏輯很簡單,只是相關的資料和文件很少,太難找,我也會盡量
在這篇文章中把常用的API標出。
再囉嗦幾嘴
這是在上一家公司開發時遇到的需求,需要將"病理圖片"解析並且在瀏覽器上顯示,在我之前的技術
沒能解決,而我之前也沒做過類似的功能,公司只給了我一個連線,是XX公司實現後的效果,要求
做到這種,公司的老專案是個CS客戶端的,已經實現了相關的功能,但是是由python實現的,年代
比較久遠,甚至在職人員都不太清楚是怎麼實現的,而且有個比較大的問題就是,很卡,讀圖在頁面
上會比較卡,不流暢,BOSS不懂技術,以為是語言的問題,所以特別要求這次要用java在瀏覽器上
實現,實際上這是一個前端優化跟快取取用的問題,跟後臺用什麼語言實現沒什麼關係,不過既然
被這麼要求了,就只能硬著頭皮做了。
正文
在鋪乾貨之前,我先寫寫當時的思路吧,其實在看了DEMO連線後,大概就清楚了是一個怎麼樣的實現
方式,在頁面上展示的其實是無數256*256的小圖拼湊在一起的,初步的思路大概就是這麼幾步
1.後端用了某種方式把一張圖片切成了無數256*256的小圖儲存起來,可能是在硬碟也可能是在快取內。
2.前端通過單純的url地址展示圖片,並且運用瀏覽器快取進行一定範圍內的儲存,當移動選定區域時再重新載入。
3.此方式似乎無法實現點開即讀,後續瞭解了一些相似的讀圖軟體,均不能做到點開即讀。
基本的思路有了,剩下就算是有跡可循了,後端的實現方式到這裡其實就有個大概的想法了,甚至程式碼都已經隱約浮現在腦中了(其實完全不是我想的那樣)
重點是前端,前端這個邏輯太複雜,想寫出這樣的效果怕是得來個資深大神。
於是在尋覓了很久後,終於發現了一些蛛絲馬跡。
一套來自微軟的方法,或者說元件?
叫做DeepZoom
我理解的可能不是那麼到位,但要我總結的話
這大概是一種協議吧,或者說是一種方法,他描述的是”把一張大圖以何種方式切割“
這個方式就是每次縮放時,把邊長縮小一倍再切割,舉例的話
如果有一個邊長10000的正方形圖片切割,那麼第一次切割就是10000/256這種思路
當切割完所有面積後,將原圖縮放成邊長5000的正方形,再次切割5000/256
以此類推
然後每一個尺寸的圖都集中放進一個資料夾中,按照序號排序,如果說的通俗點,這個序號(資料夾)的個數
其實就是根據邊長可以對摺多少次來排序的。最終邊長就是1(當然也可以自己來定義這個最小邊長)
其實這套約定為什麼要這麼定義,我們不需要去關心,我們只需要知道該怎麼做,該怎麼切就可以了。
然後就引出了一套前端庫
OpenSeaGragon
這是一個由js實現的多層圖片處理庫
支援多種”協議“(比如DeepZoom)
用法很簡單,官網openseadragon
官網上有demo,有庫的原始碼包,雖然全英文的
另外百度上也有相應的用法教程,還算比較全,這裡就不囉嗦了。
這是一套完美支援deepzoom方式的前端庫,所有上面提到的前端操作他都完成了,只要呼叫就行。
那麼剩下的問題就是後端的切圖了。
關於後端的實現,也正是本文的重點
在解決了前端問題後,我按照相應的思路試著寫了一版切圖,原始碼不貼了(因為已經沒有了),說一下當時的實現思路。
無非就是利用BufferedImage來處理圖片。
迴圈切圖,記錄座標,縮放,生成新的圖片物件,再切圖,整體下來其實還是挺好寫的。
整個util寫下來沒用上300行程式碼。
測試-跑通,OK
大功告成。
當時是這麼以為的。
但好景不長,為了測試效能,換了幾張圖片,立馬就出了問題。
java.lang.NegativeArraySizeException
怎麼會出現這種東西。
隨後看了一下,過程不囉嗦了,原因就是
圖片的邊長太長了,總畫素超過了20e以上,BufferedImage原始碼在構造時有個DataBufferIntd的轉換步驟
而其中一個引數size的來源就是邊長h*w,並且是用int來接收的,長度直接超出了int的上限值,所以報錯了。
這就頭疼了,因為想要在java中處理圖片,無論如何都要用到BufferedImage,也就是說無解了。
後來一拍腦門,想到公司客戶端也實現了相應的功能啊,可以看看以前的程式碼。
一看更鬧心了,是python實現的,並且專案是不知道經手了多少個人,找不到實現的相關程式碼,也不知道用了什麼技術。
於是就用各種關鍵字在網上搜索相關的結果,大多都沒什麼用。
後來用.svs這個關鍵字搜尋時,找到了這麼一篇文章。
醫學病理圖片(SVS格式)的格式轉換與顯示——python實現
於是瞭解到了這麼兩種技術
1.openSlide
2.libvips
隨後的開發過程中,同事幫忙找到了原專案的實現程式碼,證實後發現是使用了libvips
但本次我選用的是openSlide
其實兩種技術都是差不多的原理,但支援的格式不太相同,我們的需求中有一種是.mrxs的格式,libvips並不支援。
瞭解到這些後其實就方便了,後續的就是如何使用了,後文的內容基本就是幫大家踩坑了。
OpenSlide的使用
OpenSlide提供多種語言的對接,其中對python的支援最為良好,目前國內可查到的資料中,沒有關於java的詳細解讀
OpenSlide-java在其官網中可以找到程式碼支援,地址為openslide.org
一、準備工作
1.windows下的環境搭建
在官網下載對應版本的windows支援包,解壓到磁碟中後配置好環境變數(需要把bin和lib兩個資料夾都配置進去)
之後確保本機的VC環境,需要最新的版本,本次開發所用到的版本為VC2019的版本
2.專案中的呼叫
下載好github上所提供的openSlide-java原始碼包 地址為https://github.com/openslide/openslide-java
專案中可以進行原始碼的自定義修改和開發,但此程式碼並不能直接複製到工程目錄內直接引用,需要額外打包,其已經給準備好了打包檔案
在專案中找到 build.xml 直接以ant執行即可打包(這裡不做詳解,自行百度),打包完成後的openslide.jar預設存放於專案磁碟資料夾主目錄下
新生成的jar包通過本地包引入的方式放到目標工程中去即可使用openSlide
3.linux下的搭建
https://openslide.org/download/下載最新的包,目前版本為3.4.1
同時需要下載OpenSlide Java interface(此處存在爭議,也許只需要下載這個java支援的包就可以)
根據自己伺服器的系統選擇安裝方式,官方有提供ubantu與centOS的安裝命令
之後把下載的兩個包上傳到伺服器自定義的目錄解壓,解壓後執行configure構建新包
無論是以何種方式和系統,此處構建時都會丟失大量包與檔案,需要逐個排查下載,並且需要使用阿里雲映象,原版映象中會找不到丟失的資源。
用以上方式按照循序分別先構建3.4.1再構建OpenSlide Java interface
構建成功後,將會生成兩個檔案,分別是libopenslide-jni.so與openslide.jar
(這裡標註一下,後生成的openSlide.jar與條目2中構建的jar包是否相同暫未確定,主要是為了生成libopenslide-jni.so)
生成後,在專案啟動命令中以 -Djava.library.path= 命令引入檔案所在的資料夾即可執行。
關於linux伺服器上的環境部署還有很多需要待確認的步驟,以上方式可以保證穩定執行,但未必是最精簡的方式。
4.API解讀
整個openslide的核心API其實只有OpenSlide物件的構建。
構建一個OpenSlide物件很簡單,只要傳入File即可
OpenSlide os = new OpenSlide(File);
構建出的os物件幾乎可以支援所有的病理圖片格式,已知可穩定支援的有.SVS .TIFF .TIFF .NDPI .MRXS
os.close();
關閉openslide物件的方法,構建完成os物件後如若不再使用,要及時關閉,否則一直佔用記憶體。
os.createThumbnailImage(maxSize);
os.createThumbnailImage(x, y, w, h, maxSize);
os.createThumbnailImage(x, y, w, h, maxSize, bufferedImageType);
建立一張縮圖的方法,返回物件為bufferedImage 會有畫素限制(等同bufferImage最大畫素)
maxSize引數為最大尺寸 這個尺寸可以是圖片的寬也可以是圖片的高。
x與y是縮圖起始座標,如果想獲得一張完整原圖的縮圖,可以輸入0,0
w,h則為所希望擷取原圖範圍的寬高,如果希望獲得一張完整原圖的縮圖可以輸入原圖的寬高。
這裡再詳細解答一點。此w跟h是根據maxSize隨動的
舉個例子
比如原圖邊長為1024的正方形圖片
我們希望獲取邊長為512的整圖縮圖時,輸入的引數應該是 (0,0,1024,1024,512)
若我們希望獲取一個寬度為512但是高度為256(原圖一半+一倍縮放)這種圖片時,那輸入的引數則變為(0,0,1024,512,512)
是否看出不同了?
沒錯,就是這個h,這個h是根據maxSize來變化的,我們可以視為 h=(希望所得邊長)*(原圖邊長/maxSize)
w也同理。
bufferedImageType即為bufferedImage。getType獲取的值,一般傳入1即可,有興趣的可以自行百度。
因為createThumbnailImage返回的是一個bufferedImageType物件,所以我們可以根據這個方法來制定如何切圖,比如先切一部分,再對這部分進行單獨切圖處理。本次功能的完成就重點運用了這個函式。
createThumbnailImage方法的底層同樣運用了ImageIO相關來實現,所以仍然會存在畫素上限的問題,原始碼中其實對於不同尺寸的縮放是採用了不同層級來獲取的,但如果目標圖片只有原尺寸1層,那麼此方法將無法處理。
並且目前來看,這類圖片整個openSlide的相關支援也無法處理(比如你希望從一個邊長102400*102400的原圖獲得一張256*256的全圖鳥瞰,如果層級為0,將無法獲得,原因是輸入maxSize後,其實現方法是按照比例獲取縮放倍數,再得出縮放層級然後取出尺寸較小的圖片,當層級為0時,只能取出原圖)
os.dispose();
關閉連線並且釋放資源。
os.getAssociatedImages();
獲取附加圖片,返回物件為mMap<String, AssociatedImage>,AssociatedImage物件中有一個 .toBufferedImage()的方法用來得到一個bufferedImage物件,對此圖片進行重新渲染可得到條形碼照片(若不重新渲染則會失真)
此方法複用性較高,這裡給出一段獲取附帶圖片的程式碼,可以直接拿著用,如果返回是空,那就是沒有附帶圖片。
/**
* 獲取影象中的附帶影象
* @param inFile 原始圖片
* @param outPut 輸出資料夾
* @return 圖片所在地址
*/
public static String getOrderImg(File inFile,File outPut) {
String fileName = inFile.getName();
String nameWithoutExtension = fileName.substring(0, fileName.lastIndexOf('.'));
OpenSlide os;
try {
os = new OpenSlide(inFile);
Map<String, AssociatedImage> map = os.getAssociatedImages();
if(map.keySet().size()>0) {
AssociatedImage aimg = map.get("label");
BufferedImage ar =aimg.toBufferedImage();
BufferedImage result = new BufferedImage(ar.getWidth(), ar.getHeight(), 1);
Graphics2D g = result.createGraphics();
g.drawImage(ar, 0, 0, null);
os.close();
ImageIO.write(result, "jpg", new File(outPut +File.separator+nameWithoutExtension+ "_others" + ".jpg"));
return (nameWithoutExtension+ "_others" + ".jpg");
}else {
return "";
}
} catch (IOException e) {
System.out.println("######建立OS物件失敗");
e.printStackTrace();
return "";
}
}
os.getBestLevelForDownsample(downsample);
根據輸入的縮放倍數(double且不可為0)獲取到最佳縮放層級,返回的是int
os.getLevel0Height();
os.getLevel0Width();
獲取圖片原尺寸的高與寬,也是層級=0時的尺寸
os.getLevelCount();
獲取圖片的層級,即一共有多少層,返回int
os.getLevelDownsample(level);
根據輸入的層級得到相應的縮放倍數,返回double
os.getLevelHeight(level);
os.getLevelWidth(level);
根據輸入的層級得到相應層級的高與寬
os.getProperties();
獲取圖片自帶的描述資訊,返回map<String,String>
os.paintRegion(g, dx, dy, sx, sy, w, h, downsample);
os.paintRegionARGB(dest, x, y, level, w, h);
os.paintRegionOfLevel(g, dx, dy, sx, sy, w, h, level);
繪圖相關的三個方法。
其實原始碼中最終都是由paintRegionARGB這個方法來實現的,只是輸入的引數不同用以達成不同的效果。
首先是g這個引數,其實就是Graphics2D
dx跟dy則是所需要切割的一個座標點,他根據sx,sy來確認一個方形區域用以切割,w跟h自然就是切割的目標寬高。
而downsample跟level不難理解了,前者是縮放倍數,後者是縮放層級。
我們可以通過這樣一段程式碼來獲取到原圖中一塊區域
包括createThumbnailImage這個方法其實也是由這個來實現的。
其實我還想重點講一下paintRegionARGB這個方法的引數使用,比較繞,並不是常規的理解。
簡單來說就是傳入怎樣的level,也一樣要傳入相對的xy和相對的wh,舉例就是如果我想在h尺度上以座標0,0 0,256兩點起始,橫著擷取寬為w等長的一條
在原圖次存下各個引數都很好理解,分別是
x=0;y=0;level=0;w=w(原始寬);h=256;
但當我想縮放一倍後,那麼傳入的引數
x=0;y=0;leve=1(假設原圖提供的第二層1正好就是縮放了一倍);w=w;h=512;
沒錯,同一條的話,邊長傳入的h放大了一倍,為什麼是放大了一倍實話說我在原始碼中沒太屢清楚,寫法問題吧,這個原始碼其實是可以改的。
只是改過了之後需要自己再打包,我當時覺得麻煩就直接這麼用了。
如果不明白的話,其實自己多試驗幾次就好了。不是很難理解,只是稍微有點繞。
大部分的核心方法基本就是上面這些了。
這篇整個API其實都是我當初開完完畢就記錄下來的note
現在我換了東家,電腦上也沒再進行相關的環境配置,看不了原始碼了,所以也懶得再寫下去了。
最後的最後補充一點應該寫在最前面的小知識,也是我做這個專案才瞭解到的
1.病理圖片又稱為醫學病理圖片 目前主要應用於電子顯微鏡成像 其格式有很多 市面上常見的有.SVS .TIF .TIFF .NDPI .MRXS 等
2.上條所提到的病理圖片雖然格式不同 但構造基本相同 都是由多層級構成以最大為原尺寸以此縮放 根據圖片的大小不同層級也不同
3.原圖片檔案中可會附帶附加圖片 通常為切片上的條形碼照片 格式通常為jpg或者png
4.通過掃描器產出的原始檔圖片再次轉碼轉換格式時 可能會丟失層級或者附帶圖片
5.此類圖片畫素極高 通常為10億畫素到100億畫素不等 多種語言自帶的圖片處理工具均無法讀取 市面上常用的支援有VIPS和OpenSlide。
寫的可能不是很詳細,也沒怎麼貼程式碼,主要是覺得這個技術的複用性不是很高,而且搞不好每個人的需求都不太一樣,程式碼很難複用。
實話說java來做這樣的處理還是挺麻煩的,網上對於這類資料的處理大多采用python,教程也很多,java基本沒有。
如果還有什麼不明白的,可以留言詢問,我要是看到了的話會第一時間回覆。
來源:呼和浩特SEO