介绍

GraphicsMagick 是个图片处理库,是从ImageMagick 5.5.2分支出来的,但是现在他变得更稳定和更轻、更快一些

GraphicsMagick 号称图像处理领域的瑞士军刀。 短小精悍的代码却提供了一个鲁棒、高效的工具和库集合,来处理图像的读取、写入和操作,支持超过88种图像格式,包括重要的DPX、GIF、JPEG、JPEG-2000、PNG、PDF、PNM和TIFF。 通过使用 OpenMP 可是利用多线程进行图片处理,增强了通过扩展 CPU 提高处理能力。GraphicsMagick可以再绝大多数的平台上使用,Linux、Mac、Windows都没有问题。

GraphicsMagick 支持大图片的处理,并且已经做过GB级别的图像处理实验。GraphicsMagick 能够动态的生成图片,特别适用于互联网的应用。可以用来处理调整尺寸、旋转、加亮、颜色调整、增加特效等方面。GaphicsMagick 不仅支持命令行的模式,同时也支持C、C++、Perl、PHP、Tcl、Ruby等的调用。

安装

虽然在上篇中已经提到过如何安装 GraphicsMagick,这里还要再啰嗦一遍,因为这里有个小坑,希望对大家有所帮助。

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

brew安装

Mac 可以使用 brew 命令:

brew install libpng
brew install libjpeg
#通过 brew 安装 GraphicsMagick(libpng 等依赖包会一并下载)
brew install graphicsmagick// 删除命令
brew uninstall graphicsmagick
brew cleanup -s

查看 GraphicsMagick 的版本以及安装路径:

% gm -version
GraphicsMagick 1.3.38 2022-03-26 Q16 http://www.GraphicsMagick.org/
......
Configured using the command:./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/toolstar -xvf GraphicsMagick-1.3.37.tar.gz -C /hresh/tool/# 进入GraphicsMagick安装目录
./configure --prefix=/hresh/tool/GraphicsMagick-1.3.37 --enable-shared --enable-openmp-slowmake && make install

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

export GMAGICK_HOME="/hresh/tool/GraphicsMagick-1.3.37"
export PATH="$GMAGICK_HOME/bin:$PATH"
export LD_LIBRARY_PATH="$GMAGICK_HOME/lib/"
export OMP_NUM_THREADS=6

OMP_NUM_THREADS 环境变量,表示GM可使用的线程数。必须设置OMP_NUM_THREADS 环境变量才可以真正使用起多线程(openmp)。

查看 GraphicsMagick 的版本以及安装路径:

% gm -version
GraphicsMagick 1.3.37 20201226 Q16 http://www.GraphicsMagick.org/Configured using the command:./configure  '--prefix=/hresh/tool/GraphicsMagick-1.3.37' '--enable-shared' '--enable-openmp-slow'

删除 GraphicsMagick

make distclean
make uninstall

OOM问题

我们在之前一篇文章中介绍过如何通过 Im4Java 给图片添加图片水印,代码如下所示:

public static void addImgWatermark(String srcImagePath, String destImagePath, String waterImgPath)throws Exception {// 原始图片信息BufferedImage targetImg = ImageIO.read(new File(srcImagePath));// 水印图片BufferedImage watermarkImage = ImageIO.read(new File(waterImgPath));int w = targetImg.getWidth();int h = targetImg.getHeight();IMOperation op = new IMOperation();// 水印图片位置op.geometry(watermarkImage.getWidth(), watermarkImage.getHeight(),w - watermarkImage.getWidth() - 300, h - watermarkImage.getHeight() - 100);// 水印透明度op.dissolve(90);// 水印op.addImage(waterImgPath);// 原图op.addImage(srcImagePath);// 目标op.addImage(destImagePath);ImageCommand cmd = getImageCommand(CommandType.imageWaterMark);cmd.run(op);
}

当时只考虑基本功能实现了,并未注意细节问题,经同事提醒,发现 ImageIO.read()这种方式来获取原图片的宽高信息,会将整个图片流读取到内存,浪费了大量的空间并且还增加了 OOM 风险。

通过BufferedImage获取宽高

测试代码如下:

public static void addImgWatermark(String srcImagePath, String destImagePath,String waterImgPath) {System.out.println(Thread.currentThread().getName() + "开始生成图片水印。。。。。。。");try {// 原始图片信息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.dissolve(90);// 水印op2.addImage(waterImgPath);// 原图op2.addImage(srcImagePath);// 目标op2.addImage(destImagePath);ImageCommand cmd2 = getImageCommand(CommandType.imageWaterMark);cmd2.run(op2);} catch (Exception e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "成功生成图片水印。。。。。。。。。。");
}public static void main(String[] args) throws Exception {ExecutorService executorService = new ThreadPoolExecutor(20, 25, 30, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());try {for (int i = 1; i <= 17; i++) {executorService.execute(new ImageThread2());}} catch (Exception e) {e.printStackTrace();} finally {executorService.shutdown();}
}class ImageThread2 implements Runnable {@Overridepublic void run() {String projectPath = System.getProperty("user.dir");// 图片大小为7.9MString srcImgPath = projectPath + "/src/main/resources/static/sky.png";String waterImgPath = projectPath + "/src/main/resources/static/icon.png";String path = projectPath + "/src/main/resources/static/out/concurrency/im4_image.jpg";Im4JavaUtil.addImgWatermark(srcImgPath, path, waterImgPath);}
}

控制台输出结果为:

可以看出,并发情况下 ImageIO.read()会引发 OOM 异常,这是为什么呢?

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

Raster 表示像素矩形数组的类,封装存储样本值的 DataBuffer,以及描述如何在 DataBuffer 中定位给定样本值的 SampleModel。我们获取图片的宽高,就是从 raster 对象中拿到的。

每次生成 BufferedImage 对象,都要读取图片数据流到内存中,即生成 Raster 对象,最终导致 JVM 内存空间不足,引发 OOM 异常。

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

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps

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

HeapPSYoungGen      total 282624K, used 138032K [0x000000076ab00000, 0x000000077c180000, 0x00000007c0000000)eden space 280576K, 48% used [0x000000076ab00000,0x00000007730be518,0x000000077bd00000)from space 2048K, 52% used [0x000000077bf00000,0x000000077c00dec8,0x000000077c100000)to   space 2048K, 0% used [0x000000077bd00000,0x000000077bd00000,0x000000077bf00000)ParOldGen       total 2796544K, used 2717164K [0x00000006c0000000, 0x000000076ab00000, 0x000000076ab00000)object space 2796544K, 97% used [0x00000006c0000000,0x0000000765d7b378,0x000000076ab00000)Metaspace       used 6753K, capacity 6890K, committed 7040K, reserved 1056768Kclass space    used 749K, capacity 803K, committed 896K, reserved 1048576K

可以看到老年代内存占用比例极高,由此推荐因内存来不及回收,最终引发内存溢出。

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

通过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];public static int[] getImgSize(String filePath) throws Exception {int[] size = new int[2];try (ImageInputStream in = ImageIO.createImageInputStream(new File(filePath))) {Iterator<ImageReader> readers = ImageIO.getImageReaders(in);if (readers.hasNext()) {ImageReader reader = readers.next();try {reader.setInput(in);int width = reader.getWidth(0);int height = reader.getHeight(0);size[0] = width;size[1] = height;} finally {reader.dispose();}}}return size;
}

开启同样多的线程,执行代码不会再抛出 OOM 异常,GC 日志如下:

HeapPSYoungGen      total 76288K, used 29601K [0x000000076ab00000, 0x0000000770000000, 0x00000007c0000000)eden space 65536K, 37% used [0x000000076ab00000,0x000000076c3326d8,0x000000076eb00000)from space 10752K, 44% used [0x000000076eb00000,0x000000076efb5e60,0x000000076f580000)to   space 10752K, 0% used [0x000000076f580000,0x000000076f580000,0x0000000770000000)ParOldGen       total 175104K, used 8K [0x00000006c0000000, 0x00000006cab00000, 0x000000076ab00000)object space 175104K, 0% used [0x00000006c0000000,0x00000006c0002000,0x00000006cab00000)Metaspace       used 6326K, capacity 6552K, committed 6784K, reserved 1056768Kclass space    used 714K, capacity 790K, committed 896K, reserved 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];public static int[] getImgSize(String filePath) throws Exception {int[] size = new int[2];try (ImageInputStream in = ImageIO.createImageInputStream(new File(filePath))) {Iterator<ImageReader> readers = ImageIO.getImageReaders(in);if (readers.hasNext()) {ImageReader reader = readers.next();try {reader.setInput(in);int width = reader.getWidth(0);int height = reader.getHeight(0);size[0] = width;size[1] = height;} finally {reader.dispose();}}}return size;
}

想要搞清楚 BufferedImage 和 ImageReader 的差异,还是深入源码探究一番。

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

// ImageIO
public static BufferedImage read(File input) throws IOException {if (input == null) {throw new IllegalArgumentException("input == null!");}if (!input.canRead()) {throw new IIOException("Can't read input file!");}ImageInputStream stream = createImageInputStream(input);if (stream == null) {throw new IIOException("Can't create an ImageInputStream!");}BufferedImage bi = read(stream);if (bi == null) {stream.close();}return bi;
}public static BufferedImage read(ImageInputStream stream)throws IOException {if (stream == null) {throw new IllegalArgumentException("stream == null!");}Iterator iter = getImageReaders(stream);if (!iter.hasNext()) {return null;}ImageReader reader = (ImageReader)iter.next();ImageReadParam param = reader.getDefaultReadParam();reader.setInput(stream, true, true);BufferedImage bi;try {bi = reader.read(0, param);} finally {reader.dispose();stream.close();}return bi;
}// com.sun.imageio.plugins.png.PNGImageReaderpublic BufferedImage read(int imageIndex, ImageReadParam param)throws IIOException {if (imageIndex != 0) {throw new IndexOutOfBoundsException("imageIndex != 0!");}readImage(param);return theImage;
}private void readImage(ImageReadParam param) throws IIOException {readMetadata();// 这里拿到的宽高,后续int width = metadata.IHDR_width;int height = metadata.IHDR_height;// Init default valuessourceXSubsampling = 1;sourceYSubsampling = 1;sourceMinProgressivePass = 0;sourceMaxProgressivePass = 6;sourceBands = null;destinationBands = null;destinationOffset = new Point(0, 0);......// 接下来准备生成 BufferedImage 对象,即theImage
}// 通过readHeader()获取图片宽高
private void readMetadata() throws IIOException {if (gotMetadata) {return;}readHeader();......
}// javax.imageio.ImageTypeSpecifier
// 在该方法中创建BufferedImage对象
public BufferedImage createBufferedImage(int width, int height) {try {SampleModel sampleModel = getSampleModel(width, height);WritableRaster raster =Raster.createWritableRaster(sampleModel,new Point(0, 0));return new BufferedImage(colorModel, raster,colorModel.isAlphaPremultiplied(),new Hashtable());} catch (NegativeArraySizeException e) {// Exception most likely thrown from a DataBuffer constructorthrow new IllegalArgumentException("Array size > Integer.MAX_VALUE!");}
}

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

// com.sun.imageio.plugins.png.PNGImageReader
public int getWidth(int imageIndex) throws IIOException {if (imageIndex != 0) {throw new IndexOutOfBoundsException("imageIndex != 0!");}readHeader();return metadata.IHDR_width;
}

对比两者的调用链路,可以发现通过 ImageReader 获取图片宽高的方式链路更短;除此之外,内存占用更少,所以更不容易产生内存问题。

OpenMP

一开始在 Mac 上尝试测试 OpenMP,反复鼓捣后还是失败了,归根结底是因为本机默认不支持 OpenMP,感兴趣的朋友可以参考在 macOS 平台上安装 OpenMP 库,试一试能否在 Mac 上测试 OpenMP。

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

测试

gm benchmark [ 选项... ] 命令

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

本次测试使用如下命令:

gm benchmark -iterations 100 -stepthreads 1 +原命令语句

-iterations 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
make
make install

然后进入图片所在目录,执行下述命令:

# gm benchmark -iterations 100 -stepthreads 1 convert -resize 100x100 -quality 90 +profile "*" mountain-landscape.jpg 123.jpg
Results: 1 threads 100 iter 52.41s user 52.747874s total 1.896 iter/s 1.908 iter/cpu 1.00 speedup 1.000 karp-flatt

结果中各参数含义如下:

  • threads- 使用的线程数。
  • iter - 执行的命令迭代次数。
  • user - 消耗的总用户时间。
  • total - 消耗的总时间。
  • iter/s - 每秒的命令迭代次数。
  • iter/cpu - 每次迭代消耗的 CPU 时间。
  • speedup - 与一个线程相比的加速。
  • karp-flatt - 加速效率的 Karp-Flatt 度量。

根据结果可知,处理一张图片耗时 524ms。

启用OpenMP

重新执行编译命令:

./configure --prefix=/hresh/tool/GraphicsMagick-1.3.37 --enable-shared --enable-openmp-slow
make
make install

然后进入图片所在目录,执行下述命令:

# export OMP_NUM_THREADS=2
# gm benchmark -iterations 100 -stepthreads 1 convert -resize 100x100 -quality 90 +profile "*" mountain-landscape.jpg 123.jpg
Results: 1 threads 100 iter 47.84s user 48.102332s total 2.079 iter/s 2.090 iter/cpu 1.00 speedup 1.000 karp-flatt
Results: 2 threads 100 iter 48.95s user 36.630871s total 2.730 iter/s 2.043 iter/cpu 1.31 speedup 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 代码

public static void addImgWatermark(String srcImagePath, String destImagePath,String waterImgPath) {System.out.println(Thread.currentThread().getName() + "开始生成图片水印。。。。。。。");try {// 原始图片信息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.dissolve(90);// 水印op2.addImage(waterImgPath);// 原图op2.addImage(srcImagePath);// 目标op2.addImage(destImagePath);ImageCommand cmd2 = getImageCommand(CommandType.imageWaterMark);cmd2.run(op2);} catch (Exception e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "成功生成图片水印。。。。。。。。。。");
}public static void main(String[] args) throws Exception {ExecutorService executorService = new ThreadPoolExecutor(20, 25, 30, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());try {for (int i = 1; i <= 16; i++) {executorService.execute(new ImageThread2());}} catch (Exception e) {e.printStackTrace();} finally {executorService.shutdown();}
}class ImageThread2 implements Runnable {@Overridepublic void run() {String projectPath = System.getProperty("user.dir");// 图片大小为7.9MString srcImgPath = projectPath + "/src/main/resources/static/sky.png";String waterImgPath = projectPath + "/src/main/resources/static/icon.png";String path = projectPath + "/src/main/resources/static/out/concurrency/im4_image.jpg";Im4JavaUtil.addImgWatermark(srcImgPath, path, waterImgPath);}
}

经过测试得出如下结果:GraphicsMagick 添加图片水印操作最多同时开启 16个线程。

Graphics2D 代码

public static void graphics2DDrawImg(String srcImgPath, String waterImgPath, String outPath) {System.out.println(Thread.currentThread().getName() + "开始生成图片水印。。。。。。。");try {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 icon = ImageIO.read(new File(waterImgPath));g.drawImage(icon, imgLeftMargin, imgTopMargin, icon.getWidth(),icon.getHeight(), null);FileOutputStream outImgStream = new FileOutputStream(outPath);ImageIO.write(bufferedImage, "jpg", outImgStream);g.dispose();outImgStream.close();} catch (IOException e) {e.getStackTrace();}System.out.println(Thread.currentThread().getName() + "成功生成图片水印。。。。。。。。。。");
}public static void main(String[] args) throws Exception {ExecutorService executorService = new ThreadPoolExecutor(20, 25, 30, TimeUnit.SECONDS,new LinkedBlockingDeque<>(5),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());try {for (int i = 1; i <= 8; i++) {executorService.execute(new ImageThread());}} catch (Exception e) {e.printStackTrace();} finally {executorService.shutdown();}
}class ImageThread implements Runnable {@Overridepublic void run() {String projectPath = System.getProperty("user.dir");String srcImgPath = projectPath + "/src/main/resources/static/sky.png";String waterImgPath = projectPath + "/src/main/resources/static/icon.png";String path = projectPath + "/src/main/resources/static/out/concurrency/g2d_image.jpg";Graphics2DUtil.graphics2DDrawImg(srcImgPath, waterImgPath, path);}
}

测试结果显示,Graphics2D 添加图片水印操作最多开启 7个线程。抛出 OOM 异常时截图如下:

综合来看,Graphics2D 是 Java 自带的图像处理工具类,处理图像时,与内存交互的操作比较频繁,加之会受到 JVM 的内存限制,所以更容易产生 OOM 异常。而 GraphicsMagick 进行图片处理时是直接读取图片到物理内存,不受 JVM 管理,所以更加安全一些

总结

目前市面上成熟的图像处理库:GraphicsMagick 和 OpenCV。上述两款图像处理库都可以做到跨平台,在多种编译器上执行,都可以很容易实现多进程模式,充分发挥多核 CPU 的优势。GraphicsMagick 是前段时间才接触使用,OpenCV 在学习 Python 时了解过,在 Python 中应用比较广泛。

目前我使用的是 Java 语言,而 JDK 自带的一套图片处理库——Graphics2D,它的特点是稳定简单,但是对图片处理来说,性能确实不好!不过 Java 方面也提供了类似 JNI 方式支持 GraphicsMagick+im4java 处理图像。但是要原生态支持 opencv 就比较繁琐了,要用 JNI 方式调用大量动态或静态库,存在如下两个问题:一个性能问题,二是如果出现内存问题也不好控制。

当然选用某一技术时要结合实际需要,性能好的不一定最好,合适自己的才是最好。就拿我遇到的项目来说,基本没有高并发的图片处理场景,加之使用 Graphics2D 实现起来比较简单,所以最终选择 Graphics2D,而非 GraphicsMagick+im4java。

GraphicsMagick之实践出真知相关推荐

  1. 反思供应链项目:实践出真知 多反思提升效率的方法

    获得的提升: 代码能力  沟通能力  思维能力  变通能力  使用代码工具的能力  知识面 都有了提升 得到的认知: 1.实践出真知 2.实际做了才是自己的,只是看明白了,不是自己的 3.加班加的也是 ...

  2. 实践出真知之Spring Cloud之基于Eureka、Ribbon、Feign的真实案例

    转载自  实践出真知之Spring Cloud之基于Eureka.Ribbon.Feign的真实案例 Eureka是Spring Cloud Eureka的简称,是Netflix提供的组件之一.通过E ...

  3. qnap raid5升级raid6_实践出真知!100TB的RAID5到底能否重建成功?

    实践出真知!100TB的RAID5到底能否重建成功? 2019-04-04 18:40:37 390点赞 996收藏 419评论 小编注:此篇文章来自即可瓜分10万金币,周边好礼达标就有,邀新任务奖励 ...

  4. 「实践出真知」如何打造一流的视觉AI技术

    分享嘉宾:邓亚峰 格灵深瞳 CTO 内容来源:AI先行者大会<如何打造一流的视觉AI技术> 出品社区:DataFun 注:我爱计算机视觉获官方授权发布 本次分享主要分以下几个部分:首先简要 ...

  5. 实践出真知:博云微服务经验之避坑指南

    目前每个企业都想做微服务,但如何做好微服务?微服务改造过程中有哪些必须重视的问题?博云通过自己的实践,总结了一些经验之谈.日前InfoQ对博云高级解决方案架构师赵安全就此话题进行了专访,以兹各位对微服 ...

  6. 实践出真知:全网最强秒杀系统架构解密!!

    很多小伙伴反馈说,高并发专题学了那么久,但是,在真正做项目时,仍然不知道如何下手处理高并发业务场景!甚至很多小伙伴仍然停留在只是简单的提供接口(CRUD)阶段,不知道学习的并发知识如何运用到实际项目中 ...

  7. 2021-9-1 unity实践出真知

    文章目录 对比前一天没看任何教程做的东西,看了教程的我顿悟了!(涉及多个高能知识点,建议保存) 关于绘制地图 昨天 今天 关于摄像机跟随 昨天 今天 关于2D游戏中的UI 昨天 今天 关于动画状态机的 ...

  8. DevOps方法论掌握这四点,实践出真知

    01. 需求管理模型和敏稳双态开发 在研发产品之前,我们都需要先了解客户的需求.常见的需求理论模型有三种,可基于不同业务和产品复杂度的需求层次结构进行选择. 简单的业务和产品:拆分成两层,产品需求➡技 ...

  9. Unity3D网络游戏实战——实践出真知:大乱斗游戏

    前言 这一章是教我们做一个大乱斗游戏.但是书中的代码有些前后不一致导致运行错误,如果你也碰到了这样的情况,可以参考我的代码 我们要完成的主要有以下这些事 左键操控角色行走 右键操控角色攻击 受到攻击掉 ...

最新文章

  1. 2021年大数据ELK(六):安装Elasticsearch
  2. MySQL内部执行流程
  3. 除 Intel Realsense Dxxx 外 各市面深度摄像头对比(小觅智能 D1000-IR-120/Color、INDEMIND、领晰(LEADSENSE))(212)
  4. Libevent事件的创建-scoke服务的创建-特征的获取和配置
  5. javascript基础入门_javascript基础入门学习第一篇
  6. python语言有哪些类型的运算符_python(4)-变量 数据类型和运算符
  7. Nginx的应用之动静分离
  8. java 递归调整为队列
  9. 基于PaddleRec的用户点击率预测
  10. 拓展 欧几里得算法 求逆元_ECC椭圆曲线加密算法:有限域和离散对数
  11. Linux select/poll/epoll
  12. 阵列信号处理仿真一——延时求和滤波器
  13. 空调基础知识培训课件
  14. 马哥python培训视频
  15. 6取余11c语言,中国剩余定理“大衍求一术”手算方法及四个习题
  16. PHP删除多选checkbox,php一次性删除前台checkbox多选内容的简单示例
  17. 【卫星影像三维重建】完整的卫星立体重建
  18. 【Pandas总结】第十节 Pandas 合并数据集_pd.pivot_table()
  19. 房屋租赁合同可不可以用笔修改
  20. PDF编辑时怎样给PDF文件添加页码

热门文章

  1. 失传百年的致富经典(一):投资真经(股票,债券,基金)
  2. 苹果电脑能装鸿蒙,纯小白必看!鸿蒙编译及烧录环境分开部署For Mac
  3. 一年经验大数据开发网易游戏社招面经(已拿offer)
  4. VSCode代码风格笔记(Vetur)
  5. 怎么把计算机原有用户数据删除,电脑怎么清除数据
  6. 【锐捷交换机】路由器/交换机连接及问题排查
  7. 李乐逸前端的学习内容 HTML篇
  8. c++如何控制键盘输入
  9. 科技哲学学期要点归纳
  10. AT89C51单片机项目——秒表系统