将JPG图像根据色彩范围转换为PNG透明图像(kotlin)

简介: 这实际上是一个十分普遍的需求,在kotlin中如何完成这一任务呢?其实这样简单的操作不需要任何三方库,只需要BufferedImage的原生功能就能做到,约60行代码

这实际上是一个十分普遍的需求,在kotlin中如何完成这一任务呢?其实这样简单的操作不需要任何三方库,只需要BufferedImage的原生功能就能做到,约60行代码,首先我们准备一张封面这样的图,截选成正方形的(因为封面图必须是长条的所以我没传正方形的图),命名为zx.jpg,放到resources中,然后正常的读取:

读取图像

fun main() {
    val url = System::javaClass.javaClass.getResource("/zx.jpg")!!
    val jpg = ImageIO.read(url)
    val width = jpg.width
    val height = jpg.height
    println("$width,$height")
}

能打印出宽高,则说明读取成功,代码很好理解,先找到resources目录里zx,jpg的url,这个是必须被找到的,不然后面全都无法进行,所以后面应该加上!!,防止类型变成URL?后面还要判空,然后用ImageIO去读取图片。

处理图像

接下来就是核心的部分,根据宽高建立png的空白BufferedImage,然后把jpg写到png中:

val png = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
for (x in 0 until width) {
    for (y in 0 until height) {
        if (jpg.getRGB(x, y) == -1) {
            png.setRGB(x, y, 0)
            continue
        }
        png.setRGB(x, y, jpg.getRGB(x, y))
    }
}

这里颜色模式要选TYPE_INT_ARGB,因为要支持透明度。根据宽高循环所有像素自不必说,判断色彩如果是-1,也就是纯白色,就可以直接设为Color(0, 0, 0, 0).rgb,也就是0,就是透明色啦。

接下来写出图像就可以了:

val file = File(File(url.toURI()).parent, "out.png")
file.createNewFile()
ImageIO.write(png, "PNG", file)

我为了方便对比图像直接写到同级目录了,要到build\resources\main目录里去找这个out.png,写出图像后可以看到如下效果:

image.png

可以看到人物的头发边和手边有很多白色区域,为什么没有被替换成透明呢?其实这就是所谓的“电子包浆”,是jpg的一大特色。因为jpg各种压缩导致图片会有很多浅色噪点,看着是纯白色,实际上是浅白,稍微带点深色的。

使用色彩范围

这时候只能试一试设置一个色彩范围了,这张图我将低于0x11000的色彩区域的都设为透明,这个数值也可以称为tolerance(容差值),只需要修改下循环中的这个判断就好了:

for (x in 0 until width) {
    for (y in 0 until height) {
        if (jpg.getRGB(x, y) > -0x11000) {
            png.setRGB(x, y, 0)
            continue
        }
        png.setRGB(x, y, jpg.getRGB(x, y))
    }
}

然后就能看到这样的效果:

image.png

但这样一看,更不行了,因为面部和头发中的一些亮点也很白,导致被一起替换透明了,而边缘的色彩还没完全笼盖进去,还有相当多的白边,这是两头不讨好了。

这时候不妨想一想,中间的部分和背景到底有哪些区别?人物的主体部分不会有白色?当然不能这样假设,其实唯一的区别就是背景与主体是阻隔开的。既然是从左往右,从上到下扫描图像,那么就这样想,如果左边上边都是透明了,那这块就也透明掉,不然就说明之间被人物阻隔开了,就不要去管。

为了方便阅读,我多加了一个分支来判断扫描区域是背景还是人物:

依照扫描区域替换

for (x in 0 until width) {
    for (y in 0 until height) {
        if (jpg.getRGB(x, y) > -0xc0000) {
            if (x < 1 || y < 1
                || png.getRGB(x - 1, y) == 0
                || png.getRGB(x, y - 1) == 0
            ) {
                png.setRGB(x, y, 0)
                continue
            }
        }
        png.setRGB(x, y, jpg.getRGB(x, y))
    }
}

因为不用担心替换到人物区域了,我将容差值进一步提高到c0000

image.png

可以看到左上角的白边基本没有了,但左下角的还是有,这时候需要回溯扫描一下。

回溯扫描

这个过程并非一言两语就能说清楚,首先看看现在这样扫描的盲区在哪里,先画一个简单的像素图:

image.png

粉点是图像人物区域,蓝点是上述方法的扫描区域,灰点就是漏掉的两块灰色盲区,现在希望的是能将这些灰色盲区在回溯时能扫描进来。也就是原本是从左上角往右下角扫描的,但遇到一定的情况,需要再从右下辗转回到左上方,那么这个情况应该怎么判断呢?

首要的任务就是找到到需要回溯的点,如下图所示:

image.png

这4个绿色的像素点,就是需要检测出来的,它有什么特质呢?可以观察他们附近蓝点的扫描方向,一个是从左边扫到右边时,漏掉了上面的绿点。一个是从上面扫到下面时,漏掉了左边的绿点。所以只需要根据这个条件来找即可,也就是把循环中的条件改成这样:

if (jpg.getRGB(x, y) > -0xc0000) {
    if (x < 1 || y < 1) {
        png.setRGB(x, y, 0)
        continue
    } else if (png.getRGB(x - 1, y) == 0) {
        png.setRGB(x, y, 0)
        if (png.getRGB(x, y - 1) != 0) {
            //向上边往回找
        }
        continue
    } else if (png.getRGB(x, y - 1) == 0) {
        png.setRGB(x, y, 0)
        if (png.getRGB(x - 1, y) != 0) {
            //向左边往回找
        }
        continue
    }
}
png.setRGB(x, y, jpg.getRGB(x, y))

如代码所示,就是把以前的判断拆开,下一步就是继续往前找,如图所示:

image.png

到这一步已经判定出图上这个绿点是一个回溯口,以这个点切入往上一直往回找了,但如果接下来也一直都只是“向上边往回找”这一个方向,显然是不行的,因为棕色区域就没办法找到了,所以应当是除了反方向的另外三个方向都要找,也就是3个红点的位置,都要检查检查,这样才不会漏掉各种角落。

说到要按方向找,那就应当定义一个枚举了:

enum class Direction {
    Left, Right, Up, Down
}

这个枚举就方便写逻辑时理清楚方向,不然直接用数值代表方向太难懂了,要用字符串表示也有点浪费空间。

然后就考虑定义一个回溯查找的方法,它应当是递归的,因为寻找没有可预期的尽头,要一直找到没有可以设置透明的蓝点为止,那么它最基本的需要一个坐标,有了坐标才知道当前找到哪了。此外还需要一个方向,不能再往回找已经回溯过的点位,不然就成瞎找了,四处乱找。

综上所述这个方法只需要避免重复去往回找来时的方向就行,这个方法要放到循环之前,这里有小提示,kotlin是可以方法里面套方法的,就像千层饼一层套一层。具体逻辑很易懂:

fun backtrack(ix: Int, iy: Int, direction: Direction) {
    if (png.getRGB(ix, iy) == 0 || png.getRGB(ix, iy) <= -0xc0000) {
        return
    }
    png.setRGB(ix, iy, 0)
    if (direction != Direction.Left) {
        backtrack(ix + 1, iy, Direction.Right)
    }
    if (direction != Direction.Right) {
        backtrack(ix - 1, iy, Direction.Left)
    }
    if (direction != Direction.Up) {
        backtrack(ix, iy + 1, Direction.Down)
    }
    if (direction != Direction.Down) {
        backtrack(ix, iy - 1, Direction.Up)
    }
}

可以说一目了然,第一个判断来看这个点位是不是命中的蓝点,也就是尚未被设置透明但应该被设为透明的点。过了这个判断就说明命中了,直接设为透明。然后看看是哪个方向找过来的,就不要找反方向的点位,其余3个点位全都要去找一找。

然后应用到上面的循环里就可以了:

if (x < 1 || y < 1) {
    png.setRGB(x, y, 0)
    continue
} else if (png.getRGB(x - 1, y) == 0) {
    png.setRGB(x, y, 0)
    if (png.getRGB(x, y - 1) != 0) {
        backtrack(x, y - 1, Direction.Up)
    }
    continue
} else if (png.getRGB(x, y - 1) == 0) {
    png.setRGB(x, y, 0)
    if (png.getRGB(x - 1, y) != 0) {
        backtrack(x - 1, y, Direction.Left)
    }
    continue
}

再运行一次,结果栈溢出了:

image.png

这个呀其实并非是因为使用了太多内存,而是因为递归的次数太多了,同时默认的单个线程栈空间又太小了,只有1MB,这次个扫描又恰好超出了一点点,所以只需要加上VM参数:-Xss2m,调为2M就可以跑了,当然只要线程不多,现在的电脑内存条少说也是十几GB,给-Xss调个128m也不算过分,内存占用基本是看不出多大变化的。

在IDEA中如下图操作:

image.png

然后就能看到效果了,可以说是相当完美:

image.png

综合源代码

import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO

enum class Direction {
    Left, Right, Up, Down
}

fun main() {
    val url = System::javaClass.javaClass.getResource("/zx.jpg")!!
    val jpg = ImageIO.read(url)
    val width = jpg.width
    val height = jpg.height
    val png = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB)
    val tolerance = -0xc0000
    fun backtrack(ix: Int, iy: Int, direction: Direction) {
        if (png.getRGB(ix, iy) == 0 || png.getRGB(ix, iy) <= tolerance) {
            return
        }
        png.setRGB(ix, iy, 0)
        if (direction != Direction.Left) {
            backtrack(ix + 1, iy, Direction.Right)
        }
        if (direction != Direction.Right) {
            backtrack(ix - 1, iy, Direction.Left)
        }
        if (direction != Direction.Up) {
            backtrack(ix, iy + 1, Direction.Down)
        }
        if (direction != Direction.Down) {
            backtrack(ix, iy - 1, Direction.Up)
        }
    }
    for (x in 0 until width) {
        for (y in 0 until height) {
            if (jpg.getRGB(x, y) > tolerance) {
                if (x < 1 || y < 1) {
                    png.setRGB(x, y, 0)
                    continue
                } else if (png.getRGB(x - 1, y) == 0) {
                    png.setRGB(x, y, 0)
                    if (png.getRGB(x, y - 1) != 0) {
                        backtrack(x, y - 1, Direction.Up)
                    }
                    continue
                } else if (png.getRGB(x, y - 1) == 0) {
                    png.setRGB(x, y, 0)
                    if (png.getRGB(x - 1, y) != 0) {
                        backtrack(x - 1, y, Direction.Left)
                    }
                    continue
                }
            }
            png.setRGB(x, y, jpg.getRGB(x, y))
        }
    }
    val file = File(File(url.toURI()).parent, "out.png")
    file.createNewFile()
    ImageIO.write(png, "PNG", file)
}

小结

这只是一个非常简单的小应用,因为想写的便于阅读和理解,所以有好多可以优化的地方。扫描过程其实非常粗糙,会扫到很多重复的点位,所以后面我会再写一篇文章谈一谈类似任务中更复杂的场景,会顺带再优化一下这个整体的过程。

本文写作于2023年5月31日并发布于lyrieek的掘金,于2023年7月17日进行修订发布于lyrieek的阿里云开发者社区。

目录
相关文章
|
5月前
|
Web App开发 XML 存储
一篇文章讲明白JPG、PNG、GIF、SVG等格式图片区别
一篇文章讲明白JPG、PNG、GIF、SVG等格式图片区别
|
5月前
|
存储 编解码 API
【图像文本化】Base64编解码OpenCV4中 Mat 对象
【图像文本化】Base64编解码OpenCV4中 Mat 对象
79 0
|
人工智能
将 JPEG 和 PNG 位图转换为 SVG 矢量图,可无限放大
将 JPEG 和 PNG 位图转换为 SVG 矢量图,可无限放大
308 0
|
存储 传感器 计算机视觉
CR2转PNG格式图像转换器
CR2是指由佳能公司开发的一种数字相机RAW图像格式,它存储了相机直接从图像传感器中读取的未经处理的图像数据。这种格式的图像通常比JPEG格式的图像更高质量,因为它们捕捉到了更多的细节和颜色深度,但它们也需要更多的后期处理才能得到最终的图像。
325 0
|
编解码 前端开发 搜索推荐
Python3.7将普通图片(png)转换为SVG图片格式并且让你的网站Logo(图标)从此”动”起来
在之前的几篇文章中,介绍了业界中比较火爆的图片技术SVG(Scalable Vector Graphics),比如[Iconfont(矢量图标)+iconmoon(图标svg互转)配合javascript来打造属于自己的个性化社交分享系统](https://v3u.cn/a_id_143),我们可以使用svg来打造精美炫酷的分享小图标(icon),这一次我们使用python来将普通的静态的网站logo图片转换为带路径(path)的svg图片,这样就可以让网站logo能够变成动态的,作为一名不折腾不舒服斯基,一枚炫酷自带动画的网站logo自然能够满足我们的折腾欲,同时亦能击中我们的虚荣心。
Python3.7将普通图片(png)转换为SVG图片格式并且让你的网站Logo(图标)从此”动”起来
|
Python
Python 技术篇-用PIL库修改图片透明度实例演示,改变png图片色道为RGBA、RGB
Python 技术篇-用PIL库修改图片透明度实例演示,改变png图片色道为RGBA、RGB
965 0
Python 技术篇-用PIL库修改图片透明度实例演示,改变png图片色道为RGBA、RGB
PNG格式图片常见转换方法
前言 最近碰到一个需要将图片由原始的PNG转化为JPG的需求,由于PNG图片本身质量等原因,导致转化为JPG之后,存在失真的问题,后来一个同事分享了下述的HighQualityPNGToJPG方法解决PNG转JPG失真的问题。
2045 0
|
存储 C#
[开源]基于WPF实现的Gif图片分割器,提取GIf图片中的每一帧
原文:[开源]基于WPF实现的Gif图片分割器,提取GIf图片中的每一帧   不知不觉又半个月没有更新博客了,今天终于抽出点时间,来分享一下前段时间的成果。   在网上,我们经常看到各种各样的图片,尤其是GIF图片的动态效果,让整个网站更加富有表现力!有时候,我们看到一些比较好看的GIF图片或者一些奇特的Gif图片,我们想要停留在某一帧看的清楚一点或者了解这个Gif动画到底是怎么实现的,怀着这种好奇的心理,我们来看一下,今天的开源项目,用WPF来实现GIF图片的预览和分离和保存。
1190 0
|
算法 计算机视觉
使用OpenCV+C++将Gif文件分解并且转换为视频文件
原文链接 目标: 认识 Gif 利用 FreeImage 将Gif解析为 Mat; 利用 FreeImage 获取多帧Gif图像; 将获取的多帧图像保存,并利用OpenCv生成为视频文件。
2774 0
|
Web App开发 前端开发
Canvas合成自定义Gif图
上次介绍了Canvas自定义图片大小及蒙版与生成gif图,没有详细的说明如何生成gif图,生成gif图的过程也可以进行一些优化。 我们无需借助第三方库,直接使用canvas原生的api就可以完成很多的功能。