昨天是情人节,大家想必都非常愉快的度过了节日~我也是
好了,废话不多说,今天给大家带来是一个比较有意思的项目,通过切割目标图片,获得10000个方块,用我们所选择到的图片,对应的填充方块实现一个千图成像的效果.你可以用它来拼任何你想拼的有意义的大图.(比如我,就想用它把我和对象恋爱到结婚拍的所有照片用来做一个超级超级超级超级大的婚纱照,在老家鄱阳湖的草地上铺着,用无人机高空俯瞰,啧,挺有意思~在这里先埋个点,希望几年后能够实现)
首先,这篇文章是基于我的上一篇fabric入门篇所出的一篇实用案例,也是我自己用来练手总结所用,在此分享给大家,一起成长!
(界面的样式,在这里我就不过多表述了,我们主要讲逻辑功能的实现)
//初始化画布
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 个值,所以这里我就不做大了
需要注意的是,我们通过本地图片绘制到画布,需要将拿到的 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(); //绘制网格线条
});
},
//栅格线
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/24281841 或这里https://baike.baidu.com/item/%E9%A2%9C%E8%89%B2%E7%A9%BA%E9%97%B4/10834848?fr=aladdin
由于颜色在空间中的分布如上面的介绍所示,这里我们采用中学学过的欧氏距离法,来求两个颜色的绝对距离,通过它们的远近就知道两个颜色的相似程度的大小
首先我们了解一下欧氏距离的基本概念
欧几里得度量(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/Color_difference#/sRGB 或者你也可以使用类似 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);
},
这个情人节过的,属实是有点充实,现在是早上六点半~我又肝了一波,睡觉睡觉,保命要紧,白天还要出去玩
升华一下:
浪漫的七夕,连空气中都飘荡着一股爱情的味道。对对有情人欢喜相邀,黄昏后,柳梢头,窃窃私语,良辰美景,月圆花好!祝福天下有情人,幸福快乐!
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/aDsJkmayInsBfTazq0Qw6g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。