关于前端:我用-10000-张图片合成我们美好的瞬间

5次阅读

共计 10682 个字符,预计需要花费 27 分钟才能阅读完成。

月亮照回湖心 野鹤奔向闲云

前言

昨天是情人节, 相比大家都十分欢快的度过了节日~ 我也是😚

好了, 废话不多说, 明天给大家带来是一个十分有意思的我的项目, 通过切割指标图片, 取得 10000 个方块, 用咱们所抉择到的图片, 对应的填充方块实现一个千图成像的成果. 你能够用它来拼任何你想拼的有意义的大图.(比方我, 就想用它把我和对象恋爱到结婚拍的所有照片用来做一个超级超级超级超级大的婚纱照, 在老家鄱阳湖的草地上铺着, 用无人机低空鸟瞰, 啧, 挺有意思~ 在这里先埋个点, 心愿几年后可能实现😊)

首先, 这篇文章是基于我的上一篇 fabric 入门篇所出的一篇实用案例, 也是我本人用来练手总结所用, 在此分享给大家, 一起成长!

进入正题

首先咱们初始一个 800*800 的画布

(界面的款式, 在这里我就不过多表述了, 咱们次要讲逻辑性能的实现)

// 初始化画布
initCanvas() {
    this.canvas = new fabric.Canvas("canvas", {
        selectable: false,
        selection: false,
        hoverCursor: "pointer",
    });
    this.ctx = canvas.getContext("2d");
    this.addCanvasEvent();// 给画布增加事件},

依据本人电脑的配置来自定义画布的大小, 目前还没找到间接在 web 端做相似千图成像的, 在 web 端实现这个性能的确是很耗费性能的, 因为须要解决的数据量好大, 计算量也大
须要留神的是: 800*800 的画布有 640000 个像素, 通过 ctx.getImageData 获取到的每个像素是 4 个值, 就是 2560000 个值, 咱们前面须要解决这 2560000 个值, 所以这里我就不做大了

用 fabric 绘制指标图片

须要留神的是, 咱们通过本地图片绘制到画布, 须要将拿到的 file 文件通过 window.URL.createObjectURL(file) 将文件转为 blob 类型的 url

像你喜爱用 elementUI 的 upload 组件, 你就这么写

// 指标图片抉择回调
slectFile(file, fileList) {let tempUrl = window.URL.createObjectURL(file.raw);
    this.drawImage(tempUrl);
},

这里我不喜爱它的组件, 因为前面抉择资源图片的时候, 抉择数千张图片会有文件列表, 我又不想暗藏它 (次要还是想分享一下自定义的文件抉择)
所以我是这么写的

export function inputFile() {return new Promise(function (resolve, reject) {if (document.getElementById("myInput")) {let inputFile = document.getElementById("myInput");
            inputFile.onchange = (e) => {let urlArr = [];
                for (let i = 0; i < e.target.files.length; i++) {urlArr.push(URL.createObjectURL(e.target.files[i]));
                }
                resolve(urlArr);
            };
            inputFile.click();} else {let inputFile = document.createElement("input");
            inputFile.setAttribute("id", "myInput");
            inputFile.setAttribute("type", "file");
            inputFile.setAttribute("accept", "image/*");
            inputFile.setAttribute("name", "file");
            inputFile.setAttribute("multiple", "multiple");
            inputFile.setAttribute("style", "display: none");
            inputFile.onchange = (e) => {// console.log(e.target.files[0]);
                // console.log(e.target.files);
                // let tempUrl = URL.createObjectURL(e.target.files[0]);
                // console.log(tempUrl);
                let urlArr = [];
                for (let i = 0; i < e.target.files.length; i++) {urlArr.push(URL.createObjectURL(e.target.files[i]));
                }
                resolve(urlArr);
            };
            document.body.appendChild(inputFile);
            inputFile.click();}
    });
}

通过以上办法拿到文件后, 我在外面曾经将图片文件转为了 blob 的 URL 供咱们应用 (须要留神的是文件的抉择是异步的, 所以这里须要用 promise 来写)

// 绘制指标图片
drawImage(url) {fabric.Image.fromURL(url, (img) => {
        // 设置缩放比例, 长图的缩放比为 this.canvas.width / img.width, 宽图的缩放比为 this.canvas.height / img.height
        let scale =
            img.height > img.width
                ? this.canvas.width / img.width
                : this.canvas.height / img.height;
        img.set({
            left: this.canvas.height / 2, // 间隔右边的间隔
            originX: "center", // 图片在原点的对齐形式
            top: 0,
            scaleX: scale, // 横向缩放
            scaleY: scale, // 纵向缩放
            selectable: false, // 可交互
        });
        // 图片增加到画布的回调函数
        img.on("added", (e) => {
            // 这里有个问题,added 后获取的是之前的画布像素数据, 其余手动触发的事件, 不会有这种问题
            // 故用一个异步解决
            setTimeout(() => {this.getCanvasData();
            }, 500);
        });
        this.canvas.add(img); // 将图片增加到画布
        this.drawLine(); // 绘制网格线条});
},

绘制完图片后顺便在画布上绘制 100*100 的栅格

// 栅格线
drawLine() {
    const blockPixel = 8;
    for (let i = 0; i <= this.canvas.width / blockPixel; i++) {
        this.canvas.add(new fabric.Line([i * blockPixel, 0, i * blockPixel, this.canvas.height], {
                left: i * blockPixel,
                stroke: "gray",
                selectable: false, // 是否可被选中
            })
        );
        this.canvas.add(new fabric.Line([0, i * blockPixel, this.canvas.height, i * blockPixel], {
                top: i * blockPixel,
                stroke: "gray",
                selectable: false, // 是否可被选中
            })
        );
    }
},

绘制结束后能够看到图片加网格线的成果, 还是挺难看的~😘

将图片色彩分块保留在数组中

一开始这么写把浏览器跑崩了


我哭 😥, 这么写循环嵌套太多 (而且基数是 800*800*4==2560000–> 得好好写, 要不然对不起 pixelList 被我疯狂操作了 2560000 次) 得优化一下写法, 既然浏览器炸了, 笨办法行不通, 那只能换了~

首先阐明, 这里咱们每个小块的长宽给的是 8 个像素 (越小前面合成图片的精度越精密, 越大越含糊)

// 获取画布像素数据
getCanvasData() {for (let Y = 0; Y < this.canvas.height / 8; Y++) {for (let X = 0; X < this.canvas.width / 8; X++) {
            // 每 8 * 8 像素的一块区域一组
            let tempColorData = this.ctx.getImageData(X * 8, Y * 8, 8, 8).data;
            // 将获取到数据每 4 个一组, 每组都是一个像素
            this.blockList[Y * 100 + X] = {position: [X, Y], color: []};
            for (let i = 0; i < tempColorData.length; i += 4) {this.blockList[Y * 100 + X].color.push([tempColorData[i],
                    tempColorData[i + 1],
                    tempColorData[i + 2],
                    tempColorData[i + 3],
                ]);
            }
        }
    }
    console.log(mostBlockColor(this.blockList));
    this.mostBlockColor(this.blockList);// 获取每个小块的主色调
    this.loading = false;
},

😅 换了一种写法后, 这里咱们将每个 8*8 的像素块划为一组, 失去 10000 个元素, 每个元素里都有 4 个值, 别离代表着 RGBA 的值, 前面咱们会用对应的 10000 张图片填充对应的像素块

拿到画布上的所有像素值后, 咱们需要求出每个小方块的主色调
前面咱们须要通过这些小方块的主色调通过求它与资源图片的色差, 来决定该方块具体是填充哪一张图片 😊
到这里很兴奋, 感觉是快实现了一半了, 其实不然, 前面更抓头皮 😭

 // 获取每个格子的主色调
mostBlockColor(blockList) {for (let i = 0; i < blockList.length; i++) {let colorList = [];
        let rgbaStr = "";
        for (let k = 0; k < blockList[k].color.length; k++) {rgbaStr = blockList[i].color[k];
            if (rgbaStr in colorList) {++colorList[rgbaStr];
            } else {colorList[rgbaStr] = 1;
            }
        }
        let arr = [];
        for (let prop in colorList) {
            arr.push({// 如果只获取 rgb, 则为 `rgb(${prop})`
                color: prop.split(","),
                // color: `rgba(${prop})`,
                count: colorList[prop],
            });
        }
        // 数组排序
        arr.sort((a, b) => {return b.count - a.count;});
        arr[0].position = blockList[i].position;
        this.blockMainColors.push(arr[0]);
    }
    console.log(this.blockMainColors);
},

脑瓜子不好使, 草稿纸都用上了

获取每张资源图的主色调

export function getMostColor(imgUrl) {return new Promise((resolve, reject) => {
        try {const canvas = document.createElement("canvas");
            // 设置 canvas 的宽高都为 20, 越小越快, 然而越小越不准确
            canvas.width = 20;
            canvas.height = 20;
            const img = new Image(); // 创立 img 元素
            img.src = imgUrl; // 设置图片源地址
            img.onload = () => {const ctx = canvas.getContext("2d");
                const scaleH = canvas.height / img.height;
                img.height = canvas.height;
                img.width = img.width * scaleH;
                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
                console.log(img.width, img.height);
                // 获取像素数据
                let pixelData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
                let colorList = [];
                let color = [];
                let colorKey = "";
                let colorArr = [];
                // 分组循环
                for (let i = 0; i < pixelData.length; i += 4) {color[0] = pixelData[i];
                    color[1] = pixelData[i + 1];
                    color[2] = pixelData[i + 2];
                    color[3] = pixelData[i + 3];
                    colorKey = color.join(",");
                    if (colorKey in colorList) {++colorList[colorKey];
                    } else {colorList[colorKey] = 1;
                    }
                }
                for (let prop in colorList) {
                    colorArr.push({color: prop.split(","),
                        count: colorList[prop],
                    });
                }
                // 对所有色彩数组排序, 取第一个为主色调
                colorArr.sort((a, b) => {return b.count - a.count;});
                colorArr[0].url = imgUrl;
                console.log(`%c rgba(${colorArr[0].color.join(",")})`,
                    `background: rgba(${colorArr[0].color.join(",")})`
                );
                resolve(colorArr[0]);
            };
        } catch (e) {reject(e);
        }
    });
}

咱们随机抉择一些文件后, 将他们的主色调打印进去看看成果

色彩空间

要求色彩的色差, 咱们首先须要一起来理解一下色彩的定义, 色彩有很多种示意形式, 它们的规范都不雷同, 有 CMYK,RGB,HSB,LAB 等等 …
这里咱们是 RGBA 的, 它就是 RGB 的色彩模型附加了额定的 Alpha 信息

RGBA 是代表 Red(红色)Green(绿色)Blue(蓝色)和 Alpha 的色调空间。尽管它有的时候被形容为一个色彩空间,然而它其实仅仅是 RGB 模型的附加了额定的信息。采纳的色彩是 RGB,能够属于任何一种 RGB 色彩空间,然而 Catmull 和 Smith 在 1971 至 1972 年间提出了这个不可或缺的 alpha 数值,使得 alpha 渲染和 alpha 合成变得可能。提出者以 alpha 来命名是源于经典的线性插值方程 αA + (1-α)B 所用的就是这个希腊字母。


其余色彩的相干介绍能够看
这里:https://zhuanlan.zhihu.com/p/…
或这里 https://baike.baidu.com/item/…

求色彩差别的办法

因为色彩在空间中的散布如下面的介绍所示, 这里咱们采纳中学学过的欧氏间隔法, 来求两个色彩的相对间隔, 通过它们的远近就晓得两个色彩的类似水平的大小

首先咱们理解一下欧氏间隔的基本概念

欧几里得度量(euclidean metric)(也称欧氏间隔)是一个通常采纳的间隔定义,指在 m 维空间中两个点之间的实在间隔,或者向量的天然长度(即该 点到原点的间隔)。在二维和三维空间中的欧氏间隔就是两点之间的理论间隔。

将公式转化为代码:

// 计算色彩差别
colorDiff(color1, color2) {
    let distance = 0;// 初始化间隔
    for (let i = 0; i < color1.length; i++) {distance += (color1[i] - color2[i]) ** 2;// 对两组色彩 r,g,b[a]的差的平方求和
    }
    return Math.sqrt(distance);// 开平方后失去两个色彩在色调空间的相对间隔
},

计算色彩差别的办法有多种, 能够看 wikiwand:https://www.wikiwand.com/en/C…
或者你也能够应用相似 ColorRNA.js 等色彩解决库进行比照, 这里咱们不做过多形容

计算差值后渲染图片

在这里咱们须要将每个像素块的主色调与所有资源图片的主色调作比拟, 取差别最小的那张渲染到对应的方块上

// 生成图片
generateImg() {
    this.loading = true;
    let diffColorList = [];
    // 遍历所有方块
    for (let i = 0; i < this.blockMainColors.length; i++) {diffColorList[i] = {diffs: [] };
        // 遍历所有图片
        for (let j = 0; j < this.imgList.length; j++) {diffColorList[i].diffs.push({url: this.imgList[j].url,
                diff: this.colorDiff(this.blockMainColors[i].color, this.imgList[j].color),
                color: this.imgList[j].color,
            });
        }
        // 对比拟过的图片进行排序, 差别最小的放最后面
        diffColorList[i].diffs.sort((a, b) => {return a.diff - b.diff;});
        // 取第 0 个图片信息
        diffColorList[i].url = diffColorList[i].diffs[0].url;
        diffColorList[i].position = this.blockMainColors[i].position;
        diffColorList[i].Acolor = this.blockMainColors[i].color;
        diffColorList[i].Bcolor = diffColorList[i].diffs[0].color;
    }
    this.loading = false;
    console.log(diffColorList);
    // 便当每一个方块, 对其渲染
    diffColorList.forEach((item) => {fabric.Image.fromURL(item.url, (img) => {
            let scale = img.height > img.width ? 8 / img.width : 8 / img.height;
            // img.scale(8 / img.height);
            img.set({left: item.position[0] * 8,
                top: item.position[1] * 8,
                originX: "center",
                scaleX: scale,
                scaleY: scale,
            });
            this.canvas.add(img);
        });
    });
},

好家伙!!! 这是什么玩意??? 这搞了一早晨, 出个这?

我哭了, 当初都五点多了, 我还没睡呢~

不摈弃不放弃, 坚持到底就是胜利

仔细分析了下每一个步骤, 逐渐查找问题所在
从最开始的指标图片像素数据开始看像素数据的正确性, 然而没找到问题所在, 数据都没啥问题, 初步判断是计算像素块的主色调上出了问题, 于是想到, 会不会主色调并不是取一张图片或者一块像素块中呈现最多次数的色彩为主色调, 而是取它们的所有色彩的平均值作为主色调呢?
想到这里, 我很兴奋!
差点吵醒曾经酣睡的瓜娃子, 我开始从新梳理

这里, 我对每个 8*8 的小方块都改成了通过平均值求主色调

// 获取每个格子的主色调
mostBlockColor(blockList) {for (let i = 0; i < blockList.length; i++) {
        let r = 0,
            g = 0,
            b = 0,
            a = 0;
        for (let j = 0; j < blockList[i].color[j].length; j++) {r += blockList[i].color[j][0];
            g += blockList[i].color[j][1];
            b += blockList[i].color[j][2];
            a += blockList[i].color[j][3];
        }
        // 求取平均值
        r /= blockList[i].color[0].length;
        g /= blockList[i].color[0].length;
        b /= blockList[i].color[0].length;
        a /= blockList[i].color[0].length;
        // 将最终的值取整
        r = Math.round(r);
        g = Math.round(g);
        b = Math.round(b);
        a = Math.round(a);
        this.blockMainColors.push({position: blockList[i].position,
            color: [r, g, b, a],
        });
    }
    console.log(this.blockMainColors);
}

而后, 对每张图片也改成了通过平均值求主色调

export function getAverageColor(imgUrl) {return new Promise((resolve, reject) => {
        try {const canvas = document.createElement("canvas");
            // 设置 canvas 的宽高都为 20, 越小越快, 然而越小越不准确
            canvas.width = 20;
            canvas.height = 20;
            const img = new Image(); // 创立 img 元素
            img.src = imgUrl; // 设置图片源地址
            img.onload = () => {console.log(img.width, img.height);
                let ctx = canvas.getContext("2d");
                const scaleH = canvas.height / img.height;
                img.height = canvas.height;
                img.width = img.width * scaleH;
                ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
                // 获取像素数据
                let data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
                let r = 0,
                    g = 0,
                    b = 0,
                    a = 0;
                // 取所有像素的平均值
                for (let row = 0; row < canvas.height; row++) {for (let col = 0; col < canvas.width; col++) {r += data[(canvas.width * row + col) * 4];
                        g += data[(canvas.width * row + col) * 4 + 1];
                        b += data[(canvas.width * row + col) * 4 + 2];
                        a += data[(canvas.width * row + col) * 4 + 3];
                    }
                }
                // 求取平均值
                r /= canvas.width * canvas.height;
                g /= canvas.width * canvas.height;
                b /= canvas.width * canvas.height;
                a /= canvas.width * canvas.height;

                // 将最终的值取整
                r = Math.round(r);
                g = Math.round(g);
                b = Math.round(b);
                a = Math.round(a);
                console.log(`%c ${"rgba(" + r + "," + g + "," + b + "," + a + ")"}
                                                                        `,
                    `background: ${"rgba(" + r + "," + g + "," + b + "," + a + ")"};`
                );
                resolve({color: [r, g, b, a], url: imgUrl });
            };
        } catch (e) {reject(e);
        }
    });
}

激动人心的时候到了!!!!!!!!!!!!! 啊啊啊啊啊!! 我很冲动, 胜利就在眼前, 临门一 jor 了!

一顿操作, 抉择指标图片, 抉择资源图片, 点击生成图片按钮后, 我开始了期待胜利的号召!

我去, 更丑了, 这咋回事

紧接着我间接热血了起来, 遇到这种有挑战的事件我就很有劲头, 我要搞不过它, 那不合乎我的气质,
于是我开始剖析解决过的小块主色调, 我发现它们如同都有法则

我想是什么影响到了呢, 图片绘制下来不可能会一样的色彩啊, 一样的色彩是什么呢???

wo kao~ 不会是我画的 100*100 的线条吧

于是我回到,drawLine函数, 我把它给正文掉了~

nice!

每一个方块都能够交互的拉伸旋转, 挪动, 到这里画布的基本功能就曾经完结啦~ 撒花~🌹🏵🌸💐🌺🌻🌼🌷

咱们还能够把生成好的图片导出来, 机器好的小伙伴们能够定义一个很大的画布, 或者给图片做上编号, 打印进去, 是能够用来做微小的合成图的 (比方我后面提到的婚纱照等等, 还是很有意思的)

 // 导出图片
exportCanvas() {
    const dataURL = this.canvas.toDataURL({
        width: this.canvas.width,
        height: this.canvas.height,
        left: 0,
        top: 0,
        format: "png",
    });
    const link = document.createElement("a");
    link.download = "canvas.png";
    link.href = dataURL;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
},

这个情人节过的, 属实是有点空虚, 当初是早上六点半~ 我又肝了一波, 睡觉睡觉, 保命要紧, 白天还要出去玩 😅😅

最初

升华一下:

浪漫的七夕,连空气中都浮荡着一股恋情的滋味。对对有情人欢喜相邀,傍晚后,柳梢头,窃窃私语,吉日良辰,月圆花好!祝愿天下有情人,幸福快乐!

这个我的项目我放在我的 github 上(https://github.com/wangrongding), 喜爱的小伙伴, 记得要点个赞哦~

很快乐能够和大家一起变强!能够关注我的公众号,前埔寨。我组建了一个前端技术交换群, 如果你想与气味相投的小伙伴一起交流学习,也能够加我集体微信(ChicSparrow),我拉你入群一起加油吧! 我是荣顶,和我一起在键帽与字符上横跳,于代码和程序中穿梭。🦄

正文完
 0