Java图像缩放

本文详细介绍了Java中图像缩放的原理与实现方法,包括几何变换、插值算法、不同实现库的比较,以及放大和缩小的效果分析,强调了没有最佳算法,需要在平滑与细节保留之间取得平衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

         图像的缩放(scale)是图像处理里很常见的运算,太常见了以致于我们根本不会认为它有什么特别的地方。比如我们在HTML里用<img> 加载一幅图片,然后指定它的宽度和高度,浏览器就自动帮我们把图片缩放到了合适的尺寸。看起来这应该是已经很简单的事情,不过仔细想想就会发现没有想象的那么简单。事实上,不存在”最优“的缩放算法。对于所有的图片,很难说某种算法是最好的。

         首先我们来考虑放大一个图片,考虑最简单的情况,把2个像素的图片放到2倍成4个像素的图片,像素2和4的值对应原来图片像素1和2的值,那么新增的像素 1和3呢?直觉的,你会想到像素2的值是原来像素1,像素3是原来像素2。为什么可以这样做呢?我们刚才做的事情用术语来说就是“插值 (interpolation)",因为像素的值的物理意义比如亮度是连续的量,所以可以用最接近的点来预测当前点的情况。除了最简单的最近邻 (NEAREST NEIGHBOR),常用的还包括线性插值,三次插值等等。

         相反的,我们考虑缩小一幅图片,比如把4个像素点缩小到2个像素点,那么缩放后像素1的值到底是取原来像素1的值还是原来像素2的值呢?或者取它们的平均值?如果像素1和2相同,那么取1或2和取平均值没有任何区别,如果像素1和2差别很大呢?那么取平均值其实就是在做平滑(smoothing),从频率角度分析,就是低通滤波,也就是把高频的信息去掉了。什么是高频?通俗的说就是变化剧烈的。比如图片的边缘,一般都是颜色变化较大的地方(很多边缘抽取的算法都会用到差分/微分,其实就是变化大的点)。这样的话图片看起来比较平滑,但是同时也可能损失一些对比度。

         另外我们考虑的缩放都是整数倍的,如果不是整数倍呢?那么也需要插值。从上面的例子可以发现,缩放一幅图片并不如想象中的那么容易。下面形式化的介绍图片缩放的流程。

几何变换(Geometrical Transformation)

         常见的变换包括仿射变换(_AffineTransform),投影变换(_projection transformation),和非线性的变换(比如双线性变换)

  1.    仿射变换
    包括平移,旋转和缩放,它的特点是直线经过仿射变换还是直线,仿射变化不改变两点间的距离,平行的直线变换后还是平行的直线。3个点可以确定变换的所有参数。所有正方形仿射变换后变成了平行四边形。
  2.    投影变换
    会改变两点的距离,正方形变换成普通四边形,它可以由4个点确定变换参数。
  3.    非线性变换

          我们缩放就属于第一种仿射变换。

源->目标 or 目标->源

        上面讨论的都加上图像是个连续的二元函数,但实际上在计算机里都是数字化的图像,也就是连续图像采样后的结果。

         源->目标

                把源图像的点通过变换函数映射到目标图像上,由于是连续的函数,所以新的坐标可能不是整数,所以常见的做法是把这个点挪到最接近的整坐标点上。它的缺点是有些点不会出现在目标图片上(浪费计算);

         目标->源

                对目标图片的每个点,通过逆变换找到源图片的点,如果这个点不是整数,那么需要通过周围的整数插值。

         一般都使用第二种方式。

插值(interpolation)

         插值可以说是缩放的核心。常见的插值方法包括最近邻插值,线性插值和3次样条插值。由于图像是2维的,所以对应的就是最近邻插值(这个与维度无关了),BILINEAR(翻译成双线性?),BICUBIC。

         最近邻和Bi-Linear没什么好说的,主要说说BI-Cubic。

        在说它之前先得说说离散信号的采样和重建。

        根据奈奎斯特(Harry Nyquist)采样定理,一个带限的连续信号,如果带宽小于采样频率/2的话,那么原始信号可以由采样后的离散信号完全重建出来。重建时会用到sinc函数 ,它的频率响应是个矩形窗。理想情况下应该用它来插值。sinc函数是延展到无穷的,所以一幅图片当前点的值依赖于所有点的值,不过sinc函数随着x的增大衰减的非常快,一般到了第3个旁瓣后就基本等于0了。sinc函数是一个理想的低通滤波器,它可以很好的保证图片缩放是的平滑,但是它的缺点是图片的剧烈变化也会被它平滑掉。与此对应,最近邻插值的时域是一个分段的线段(矩形窗),它能够较好的反应图片的变化,但它的去点是连续的区域变得不平滑了。所以从理论上来说,没有一种算法是”最好“的。好像也有基于图像内容的去重(http://www.semanticmetadata.net/2007/09/20/content-based-image-resizing-update-to-v2/),semanticmetadata这个域名里有不少多媒体搜索有趣的东西,包括我们用过的Lire,其实用的是Caliph & Emir,用它来提取图像的特征。

        实际应用是一般是采取折中的方案,既需要保证连续区域的平滑,也要尽量保持变化的信息。常见的就是用分段三次函数来”模拟"sinc函数,常见的Bi- cubic就是其中的一种,而且即使bi-cubic,也会有一个参数来控制上面两个要求的权衡,默认值是1,不过我看书《数字图像处理:Java语言实现》里说,取0.5可能更好。不过一般的实现都不能修改这个参数。

        类似的函数还包括Lanczos 函数。

实现

        上面说了一些基本的理论,当然我们可以自己造轮子,不过有现成的还是最好用现成的吧。(个人看法:即使自己不造轮子,也应该知道轮子的构造原理,这样才能用得好)

   awt.Image / Java2D

                 可能Java程序员最熟悉的方式,因为Java1.0里就有的功能。Image.getScaledInstance(),这是老的API,与它对应的是Graphics.drawImage()。关于它的用法,我找到的最好的资料就是Chris Campbell的博客。它的优点是效果还不错,不过速度比较慢。不过后面提到的2分的方法能加速不少,可以看上面的博客,里面有时间对比实验。

      这个API的参数

  • VALUE_INTERPOLATION_NEAREST_NEIGHBOR: Specific hint that provides higher performance, but lower-quality, "blocky" results.
  • VALUE_INTERPOLATION_BILINEAR: Specific hint that is typically a bit slower, but provides higher-quality, "filtered" results.
  • VALUE_INTERPOLATION_BICUBIC

       从名字可以看出它的插值算法。

       虽然这个API很老,但是缩放的质量还是不错的(我指的是使用Bi-cubic插值),不过效率差了点。Campbell在那篇blog里给出了一种解决方法,比如要给一幅图片放到4.5倍,那么先给它两次放到2倍,这样得到4倍的图像,然后再放大4.5/4=1.125倍。放到或者缩小2倍是不用插值的 (或者说插值非常简单)。

       使用代码示例:

	public void scale(String inPath, String outPath, float scale) {
		try{
			Image image = ImageIO.read(new File(inPath));
			int w=(int)(image.getWidth(null)*scale);
			int h=(int)(image.getHeight(null)*scale);

			BufferedImage tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
	        Graphics2D g2 = tmp.createGraphics();
	        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
	        g2.drawImage(image, 0, 0, w, h, null);
	        g2.dispose();
	        FileOutputStream newimage = new FileOutputStream(outPath);
	        ImageIO.write(tmp, "JPEG", newimage);
		}catch(Exception e){
			e.printStackTrace();
		}
	}

把图片2倍2倍的缩放的代码原文的链接里提供了,而且后面的Nobel Joergensen的工具包里也封装了一个。这里就不贴代码了。

    JAI

         Sun提供的一个图像处理的包,用的人还比较多,封装的API使用起来比较方便,这是一个入门的JAI Tutorial

         注意:不同操作系统需要安装不同的实现,因为它会利用不同的硬件做加速。不过jar包是统一的。

         使用代码如下:

		PlanarImage input = JAI.create("fileload", inPath);
		ParameterBlock pb = new ParameterBlock();
		pb.addSource(input);
		pb.add(scale);
		pb.add(scale);
		pb.add(0.0f);
		pb.add(0.0f);
		pb.add(new InterpolationBicubic(8)); //可以使用最近邻插值或者线性插值,具体参考JAI 文档
		PlanarImage scaledImage = JAI.create("scale", pb);
		JAI.create("filestore", scaledImage, outPath, "JPEG");

          不过它的做缩小的效果并不好,关于它的讨论可以参考这个帖子 ,另外下面会实际比较一下不同方法的效果。

    JMagick

          ImageMagick的JNI接口。由于JNI的实现,使用起来不太方便,所以没有深入尝试。网上它的资料还是比较多的。

    ImageJ

          这个图像处理库应用比较广泛,美国国立卫生研究院(NIH)支持下开发的。首先它是一个类似于Photoshop的图像处理软件,然后也提供插件是的java开发以及jar包支持嵌入。

          图像缩放的功能在有插值插件(interpolation plugin)实现,默认只实现常见的最近邻,bilinear和bicubic。另外有一些第三方的实现了类似lanczos插值,这个链接就是一个ImageJ的插件,看起来很有趣,不过我没试过。

    Java-Image-Scaling

          它的说明参考作者的blog  。作者实现了3阶的Lanczos插值,并认为它的效果是最好的。不过《数字图像处理:Java语言实现》一书作者认为:虽然Lanczos最近引起了广泛的关注,不过它的效果也好得有限(虽然可能有一些好处),一般常用的还是Bi-Cubic,比如著名的PhotoShop软件,不过它对放大和缩小提供了不同的参数;据该blog作者说,Linux下著名的GIMP软件使用的是Lanczos插值。另外作者在文章里比较了不同算法的效果,不过我肉眼凡胎的,实在没看出那些tomato有什么区别。

    ImageResize4J

        商业软件

    ThumbMaster

        商业软件。其实很不值得一提。我昨天下载想试试效果,结果根本不能用,说过期了。没办法,我反编译了一下代码,只有两个类。放大使用的就是Bi-cubic算法,缩小使用的是Lanczos算法,但是根本不能用。引用一下论坛里别人的话(http://forums.sun.com/thread.jspa?threadID=5332078):

Resizing images using JAI instead of getScaledInstance will give much better performance, and allows all of the power of the JAI APIs to be leveraged (native acceleration, tiling, border extenders, etc.). For more information, and a download of the software, visit http://www.devella.net/thumbmaster.

I highly doubt this. The native acceleration you speak of mostly refers to saving and loading images. The scaling on the fly approach in my post above most definetly uses the graphics card for fast scaling.

As it is, a user of JAI can get the same scaling quality as getScaledInstance very simply by the following code.

RenderingHints qualityHints = new RenderingHints(
    RenderingHints.KEY_RENDERING,
    RenderingHints.VALUE_RENDER_QUALITY);

RenderedOp resizedImage =
            JAI.create("SubsampleAverage",paramBlock, qualityHints);



I surmise that your ThumbMaster is no better, except you're asking 50 dollars for it.


简单的比较

       放大

      

     
                                                                             
      5个图片分别是JAI最近邻插值,Bi-Cubic插值,Lanczos插值和多次2分插值,和原始图片。放大的倍数是2.5

      仔细观察,可以发现最近邻插值确实不好(虽然较快),床的边缘都是模糊的,Bi-Cubic的效果不错,Lanczos在细微的地方(比如床头靠背上的纹路)感觉好一点。不过差别不大。

    缩小

 算法的顺序和放大一样,分别是最近邻插值,Bi-Cubic,Lanczos和2分的。

 可以看到,最近邻插值的花瓣都不太平滑,有锯齿的感觉。另外花蕊也很模糊,看不出线来。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值