wasm实例开发006-证件照换底色

在本系列前面的文章中我们介绍了一些基础的wasm应用,本文我们将介绍如何通过wasm应用来更换证件照的背景色,如红底变蓝底,蓝底变红底等等,在开始之前我们需要对颜色距离的计算做一些了解。

一、如何计算两个颜色之间的距离

对于两个坐标之间的距离,大部分人知道该如何计算,但是对于两个颜色之间的距离可能很多人不是太了解,关于颜色之间距离的计算方法有很多种,比如欧几里得距离、曼哈顿距离等等,本文将选用Delta E(CIELAB)算法来计算颜色之间的距离。

二、CIELAB距离

CIELAB距离是专门用于计算颜色之间距离的一种指标。它考虑了人眼对颜色的感知,比欧几里得距离更符合实际需求。CIELAB距离的计算方式较为复杂,需要先将RGB颜色转换为LAB颜色空间中的值,然后计算它们在该空间中的距离,其计算公式如下:

dE = sqrt((L2-L1)^2 + (a2-a1)^2 + (b2-b1)^2)

其中,L、a和b分别表示LAB颜色空间中的亮度、色相和饱和度。

在canvas中,我们常使用到的颜色是sRGB颜色,所以我们需要先将sRGB颜色转换成LAB颜色,将sRGB颜色转换到LAB颜色,需要使用XYZ来过渡:sRGB -> XYZ -> Lab,具体计算步骤可以参考这篇文章:https://blog.csdn.net/bby1987/article/details/109522126

三、C代码编写

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>

const double XYZ_REF_WHITE[] = { 0.95047, 1.0, 1.08883 }; 

double gamma(double pixel) {
    if (pixel > 0.04045) {
        return pow((pixel + 0.055) / 1.055, 2.4);
    }
    return pixel / 12.92;
}

double xyz_convert(double pixel) {
    if (pixel > pow(6.0 / 29.0, 3.0)) {
        return pow(pixel, 1.0 / 3.0);
    }
    return 7.787 * pixel + (16.0 / 116.0);
}

double *rgb_to_lab(double *color) {
    double r = color[0] / 255.0;
    double g = color[1] / 255.0;
    double b = color[2] / 255.0;

    double ir = gamma(r);
    double ig = gamma(g);
    double ib = gamma(b);

    double x = ir * 0.4124564 + ig * 0.3575761 + ib * 0.1804375;
    double y = ir * 0.2126729 + ig * 0.7151522 + ib * 0.0721750;
    double z = ir * 0.0193339 + ig * 0.1191920 + ib * 0.9503041;
    
    double ix = xyz_convert(x / XYZ_REF_WHITE[0]);
    double iy = xyz_convert(y / XYZ_REF_WHITE[1]);
    double iz = xyz_convert(z / XYZ_REF_WHITE[2]);

    double *ret = (double *)malloc(3 * sizeof(double));
    ret[0] = (166.0 * iy) - 16;
    ret[1] = 500.0 * (ix - iy);
    ret[2] = 200.0 * (iy - iz);

    return ret;
}

double calc_rgb_distance(double *rgb1, double *rgb2) {
    double *lab1 = rgb_to_lab(rgb1);
    double *lab2 = rgb_to_lab(rgb2);

    double deltaL = lab1[0] - lab2[0];
    double deltaA = lab1[1] - lab2[1];
    double deltaB = lab1[2] - lab2[2];

    double delta = sqrt(pow(deltaL, 2.0) + pow(deltaA, 2.0) + pow(deltaB, 2.0));

    return delta;
}

uint8_t *change_bg_color(uint8_t *imageData, int width, int height, int threshold, double *bgColor, uint8_t *targetColor) {
    int *marked = (int *)malloc(width * height * 4 * sizeof(int));
    for (int i=0; i<height; i++) {
        for (int j=0; j<width; j++) {
            int index = (i * width + j) * 4;
            int r = imageData[index];
            int g = imageData[index + 1];
            int b = imageData[index + 2];
            int a = imageData[index + 3];
            double color[] = { r, g, b };
            double distance = calc_rgb_distance(color, bgColor);

            if (i == 0 && j == 0) {
                if (distance <= threshold) {
                    marked[index] = 1;
                }   else {
                    marked[index] = 0;
                }
                continue;
            }

            int hIndex = index - 4;
            int vIndex = index - 4 * width;

            if (distance <= threshold) {
                if (hIndex >= 0 && marked[hIndex] == 1) {
                    marked[index] = 1;
                    continue;
                }

                if (vIndex >= 0 && marked[vIndex] == 1) {
                    marked[index] = 1;
                    continue;
                }

                marked[index] = 0;
            } else {
                marked[index] = 0;
            }
        }
    }

    uint8_t *newImageData = (uint8_t *)malloc(width * height * 4 * sizeof(uint8_t));
    for (int i=0; i<height; i++) {
        for (int j=0; j<width; j++) {
            int index = (i * width + j) * 4;
            int r = imageData[index];
            int g = imageData[index + 1];
            int b = imageData[index + 2];
            int a = imageData[index + 3];
            newImageData[index + 3] = a;
            if (marked[index] == 1) {
                newImageData[index] = targetColor[0];
                newImageData[index + 1] = targetColor[1];
                newImageData[index + 2] = targetColor[2];
            } else {
                newImageData[index] = r;
                newImageData[index + 1] = g;
                newImageData[index + 2] = b;
            }
        }
    }

    return newImageData;
}
    

int main() {
    // double color1[] = { 255, 0, 0 };
    // double color2[] = { 255, 1, 3 };
    // double distance = calc_rgb_distance(color1, color2);
    // printf("distance: %f\n", distance);
    return 0;
}

其中change_bg_color函数是最终暴露给js调用的函数,该函数接受以下参数:

①、imageData 图像像素数据

②、width 图片宽度

③、height 图片高度

④、threshold 容差

⑤、bgColor 原背景颜色:[r, g, b],注意这里使用的是double类型方便后期运算

⑥、targetColor 新背景颜色:[r, g, b]

该方法主要是计算了每一个像素点与原背景色之间的距离以决定是否需要用新背景色替换,而且这里规定了背景色是连续的,如果是不连续的背景处理出来可能有问题。

四、将C程序编译成wasm应用

将上面的C程序保存为cbg.c, 然后在.c文件相同目录下新建build.sh文件,输入以下内容并保存:

#! /usr/bin/env bash

emcc -o cbg.js cbg.c \
    -Oz \
    -s ALLOW_MEMORY_GROWTH=1 \
    -s EXPORTED_FUNCTIONS='["_malloc", "_free", "_change_bg_color"]' \
    -s LINKABLE=1

五、使用js调用wasm应用

这里我们直接自己操作内存,将数据传递给C程序,需要注意bgColor在C程序里面使用的是double类型,所以我们要使用Float64Array来传递数据:

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const { data, width, height } = imageData;

// 图片像素数据
const inputTypedData = new Uint8Array(data);
const inputBuf = _malloc(inputTypedData.byteLength);
Module.HEAPU8.set(inputTypedData, inputBuf);

// 背景色数据
const typedBgColor = new Float64Array(bgColor);
const bgColorBuf = _malloc(typedBgColor.byteLength);
Module.HEAPF64.set(typedBgColor, bgColorBuf >> 3);

// 目标色数据
const typedTargetColor = new Uint8Array(targetColor);
const targetColorBuf = _malloc(typedTargetColor.byteLength);
Module.HEAPU8.set(typedTargetColor, targetColorBuf);

const pointer = _change_bg_color(
    inputBuf,
    width,
    height,
    range.value,
    bgColorBuf,
    targetColorBuf
);
const newData = new Uint8Array(
    Module.asm.memory.buffer,
    pointer,
    width * height * 4
);
const newImageData = new ImageData(
    new Uint8ClampedArray(newData),
    width,
    height
);
ret.getContext('2d').putImageData(newImageData, 0, 0);

六、结语

本文实现了一个相对更复杂一点的wasm应用,主要需要注意的点是不同数据类型的传递方式,比如C程序的double类型在js中我们需要使用Float64Array来处理,而Uint8Array对应的C程序的uint8_t类型,该类型定义在stdint.h头文件中。

在线demo:https://demo.deanhan.cn/change-background/

如需下载本文涉及的源码及素材包,请在关注公众号后发送:changebg

系列文章导航:

第1节: Wasm实例开发001-斐波那契数列

第2节: Wasm实例开发002-最长回文字符串

第3节: wasm实例开发003-简单数据的加密解密

第4节: wasm实例开发004-矩阵乘法

第5节: wasm实例开发005-图像高斯模糊

第7节: wasm实例开发007-图片菱形渐变

  • 支付宝二维码 支付宝
  • 微信二维码 微信
相关文章