JAVA证件照抠图-算法版

该代码实现了一个证件照抠图的Java工具类,主要步骤包括灰度转换、自适应二值化、边界查找和内容填充。适用于白底证件照,其他底色需调整。采用边界跟踪算法进行内容识别,洪泛填充法进行背景替换,处理后可能存在白边,可能需要后期优化。

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

原理:

1、灰度(暂时不使用,实际使用效果不佳)

2、二值化(使用直方图双峰确定阈值)

3、边界查找

4、内容填充

注意:本demo调试适用与证件照白底,其他底图颜色需要根据实际情况更换或调试第三步之前的算法

package xxx.utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.Objects;

/**
 * 证件照抠图
 * 暂适用于白底证件照,其他底色请自行调整灰度和二值化
 *
 * @author xwt
 * @date 2023/4/20 10:35
 */
public class IdPhotoMattingUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(IdPhotoMattingUtils.class);

    private static final int[][] DOMAIN = {{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1},{0,1},{1,1}};

    private IdPhotoMattingUtils() {
    }

    /***
     * 证件照抠图
     * @param srcImage 证件照图像
     * @return 抠图后图像
     */
    public static BufferedImage mat(BufferedImage srcImage){
        if(srcImage == null){
            LOGGER.warn("证件照为空");
            return null;
        }
        BufferedImage image = srcImage;
        // 1.灰度
        image = imgGray(image);
        // 2、二值化
        double triangle = triangle(image);
        image = gray(image, triangle);
        // 3、边界查找
        int[][] imgIndex = borderTracking(image);
        // 查看是否扫描完成
        int i = 0;
        boolean b = true;
        for (int[] index : imgIndex) {
            for (int in : index) {
                i = i + in;
                if(i > 300){
                    b = false;
                    break;
                }
            }
            if(i > 300){
                break;
            }
        }
        if(b){
            LOGGER.warn("证件照无内容");
            return null;
        }
        // 4、内容填充
        return floodFill(srcImage, imgIndex);
    }

    /***
     * 灰度图像
     * @param imgSrc 原始图像
     * @return 灰度图像
     */
    public static BufferedImage imgGray(BufferedImage imgSrc) {
        //创建一个灰度模式的图片
        int width = imgSrc.getWidth();
        int height = imgSrc.getHeight();
        BufferedImage back = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
        //Graphics graphics = back.getGraphics();
        for (int j = 0; j < height; j++) {
            for (int i = 0; i < width; i++) {
                back.setRGB(i,j, imgSrc.getRGB(i, j));
            }
        }
        return back;
    }

    /***
     * 二值化自适应阈值
     * 原理:
     * 1.图像转灰度
     * 2.计算图像灰度直方图
     * 3.寻找直方图中两侧边界
     * 4.寻找直方图最大值
     * 5.检测是否最大波峰在亮的一侧,否则翻转
     * 6.计算阈值得到阈值T,如果翻转则255-T
     * @param image 灰度图像
     * @return 二值化自适应阈值
     */
    public static double triangle(BufferedImage image) {
        int i, j;
        int temp;
        boolean isflipped = false;
        int width = image.getWidth();
        int height = image.getHeight();
        int[] histogram = new int[256];
        //遍历灰度图像,统计灰度级的个数
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                int value = ((image.getRGB(x, y)) & 0xff0000) >> 16;
                histogram[value]++;
            }
        }
        //3. 寻找直方图中两侧边界
        int leftBound = 0;
        int rightBound = 0;
        int max = 0;
        int maxIndex = 0;

        //左侧为零的位置
        for(i = 0; i<256; i++) {
            if(histogram[i]>0) {
                leftBound = i;
                break;
            }
        }
        //直方图为零的位置
        if(leftBound > 0) {
            leftBound --;
        }


        //直方图右侧为零的位置
        for(i = 255; i>0; i--) {
            if(histogram[i]>0) {
                rightBound = i;
                break;
            }
        }
        //直方图为零的地方
        if(rightBound >0) {
            rightBound++;
        }

        //4. 寻找直方图最大值
        for(i = 0; i<256;i++) {
            if(histogram[i] > max) {
                max = histogram[i];
                maxIndex = i;
            }
        }
        //判断最大值是否在最左侧,如果是则不用翻转
        //因为三角法二值化只能适用于最大值在最右侧
        if(maxIndex - leftBound  < rightBound - maxIndex) {
            isflipped = true;
            i = 0;
            j = 255;
            while( i < j ) {
                // 左右交换
                temp = histogram[i]; histogram[i] = histogram[j]; histogram[j] = temp;
                i++; j--;
            }
            leftBound = 255-rightBound;
            maxIndex = 255-maxIndex;
        }

        // 计算求得阈值
        double thresh = leftBound;
        double a, b, dist = 0, tempdist;
        a = max; b = leftBound-maxIndex;
        for( i = leftBound+1; i <= maxIndex; i++ ) {
            // 计算距离 - 不需要真正计算
            tempdist = a*i + b*histogram[i];
            if( tempdist > dist) {
                dist = tempdist;
                thresh = i;
            }
        }
        thresh--;

        // 对已经得到的阈值T,如果前面已经翻转了,则阈值要用255-T
        if( isflipped ) {
            thresh = 255 - thresh;
        }
        return thresh;

    }

    /***
     * 图像二值化
     * @param b 灰度图像
     * @param triangle 阈值
     * @return 二值化图像
     */
    public static BufferedImage gray(BufferedImage b, double triangle){
        int width = b.getWidth();
        int height =b.getHeight();
        // 下面这个别忘了定义,不然会出错
        BufferedImage bufferedImageEnd = new BufferedImage(width,height, BufferedImage.TYPE_3BYTE_BGR );
        // 双层循环更改图片的RGB值,把得到的灰度值存到bufferedImage_end中,然后返回bufferedImage_end
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                // 获取到(x,y)此像素点的Colo,转化为灰度
                Color color = new Color(b.getRGB(x,y));
                double gray = (int)(color.getRed() * 0.299 + color.getGreen() * 0.587 + color.getBlue() *0.114);
                int r;
                if (gray <= triangle){
                    r = 0;
                }else{
                    r = 255;
                }
                bufferedImageEnd.setRGB(x, y, new Color(r,r,r).getRGB());
            }
        }
        return bufferedImageEnd;
    }

    /***
     * 边界跟踪算法之内边界跟踪
     * ps:存在特殊性,证件照可以完成,其他图像需要考虑是否可以正常返回起始点
     * 参考文档: https://blog.csdn.net/hanshanbuleng/article/details/84639433
     * @return 图像
     */
    public static int[][] borderTracking(BufferedImage image) {
        // 图像的行数
        int numRows = image.getHeight();
        // 图像列数
        int numCols = image.getWidth();
        // 全局-表示图像中每个像素是否已经被访问过,0表示未访问,1表示已访问。
        int[][] visitedPixels = new int[numRows][numCols];
        // 表示图像中每个像素是否已经被访问过,0表示未访问,1表示已访问。
        int[][] connectedRegions = new int[0][];
        LinkedList<Point> stack = null;
        int num = 0;
        // 头部起始点存在无法正常返回到起始点结束,因为头像特殊性,底部平滑可以避免无法返回到起始点问题,其他图像请考虑结束点判断
        //for (int i = 0; i < numRows; i++) {
        //    for (int j = 0; j < numCols; j++) {
        for (int i = numRows-1; i >= 0; i--) {
            for (int j = numCols-1; j >= 0; j--) {
                if (getPixel(image, i, j) == 0 && visitedPixels[i][j] == 0) {
                    connectedRegions = new int[numRows][numCols];
                    stack = new LinkedList<>();
                    int temp = 7;
                    int y = i;
                    int x = j;
                    temp = (temp + 6) % 8;
                    while (true){
                        boolean a = true;
                        // 查询周围8个域
                        for (int k = 0; k < DOMAIN.length; k++) {
                            if(temp == 8){
                                temp = 0;
                            }
                            // 查询域坐标
                            int domainY = y + DOMAIN[temp][1];
                            int domainX = x + DOMAIN[temp][0];
                            // 图像边界判断
                            if(numCols-1 < domainX || numRows-1 < domainY || domainY < 0 || domainX < 0){
                                temp++;
                                continue;
                            }
                            // 判断是否为黑像素,并且没有扫描过
                            if(getPixel(image, domainY, domainX) == 0 && connectedRegions[domainY][domainX] == 0){
                                num++;
                                y = domainY;
                                x = domainX;
                                connectedRegions[y][x] = 1;
                                visitedPixels[y][x] = 1;
                                stack.add(new Point(x, y, temp));
                                a = false;
                                temp = (temp + 6) % 8;
                                break;
                            }
                            temp++;
                        }
                        // 判断是否是起始,是则返回,完成边界确认
                        if(i == y && x == j){
                            break;
                        }
                        // 死胡同回退
                        if(a){
                            if(stack.isEmpty()){
                                break;
                            }
                            stack.removeLast();
                            if(stack.isEmpty()){
                                break;
                            }
                            Point last = stack.getLast();
                            y = last.getY();
                            x = last.getX();
                            temp = last.getTemp();
                            temp++;
                        }
                    }
                }
                // 避免黑点
                if(num > 100){
                    // 使用
                    int[][] ints = new int[numRows][numCols];
                    stack.forEach(iter -> ints[iter.getY()][iter.getX()] = 1);
                    return ints;
                    // 返回扫描全路径
                    //return connectedRegions;
                }
            }
        }
        return new int[0][];
    }

    /***
     * 洪泛填充法
     * @param image 原始图像
     * @param imgIndex 范围坐标
     * @return 填充后图像
     */
    public static BufferedImage floodFill(BufferedImage image, int[][] imgIndex) {
        int width = image.getWidth();
        int height = image.getHeight();
        // 1、寻找起始点
        int x = 0;
        int y = 0;
        int initX = width, initY = height;
        //imgIndex[H][W]
        for (int i = 0; i < imgIndex.length; i++) {
            for (int j = 0; j < imgIndex[i].length; j++) {
                int indexW = imgIndex[i][j];
                if(indexW == 1){
                    if(initX > j){
                        y = i;
                        initX = j;
                    }
                    if(initY > i){
                        x = j;
                        initY = i;
                    }
                }
            }
        }
        // 2、内容填充
        BufferedImage imageNew = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        Graphics graphics = imageNew.getGraphics();
        if (imgIndex[y][x] == 1) {
            return imageNew;
        }
        LinkedList<Point> queue = new LinkedList<>();
        queue.offer(new Point(x, y));
        while (!queue.isEmpty()) {
            Point point = queue.poll();
            // 边界范围
            if (point.x < 0 || point.x >= width || point.y < 0 || point.y >= height) {
                continue;
            }
            if (imgIndex[point.y][point.x] != 0) {
                continue;
            }
            imgIndex[point.y][point.x] = 1;
            imageNew.setRGB(point.x, point.y, image.getRGB(point.x, point.y));
            //graphics.drawImage(image, point.x, point.y, null);
            queue.offer(new Point(point.x - 1, point.y));
            queue.offer(new Point(point.x + 1, point.y));
            queue.offer(new Point(point.x, point.y - 1));
            queue.offer(new Point(point.x, point.y + 1));
        }
        graphics.dispose();
        return imageNew;
    }

    /***
     * 查询图片像素
     * @param image 图像
     * @param y 高
     * @param x 宽
     * @return 像素
     */
    private static int getPixel(BufferedImage image, int y, int x){
        return (image.getRGB(x, y) & 0xff0000) >> 16;
    }

    public static class Point{
        // w, h
        private int x;
        private int y;
        private int temp;

        public Point(int x, int y){
            this.x = x;
            this.y = y;
            this.temp = 0;
        }

        public Point(int x, int y, int temp) {
            this.x = x;
            this.y = y;
            this.temp = temp;
        }

        public Point() {
        }

        public int getX() {
            return x;
        }

        public void setX(int x) {
            this.x = x;
        }

        public int getY() {
            return y;
        }

        public void setY(int y) {
            this.y = y;
        }

        public int getTemp() {
            return temp;
        }

        public void setTemp(int temp) {
            this.temp = temp;
        }

        @Override
        public String toString() {
            return "Point{" +
                    "x=" + x +
                    ", y=" + y +
                    '}';
        }

        @Override
        public boolean equals(Object obj) {
            if(obj instanceof Point){
                Point obj1 = (Point) obj;
                return this.x == obj1.x && this.y == obj1.y;
            }
            return super.equals(obj);
        }

        public boolean equals(int x, int y) {
            return this.x == x && this.y == y;
        }

        @Override
        public int hashCode() {
            return Objects.hashCode(x + "" + y);
        }
    }
}

使用:

BufferedImage bi = ImageIO.read(new File("D:\\test\\c8c56e3838104fcab47af7920ce5723e.png"));
bi = IdPhotoMattingUtils.mat(bi);
ImgUtil.write(bi, new File("D:\\test\\response990.png"));

本文仅用于学习使用,抠图后存在白边,后续考虑优化使用降噪或虚化等

### 技术方案概述 在本地环境中部署抠图功能可以通过多种技术和工具实现。以下是几种常见的技术方案及其特点: #### 1. 使用 `rembg` 工具 `rembg` 是一种简单易用的开源工具,能够通过命令行完成图像背景移除任务。它支持多种形式的操作,包括处理单张图片、批量处理文件夹中的图片以及从网络链接下载图片进行处理。 - **安装与配置** 用户可以在本地创建一个虚拟环境来管理依赖项。例如,在 Conda 虚拟环境下激活名为 `py310` 的环境后执行相关命令[^1]。 ```bash conda activate py310 pip install rembg ``` - **具体操作** 处理单一图片时可使用如下命令: ```bash rembg i input_image_path output_image_path ``` 批量处理文件夹内的图片则采用以下方法: ```bash rembg p input_folder_path output_folder_path ``` #### 2. 部署 HivisionIDPhotos 项目 HivisionIDPhotos 提供了一种全面的解决方案,不仅限于简单的抠图功能,还涵盖了更换背景颜色、调整照片尺寸等功能,特别适合制作个性化证件照- **环境准备** 推荐使用 Python 本不低于 3.7(建议选用 Python 3.10),并且兼容主流操作系统如 Windows、Linux 和 macOS。如果倾向于更简便的方式,则可以选择利用 Docker 来加速部署过程[^2]。 - **实施步骤** 下载源码仓库至本地机器,并按照官方文档逐步设置必要的库文件和支持组件即可启动服务端口监听模式下的应用实例。 #### 3. 构建 OpenVINO Model Server (OVMS) 环境 对于追求高性能计算场景的应用开发者来说,借助 Intel 开发框架——OpenVINO 可显著提升推理效率。此路径涉及构建专属的人像分割模型服务器架构。 - **基础原理** 整体设计围绕着容器化的理念展开,即把 OVMS 封装进独立运行空间的同时保持与其他业务逻辑模块间的高效交互能力。典型情况下,Java 应用会调用 RESTful API 实现跨进程通讯机制从而获取实时预测结果数据流[^3]。 - **实践要点** 初期需确认目标硬件平台满足最低规格需求之后再着手定制镜像本号参数设定等工作环节直至最终验证成功为止。 #### 4. ComfyUI 插件扩展法 针对视频素材领域而言,ComfyUI 平台提供了专门适配 RVM 或者 BRIA AI 出品的新一代算法引擎接口接入可能性,使得原本复杂的动态画面剪辑变得更加直观可控。 - **前置条件核查清单** - 安装 Node.js LTS 本及以上; - Git CLI 工具链可用状态检测; - PyTorch 深度学习框架预置完毕情况评估等[^4]。 - **实际运用案例分享** 根据个人偏好挑选合适的模板样式导入编辑界面当中去定义特定区域范围边界线轮廓特征点位置关系表达式等内容要素组合起来形成完整的视觉效果呈现形式出来给观众观看体验享受乐趣时刻到来啦! --- ### 示例代码片段展示区 ```python import rembg def remove_background(input_file, output_file): with open(input_file, 'rb') as f_in: img_data = f_in.read() result = rembg.remove(img_data) with open(output_file, 'wb') as f_out: f_out.write(result) ``` ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值