想问下前端有哪些技术可以实现证件照底色替换

因业务需求,需要把所有的人物证件照底色替换为红色,请问前端有什么好的方法嘛,因图片原来的底色也不确定,可能颜色较深,或者颜色较浅,有没有可以比较兼容的方法

阅读 4.9k
3 个回答

最快捷的莫过于使用云服务商提供的抠图功能了,之前阿里云貌似有给我推过相关产品的邮件。


自己做的话挺难的,我试了一下,如果用边缘色差作为基准,可以去掉一些近乎纯色的背景,但如果背景色差略大一点,或者人像有与背景相近的颜色,效果就惨不忍睹了——
image.png
(效果太吓人了,希望没有给你留下心理阴影)
虽然这个尝试不是很成功,但基本能够标识出人像的边缘,这样一来,如果结合一些经验,比如一般情况下的半身人像的基本轮廓,就可以很好地把人像的大致边界标记出来。
然后呢?
然后有一种抠图算法叫做 Alpha Matting [1] [2] ,这种算法的良好实现能抠头发丝,但是需要有前后景的大致界限,所以基于这个算法的应用通常都会先让用户标记出大致轮廓 —— 如果你用过 PS 魔棒抠图的话,就知道我在说什么了 —— 把我们标记出来的边界稍加修改,就可以代替这个轮廓。
魔棒做得还是很不错的,涂一下就可以,而 Alpha Matting 至少需要涂抹三个区域:背景、主体、边界,比如下图中的黑白灰:
image.png
[1]
由于不会图形学也不会 openCV.js,光是倒腾这个色差分离就耗去了小半天时间,再加上没有找到 Alpha Matting 算法的 JS 实现,所以我没打算继续下去,这个东西是在东家的项目里搞的,所以只能把部分模块代码贴出来:

    /**
     * @const loadImageFromURL - 从 URL 加载图片
     * @desc 每次都返回同一个图片对象,所以不要直接使用、修改图片
     * */
    const loadImageFromURL = (() => {
        const image = new Image();
        image.setAttribute('crossOrigin', 'Anonymous');
        let resolver = null, errHandler = null;

        image.onload = () => {
            resolver(image);
        };
        image.onerror = err => {
            errHandler(err);
        };

        return URL => {
            return new Promise((resolve, reject) => {
                resolver = resolve;
                errHandler = reject;
                image.src = URL;
            });
        }
    })();

    /**
     * @method imageFileToImageData - 将图片文件转为 Uint8ClampedArray
     * @param {File} blob - 需要转换的文件对象
     * @return {Promise<ImageData>}
     */
    const imageFileToImageData = (() => {
        const canvas = document.createElement('canvas');
        canvas.setAttribute('crossOrigin', 'Anonymous');
        const context = canvas.getContext('2d');

        return async blob => {
            const image = await loadImageFromURL(await blob2URL(blob));
            const { width, height } = image;
            canvas.setAttribute('width', width);
            canvas.setAttribute('height', height);

            context.drawImage(image, 0, 0, width, height);
            return context.getImageData(0, 0, width, height);
        };
    })();

    /**
     * @method simplifiedAvatarExtractor - W.I.P 简化版头像提取器
     * @desc 该提取器基于简单的背景色识别方案,仅能提取纯色或近似纯色背景的图片
     *       并要求主体部分不包含与背景色域相近的颜色
     */
    const simplifiedAvatarExtractor = (() => {
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        const { abs } = Math;

        return async (blob, colorOffset) => {
            const imageData = await imageFileToImageData(blob);
            const { width, height, data } = imageData;
            canvas.setAttribute('width', width);
            canvas.setAttribute('height', height);
            
            // 在图片上半部分集中采样,因为半身照基本是这样的
            let half = height >> 1;

            let i = 0;
            const array = [];
            do {
                array[i] = horizonLineIterator(data, width, height, half);
                half = half >> 1;
            } while (i++ <= 4);
            // TODO 分片求中位数、频率、离散度,并根据分片距离中心的距离求权,
            //      然后将权重高的区块聚合,作为抠图的凭据,而不是靠这个既非
            //      经验,又无理论支撑的三角形定律
            const role = [];
            // let prev = cur = max = min = average = array[0][0];

            half = width >> 1;
            array.forEach(item => {
                const memory = new ArrayBuffer(40);
                const pre = new Uint16Array(memory, 0, 4);
                const cur = new Uint16Array(memory, 8, 4);
                const max = new Uint16Array(memory, 16, 4);
                const min = new Uint16Array(memory, 24, 4);
                const ave = new Uint16Array(memory, 32, 4);

                pre[0] = cur[0] = item[0] = max[0] = min[0] = ave[0] = item[0];
                pre[1] = cur[1] = item[1] = max[1] = min[1] = ave[1] = item[1];
                pre[2] = cur[2] = item[1] = max[1] = min[2] = ave[2] = item[2];
                pre[3] = cur[3] = item[3] = max[3] = min[3] = ave[3] = item[3];
                // 从前到后、从后到前,与前一个数、前平均数不能构成三角形的,
                // 就认为是遇到画面主体了
                for(let count = 4; count <= half; count += 4){
                    const R = item[0];
                    const G = item[count + 1];
                    const B = item[count + 2];
                    const A = item[count + 3];

                    ave[0] = (R + ave[0]) >> 1;
                    ave[1] = (G + ave[1]) >> 1;
                    ave[2] = (B + ave[2]) >> 1;
                    ave[3] = (A + ave[3]) >> 1;

                    if(
                        R + ave[0] > pre[0] && abs(R - ave[0]) < pre[0] &&
                        G + ave[1] > pre[1] && abs(G - ave[1]) < pre[3] &&
                        B + ave[2] > pre[2] && abs(B - ave[2]) < pre[3] &&
                        A + ave[3] > pre[3] && abs(A - ave[3]) < pre[3]
                    ){
                        if(R > max[0]){
                            max[0] = R
                        } else if(R < min[0]){
                            min[0] = R
                        }
                        if(G > max[1]){
                            max[1] = G
                        } else if(G < min[1]){
                            min[1] = G
                        }
                        if(B > max[2]){
                            max[2] = B
                        } else if(B < min[2]){
                            min[2] = B
                        }
                        if(A > max[3]){
                            max[3] = A
                        } else if(A < min[3]){
                            min[3] = A
                        }
                    } else {
                        break
                    }
                }
                role.push([max, min]);
            });

            // 先取平均,偏离平均数太远的,抛弃之
            const ave = [[0,0,0,0], [0,0,0,0]];
            const sta = [[0,0,0,0], [0,0,0,0]];
            role.forEach(item => {
                const [ max, min ] = item;
                ave[0][0] += max[0];
                ave[0][1] += max[1];
                ave[0][2] += max[2];
                ave[0][3] += max[3];

                ave[1][0] += min[0];
                ave[1][1] += min[1];
                ave[1][2] += min[2];
                ave[1][3] += min[3];
            });

            ave[0][0] /= role.length;
            ave[0][1] /= role.length;
            ave[0][2] /= role.length;
            ave[0][3] /= role.length;

            ave[1][0] /= role.length;
            ave[1][1] /= role.length;
            ave[1][2] /= role.length;
            ave[1][3] /= role.length;

            // 通过与平均数的比较,剔除反常参数
            // TODO 当然上面这句话还没做
            const ave00 = sta[0][0], ave01 = sta[0][1], ave02 = sta[0][2], ave03 = sta[0][3];
            const ave10 = sta[1][0], ave11 = sta[1][1], ave12 = sta[1][2], ave13 = sta[1][3];
            for(let count = 0; count < role.length; count++){
                const [ max, min ] = role[count];
                const max0 = max[0], max1 = max[1], max2 = max[2], max3 = max[3];
                const min0 = min[0], min1 = min[1], min2 = min[2], min3 = min[3];
                if(
                    max0 < max0 + ave00 && max0 > abs(max0 - ave00) &&
                    max1 < max1 + ave01 && max1 > abs(max1 - ave01) &&
                    max2 < max2 + ave02 && max2 > abs(max2 - ave02) &&
                    max3 < max3 + ave03 && max3 > abs(max3 - ave03)
                ){
                    if(max0 > ave00) sta[0][0] = max0;
                    if(max1 > ave01) sta[0][1] = max1;
                    if(max2 > ave02) sta[0][2] = max2;
                    if(max3 > ave03) sta[0][3] = max3;
                }
                if(
                    min0 < min0 + ave10 && min0 > abs(min0 - ave10) &&
                    min1 < min1 + ave11 && min1 > abs(min1 - ave11) &&
                    min2 < min2 + ave12 && min2 > abs(min2 - ave12) &&
                    min3 < min3 + ave13 && min3 > abs(min3 - ave13)
                ){
                    if(min0 > ave10) sta[1][0] = min0;
                    if(min1 > ave11) sta[1][1] = min1;
                    if(min2 > ave12) sta[1][2] = min2;
                    if(min3 > ave13) sta[1][3] = min3;
                }
            }

            // 使用提取出来的上下限,来界定
            const [[maxR, maxG, maxB, maxA], [minR, minG, minB, minA]] = ave;
            let vote = 0;
            for(let count = 0; count < data.length; count += 4){
                const curR = data[count];
                const curG = data[count + 1];
                const curB = data[count + 2];
                const curA = data[count + 3];
                vote = 0;
                if(curR <= maxR && curR >= minR) vote += 1;
                if(curG <= maxG && curG >= minG) vote += 1;
                if(curB <= maxB && curB >= minB) vote += 1;
                if(curA <= maxA && curA >= minA) vote += 1;
                if(vote > 1) {
                    // 置为透明
                    data[count + 3] = 0;
                }
            }

            context.putImageData(imageData, 0, 0);

            return canvas.toDataURL('img/png', 1)
        }
    })();

这个实现毕竟不是正规的图形学手段,不建议使用,不过整体思路可以参考一下:用 openCV.js 大致分离背景色,加上经验数据处理成三色标记图,然后应用 Alpha Matting 算法。
实际上使用 TenserFlow.js 训练一个神经网络也可以在浏览器里抠图,但是性能要求比较高,而作为前端,我们的一个基本素养就是不要对用户机器的性能有过高的期望。

不建议前端实现,硬要的话可以参考 稿定设计 的扣图功能

前端,这个前端不好实现吧,得人工智能识别出人像,分离背景和人,前端实现不了吧

推荐问题