1. 程式人生 > 實用技巧 >Java關於超大圖片的處理,完整實現方案思路(.tiff.svs.mxrs等一系列格式)

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