JAVA证件照抠图-算法版

原理:

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
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值