GraphicsMagick的实践

我报名了Goldstone Project Phase 1 Challenge——瓜分100,000奖池,这是我的第4篇文章, 点击查看活动详情

本文首发于 个人网站

介绍

GraphicsMagick 是一个图像处理库,从 ImageMagick 5.5.2 派生,但现在更稳定、更轻、更快

GraphicsMagick 被称为图像处理的瑞士军刀。小而简洁的代码提供了强大而高效的工具和库集合来处理图像的读取、写入和操作,支持超过 88 种图像格式,包括重要的 DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM 和国际电影节。通过使用 OpenMP 但利用多线程进行图像处理,通过扩展 CPU 来增强处理能力。 GraphicsMagick 可以在大多数平台上使用,Linux、Mac、Windows 都没有问题。

GraphicsMagick 支持大图的处理,做过 GB 级的图像处理实验。 GraphicsMagick 可以动态生成图片,尤其适用于 Internet 应用程序。可用于处理调整大小、旋转、高亮、颜色调整、添加特效等。 GaphicsMagick 不仅支持命令行模式,还支持 C、C++、Perl、PHP、Tcl、Ruby 等调用。

安装

虽然在之前 文章 我已经提到了如何安装 GraphicsMagick。在这里再重复一遍,因为这里有个小坑,希望对大家有所帮助。

在 Mac 上安装 GraphicsMagick 有两种方法。 brew命令的一键安装虽然简单,但是默认会添加一些配置信息,导致我们无法使用GraphicsMagick的OpenMP功能,所以还是手动编译安装比较好。

冲泡安装

Mac 可以使用 brew 命令:

 冲泡安装libpng  
 冲泡安装libjpeg  
 #通过brew安装GraphicsMagick(libpng等依赖会一起下载)  
 酿造安装graphicsmagick  
  
 // 删除命令  
 brew卸载graphicsmagick  
 冲泡清理 -s  
 复制代码

查看GraphicsMagick的版本和安装路径:

 % 通用版本  
 GraphicsMagick 1.3.38 2022-03-26 Q16 http://www.GraphicsMagick.org/  
 ……  
 使用命令配置:  
 ./configure '--prefix=/usr/local/Cellar/graphicsmagick/1.3.38_1' '--disable-dependency-tracking' '--disable-openmp' '--disable-static' '--enable-shared ' '--with-modules' '--with-quantum-depth=16' '--without-lzma' '--without-x' '--without-gslib' '--with-gs-font-dir =/usr/local/share/ghostscript/fonts' '--without-wmf' 'CC=clang' 'CXX=clang++' 'PKG_CONFIG_PATH=/usr/local/opt/libpng/lib/pkgconfig:/usr/local/ opt/freetype/lib/pkgconfig:/usr/local/opt/jpeg-turbo/lib/pkgconfig:/usr/local/opt /jasper/lib/pkgconfig:/usr/local/opt/libtiff/lib/pkgconfig:/ usr/local/opt/little-cms2/lib/pkgconfig:/usr/local/opt/webp/lib/pkgconfig' 'PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/local/Homebrew/Library/Homebrew/os/ mac/pkgconfig/11'  
 ......  
 复制代码

从上面可以看出,brew命令默认执行./configure命令时,包含了“--disable-openmp”命令,表示完全禁用了OpenMP(自动多线程循环),会减少GraphicsMagick 处理图像的性能。这将在下面详细描述。

手动编译安装

 mkdir /usr/local/tools  
  
 tar -xvf GraphicsMagick-1.3.37.tar.gz -C /hresh/tool/  
  
 # 进入GraphicsMagick安装目录  
 ./configure --prefix=/hresh/tool/GraphicsMagick-1.3.37 --enable-shared --enable-openmp-slow  
  
 制作 && 制作安装  
 复制代码

在 .bash_profile 文件中设置环境变量:

 导出 GMAGICK_HOME=“/hresh/tool/GraphicsMagick-1.3.37”  
 出口路径=“$GMGICK_HOME /bin:$PATH”  
 导出 LD_LIBRARY_PATH= " $GMGICK_HOME /lib/"  
 导出 OMP_NUM_THREADS=6  
 复制代码

OMP_NUM_THREADS 环境变量表示 GM 可以使用的线程数。 OMP_NUM_THREADS 环境变量必须设置为实际使用多线程 (openmp)。

查看GraphicsMagick的版本和安装路径:

 % 通用版本  
 GraphicsMagick 1.3.37 20201226 Q16 http://www.GraphicsMagick.org/  
  
 使用命令配置:  
 ./configure '--prefix=/hresh/tool/GraphicsMagick-1.3.37' '--enable-shared' '--enable-openmp-slow'  
 复制代码

删除 GraphicsMagick

 使 distclean  
 进行卸载  
 复制代码

OOM 问题

我们在上一篇文章中 文章 我已经介绍了如何通过 Im4Java 为图像添加图像水印。代码如下:

 public static void addImgWatermark (String srcImagePath, String destImagePath, String waterImgPath)  
 抛出异常 {  
 // 原始图像信息  
 BufferedImage targetImg = ImageIO.read(new File(srcImagePath));  
 //水印图片  
 BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath));  
 int w = targetImg.getWidth();  
 int h = targetImg.getHeight();  
 IMOperation op = 新的 IMOperation();  
 //水印图片位置  
 op.geometry(watermarkImage.getWidth(), watermarkImage.getHeight(),  
 w - watermarkImage.getWidth() - 300, h - watermarkImage.getHeight() - 100);  
 //水印透明度  
 op.溶解(90);  
 //水印  
 op.addImage(waterImgPath);  
 // 原始图像  
 op.addImage(srcImagePath);  
 // 目标  
 op.addImage(destImagePath);  
 ImageCommand cmd = getImageCommand(CommandType.imageWaterMark);  
 cmd.运行(操作);  
 }  
 复制代码

当时只考虑基本功能的实现,没有关注细节。经同事提醒,发现ImageIO.read()方法获取原始图片的宽高信息,会将整个图片流读入内存,浪费不少钱。空间,也增加了OOM的风险。

通过BufferedImage获取宽高

测试代码如下:

 公共静态无效addImgWatermark(字符串srcImagePath,字符串destImagePath,字符串waterImgPath){  
 System.out.println(Thread.currentThread().getName() + "开始生成图片水印...");  
 尝试 {  
 // 原始图像信息  
 BufferedImage targetImg = ImageIO.read(new File(srcImagePath));  
 //水印图片  
 BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath));  
 int w = targetImg.getWidth();  
 int h = targetImg.getHeight();  
 int watermarkImageWidth = watermarkImage.getWidth();  
 int watermarkImageHeight = watermarkImage.getHeight();  
  
 IMOperation op2 = new IMOperation();  
 //水印图片位置  
 op2.geometry(watermarkImageWidth, watermarkImageHeight,  
 w - watermarkImageWidth - 300,h - watermarkImageHeight - 100);  
 //水印透明度  
 op2.溶解(90);  
  
 //水印  
 op2.addImage(waterImgPath);  
 // 原始图像  
 op2.addImage(srcImagePath);  
 // 目标  
 op2.addImage(destImagePath);  
  
 ImageCommand cmd2 = getImageCommand(CommandType.imageWaterMark);  
 cmd2.run(op2);  
 } 捕捉(异常 e){  
 e.printStackTrace();  
 }  
 System.out.println(Thread.currentThread().getName() + "成功生成图片水印...");  
 }  
  
 公共静态 void main (String[] args) 抛出异常 {  
 ExecutorService executorService = new ThreadPoolExecutor( 20, 25, 30, TimeUnit.SECONDS,  
 新的 LinkedBlockingDeque<>(5),  
 Executors.defaultThreadFactory(),  
 新的 ThreadPoolExecutor.AbortPolicy());  
  
 尝试 {  
 for (int i = 1; i <= 17; i++) {  
 executorService.execute(new ImageThread2());  
 }  
 } 捕捉(异常 e){  
 e.printStackTrace();  
 } 最后 {  
 executorService.shutdown();  
 }  
 }  
  
 类 ImageThread2 实现 Runnable {  
  
 @覆盖  
 公共无效运行(){  
 String projectPath = System.getProperty("user.dir");  
 // 图片大小为7.9M  
 字符串 srcImgPath = projectPath + "/src/main/resources/static/sky.png";  
 字符串 waterImgPath = projectPath + "/src/main/resources/static/icon.png";  
 字符串路径 = projectPath + "/src/main/resources/static/out/concurrency/im4_image.jpg";  
 Im4JavaUtil.addImgWatermark(srcImgPath, path, waterImgPath);  
 }  
 }  
 复制代码

控制台输出为:

ImageIO.read内存溢出

可以看出ImageIO.read()在并发条件下会抛出OOM异常。为什么是这样?

BufferedImage对象中最重要的两个组件是Raster和ColorModel,分别用于存储图像的像素数据和颜色数据。

一个表示矩形像素数组的 Raster 类,封装了一个存储样本值的 DataBuffer,以及一个描述如何在 DataBuffer 中定位给定样本值的 SampleModel。我们得到图片的宽度和高度,这是从光栅对象中获取的。

每次生成一个BufferedImage对象,都必须将图像数据流读入内存,即生成一个Raster对象,最终会导致JVM内存空间不足,导致OOM异常。

除了从源码层面分析,还可以分析GC结果。首先,在执行上述代码时配置以下JVM参数:

 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps  
 复制代码

在控制台可以看到不断打印GC日志,截取部分GC结果如下:

 堆  
 PSYoungGen 总计 282624K,已使用 138032K [ 0x000000076ab00000, 0x000000077c180000, 0x00000007c0000000)  
 伊甸园空间 280576K, 48% 已使用 [0x000000076ab00000, 0x00000007730be518, 0x000000077bd00000)  
 从空间 2048K,使用 52% [0x000000077bf00000, 0x000000077c00dec8, 0x000000077c100000)  
 到空间 2048K, 0% 已使用 [0x000000077bd00000, 0x000000077bd00000, 0x000000077bf00000)  
 ParOldGen 总计 2796544K,已使用 2717164K [ 0x00000006c0000000, 0x000000076ab00000, 0x000000076ab00000)  
 对象空间 2796544K, 97% 已使用 [0x00000006c0000000, 0x0000000765d7b378, 0x000000076ab00000)  
 元空间使用6753K,容量6890K,承诺7040K,保留1056768K  
 使用的类空间 749K,容量 803K,承诺 896K,保留 1048576K  
 复制代码

可以看出老年代的内存使用率极高,所以建议内存来不及回收,最终会导致内存溢出。

另外,我们还可以通过VisualVM工具的“VisualGC”插件直观的看到内存使用情况,如下图所示:

ImageIO.read导致内存溢出

通过 ImageReader 获取宽高

针对以上问题,我们可以替换ImageIO.read()方法,代码修改如下:

 int[] targetImgSize = getImgSize(srcImagePath);  
 int w = targetImgSize[0];  
 int h = targetImgSize[1];  
  
 int[] imgSize = getImgSize(waterImgPath);  
 int watermarkImageWidth = imgSize[0];  
 int watermarkImageHeight = imgSize[1];  
  
  
 公共静态int [] getImgSize(字符串文件路径)抛出异常{  
 整数 [] 大小 = 新整数 [2];  
 尝试(ImageInputStream = ImageIO.createImageInputStream(新文件(文件路径))){  
 迭代器<ImageReader>读者 = ImageIO.getImageReaders(in);  
 if (readers.hasNext()) {  
 ImageReader 阅读器 = reader.next();  
 尝试 {  
 reader.setInput(in);  
 int 宽度 = reader.getWidth(0);  
 int height = reader.getHeight(0);  
 尺寸[0] = 宽度;  
 尺寸[1] = 高度;  
 } 最后 {  
 reader.dispose();  
 }  
 }  
 }  
 返回大小;  
 }  
 复制代码

打开相同数量的线程,执行代码将不再抛出OOM异常,GC日志如下:

 堆  
 PSYoungGen 总计 76288K,已使用 29601K [ 0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)  
 伊甸园空间 65536K, 37% 已使用 [0x000000076ab00000, 0x000000076c3326d8, 0x000000076eb00000)  
 从空间 10752K, 44% 使用 [0x000000076eb00000, 0x000000076efb5e60, 0x000000076f580000)  
 到空间 10752K, 0% 已使用 [0x000000076f580000, 0x000000076f580000, 0x0000000770000000)  
 ParOldGen 总计 175104K,使用了 8K [ 0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)  
 对象空间 175104K, 0% 已使用 [0x00000006c0000000, 0x00000006c0002000, 0x00000006cab00000)  
 元空间已用 6326K,容量 6552K,已提交 6784K,保留 1056768K  
 使用的类空间 714K,容量 790K,承诺 896K,保留 1048576K  
 复制代码

内存使用可视化图如下:

内存占用直观图

ImageReader 表现更好的原因

我们对比一下获取图片宽高的代码的区别:

 //通过BufferedImage获取图片宽高  
 BufferedImage targetImg = ImageIO.read(new File(srcImagePath));  
 BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath));  
 int w = targetImg.getWidth();  
 int h = targetImg.getHeight();  
 int watermarkImageWidth = watermarkImage.getWidth();  
 int watermarkImageHeight = watermarkImage.getHeight();  
  
 // 通过ImageReader获取图片宽高  
 int[] targetImgSize = getImgSize(srcImagePath);  
 int w = targetImgSize[0];  
 int h = targetImgSize[1];  
  
 int[] imgSize = getImgSize(waterImgPath);  
 int watermarkImageWidth = imgSize[0];  
 int watermarkImageHeight = imgSize[1];  
  
 公共静态int [] getImgSize(字符串文件路径)抛出异常{  
 整数 [] 大小 = 新整数 [2];  
 尝试(ImageInputStream = ImageIO.createImageInputStream(新文件(文件路径))){  
 迭代器<ImageReader>读者 = ImageIO.getImageReaders(in);  
 if (readers.hasNext()) {  
 ImageReader 阅读器 = reader.next();  
 尝试 {  
 reader.setInput(in);  
 int 宽度 = reader.getWidth(0);  
 int height = reader.getHeight(0);  
 尺寸[0] = 宽度;  
 尺寸[1] = 高度;  
 } 最后 {  
 reader.dispose();  
 }  
 }  
 }  
 返回大小;  
 }  
 复制代码

如果您想了解 BufferedImage 和 ImageReader 之间的区别,您应该深入研究源代码。

关于BufferedImage对象的创建,核心代码如下:

 // 图像IO  
 公共静态 BufferedImage 读取(文件输入)抛出 IOException {  
 如果(输入==空){  
 throw new IllegalArgumentException("input == null!");  
 }  
 如果(!input.canRead()){  
 throw new IIOException("无法读取输入文件!");  
 }  
  
 ImageInputStream 流 = createImageInputStream(input);  
 如果(流==空){  
 throw new IIOException("无法创建 ImageInputStream!");  
 }  
 BufferedImage bi = 读取(流);  
 if (bi == null) {  
 流.close();  
 }  
 返回双;  
 }  
  
 公共静态 BufferedImage 读取(ImageInputStream 流)  
 抛出 IOException {  
 如果(流==空){  
 throw new IllegalArgumentException("stream == null!");  
 }  
  
 迭代器 iter = getImageReaders(stream);  
 如果(!iter.hasNext()){  
 返回空值;  
 }  
  
 ImageReader 阅读器 = (ImageReader)iter.next();  
 ImageReadParam 参数 = reader.getDefaultReadParam();  
 reader.setInput(stream, true, true);  
 缓冲图像双;  
 尝试 {  
 bi = reader.read(0,参数);  
 } 最后 {  
 reader.dispose();  
 流.close();  
 }  
 返回双;  
 }  
  
 // com.sun.imageio.plugins.png.PNGImageReader  
  
 公共 BufferedImage 读取(int imageIndex,ImageReadParam 参数)  
 抛出 IIOException {  
 如果(图像索引!= 0){  
 throw new IndexOutOfBoundsException("imageIndex != 0!");  
 }  
  
 读取图像(参数);  
 返回图像;  
 }  
  
 private void readImage (ImageReadParam param) 抛出 IIOException {  
 读取元数据();  
  
 // 这里得到的宽高,后续  
 int 宽度 = 元数据.IHDR_width;  
 int 高度 = 元数据.IHDR_height;  
  
 // 初始化默认值  
 sourceXSubsampling = 1;  
 sourceYSubsampling = 1;  
 sourceMinProgressivePass = 0;  
 sourceMaxProgressivePass = 6;  
 源带=空;  
 目的地带=空;  
 目的地偏移 = 新点(0, 0);  
  
 ……  
 // 接下来,准备生成一个BufferedImage对象,theImage  
 }  
  
 // 通过readHeader()获取图片的宽高  
 private void readMetadata () 抛出 IIOException {  
 如果(得到元数据){  
 返回;  
 }  
  
 读头();  
  
 ……  
 }  
  
 // javax.imageio.ImageTypeSpecifier  
 // 在这个方法中创建一个 BufferedImage 对象  
 公共 BufferedImage createBufferedImage ( int width, int height) {  
 尝试 {  
 SampleModel sampleModel = getSampleModel(width, height);  
 WritableRaster 栅格 =  
 Raster.createWritableRaster(sampleModel,  
 新点(0, 0));  
 返回新的 BufferedImage(colorModel, raster,  
 colorModel.isAlphaPremultiplied(),  
 新哈希表());  
 } 捕捉(NegativeArraySizeException e){  
 // 最有可能从 DataBuffer 构造函数抛出的异常  
 抛出新的 IllegalArgumentException  
 ( "数组大小 > Integer.MAX_VALUE!");  
 }  
 }  
 复制代码

看完上面的代码,有没有发现ImageIO文件中的read()方法和我们写的getImgSize()方法很像?在获取到ImageReader对象的时候,我们的代码直接获取到了图片的宽高,没有其他多余的操作。 .相关源码如下:

 // com.sun.imageio.plugins.png.PNGImageReader  
 公共 int getWidth (int imageIndex) 抛出 IIOException {  
 如果(图像索引!= 0){  
 throw new IndexOutOfBoundsException("imageIndex != 0!");  
 }  
  
 读头();  
  
 返回元数据.IHDR_width;  
 }  
 复制代码

对比两者的调用链接可以发现,通过ImageReader获取图片宽高的方法链接较短;此外,内存使用较少,因此不太可能导致内存问题。

开放式MP

一开始我尝试在Mac上测试OpenMP,但是反复摆弄还是失败了。底线是该机器默认不支持 OpenMP。有兴趣的朋友可以参考 在 macOS 平台上安装 OpenMP 库 ,看看您是否可以在 Mac 上测试 OpenMP。

所以这里我们基于阿里云的服务器进行测试,它只有2核。

测试

 gm benchmark [ 选项... ] 命令  
 复制代码

基准 为一个或多个循环和/或指定的执行时间执行任意执行 gm 实用程序命令(例如 convert ),并报告一些执行指标。对于使用 OpenMP 的构建,提供了一种模式来执行越来越多的线程的基准测试,并提供了加速和多线程执行效率的报告。如果基准 用于在没有任何额外基准测试选项的情况下执行命令,该命令运行一次。

此测试使用以下命令:

 gm benchmark -iterations 100 -stepthreads 1 +原命令语句  
 复制代码

- 迭代 100 次

-stepthreads 1 线程增长步长,1表示每次增加1个线程,直到OMP_NUM_THREADS环境变量的值,OMP_NUM_THREADS环境变量必须设置好,才能真正使用多线程(openmp)。

禁用 OpenMP

进入GraphicsMagick安装目录,执行如下命令:

 ./configure --prefix=/hresh/tool/GraphicsMagick-1.3.37 --enable-shared --disable-openmp  
 制作  
 进行安装  
 复制代码

然后进入镜像所在目录,执行如下命令:

 # gm benchmark -iterations 100 -stepthreads 1 convert -resize 100x100 -quality 90 +profile "*" mountain-landscape.jpg 123.jpg  
 结果: 1 个线程 100 iter 52.41s 用户 52.747874s 总计 1.896 iter/s 1.908 iter/cpu 1.00 加速 1.000 karp-flatt  
 复制代码

结果中各个参数的含义如下:

  • 线程 — 使用的线程数。
  • 迭代器 — 要执行的命令迭代次数。
  • 用户 — 用户消耗的总时间。
  • 全部的 — 花费的总时间。
  • 迭代/秒 — 每秒的命令迭代次数。
  • 迭代器/CPU — 每次迭代消耗的 CPU 时间。
  • 加速 — 与一个线程相比的加速。
  • 鲤鱼平 — 加速效率的 Karp-Flatt 度量。

根据结果​​,处理一张图片需要524ms。

启用 OpenMP

重新执行编译命令:

 ./configure --prefix=/hresh/tool/GraphicsMagick-1.3.37 --enable-shared --enable-openmp-slow  
 制作  
 进行安装  
 复制代码

然后进入镜像所在目录,执行如下命令:

 # 导出 OMP_NUM_THREADS=2  
 # gm benchmark -iterations 100 -stepthreads 1 convert -resize 100x100 -quality 90 +profile "*" mountain-landscape.jpg 123.jpg  
 结果:1 线程 100 iter 47.84s 用户 48.102332s 总计 2.079 iter/s 2.090 iter/cpu 1.00 加速 1.000 karp-flatt  
 结果:2 线程 100 iter 48.95s 用户 36.630871s 总计 2.730 iter/s 2.043 iter/cpu 1.31 加速 0.523 karp-flatt  
 复制代码

根据结果​​,线程1处理一张图片耗时478ms,线程2处理一张图片耗时489ms。

OpenMP 是 GraphicsMagick 的特殊功能之一。为了获得最佳性能,可以将 OMP_NUM_THREADS 设置为等于可用 CPU 内核的数量。如果服务器有多个内核并运行多个程序,请将 OMP_NUM_THREADS 设置为小于内核数以确保最佳的整体系统性能。另外,CPU使用率会随着线程数的增加而增加,所以要根据实际情况调整参数。

GraphicsMagick 和 Graphics2D

解决了上面的OOM问题后,突然冒出一个想法:比较GraphicsMagick和Graphics2D在多线程环境下生成图片水印,谁更占优势?

前提:要给同一张图片添加图片水印,使用ImageIO.read。

GraphicsMagick 代码

 公共静态无效addImgWatermark(字符串srcImagePath,字符串destImagePath,字符串waterImgPath){  
 System.out.println(Thread.currentThread().getName() + "开始生成图片水印...");  
 尝试 {  
 // 原始图像信息  
 BufferedImage targetImg = ImageIO.read(new File(srcImagePath));  
 //水印图片  
 BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath));  
 int w = targetImg.getWidth();  
 int h = targetImg.getHeight();  
 int watermarkImageWidth = watermarkImage.getWidth();  
 int watermarkImageHeight = watermarkImage.getHeight();  
  
 IMOperation op2 = new IMOperation();  
 //水印图片位置  
 op2.geometry(watermarkImageWidth, watermarkImageHeight,  
 w - watermarkImageWidth - 300,h - watermarkImageHeight - 100);  
 //水印透明度  
 op2.溶解(90);  
  
 //水印  
 op2.addImage(waterImgPath);  
 // 原始图像  
 op2.addImage(srcImagePath);  
 // 目标  
 op2.addImage(destImagePath);  
  
 ImageCommand cmd2 = getImageCommand(CommandType.imageWaterMark);  
 cmd2.run(op2);  
 } 捕捉(异常 e){  
 e.printStackTrace();  
 }  
 System.out.println(Thread.currentThread().getName() + "成功生成图片水印...");  
 }  
  
 公共静态 void main (String[] args) 抛出异常 {  
 ExecutorService executorService = new ThreadPoolExecutor( 20, 25, 30, TimeUnit.SECONDS,  
 新的 LinkedBlockingDeque<>(5),  
 Executors.defaultThreadFactory(),  
 新的 ThreadPoolExecutor.AbortPolicy());  
  
 尝试 {  
 for (int i = 1; i <= 16; i++) {  
 executorService.execute(new ImageThread2());  
 }  
 } 捕捉(异常 e){  
 e.printStackTrace();  
 } 最后 {  
 executorService.shutdown();  
 }  
 }  
  
 类 ImageThread2 实现 Runnable {  
  
 @覆盖  
 公共无效运行(){  
 String projectPath = System.getProperty("user.dir");  
 // 图片大小为7.9M  
 字符串 srcImgPath = projectPath + "/src/main/resources/static/sky.png";  
 字符串 waterImgPath = projectPath + "/src/main/resources/static/icon.png";  
 字符串路径 = projectPath + "/src/main/resources/static/out/concurrency/im4_image.jpg";  
 Im4JavaUtil.addImgWatermark(srcImgPath, path, waterImgPath);  
 }  
 }  
 复制代码

经过测试,得到如下结果: GraphicsMagick 最多可以同时开启16个线程来添加图片水印。

图形二维码

 公共静态无效graphics2DDrawImg(字符串srcImgPath,字符串waterImgPath,字符串outPath){  
 System.out.println(Thread.currentThread().getName() + "开始生成图片水印...");  
 尝试 {  
 BufferedImage targetImg = ImageIO.read(new File(srcImgPath));  
 int imgWidth = targetImg.getWidth();  
 int imgHeight = targetImg.getHeight();  
 BufferedImage bufferedImage = new BufferedImage(imgWidth, imgHeight,  
 BufferedImage.TYPE_INT_BGR);  
 Graphics2D g = bufferedImage.createGraphics();  
  
 g.drawImage(targetImg, 0, 0, imgWidth, imgHeight, null);  
 g.setColor(Color.BLACK);  
  
 int imgLeftMargin = ICON_LEFT_MARGINS[0];  
 int imgTopMargin = 1000;  
  
 BufferedImage 图标 = ImageIO.read(new File(waterImgPath));  
 g.drawImage(icon, imgLeftMargin, imgTopMargin, icon.getWidth(),  
 图标.getHeight(), null);  
  
 FileOutputStream outImgStream = new FileOutputStream(outPath);  
 ImageIO.write(bufferedImage, "jpg", outImgStream);  
 g.dispose();  
 outImgStream.close();  
 } 捕捉(IOException e){  
 e.getStackTrace();  
 }  
 System.out.println(Thread.currentThread().getName() + "成功生成图片水印...");  
 }  
  
 公共静态 void main (String[] args) 抛出异常 {  
 ExecutorService executorService = new ThreadPoolExecutor( 20, 25, 30, TimeUnit.SECONDS,  
 新的 LinkedBlockingDeque<>(5),  
 Executors.defaultThreadFactory(),  
 新的 ThreadPoolExecutor.AbortPolicy());  
  
 尝试 {  
 for (int i = 1; i <= 8; i++) {  
 executorService.execute(new ImageThread());  
 }  
 } 捕捉(异常 e){  
 e.printStackTrace();  
 } 最后 {  
 executorService.shutdown();  
 }  
 }  
  
 类 ImageThread 实现 Runnable {  
  
 @覆盖  
 公共无效运行(){  
 String projectPath = System.getProperty("user.dir");  
 字符串 srcImgPath = projectPath + "/src/main/resources/static/sky.png";  
 字符串 waterImgPath = projectPath + "/src/main/resources/static/icon.png";  
 字符串路径 = projectPath + "/src/main/resources/static/out/concurrency/g2d_image.jpg";  
 Graphics2DUtil.graphics2DDrawImg(srcImgPath, waterImgPath, path);  
 }  
 }  
 复制代码

测试结果表明,Graphics2D最多可以打开7个线程来添加图片水印。抛出OOM异常时的截图如下:

Graphics2D内存溢出

总之, Graphics2D 是 Java 自带的一个图像处理工具类。在处理图像时,与内存交互的操作很频繁。另外受JVM内存的限制,更容易出现OOM异常。 GraphicsMagick 在进行图像处理时,直接将图像读取到物理内存中,不由 JVM 管理,因此更安全。 .

总结

目前市场上成熟的图像处理库:GraphicsMagick 和 OpenCV。以上两个图像处理库可以跨平台,在多种编译器上执行。它们可以轻松实现多进程模式,充分发挥多核CPU的优势。 GraphicsMagick 只是在前一段时间才使用。学习Python时使用过OpenCV,在Python中被广泛使用。

目前我使用的是Java语言,JDK自带了一套图​​像处理库——Graphics2D,特点是稳定、简单,但是对于图像处理,性能真的很差!不过Java也提供了类似的JNI方式来支持GraphicsMagick+im4java来处理图片。不过在原生态支持opencv比较繁琐。需要使用JNI调用大量的动态或静态库。有两个问题:一个是性能问题,另一个是如果有内存问题就很难控制。

当然,在选择某项技术时,要结合实际需要。性能好不一定是最好的,适合自己的才是最好的。以我遇到的项目为例,基本没有高并发的图像处理场景,而且使用Graphics2D也比较简单,所以最终选择了Graphics2D而不是GraphicsMagick+im4java。

最后,感谢同事们的指导,让我对GraphicsMagick有了更新的认识,这也是我有这篇文章的原因。希望以后能和大家有更多的技术交流。

参考

GraphicsMagick 性能测试(二)——启用多线程对性能的影响

版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议。转载请附上原文出处链接和本声明。

这篇文章的链接: https://homecpp.art/5921/9721/0755

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明

本文链接:https://www.qanswer.top/38458/33532112

posted @ 2022-09-21 12:35  哈哈哈来了啊啊啊  阅读(514)  评论(0编辑  收藏  举报