日韩欧美国产精品免费一二-日韩欧美国产精品亚洲二区-日韩欧美国产精品专区-日韩欧美国产另-日韩欧美国产免费看-日韩欧美国产免费看清风阁

LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

autohue.js:讓你的圖片和背景融為一體

freeflydom
2025年4月16日 14:42 本文熱度 59

需求

先來看這樣一個場景,拿一個網站舉例

這里有一個常見的網站 banner 圖容器,大小為為1910*560,看起來背景圖完美的充滿了寬度,但是圖片原始大小時,卻是:

它的寬度只有 1440,且 background-size 設置的是 contain ,即等比例縮放,那么可以斷定它兩邊的藍色是依靠背景色填充的。

那么問題來了,這是一個 輪播banner,如果希望添加一張不是藍色的圖片呢?難道要給每張圖片提前標注好背景顏色嗎?這顯然是非常死板的做法。

所以需要從圖片中提取到圖片的主題色,當然這對于 js 來說,也不是什么難事,市面上已經有眾多的開源庫供我們使用。

探索

首先在網絡上找到了以下幾個庫:

  • color-thief 這是一款基于 JavaScript 和 Canvas 的工具,能夠從圖像中提取主要顏色或代表性的調色板
  • vibrant.js該插件是 Android 支持庫中 Palette 類的 JavaScript 版本,可以從圖像中提取突出的顏色
  • rgbaster.js 這是一段小型腳本,可以獲取圖片的主色、次色等信息,方便實現一些精彩的 Web 交互效果

我取最輕量化的 rgbaster.js(此庫非常搞笑,用TS編寫,npm 包卻沒有指定 types) 來測試后發現,它給我在一個漸變色圖片中,返回了七萬多個色值,當然,它準確的提取出了面積最大的色值,但是這個色值不是圖片邊緣的顏色,導致設置為背景色后,并不能完美的融合。

另外的插件各位可以參考這幾篇文章:

可以發現,這些插件主要功能就是取色,并沒有考慮實際的應用場景,對于一個圖片顏色分析工具來說,他們做的很到位,但是在大多數場景中,他們往往是不適用的。

在文章 2 中,作者對比了三款插件對于圖片容器背景色的應用,看起來還是 rgbaster 效果好一點,但是我們剛剛也拿他試了,它并不能適用于顏色復雜度高的、漸變色的圖片。

思考

既然又又又沒有人做這件事,正所謂我不入地獄誰入地獄,我手寫一個

整理一下需求,我發現我希望得到的是:

  1. 圖片的主題色(面積占比最大)
  2. 次主題色(面積占比第二大)
  3. 合適的背景色(即圖片邊緣顏色,漸變時,需要邊緣顏色來設置背景色)

這樣一來,就已經可以覆蓋大部分需求了,1+2 可以生成相關的 主題 TAG、主題背景,3 可以使留白的圖片容器完美融合。

開搞

?? 本小節內容非常硬核,如果不想深究原理可以直接跳過,文章末尾有用法和效果圖 ??

思路

首先需要避免上面提到的插件的缺點,即對漸變圖片要做好處理,不能取出成千上萬的顏色,體驗太差且實用性不強,對于漸變色還有一點,即在漸變路徑上,每一點的顏色都是不一樣的,所以需要將他們以一個閾值分類,挑選出一眾相近色,并計算出一個平均色,這樣就不會導致主題色太精準進而沒有代表性。

對于背景色,需要按情況分析,如果只是希望做一個協調的頁面,那么大可以直接使用主題色做漸變過渡或蒙層,也就是類似于這種效果

但是如果希望背景與圖片完美銜接,讓人看不出圖片邊界的感覺,就需要單獨對邊緣顏色取色了。

最后一個問題,如果圖片分辨率過大,在遍歷像素點時會非常消耗性能,所以需要降低采樣率,雖然會導致一些精度上的丟失,但是調整為一個合適的值后應該基本可用。

剩余的細節問題,我會在下面的代碼中解釋

使用 JaveScript 編碼

接下來我將詳細描述 autohue.js 的實現過程,由于本人對色彩科學不甚了解,如有解釋不到位或錯誤,還請指出。

首先編寫一個入口主函數,我目前考慮到的參數應該有:

export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions)
type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }
interface autoColorPickerOptions {
  /**
   * - 降采樣后的最大尺寸(默認 100px)
   * - 降采樣后的圖片尺寸不會超過該值,可根據需求調整
   * - 降采樣后的圖片尺寸越小,處理速度越快,但可能會影響顏色提取的準確性
   **/
  maxSize?: number
  /**
   * - Lab 距離閾值(默認 10)
   * - 低于此值的顏色歸為同一簇,建議 8~12
   * - 值越大,顏色越容易被合并,提取的顏色越少
   * - 值越小,顏色越容易被區分,提取的顏色越多
   **/
  threshold?: number | thresholdObj
}

概念解釋 Lab ,全稱:CIE L*a*b ,CIE L*a*b* 是CIE XYZ色彩模式的改進型。它的“L”(明亮度),“a”(綠色到紅色)和“b”(藍色到黃色)代表許多的值。與XYZ比較,CIE L*a*b*的色彩更適合于人眼感覺的色彩,正所謂感知均勻

然后需要實現一個正常的 loadImg 方法,使用 canvas 異步加載圖片

function loadImage(imageSource: HTMLImageElement | string): Promise<HTMLImageElement> {
  return new Promise((resolve, reject) => {
    let img: HTMLImageElement
    if (typeof imageSource === 'string') {
      img = new Image()
      img.crossOrigin = 'Anonymous'
      img.src = imageSource
    } else {
      img = imageSource
    }
    if (img.complete) {
      resolve(img)
    } else {
      img.onload = () => resolve(img)
      img.onerror = (err) => reject(err)
    }
  })
}

這樣我們就獲取到了圖片對象。

然后為了圖片過大,我們需要進行降采樣處理

// 利用 Canvas 對圖片進行降采樣,返回 ImageData 對象
function getImageDataFromImage(img: HTMLImageElement, maxSize: number = 100): ImageData {
  const canvas = document.createElement('canvas')
  let width = img.naturalWidth
  let height = img.naturalHeight
  if (width > maxSize || height > maxSize) {
    const scale = Math.min(maxSize / width, maxSize / height)
    width = Math.floor(width * scale)
    height = Math.floor(height * scale)
  }
  canvas.width = width
  canvas.height = height
  const ctx = canvas.getContext('2d')
  if (!ctx) {
    throw new Error('無法獲取 Canvas 上下文')
  }
  ctx.drawImage(img, 0, 0, width, height)
  return ctx.getImageData(0, 0, width, height)
}

概念解釋,降采樣:降采樣(Downsampling)是指在圖像處理中,通過減少數據的采樣率或分辨率來降低數據量的過程。具體來說,就是在保持原始信息大致特征的情況下,減少數據的復雜度和存儲需求。這里簡單理解為將圖片強制壓縮為 100*100 以內,也是 canvas 壓縮圖片的常見做法。

得到圖像信息后,就可以對圖片進行像素遍歷處理了,正如思考中提到的,我們需要對相近色提取并取平均色,并最終獲取到主題色、次主題色。

那么問題來了,什么才算相近色,對于這個問題,在 常規的 rgb 中直接計算是不行的,因為它涉及到一個感知均勻的問題

概念解釋,感知均勻:XYZ系統和在它的色度圖上表示的兩種顏色之間的距離與顏色觀察者感知的變化不一致,這個問題叫做感知均勻性(perceptual uniformity)問題,也就是顏色之間數字上的差別與視覺感知不一致。由于我們需要在顏色簇中計算出平均色,那么對于人眼來說哪些顏色是相近的?此時,我們需要把 sRGB 轉化為 Lab 色彩空間(感知均勻的),再計算其歐氏距離,在某一閾值內的顏色,即可認為是相近色。

所以我們首先需要將 rgb 轉化為 Lab 色彩空間

// 將 sRGB 轉換為 Lab 色彩空間
function rgbToLab(r: number, g: number, b: number): [number, number, number] {
  let R = r / 255,
    G = g / 255,
    B = b / 255
  R = R > 0.04045 ? Math.pow((R + 0.055) / 1.055, 2.4) : R / 12.92
  G = G > 0.04045 ? Math.pow((G + 0.055) / 1.055, 2.4) : G / 12.92
  B = B > 0.04045 ? Math.pow((B + 0.055) / 1.055, 2.4) : B / 12.92
  let X = R * 0.4124 + G * 0.3576 + B * 0.1805
  let Y = R * 0.2126 + G * 0.7152 + B * 0.0722
  let Z = R * 0.0193 + G * 0.1192 + B * 0.9505
  X = X / 0.95047
  Y = Y / 1.0
  Z = Z / 1.08883
  const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)
  const fx = f(X)
  const fy = f(Y)
  const fz = f(Z)
  const L = 116 * fy - 16
  const a = 500 * (fx - fy)
  const bVal = 200 * (fy - fz)
  return [L, a, bVal]
}

這個函數使用了看起來很復雜的算法,不必深究,這是它的大概解釋:

  1. 獲取到 rgb 參數

  2. 轉化為線性 rgb(移除 gamma矯正),常量 0.04045 是sRGB(標準TGB)顏色空間中的一個閾值,用于區分非線性和線性的sRGB值,具體來說,當sRGB顏色分量大于0.04045時,需要通過 gamma 校正(即采用 ((R + 0.055) / 1.055) ^ 2.4)來得到線性RGB;如果小于等于0.04045,則直接進行線性轉換(即 R / 12.92

  3. 線性RGB到XYZ空間的轉換,轉換公式如下:

    • X = R * 0.4124 + G * 0.3576 + B * 0.1805
    • Y = R * 0.2126 + G * 0.7152 + B * 0.0722
    • Z = R * 0.0193 + G * 0.1192 + B * 0.9505
  4. 歸一化XYZ值,為了參考白點(D65),標準白點的XYZ值是 (0.95047, 1.0, 1.08883)。所以需要通過除以這些常數來進行歸一化

  5. XYZ到Lab的轉換,公式函數:const f = (t: number) => (t > 0.008856 ? Math.pow(t, 1 / 3) : 7.787 * t + 16 / 116)

  6. 計算L, a, b 分量

    L:亮度分量(表示顏色的明暗程度)

    • L = 116 * fy - 16

    a:綠色到紅色的色差分量

    • a = 500 * (fx - fy)

    b:藍色到黃色的色差分量

    • b = 200 * (fy - fz)

接下來實現聚類算法

/**
 * 對滿足條件的像素進行聚類
 * @param imageData 圖片像素數據
 * @param condition 判斷像素是否屬于指定區域的條件函數(參數 x, y)
 * @param threshold Lab 距離閾值,低于此值的顏色歸為同一簇,建議 8~12
 */
function clusterPixelsByCondition(imageData: ImageData, condition: (x: number, y: number) => boolean, threshold: number = 10): Cluster[] {
  const clusters: Cluster[] = []
  const data = imageData.data
  const width = imageData.width
  const height = imageData.height
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      if (!condition(x, y)) continue
      const index = (y * width + x) * 4
      if (data[index + 3] === 0) continue // 忽略透明像素
      const r = data[index]
      const g = data[index + 1]
      const b = data[index + 2]
      const lab = rgbToLab(r, g, b)
      let added = false
      for (const cluster of clusters) {
        const d = labDistance(lab, cluster.averageLab)
        if (d < threshold) {
          cluster.count++
          cluster.sumRgb[0] += r
          cluster.sumRgb[1] += g
          cluster.sumRgb[2] += b
          cluster.sumLab[0] += lab[0]
          cluster.sumLab[1] += lab[1]
          cluster.sumLab[2] += lab[2]
          cluster.averageRgb = [cluster.sumRgb[0] / cluster.count, cluster.sumRgb[1] / cluster.count, cluster.sumRgb[2] / cluster.count]
          cluster.averageLab = [cluster.sumLab[0] / cluster.count, cluster.sumLab[1] / cluster.count, cluster.sumLab[2] / cluster.count]
          added = true
          break
        }
      }
      if (!added) {
        clusters.push({
          count: 1,
          sumRgb: [r, g, b],
          sumLab: [lab[0], lab[1], lab[2]],
          averageRgb: [r, g, b],
          averageLab: [lab[0], lab[1], lab[2]]
        })
      }
    }
  }
  return clusters
}

函數內部有一個 labDistance 的調用,labDistance 是計算 Lab 顏色空間中的歐氏距離的

// 計算 Lab 空間的歐氏距離
function labDistance(lab1: [number, number, number], lab2: [number, number, number]): number {
  const dL = lab1[0] - lab2[0]
  const da = lab1[1] - lab2[1]
  const db = lab1[2] - lab2[2]
  return Math.sqrt(dL * dL + da * da + db * db)
}

概念解釋,歐氏距離:Euclidean Distance,是一種在多維空間中測量兩個點之間“直線”距離的方法。這種距離的計算基于歐幾里得幾何中兩點之間的距離公式,通過計算兩點在各個維度上的差的平方和,然后取平方根得到。歐氏距離是指n維空間中兩個點之間的真實距離,或者向量的自然長度(即該點到原點的距離)。

總的來說,這個函數采用了類似 K-means 的聚類方式,將小于用戶傳入閾值的顏色歸為一簇,并取平均色(使用 Lab 值)。

概念解釋,聚類算法:Clustering Algorithm 是一種無監督學習方法,其目的是將數據集中的元素分成不同的組(簇),使得同一組內的元素相似度較高,而不同組之間的元素相似度較低。這里是將相近色歸為一簇。

概念解釋,顏色簇:簇是聚類算法中一個常見的概念,可以大致理解為 "一類"

得到了顏色簇集合后,就可以按照count大小來判斷哪個是主題色了

  // 對全圖所有像素進行聚類
  let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
  clusters.sort((a, b) => b.count - a.count)
  const primaryCluster = clusters[0]
  const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
  const primaryColor = rgbToHex(primaryCluster.averageRgb)
  const secondaryColor = rgbToHex(secondaryCluster.averageRgb)

現在我們已經獲取到了主題色、次主題色 ??????

接下來,我們繼續計算邊緣顏色

按照同樣的方法,只是把閾值設小一點,我這里直接設置為 1 (threshold.top 等都是1)

  // 分別對上、右、下、左邊緣進行聚類
  const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
  topClusters.sort((a, b) => b.count - a.count)
  const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor
  const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
  bottomClusters.sort((a, b) => b.count - a.count)
  const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor
  const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
  leftClusters.sort((a, b) => b.count - a.count)
  const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor
  const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
  rightClusters.sort((a, b) => b.count - a.count)
  const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor

這樣我們就獲取到了上下左右四條邊的顏色 ??????

這樣大致的工作就完成了,最后我們將需要的屬性導出給用戶,我們的主函數最終長這樣:

/**
 * 主函數:根據圖片自動提取顏色
 * @param imageSource 圖片 URL 或 HTMLImageElement
 * @returns 返回包含主要顏色、次要顏色和背景色對象(上、右、下、左)的結果
 */
export default async function colorPicker(imageSource: HTMLImageElement | string, options?: autoColorPickerOptions): Promise<AutoHueResult> {
  const { maxSize, threshold } = __handleAutoHueOptions(options)
  const img = await loadImage(imageSource)
  // 降采樣(最大尺寸 100px,可根據需求調整)
  const imageData = getImageDataFromImage(img, maxSize)
  // 對全圖所有像素進行聚類
  let clusters = clusterPixelsByCondition(imageData, () => true, threshold.primary)
  clusters.sort((a, b) => b.count - a.count)
  const primaryCluster = clusters[0]
  const secondaryCluster = clusters.length > 1 ? clusters[1] : clusters[0]
  const primaryColor = rgbToHex(primaryCluster.averageRgb)
  const secondaryColor = rgbToHex(secondaryCluster.averageRgb)
  // 定義邊緣寬度(單位像素)
  const margin = 10
  const width = imageData.width
  const height = imageData.height
  // 分別對上、右、下、左邊緣進行聚類
  const topClusters = clusterPixelsByCondition(imageData, (_x, y) => y < margin, threshold.top)
  topClusters.sort((a, b) => b.count - a.count)
  const topColor = topClusters.length > 0 ? rgbToHex(topClusters[0].averageRgb) : primaryColor
  const bottomClusters = clusterPixelsByCondition(imageData, (_x, y) => y >= height - margin, threshold.bottom)
  bottomClusters.sort((a, b) => b.count - a.count)
  const bottomColor = bottomClusters.length > 0 ? rgbToHex(bottomClusters[0].averageRgb) : primaryColor
  const leftClusters = clusterPixelsByCondition(imageData, (x, _y) => x < margin, threshold.left)
  leftClusters.sort((a, b) => b.count - a.count)
  const leftColor = leftClusters.length > 0 ? rgbToHex(leftClusters[0].averageRgb) : primaryColor
  const rightClusters = clusterPixelsByCondition(imageData, (x, _y) => x >= width - margin, threshold.right)
  rightClusters.sort((a, b) => b.count - a.count)
  const rightColor = rightClusters.length > 0 ? rgbToHex(rightClusters[0].averageRgb) : primaryColor
  return {
    primaryColor,
    secondaryColor,
    backgroundColor: {
      top: topColor,
      right: rightColor,
      bottom: bottomColor,
      left: leftColor
    }
  }
}

還記得本小節一開始提到的參數嗎,你可以自定義 maxSize(壓縮大小,用于降采樣)、threshold(閾值,用于設置簇大?。?/p>

為了用戶友好,我還編寫了 threshold 參數的可選類型:number | thresholdObj

type thresholdObj = { primary?: number; left?: number; right?: number; top?: number; bottom?: number }

可以單獨設置主閾值、上下左右四邊閾值,以適應更個性化的情況。

autohue.js 誕生了

名字的由來:秉承一貫命名習慣,auto 家族成員又多一個,與顏色有關的單詞有好多個,我取了最短最好記的一個 hue(色相),也比較契合插件用途。

此插件已在 github 開源:GitHub autohue.js

npm 主頁:NPM autohue.js

在線體驗:autohue.js 官方首頁

安裝與使用

pnpm i autohue.js
import autohue from 'autohue.js'
autohue(url, {
  threshold: {
    primary: 10,
    left: 1,
    bottom: 12
  },
  maxSize: 50
})
  .then((result) => {
    // 使用 console.log 打印出色塊元素s
    console.log(`%c${result.primaryColor}`, 'color: #fff; background: ' + result.primaryColor, 'main')
    console.log(`%c${result.secondaryColor}`, 'color: #fff; background: ' + result.secondaryColor, 'sub')
    console.log(`%c${result.backgroundColor.left}`, 'color: #fff; background: ' + result.backgroundColor.left, 'bg-left')
    console.log(`%c${result.backgroundColor.right}`, 'color: #fff; background: ' + result.backgroundColor.right, 'bg-right')
    console.log(`%clinear-gradient to right`, 'color: #fff; background: linear-gradient(to right, ' + result.backgroundColor.left + ', ' + result.backgroundColor.right + ')', 'bg')
    bg.value = `linear-gradient(to right, ${result.backgroundColor.left}, ${result.backgroundColor.right})`
  })
  .catch((err) => console.error(err))

最終效果

復雜邊緣效果

縱向漸變效果(這里使用的是 left 和 right 邊的值,可能使用 top 和 bottom 效果更佳)

純色效果(因為單獨對邊緣采樣,所以無論圖片內容多復雜,純色基本看不出邊界)

突變邊緣效果(此時用css做漸變蒙層應該效果會更好)

橫向漸變效果(使用的是 left 和 right 的色值),基本看不出邊界

轉自https://juejin.cn/post/7471919714292105270
?


該文章在 2025/4/16 14:42:10 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

主站蜘蛛池模板: 国产精品人一成在线观看 | 成年免费视频播放网站推荐 | 日韩中字在线 | 亚洲国产丝袜一区二区 | 亚洲一区不卡视频 | 欧美日韩国产区在线观看 | 欧美性色欧 | 国产午夜福利短视频在线观看 | 国产91丝袜在线观看 | 国精产品一区二区三区四区糖心 | 另类专区亚洲97在线视频 | 青春草在线视频免费观看 | 欧美一区在线播放 | 亚洲人成网站免费播放 | 国产黑色丝 | 可以免费看 | 日本激情在线观看免费观看 | 99国产婷婷综合在线视频 | 欧美精品亚洲精品日韩专区va | 秋霞伦理电影在线看 | 在线免费观看区一区二 | 97在线观看高清视频免费 | 国产午夜福利精品在线观看不 | 日本免费一级婬片a级中文字幕 | 岛国成人免费大片在 | 欧产日产国产精品精品mp4 | 亚洲人成网站在线观看 | 精品亚洲成a人在线观看 | 免费观看亚洲人成网站 | 日韩亚洲欧美一区噜噜噜 | 午夜日b视频 | 自拍偷自拍亚洲精品偷一 | 国产亚洲精品激情都市 | 免费jjzz | 亚洲第二页 | 网站在线观看 | 涩色亚洲激情第二页 | 丰满在线观看 | 亚洲中文字幕精品一区二区三区 | 亚洲人成人一区二区三区 | 亚洲精品国产精品乱码不卡√ |