上篇我们把文件上传的Html文件写好了,也把基本的读取图片数据写完了,本篇就具体如何实现分解来详解。
完整项目地址:https://github.com/dengxuhui/ImagePackerWeb
如果想直接使用该功能的同学:http://dengxuhui.cn/
当我们点击”点击分解图片“按钮时会触发onClick方法
this.btnUpload.onclick = function (e) {
if (e.currentTarget != $this.btnUpload) return;
var dropzone = window.DropZoneLogic.dropzone;
var len = dropzone.files.length;
$this.ctx.clearRect(0,0,$this.ctx.width,$this.ctx.height);
$this.isSpliting = true;
for (var i = 0; i < len; ++i) {
$this.saveFileToCanvas(dropzone.files[i]);
}
}
/**
* 保存数据
* @param {File} file
*/
saveFileToCanvas(file) {
var $this = this;
this.preFix = file.name.split(".")[0];
createImageBitmap(file).then((data) => {
$this.startSplit(data, $this);
});
}
触发onclick后我们通过canvas为载体,将图片文件的数据绘制到canvas上,然后通过读取像素数据来划分图像数据。
这里需要注意的是createImageBitmap返回一个ImageBitmap数据对象。这是一个异步方法,返回的是Promise状态。
接下来一步就开始真正分解图片了。
绘制图集到Canvas
首先我们将整张图集先绘制到Canvas上,这一步是为了通过canvas绘制的图形方便获取每个像素数据。
//将image对象绘制到cavans上
$this.ctx.drawImage(data, 0, 0);
var w = data.width > ViewLogic.WIDTH ? ViewLogic.WIDTH : data.width;
var h = data.height > ViewLogic.HEIGHT ? ViewLogic.HEIGHT : data.height;
$this.atlasH = h;
$this.atlasW = w;
这里需要注意一点,默认我们设置的Canvas最大尺寸为2048x2048,当图片大于2048我们还是去2048,这时候图片会被截取,另外2048大小的图集恐怕目前这个算法跑也会跑1很多钟。
使用一个二维数组来标识每个像素点是否有颜色
接下来我们通过获取ImageData中每个像素的数据来判定每个像素点是否有颜色,这也是我们用来区分碎图的边缘的评判标准。
getColors($this) {
var has = [];
var count;
for (var i = 0; i < $this.atlasW; ++i) {
has[i] = [];
for (var j = 0; j < $this.atlasH; ++j) {
var piexel = $this.ctx.getImageData(i, j, 1, 1);
count = 0;
if (piexel.data[0] < 4) count++;
if (piexel.data[1] < 4) count++;
if (piexel.data[2] < 4) count++;
if (piexel.data[3] < 3 || (count > 2 && piexel.data[3] < 30)) has[i][j] = false;
else has[i][j] = true;
}
}
console.log("GET Colors Complete");
return has;
}
使用这个方法有两个缺点:
下面贴出相对于之上的优化代码,通过优化速度从秒级分解直接降到毫秒级,可见我们直接调用Canvas的API是多么耗时
//我们一次性就可以获取所有像素区域数据,然后通过计算获取数组下标索引即可,不必每次使用Canvas方法
getColorsNew($this){
$this.btnUpload.innerText = "正在解析像素....";
var has = [];
var count;
var sT = new Date().getTime();
var allPixel = $this.ctx.getImageData(0,0,$this.atlasW,$this.atlasH);
for (var i = 0; i < $this.atlasW; ++i) {
has[i] = [];
for (var j = 0; j < $this.atlasH; ++j) {
// var pixel = $this.ctx.getImageData(i, j, 1, 1);
//计算公式:(y * width + x) * 4 4数组中每4个数据表示一个像素的数据
var startIndex = (j * $this.atlasW + i) * 4;
count = 0;
if (allPixel.data[startIndex] < 4) count++;
if (allPixel.data[startIndex + 1] < 4) count++;
if (allPixel.data[startIndex + 2] < 4) count++;
if (allPixel.data[startIndex + 3] < 3 || (count > 2 && allPixel.data[startIndex + 3] < 30)) has[i][j] = false;
else has[i][j] = true;
}
}
var eT = new Date().getTime();
//结果表明运行时间成指数及降低,这也让我们记住直接调用Canvas的API是很慢的方法,最好都讲数据同步到本地再使用
console.log("用时:" + (eT - sT) + "毫秒");
console.log("GET Colors Complete");
return has;
}
划分每个碎图的矩形包围盒区域
在这一步之前我们先新建一个简单的Rectangle对象,来保存像素点区域的x,y,width,height属性
class Rectangle {
constructor(x = 0, y = 0, width = 0, height = 0) {
var $this = this;
$this.x = x;
$this.y = y;
$this.width = width;
$this.height = height;
}
}
接下来我们就可以从左上角0,0点开始利用一个嵌套循环来遍历每个矩形点来划分碎图区域
var rects = [];//Rectangle Array
var rect = null;//Rectangle Pointer
for (var i = 0; i < $this.atlasW; ++i) {
for (var j = 0; j < $this.atlasH; ++j) {
if ($this.isExist(colors, i, j)) {
rect = $this.getRect(colors, i, j);
if (rect.width > 5 && rect.height > 5) {
rects.push(rect);
}
}
}
}
首先我们每取得一个坐标都去判定这个坐标是否是有颜色的像素,如果在图集范围内就去colors二维数组中取有没有这个颜色
isExist(colors, x, y) {
if (x < 0 || y < 0 || x >= colors.length || y >= colors[0].length) return false;
return colors[x][y];
}
当取得有这个坐标像素点是有颜色或者还没有被使用,就进行下一步,确定这个碎图的矩形范围。
getRect(colors, x, y) {
var rect = new Rectangle(x, y, 1, 1);
var flag;
do {
flag = false;
while (this.R_Exist(colors, rect)) { rect.width++; flag = true }
while (this.D_Exist(colors, rect)) { rect.height++; flag = true }
while (this.L_Exist(colors, rect)) { rect.width++; rect.x--; flag = true }
while (this.U_Exist(colors, rect)) { rect.height++; rect.y--; flag = true }
} while (flag);
this.clearRect(colors, rect);
rect.width++;
rect.height++;
return rect;
}
这一步我们通过从目标点x,y开始,首先向右检测扩张,一旦遇到又侧像素点无颜色停止,同理接下来这样去检测它的下方、左方、上方。需要注意的是检测左方或者上方的时候,如果有像素,rectangle的x,y坐标也要相应的挪一个像素。
当循环完成后,我们把rect中的像素从colors二维数组中清除,以确保rectangle的唯一性。
下面只贴出右侧像素检测的方法,其余方法基本相同
R_Exist(colors, rect) {
var right = rect.x + rect.width;
if (right >= colors.length || rect.x < 0) return false;
for (var i = 0; i < rect.height; i++) {
if (this.isExist(colors, right + 1, rect.y + i)) return true;
}
return false;
}
最后我们得到一个rect对象,来标识碎图在图集中的像素区域,我们这里将那种小于5像素的碎图直接抛弃不要。这样我们就得到一个在图集中的碎图矩形区域数组。接下来就需要将每张图单独从之前获得的信息中切出来单独保存起来。
每个碎图数据获取
上一步拿到了每个碎图的矩形区域之后,这一步就比较简单勒
var imageDataAry = [];
for (var i = 0; i < rects.length; ++i) {
var data = $this.ctx.getImageData(rects[i].x, rects[i].y, rects[i].width, rects[i].height);
imageDataAry.push(data);
}
$this.ctx.clearRect(0, 0, ViewLogic.WIDTH, ViewLogic.HEIGHT);
我们分别从当前canvas中获取每个区域的像素数据,然后保存到imageDataAry中。
绘制每个碎图并保存及下载
图片下载的方法有很多,Js没有直接下载文件的接口,所以有很多变种的方法实现,这里我们使用标签的下载实现方式。
因为我源代码中还利用了其它方式下载,所以我们这里新建一个downloadMethodByCreateHerfA方法来标识通过标签下载文件。
downloadMethodByCreateHerfA(imageDataAry) {
var $this = this;
var dLink = document.createElement("a");
for (var i = 0; i < imageDataAry.length; ++i) {
$this.canvas.width = imageDataAry[i].width;
$this.canvas.height = imageDataAry[i].height;
$this.ctx.putImageData(imageDataAry[i], 0, 0);
var imgUrl = $this.canvas.toDataURL("image/png", 1);
dLink.download = $this.preFix + "_" + i;
dLink.href = imgUrl;
dLink.dataset.downloadurl = ["image/png", dLink.download, dLink.href].join(":");
document.body.appendChild(dLink);
dLink.click();
}
document.body.removeChild(dLink);
}
这里我们通过每个ImageData将其绘制到Canvas中,然后通过Canvas中的toDataURL方法把ImageData转换为base64数据,接着我们通过新建的标签,然后自动触发自动下载。
至此图集从上传到下载的全过程就算写完了,功能是基本能达到,不过也来说说如上实现的几个缺点
总结完了,不过这个软件还没完,接下来需要实现如下功能