前言
这一篇来介绍一个比较“硬核”的知识,数字图像处理–卷积矩阵。并手把手教你撸一个数字图像处理的小工具。
卷积矩阵(Convolution Matrix)
简介
这是一个偏数学领域的概念,大多数滤波器都使用卷积矩阵。如果觉得好玩,你可以使用卷积矩阵构建自定义过滤器。
什么是卷积矩阵? 如果不使用数学工具的话,应该很少人知道卷积的具体概念以及原理,具体介绍可以看 维基百科-卷积 。 卷积在科学、工程和数学上都有很多应用,这里主要我们用于图像处理,比如图像模糊、锐化、边缘检测等等。
卷积被当作矩阵处理,并且一般被成为“核”。图像(数字图像处理中)是直角坐标中像素的二维集合,使用的内核取决于你想要处理的图像效果。
内核一般是 5×5 或 3×3 矩阵, 这里只考虑 3×3 矩阵,它是最常用的,并且足以满足想要的所有效果。 如果 5×5 矩阵内核的所有边界值都设置为零,则会将其视为 3×3 矩阵。
过滤器需要依次处理图像的每个像素,对于图像集合中的每一个像素,我们称之为“初始像素”,它将这个像素的值和周围 8 个像素的值乘以核对应的值。然后将这些结果相加,并将初始像素设置为这个最终结果值。
如图所示。
左侧是图像矩阵,每个像素都标有值。红色边框内是初始像素。 绿色边框是内核操作区,中间是核(卷积矩阵),右边是卷积处理后的结果。
具体计算过程:
过滤器从左到右,从上到下依次读取内核活动区域的所有像素。 它将它们各自的值乘以核对应的值并相加结果。 初始像素变成了 42:
`
(400)+(421)+(46*0)
+(460)+(500)+(55*0)
+(520)+( 560)+(58*0)
= 42 `。
作为图形结果,初始像素向下移动一个像素。
开始实践
这里主要分为几个步骤: 先预置定义几种卷积核,加载图片到 canvas 画布,
然后逐像素对原始图像进行过滤器(卷积)处理,最后把处理后的像素绘制到canvas 上。
并且让这几种卷积核循环运行并显示其处理效果。
- 我们定义预置几个卷积核,并对一个主函数。
// 卷积核
const ConvolutionKernel = {
'normal': [
0, 0, 0,
0, 1, 0,
0, 0, 0,
],
'sharpen': [
0, -1, 0,
-1, 5, -1,
0, -1, 0,
],
'blur': [
1, 1, 1,
1, 1, 1,
1, 1, 1,
],
'blur2': [
0, 1, 0,
0, -1, 0,
0, 1, 0,
],
'edgedetect': [
0, 1, 0,
1, -4, 1,
0, 1, 0,
],
'emboss': [
-2, -1, 0,
-1, 1, 1,
0, 1, 2,
]
}
// 主函数 processImg
export const processImg = ({
el,
type,
imgPath,
hookMounted
} = {}) => {
const options = mergeObj(defaultOpt, {
el,
type,
imgPath,
hookMounted
});
let promise = processImageData({
...options
}).catch(e => console.log(e));
return Object.assign(promise, { data: options });
}
复制代码
- 定义一下要处理的图片路径, 并且定义一个加载图片的函数。
const IMGPath = '/assets/js.png';
export const loadImg = (src) =>
new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => resolve(img);
img.onerror = reject;
img.crossOrigin = 'anonymous'
img.src = src;
})
复制代码
- 创建canvas-2d 并加载图片进行绘制。
const noop = () => {}
const genCanvasAndImg = (el, imgPath, hookMounted = noop) =>
new Promise((resolve, reject) => {
loadImg(imgPath).then((img) => {
let canvas = handleContainer(el, img);
hookMounted(canvas)
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, img.width, img.height);
resolve(ctx);
}).catch(reject);
})
复制代码
- 逐像素进行过滤器(卷积)处理
const kernelWeight = 1;
// 卷积处理
const handleConvolution = (imageData, kernel) => {
if (!imageData instanceof ImageData || !Array.isArray(kernel)) return;
const [width, height] = [imageData.width, imageData.height];
let oldData = [...imageData.data];
let newData = imageData.data;
// 每个像素点乘以 3x3 矩阵 参考:https://docs.gimp.org/2.6/en/plug-in-convmatrix.html
// 暂时先这么处理,考虑到性能 应优化到 worker 线程
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// 边界直接复用原始像素
if(x == 0 || x == width - 1 || y == 0 || y == height - 1) continue;
// 上左 像素点
const topLeft = getPixelByCoord(oldData, x - 1, y - 1, width);
// 上中
const topMid = getPixelByCoord(oldData, x, y - 1, width);
// 上右
const topRight = getPixelByCoord(oldData, x + 1, y - 1, width);
// 中左
const midLeft = getPixelByCoord(oldData, x - 1, y, width);
// 中中 / 当前
const curPix = getPixelByCoord(oldData, x, y, width);
// 中右
const midRight = getPixelByCoord(oldData, x + 1, y, width);
// 下左
const bottomLeft = getPixelByCoord(oldData, x - 1, y + 1, width);
// 下中
const bottomMid = getPixelByCoord(oldData, x, y + 1, width);
// 下右
const bottomRight = getPixelByCoord(oldData, x + 1, y + 1, width);
// calculate acc
const sumR = (topLeft[0] * kernel[0] + topMid[0] * kernel[1] + topRight[0] * kernel[2] +
midLeft[0] * kernel[3] + curPix[0] * kernel[4] + midRight[0] * kernel[5] +
bottomLeft[0] * kernel[6] + bottomMid[0] * kernel[7] + bottomRight[0] * kernel[8]) / kernelWeight;
const sumG = (topLeft[1] * kernel[0] + topMid[1] * kernel[1] + topRight[1] * kernel[2] +
midLeft[1] * kernel[3] + curPix[1] * kernel[4] + midRight[1] * kernel[5] +
bottomLeft[1] * kernel[6] + bottomMid[1] * kernel[7] + bottomRight[1] * kernel[8]) / kernelWeight;
const sumB = (topLeft[2] * kernel[0] + topMid[2] * kernel[1] + topRight[2] * kernel[2] +
midLeft[2] * kernel[3] + curPix[2] * kernel[4] + midRight[2] * kernel[5] +
bottomLeft[2] * kernel[6] + bottomMid[2] * kernel[7] + bottomRight[2] * kernel[8]) / kernelWeight;
// const sumA = (topLeft[3] * kernel[0] + topMid[3] * kernel[1] + topRight[3] * kernel[2]
// + midLeft[3] * kernel[3] + curPix[3] * kernel[4] + midRight[3] * kernel[5]
// + bottomLeft[3] * kernel[6] + bottomMid[3] * kernel[7] + bottomRight[3] * kernel[8]) / kernelWeight;
const index = 4 * x + 4 * width * y;
const alpha = curPix[3];
newData[index] = parseInt(sumR);
newData[index + 1] = parseInt(sumG);
newData[index + 2] = parseInt(sumB);
newData[index + 3] = alpha;
// console.log(sumR, sumG, sumB, alpha)
}
}
return imageData;
}
const getPixelByCoord = (imageData, x, y, width) => {
const index = 4 * x + 4 * width * y;
let [r, g, b, a] = [imageData[index], imageData[index + 1], imageData[index + 2], imageData[index + 3]];
// console.log(index, [r, g, b, a])
return [r, g, b, a];
}
复制代码
- 把处理后的像素绘制再到canvas
const processImageData = ({
el,
type,
imgPath,
hookMounted
}) =>
genImageData(el, imgPath, hookMounted)
.then(({
ctx,
data
}) => {
console.log('ConvolutionKernel', ConvolutionKernel[type])
const imageData = handleConvolution(data, ConvolutionKernel[type]);
// 绘制 新的 imageData 到 canvas
ctx.putImageData(imageData, 0, 0);
return ctx;
});
const genImageData = (el, imgPath, hookMounted) =>
genCanvasAndImg(el, imgPath, hookMounted)
.then(ctx => {
const data = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
return {
data,
ctx
}
})
.catch(e => {
console.log(e)
});
复制代码
- 循环处理并显示
// ["normal", "blur", "blur2", "edgedetect", "emboss", "sharpen"]
Object.keys(ConvolutionKernel).slice(0)
.forEach((v, i) => setTimeout(() => {
//
processImg({ el: '#app', type: v, imgPath: IMGPath });
console.log(v);
}, 1000 * i));
复制代码
最后效果, 如图所示。
总结
这里我们主要,介绍了一个图像处理相关知识点,并利用不同对卷积核对数字图像进行处理,
当然了,这里知识列出了几种常见对卷积核,在图像处理领域,这些核往往想要配合使用来实现各种目的,比如图像识别预处理,边缘检测等等。
思考题:
上述提到的的图像处理技术能否在图像 OCR 识别中进行运用呢 ?是否可以通过传统相似性计算来进行
识别准确率的量化? 动手实践一下吧 ~
学到了一个知识点,并把它运用在项目中
与 拿到了一个项目,能把其中对各个技术实现方案或知识点进行优劣权衡
是两种不同对境界, 后者需要用于大量的知识储备以及实践经验, 前者则是慢慢积累量变的过程。 循环往复。由于互联网技术更新愈来愈快,需要每个技术人都得不断学习与更新。