js 像素级别操作实现图片灰度化

1,979 阅读3分钟

js 像素级别操作实现图片灰度化

实现思路

  1. 图片转换成 canvas
  2. 使用 canvas 的 CanvasRenderingContext2D 对象的 getImageData 方法获取 ImageData
  3. 遍历理上一步中获取的 ImageData 对象并利用灰度算法生成新的 ImageData 对象
  4. 调用 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);
  }
}

其余算法省略