Java關於超大圖片的處理,完整實現方案思路(.tiff.svs.mxrs等一系列格式)

前言

這篇文章其實早就想寫了,最近事情比較多,耽擱了,詳細的代碼可能不會提供多少,但我會盡可能把前端

開發的過程和思路寫清楚,其實總體邏輯很簡單,只是相關的資料和文檔不多,太難找,我也會盡可能java

在這篇文章中把經常使用的API標出。python

再囉嗦幾嘴linux

這是在上一家公司開發時遇到的需求,須要將"病理圖片"解析而且在瀏覽器上顯示,在我以前的技術git

沒能解決,而我以前也沒作過相似的功能,公司只給了我一個鏈接,是XX公司實現後的效果,要求github

作到這種,公司的老項目是個CS客戶端的,已經實現了相關的功能,可是是由python實現的,年代windows

比較久遠,甚至在職人員都不太清楚是怎麼實現的,並且有個比較大的問題就是,很卡,讀圖在頁面後端

上會比較卡,不流暢,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、準備工做

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.soopenslide.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基本沒有。

若是還有什麼不明白的,能夠留言詢問,我要是看到了的話會第一時間回覆。

相關文章
相關標籤/搜索