天天看點

canvas 操作圖像記錄:自适應+提取背景色+灰階處理

目錄

  • 繪制圖像,并自适應水準垂直居中
    • 繪制圖像
    • 自适應水準垂直居中
  • 圖像資料的處理
  • 圖像灰階處理
    • 公式
    • 平均值
    • 圖像資料繪制到 canvas 上
    • 灰階處理方法
  • 提取圖像的主題色:平均值法(單色背景)、最多色值法(雙色背景)
    • rgba 二維數組
    • 平均值法(單色背景)
    • 最多色值法(漸變背景)
    • 中位切分法實作
用個小 demo 記錄一下,如何在 canvas 上操作圖像。

點選線上體驗

利用的是 canvas 的 api

drawImage

void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
           

參數了解:

canvas 操作圖像記錄:自适應+提取背景色+灰階處理

在 canvas 上繪制圖像,會存在這麼幾種情況:

  • 圖像的長寬都比canvas的長寬大
  • 圖像的長比canvas的大
  • 圖像的寬比canvas的大
  • 圖像的長寬都比canvas的小

是以在自适應時需要根據這幾種情況分别處理,利用寬高比,并且保證圖像的寬高比始終一緻。

其次水準垂直居中,利用的是 css 中處理水準垂直居中的方案:

top = (box.height - div.height) / 2
left = (box.width - div.width) / 2
           

于是就有了以下的方法:

// 計算圖檔居中繪制到畫布上時 的寬高及起點坐标位置
function calculate(canvasWidth, canvasHeight, imgWidth, imgHeight) {
  let x = 0;
  let y = 0;

  const canvasWHRadio = canvasWidth / canvasHeight
  const imgWHRadio = imgWidth / imgHeight
  
  if (imgWidth < canvasWidth && imgHeight < canvasHeight) {
    x = (canvasWidth - imgWidth) * 0.5
    y = (canvasHeight - imgHeight) * 0.5
  } else if (imgWHRadio > canvasWHRadio) {
    imgHeight = canvasWidth / imgWHRadio
    imgWidth = canvasWidth
    y = (canvasHeight - imgHeight) * 0.5
  } else {
    imgWidth = canvasHeight * imgWHRadio
    imgHeight = canvasHeight
    x = (canvasWidth - imgWidth) * 0.5
  }

  return {
    x,
    y,
    width: imgWidth,
    height: imgHeight
  }
}
           

要對圖像進行處理,比如灰階化,提取顔色等。都是在圖像資料上進行處理的。

擷取圖像資料,要用 canvas 提供的 api

getImageData

ImageData ctx.getImageData(sx, sy, sw, sh);
           

擷取到的資料中,包含擷取到的矩形圖像的

width

height

data

。要處理的就是 data 了。

data 是一個大的類數組,類型是Uint8ClampedArray(8位無符号整型固定數組),限定了數組值在[0-255]。其中,每 4 位表示一個 rgba 值。分别對應 r(紅)、g(綠)、b(藍)、a(透明度)。

RGB圖轉灰階圖經典的心理學公式:Gray = R0.299 + G0.587 + B*0.114

人眼對綠色的敏感度最高,對紅色的敏感度次之,對藍色的敏感度最低,是以使用不同的權重将得到比較合理的灰階圖像。

function getGrayColor (r, g, b) {
  // 心理學灰階公式: Gray = R*0.299 + G*0.587 + B*0.114
  // 考慮精度:Gray = (R*299 + G*587 + B*114) / 1000
  // 考慮精度 + 速度:Gray = (R*38 + G*75 + B*15) >> 7
  return (r * 38 + g * 75 + b * 15) >> 7
}
           

公式各種變體參考:從RGB色轉為灰階色算法

求出 rgb 的平均值,并把這個平均值賦給 rgb。處理出來的灰階圖可能會比較生硬,沒有公式法處理出來的灰階圖柔和。

function getGrayColorByAvg (r, g, b) {
  // 平均值法
  const avg = (r + g + b) / 3
  return avg
}
           

處理好圖像資料了,灰階處理,再将圖像資料繪制到 canvas 上,利用的是

putImageData

void ctx.putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);
           

function imgGray () {
  const {ctx, drawImgW, drawImgH, drawImgX, drawImgY} = global;
  let imgData = ctx.getImageData(drawImgX, drawImgY, drawImgW, drawImgH);
  global.imgData = imgData;

  let copyImgData = new ImageData(new Uint8ClampedArray([...imgData.data]), imgData.width, imgData.height)

  for (let i=0; i<copyImgData.data.length; i+=4) {
    const R = copyImgData.data[i];
    const G = copyImgData.data[i+1];
    const B = copyImgData.data[i+2];
    const gray = getGrayColor(R, G, B)
    copyImgData.data[i] = gray;
    copyImgData.data[i+1] = gray;
    copyImgData.data[i+2] = gray;
  }

  ctx.putImageData(copyImgData, drawImgX, drawImgY);
}
           

圖像的主題色有什麼用呢?

用處之一就是作為圖像的背景色,當圖像沒加載出來之前,可以先用主題色填充。或者讓圖像的容器填充圖像的背景色填補空白部分,讓圖像觀感體驗更好。

關于提取圖像的主題色,其實是門深奧的技術。

主要是這麼幾種:顔色量化算法(中為切分法、八叉樹法)、聚類算法、顔色模組化。詳情可參考圖像主題色提取算法。

這些算法比較複雜,下面介紹的是比較簡單粗暴的。

const perChunkSize = 4;
const imgRgbaData = Array.from(imgData.data).reduce((rgba, item, index) => {
  const subIndex = Math.floor(index / perChunkSize);
  if (!rgba[subIndex]) {
      rgba[subIndex] = []
  }
  rgba[subIndex].push(item)
  return rgba;
}, [])
           

提取圖像的主題色,最簡單的方法是将圖像資料的所有 r、g、b 值加起來,再除以圖像的面積,求其平均值。

該方法的缺點在于:無法計算透明背景的主色調,主色調會被png圖檔透明區域的大小所影響。優點就是簡單明了,友善快捷。

主題色求出來了,互補色也比較簡單。就是用 255 - 主色調。即用 255 分别減去主色調的 r,g,b 的值分别得到一個新的 r,g,b 的值作為互補色調。

互補色有什麼用呢?

用處之一就是,填充文字的顔色,讓文字顯示正常。文字的顔色和主題色背景的顔色互斥(互補)時,會比較容易進入眼睛被看到。

function getColorByAvg (imgRgbaData, sizes) {
  // 主色,平均值。将圖檔每一個像素點的r,g,b通道的值分别累加,然後分别用累加的r,g,b的值除以圖檔總像素點的個數,分别得到一個平均的r,g,b值并作為圖檔主色調的rgb值
  const mainColor = {
      r: 0,
      g: 0,
      b: 0
  }
  imgRgbaData.forEach(rgba => {
      mainColor.r += rgba[0]
      mainColor.g += rgba[1]
      mainColor.b += rgba[2]
  })

  const area = sizes.width * sizes.height
  mainColor.r = mainColor.r / area | 0
  mainColor.g = mainColor.g / area | 0
  mainColor.b = mainColor.b / area | 0

  // 互補色,255 - 主色調。用255分别減去主色調的r,g,b的值分别得到一個新的r,g,b的值作為互補色調
  const reverseColor = {
      r: 255 - mainColor.r,
      g: 255 - mainColor.g,
      b: 255 - mainColor.b
  }

  return {
    bgColor: `rgb(${mainColor.r}, ${mainColor.g}, ${mainColor.b})`,
    txtColor: `rgb(${reverseColor.r}, ${reverseColor.g}, ${reverseColor.b})`
  }
}
           

這種方法比較複雜一些。統計出每種顔色被使用到的次數,再根據次數降序排序,根據灰階值降序排序。取出第1個和第10個最為漸變色。

互補色利用灰階公式,比中間值 125 大的為白色,反之為黑色。

該方案借鑒的是grade.js

function get2ColorByCount (imgRgbaData) {
  const filterData = imgRgbaData.filter(rgba => rgba.slice(0, 3).every(val => val > 0 && val < 255))
  // 統計每一種顔色的使用次數
  const countData = filterData.reduce((obj, rgba, index) => {
      const key = rgba.join('|')
      obj[key] = obj[key] ? ++(obj[key]) : 1;
      return obj
  }, {});

  let sortData = Object.keys(countData).map(key => {
      const rgba = key.split('|');
      const gray = getGrayColor(rgba[0], rgba[1], rgba[2])
      return {
          rgba,
          count: countData[key],
          gray
      }
  })
  sortData = sortData.sort((a, b) => a.count - b.count).reverse()
  sortData = sortData.slice(0, 10).sort((a, b) => a.brightness - b.brightness).reverse()

  const start = sortData[0].rgba
  const end = sortData[sortData.length - 1].rgba

  const rgb = [(start[0] / 2 + end[0] / 2) | 0, (start[1] / 2 + end[1] / 2) | 0, (start[2] / 2 + end[2] / 2) | 0]
  const color = getGrayColor(rgb[0], rgb[1], rgb[2]) > 255 / 2 ? '#000' : '#fff'

  return {
    bgColor: [`rgb(${start[0]}, ${start[1]}, ${start[2]})`, `rgb(${end[0]}, ${end[1]}, ${end[2]})`],
    txtColor: color
  }
}
           

color-thief

不僅提取出了主題色,還提取出了互補色,配色。可以說非常厲害了。

以上就是記錄的全部了。