js 像素级别操作实现图片灰度化
实现思路
- 图片转换成 canvas
- 使用 canvas 的 CanvasRenderingContext2D 对象的 getImageData 方法获取 ImageData
- 遍历理上一步中获取的 ImageData 对象并利用灰度算法生成新的 ImageData 对象
- 调用 putImageData 方法,使用上一步处理后的ImageData 对象生成灰度图片
基础介绍
ImageData
ImageData 接口描述 元素的一个隐含像素数据的区域。使用 ImageData() 构造函数创建或者使用和 canvas 在一起的 CanvasRenderingContext2D 对象的创建方法: createImageData() 和 getImageData()。也可以使用 putImageData() 设置 canvas 的一部分。
- ImageData 是图片的数据化,它具备以下属性:
- data:Uint8ClampedArray [r,g,b,a, r,g,b,a, r,g,b,a, r,g,b,a......]
- width:整数
- heidth:整数
构造函数
interface ImageData {
/**
* Returns the one-dimensional array containing the data in RGBA order, as integers in the
* range 0 to 255.
*/
readonly data: Uint8ClampedArray;
/**
* Returns the actual dimensions of the data in the ImageData object, in
* pixels.
*/
readonly height: number;
readonly width: number;
}
declare var ImageData: {
prototype: ImageData;
new (width: number, height: number): ImageData;
new (array: Uint8ClampedArray, width: number, height: number): ImageData;
};
三个参数,第一个 是 Uint8ClampedArray 的实例,第二个和第三个表示的是 width 和 height,必须保证 Uint8ClampedArray 的 length = 4*width*height 才不会报错,如果第一个参数 Uint8ClampedArray 没有的话,自动按照 width 和 height 的大小,以 0 填充整个像素矩阵。
使用给定的 Uint8ClampedArray 创建一个 ImageData 对象,并包含图像的大小。如果不给定数组,会创建一个“完全透明”(因为透明度值为 0) 的黑色矩形图像。注意,这是最常见的方式去创建这样一个对象,在 createImageData() 不可用时。
以 2*2 图片为例:
- data:Uint8ClampedArray [0,1,2,3, 4,5,6,7,8,9,10,11,12,13,14,15]
灰度算法
传统灰度算法
Gray = R * 0.299 + G * 0.587 + B * 0.114;
平均灰度算法
Gray = (R + G + B) / 3;
最大值灰度算法
Gray = Math.max(R, G, B);
加权平均值灰度算法
Gray = R G B的加权平均值
效果图
示例代码
html
<div id="app">
<input type="file" @change="onFileChange" />
<div>
<label>效果:</label>
<select v-model="data.algId">
<option v-for="item in algList" :value="item.value"
>{{item.label}}</option
>
</select>
</div>
<div>
<label>放大比列:</label>
<input type="number" v-model="data.scale" placeholder="请输入放大比列" />
</div>
<hr />
<div style="display: flex;vertical-align: top">
<label>源文件:</label>
<div id="source"></div>
<label>直接绘制canvas:</label>
<canvas id="canvas"></canvas>
</div>
<label>处理后结果:</label>
<canvas id="convert-canvas"></canvas>
<hr />
</div>
script
import { createApp, reactive } from 'https://unpkg.com/petite-vue?module';
import { fileToImage, chunkArray } from './utils.js';
import AlgFactory, { ALG_TYPE } from './AlgFactory.js';
const canvas = document.getElementById('canvas');
const convertCanvas = document.getElementById('convert-canvas');
const ctx = canvas.getContext('2d');
const ctx2 = convertCanvas.getContext('2d');
const source = document.getElementById('source');
const algList = [
{ label: '放大', value: ALG_TYPE.SCALE },
// {label: '重复', value: ALG_TYPE.REPEAT},
{ label: '传统灰度算法', value: ALG_TYPE.TRADITION_ASH_ALG },
{ label: '平均灰度算法', value: ALG_TYPE.AVARAGE_ASH_ALG },
{ label: '最大值灰度算法', value: ALG_TYPE.MAX_ASH_ALG },
{ label: '加权平均值灰度算法', value: ALG_TYPE.WEIGHTED_AVERAGE_ASH_ALG },
];
const data = reactive({
scale: '1',
algId: ALG_TYPE.TRADITION_ASH_ALG,
});
const onFileChange = async (e) => {
const f = e.target.files[0];
try {
const img = await fileToImage(f);
const { width, height } = img;
resetCanvas(img);
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, width, height);
const res = chunkArray(imageData.data, 4);
const newImageData = [];
const alg = AlgFactory.create(data.algId);
alg.convertData(res, newImageData, width, height, data.scale);
const matrix_obj = new ImageData(
Uint8ClampedArray.from(newImageData),
data.scale * width,
data.scale * height
);
ctx2.putImageData(matrix_obj, 0, 0);
} catch (e) {
console.error(e);
}
};
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx2.clearRect(0, 0, convertCanvas.width, convertCanvas.height);
}
function setSourceImg(img) {
source.textContent = '';
source.appendChild(img);
}
function resizeCanvas(width, height) {
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
convertCanvas.setAttribute('width', data.scale * width);
convertCanvas.setAttribute('height', data.scale * height);
}
function resetCanvas(img) {
const { width, height } = img;
clearCanvas();
setSourceImg(img);
resizeCanvas(width, height);
}
createApp({
data,
algList,
onFileChange,
}).mount();
utils.js
export function chunkArray(array, size = 1) {
function baseSlice(array, start, end) {
var index = -1,
length = array.length;
if (start < 0) {
start = -start > length ? 0 : length + start;
}
end = end > length ? length : end;
if (end < 0) {
end += length;
}
length = start > end ? 0 : (end - start) >>> 0;
start >>>= 0;
var result = Array(length);
while (++index < length) {
result[index] = array[index + start];
}
return result;
}
var length = array == null ? 0 : array.length;
if (!length || size < 1) {
return [];
}
var index = 0,
resIndex = 0,
result = Array(Math.ceil(length / size));
while (index < length) {
result[resIndex++] = baseSlice(array, index, (index += size));
}
return result;
}
export function fileToImage(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file); //读取图像文件 result 为 DataURL, DataURL 可直接 赋值给 img.src
reader.onload = function (event) {
const img = document.createElement('img');
img.src = event.target.result; //base64
img.onload = () => resolve(img);
img.onerror = reject;
};
reader.onerror = reject;
});
}
AlgFactory.js
import AvarageAshAlgorithm from './class/AvarageAshAlgorithm.js';
import MaxAshAlgorithm from './class/MaxAshAlgorithm.js';
import ScaleAlgorithm from './class/ScaleAlgorithm.js';
import TraditionAshAlgorithm from './class/TraditionAshAlgorithm.js';
import WeightedAverageAshAlgorithm from './class/WeightedAverageAshAlgorithm.js';
export const ALG_TYPE = {
SCALE: '1',
REPEAT: '2',
TRADITION_ASH_ALG: '3',
AVARAGE_ASH_ALG: '4',
MAX_ASH_ALG: '5',
WEIGHTED_AVERAGE_ASH_ALG: '6',
};
export default class AlgFactory {
static create(type) {
switch (type) {
case ALG_TYPE.SCALE:
return new ScaleAlgorithm();
case ALG_TYPE.REPEAT:
break;
case ALG_TYPE.TRADITION_ASH_ALG:
return new TraditionAshAlgorithm();
case ALG_TYPE.AVARAGE_ASH_ALG:
return new AvarageAshAlgorithm();
case ALG_TYPE.MAX_ASH_ALG:
return new MaxAshAlgorithm();
case ALG_TYPE.WEIGHTED_AVERAGE_ASH_ALG:
return new WeightedAverageAshAlgorithm();
default:
break;
}
}
}
Algorithm.js
export default class Algorithm {
__convertData(width, height, fn, scale = 1) {
for (let i = 0; i < height; i++) {
for (let m = 0; m < scale; m++) {
for (let j = 0; j < width; j++) {
for (let k = 0; k < scale; k++) {
fn(i, j);
}
}
}
}
}
}
AvarageAshAlgorithm.js
import Algorithm from './Algorithm.js';
export default class AvarageAshAlgorithm extends Algorithm {
convertData(sourceArray, imageData, width, height) {
const fn = (i, j) => {
const data =
(sourceArray[i * width + j][0] +
sourceArray[i * width + j][1] +
sourceArray[i * width + j][2]) /
3;
imageData.push(data, data, data, sourceArray[i * width + j][3]);
};
super.__convertData(width, height, fn);
}
}
其余算法省略