使用javacv給圖片去白邊並打包上線

環境

java1.8.0_191、javacv1.5.2、opencv4.1.2、spring boot 1.5.10、centOS7.2 x64

問題描述

注意:前面是解決問題的一個過程描述,如果想看javacv、linux上線打包的重點部分就直接跳到最後的問題解決中第二種思路

業務場景是將一些報表圖片通過彩信發送到手機,因爲是發送彩信,所以對每張圖片的大小有很大的限制。這裏是保存文本表格,它們邊緣清晰,有大塊相同顏色區域,所以使用了png的圖片格式壓縮效果是最好的,但是多一種色彩、圖片位深度不同都會導致圖片大小的不同。
業務代碼大致邏輯是使用httpClient獲取到網頁table後,通過HtmlImageGenerator這個工具解析html生成png的。

        <dependency>
            <groupId>com.github.xuwei-k</groupId>
            <artifactId>html2image</artifactId>
            <version>0.1.0</version>
        </dependency>

這種html轉img的包有很多,都是大同小異的,使用起來也很方便,但是存在一些坑,比如linux上部署需要字體文件,還有現在在比較寬的圖片上會有bug的問題。
在這裏插入圖片描述
可以看出圖片下面有大片白色的區域,十幾張圖片裏面會有幾張就是這樣的,感覺矮胖矮胖的圖片容易出現這種情況。。。
它是通過Dimension prefSize = editorPane.getPreferredSize();來獲取寬高的,getPreferredSize方法計算的高度不準確,導致生成圖片有很長空白部分。editorPane是javax.swing裏面的,由於對這塊不熟悉,所以想到用其他的方式來解決問題。

問題解決

第一種思路

想到它是一個表格,每行的高度是固定的,只需要解析這段html,就可以通過tr的數量*高度來算出總高度,再加上header和footer就行了

				HttpEntity entity = response.getEntity();
                String content = EntityUtils.toString(entity, "utf-8");
                // 通過jsoup解析html,獲取dom節點
                Document document = Jsoup.parse(content);
                Element main = document.getElementById("main");
                Elements trList = main.getElementsByTag("tr");
                // head + foot + offset
                int height = 17 + 36 + 167;
                for (Element postItem : trList) {
                    height += 17;
                }
                // table -> png
                HtmlImageGenerator imageGenerator =HtmlImageGenerator();
                // 重新設定寬高
                imageGenerator.setSize(new Dimension(imageGenerator.getSize().width, height));
                imageGenerator.loadHtml(content);
                // 生成圖片
                BufferedImage image = imageGenerator.getBufferedImage();

在這裏插入圖片描述
這樣修改之後,再把固定高度減小一點確實能夠貼到底部,達到去白邊的效果,下面的白色是一種透明色,在手機上看不是很明顯。
好景不長,後面又增加了合併單元格行列的功能,然後不好計算高度,把固定高度設置高一點點,最後圖片數量的增多,超出大小了,就使用了24位的色深來節約空間,結果確實節約了40%左右的大小

		// 這裏將色彩空間RGB -> BGR
//      BufferedImage img = new BufferedImage(prefSize.width, editorPane.getHeight(), BufferedImage.TYPE_INT_ARGB);
        BufferedImage img = new BufferedImage(prefSize.width, editorPane.getHeight(), BufferedImage.TYPE_3BYTE_BGR);

但是這樣就引發了一個問題,因爲色深從32->24,少了alpha通道,就是沒有了透明色,原來下面的白色變成了黑色,放在手機上看就很明顯,所以問題還是回到了精確計算高度的問題上。

第二種思路

使用了opencv框架,它是著名的計算機視覺庫,程序裏用的是javacv(封裝了opencv等一系列框架,通過JNI調用的動態鏈接庫)
javacv的github地址
大致就是通過gray->canny->contours->cut方法截取了圖片中最大的矩形,也就是去除了超長的邊框,達到了精確計算寬高的效果。
下面開始實操
首先引入maven依賴

        <!-- https://mvnrepository.com/artifact/org.bytedeco/javacv-platform -->
        <!-- -Djavacpp.platform.custom -Djavacpp.platform.host -Djavacpp.platform.linux-x86_64 -Djavacpp.platform.windows-x86_64 -->
        <!-- references: https://github.com/bytedeco/javacpp-presets/wiki/Reducing-the-Number-of-Dependencies-->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv-platform</artifactId>
            <version>1.5.2</version>
        </dependency>

處理圖片的邏輯

import org.bytedeco.javacv.Java2DFrameConverter;
import org.bytedeco.javacv.OpenCVFrameConverter;
import org.bytedeco.opencv.opencv_core.*;
import org.opencv.imgproc.Imgproc;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import static org.bytedeco.opencv.global.opencv_imgproc.*;

@Slf4j
public class Test {
 /**
     * BufferedImage 轉 mat
     * 參考https://github.com/bytedeco/javacv-examples/blob/master/OpenCV_Cookbook/src/main/scala/opencv_cookbook/OpenCVUtils.scala
     * @param original
     * @return
     */
    public static Mat bufImg2Mat(BufferedImage original) {
        OpenCVFrameConverter.ToMat openCVConverter = new OpenCVFrameConverter.ToMat();
        Java2DFrameConverter java2DConverter = new Java2DFrameConverter();
        Mat mat = openCVConverter.convert(java2DConverter.convert(original));
        return mat;
    }

    /**
     * mat轉BufferedImage
     * 參考https://github.com/bytedeco/javacv-examples/blob/master/OpenCV_Cookbook/src/main/scala/opencv_cookbook/OpenCVUtils.scala
     * @param matrix
     * @return
     */
    public static BufferedImage mat2BufImg(Mat matrix) {
//        Mat tempMat=new Mat();
//        cvtColor(matrix,tempMat,COLOR_RGB2BGR555);
        // table->png那一步是BufferedImage.TYPE_3BYTE_BGR
        OpenCVFrameConverter.ToMat openCVConverter = new OpenCVFrameConverter.ToMat();
        Java2DFrameConverter java2DConverter = new Java2DFrameConverter();
        return java2DConverter.convert(openCVConverter.convert(matrix));
    }

    public static BufferedImage cutWhite(Mat matrix) {
        Mat grayMat = matrix.clone();
        // 轉灰度
        cvtColor(matrix, grayMat, Imgproc.COLOR_BGR2GRAY);
        // canny化
        Mat canny_output = matrix.clone();
        Canny(grayMat, canny_output, 10, 10 * 2, 3, false);
        // 查找邊緣
        MatVector contours = new MatVector();
        findContours(canny_output, contours, new Mat(), RETR_TREE, CHAIN_APPROX_SIMPLE, new Point(0, 0));
        // 篩選contours中的輪廓,我們需要最大的那個輪廓
        int min_width = (int) (matrix.cols() * 0.75);          // 矩形的最小寬度
        int min_height = (int) (matrix.rows() * 0.3);         // 矩形的最小高度

        Rect bbox = new Rect();
        for (int t = 0; t < contours.size(); ++t)        // 遍歷每一個輪廓
        {
            RotatedRect minRect = minAreaRect(contours.get(t));        // 找到每一個輪廓的最小外包旋轉矩形,RotatedRect裏面包含了中心座標、尺寸以及旋轉角度等信息
            if (minRect.size().width() > min_width && minRect.size().height() > min_height)   //篩選最小外包旋轉矩形
            {
                min_width = (int) minRect.size().width(); // 保存這個寬度,篩選出最大的寬度
                Mat vertices = new Mat();       // 定義一個4行2列的單通道float類型的Mat,用來存儲旋轉矩形的四個頂點
                boxPoints(minRect, vertices);    // 計算旋轉矩形的四個頂點座標
                bbox = boundingRect(vertices);   //找到輸入點集的最小外包直立矩形,返回Rect類型
                log.info("最小外包矩形:{}, 寬度: {}, 高度: {}", bbox, bbox.width(), bbox.height());
            }
        }
        // 如果成功截取到了
        if (bbox.width() > 0 && bbox.height() > 0) {
            Mat roiImg = matrix.apply(bbox.height(bbox.height() + 1));        //從原圖中截取興趣區域
            return mat2BufImg(roiImg);
        }
        // 否則返回原圖
        return mat2BufImg(matrix);
    }

}

然後啓動的時候需要在官網下載一個dll文件,win版本的,是構建好了的
opencv4.1.2\build\java\x64\opencv_java412.dll
在這裏插入圖片描述
啓動的時候增加一個參數: -Djava.library.path=./lib
使其能夠找到這個動態鏈接庫
在spring boot的啓動類裏面加上這個,導入opencv_java412動態鏈接庫

		System.loadLibrary(Core.NATIVE_LIBRARY_NAME);

在啓動起來就沒有白色的邊框了,win下測試成功了,現在開始上線

部署上線

根據 這篇介紹最小化javacv打包的帖子

mvn clean package -DskipTests -Djavacpp.platform.custom -Djavacpp.platform.linux-x86_64

使用這樣的mvn命令對spring boot應用進行打包,只針對linux生成javacv的包,大小隻有100M左右,如果全量的話就是800多M,裏面有兼容各種系統android、linux、win等,實際上部署只需linux的就行了

代碼有了,就只需要linux上有opencv的運行環境了
跟win不一樣,它需要libopencv_java412.so動態鏈接庫文件,win的是opencv_java412.dll。這個文件是需要自己下載源碼編譯出來的,不像win可以直接下載編譯好的文件用。
在這裏插入圖片描述
點擊下載源碼,然後把源碼複製到linux虛擬機裏

// 首先安裝所需要的依賴
yum install -y gcc gcc-c++ make automake ant
// 如果沒有安裝cmake
wget https://cmake.org/files/v3.12/cmake-3.12.0-rc1.tar.gz
tar -zxvf cmake-3.12.0-rc1.tar.gz
cd cmake-3.12.0-rc1
./bootstrap
gmake
gmake install
// 構建makefile
cd opencv-4.1.2
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/home/opencv  -D BUILD_DOCS=OFF -D BUILD_EXAMPLES=OFF -D BUILD_TESTS=OFF -D BUILD_PERF_TESTS=OFF -D BUILD_opencv_python=NO -D BUILD_opencv_python2=NO -D BUILD_opencv_python3=NO -DBUILD_SHARED_LIBS=OFF -DBUILD_WITH_STATIC_CRT=ON -DBUILD_TIFF=ON -DBUILD_ZLIB=ON -DBUILD_JASPER=ON -DBUILD_JPEG=ON -DBUILD_PNG=ON -DBUILD_OPENEXR=ON ..
// 然後編譯安裝
make –j8 (8線程並行編譯)
make install

需要注意的是,這是在本地編譯安裝的opencv,放線上,如果只有libopencv_java412.so文件還不夠,可能會說libpng15.so.15、libthai.so.0、libfribidi.so.0等等被libopencv_java412.so依賴的鏈接庫文件找不到,並且把這些文件放進-Djava.library.path=./lib目錄下也還是沒用, 因爲線上的權限是被限制的,也不能去改一些配置和安裝軟件,所以這裏就用了個粗暴的方法,將libopencv_java412.so需要的鏈接庫文件通過System.load全部引入
在這裏插入圖片描述
通過ldd命令可以看到libopencv_java412.so依賴了其他的很多.so文件,如果出現not found字樣的,就說明找不到依賴的鏈接庫,需要自己去引入,我把**/usr/lib64**下的所有文件都拖到lib目錄下了,然後下面就開始引用

public static void main(String[] args) {
		// todo 使用配置方式
		// 線上需要的鏈接庫文件
		System.load("/lib/libpng15.so.15");
		System.load("/lib/libthai.so.0");
		System.load("/lib/libfribidi.so.0");
		System.load("/lib/libglib-2.0.so.0");
		System.load("/lib/libgraphite2.so.3");
		System.load("/lib/libharfbuzz.so.0");
		System.load("/lib/libpango-1.0.so.0");
		System.load("/lib/libfontconfig.so.1");
		System.load("/lib/libpangoft2-1.0.so.0");
		System.load("/lib/libpixman-1.so.0");
		System.load("/lib/libGLdispatch.so.0");
		System.load("/lib/libEGL.so.1");
		System.load("/lib/libXau.so.6");
		System.load("/lib/libxcb.so.1");
		System.load("/lib/libxcb-shm.so.0");
		System.load("/lib/libxcb-render.so.0");
		System.load("/lib/libX11.so.6");
		System.load("/lib/libXrender.so.1");
		System.load("/lib/libXext.so.6");
		System.load("/lib/libGLX.so.0");
		System.load("/lib/libGL.so.1");
		System.load("/lib/libcairo.so.2");
		System.load("/lib/libpangocairo-1.0.so.0");
		System.load("/lib/libgdk_pixbuf-2.0.so.0");
		System.load("/lib/libXfixes.so.3");
		System.load("/lib/libXrender.so.1");
		System.load("/lib/libXinerama.so.1");
		System.load("/lib/libXi.so.6");
		System.load("/lib/libXrandr.so.2");
		System.load("/lib/libXcursor.so.1");
		System.load("/lib/libXcomposite.so.1");
		System.load("/lib/libXdamage.so.1");
		System.load("/lib/libXext.so.6");
		System.load("/lib/libgdk-x11-2.0.so.0");
		System.load("/lib/libatk-1.0.so.0");
		System.load("/lib/libgtk-x11-2.0.so.0");

		System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
		SpringApplication.run(MyApplication.class, args);
	}

還是放到了spring boot的啓動類裏面了,雖然不優雅,就暫時性解決問題就行

然後放到服務器上運行起來,如果差什麼.so文件,或者版本對不上的,都可以自己用System.load引入,期間遇到過libopenblas_nolapck.so.0找不到,是libopenblas.so.0文件名的問題,把文件名改正確就行了

關於編譯好的鏈接庫下載地址: libopencv_java412.so

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章