獲取圖片主色調 (Dominant Color) 是圖片推薦、配色生成、UI 適配的基礎需求。本文對比三種方案:全像素平均法 (原文)、K-Means 聚類法 (業界標準)、ImageMagick 直方圖法 (最高效),並給出生產級代碼與選型建議。
| 方案 | 原理 | 時間複雜度 | 準確度 | PHP 實現難度 | 典型場景 |
|---|---|---|---|---|---|
| 全像素平均 | 遍歷所有像素求 RGB 算術平均 | O(N) N=像素總數 | 低 (易受背景大面積單色干擾) | 極簡 (GD 原生) | 縮略圖、極簡場景 |
| K-Means 聚類 | RGB 空間聚類,取最大簇質心 | O(K×N×I) K=簇數 I=迭代次 | 高 (能提取視覺主色) | 中 (需自行實現或引入庫) | 配色生成、設計輔助、電商主圖 |
| ImageMagick 直方圖 | 內建 `getImageHistogram` + 排序 | O(N) C層優化 | 高 (可獲取 Top-N 色及佔比) | 低 (Imagick 一行代碼) | 生產環境首選、批量處理 |
<?php
function getAverageColor(string $imagePath): array {
$img = @imagecreatefromstring(file_get_contents($imagePath));
if (!$img) throw new \RuntimeException("Invalid image: $imagePath");
$w = imagesx($img);
$h = imagesy($img);
$total = $w * $h;
// 為了性能,大圖可降採樣 (每隔 N 像素取樣)
$step = max(1, (int)sqrt($total / 50000)); // 目標採樣 ~5萬點
$rSum = $gSum = $bSum = 0;
$count = 0;
for ($x = 0; $x < $w; $x += $step) {
for ($y = 0; $y < $h; $y += $step) {
$rgb = imagecolorat($img, $x, $y);
$rSum += ($rgb >> 16) & 0xFF;
$gSum += ($rgb >> 8) & 0xFF;
$bSum += $rgb & 0xFF;
$count++;
}
}
imagedestroy($img);
return [
'r' => (int)round($rSum / $count),
'g' => (int)round($gSum / $count),
'b' => (int)round($bSum / $count),
'hex' => sprintf('#%02X%02X%02X',
(int)round($rSum / $count),
(int)round($gSum / $count),
(int)round($bSum / $count)
),
];
}
缺陷:白背景產品圖 → 平均色偏白;漸變背景 → 平均色為中性灰,均非視覺主色。
<?php
/**
* 簡易 K-Means++ 初始化 + Lloyd 算法
* 返回 Top-N 主色及其佔比
*/
function getDominantColorsKMeans(string $imagePath, int $k = 5, int $maxIter = 10, int $sample = 20000): array {
$img = @imagecreatefromstring(file_get_contents($imagePath));
if (!$img) throw new \RuntimeException("Invalid image");
$w = imagesx($img);
$h = imagesy($img);
$total = $w * $h;
// 1. 隨機採樣像素 (避免全量參與聚類)
$pixels = [];
$step = max(1, (int)sqrt($total / $sample));
for ($x = 0; $x < $w; $x += $step) {
for ($y = 0; $y < $h; $y += $step) {
$rgb = imagecolorat($img, $x, $y);
$pixels[] = [
($rgb >> 16) & 0xFF,
($rgb >> 8) & 0xFF,
$rgb & 0xFF,
];
if (count($pixels) >= $sample) break 2;
}
}
imagedestroy($img);
$n = count($pixels);
if ($n === 0) return [];
// 2. K-Means++ 初始化質心
$centroids = [$pixels[array_rand($pixels)]];
for ($i = 1; $i < $k; $i++) {
$distances = [];
foreach ($pixels as $p) {
$minDist = PHP_INT_MAX;
foreach ($centroids as $c) {
$d = ($p[0]-$c[0])**2 + ($p[1]-$c[1])**2 + ($p[2]-$c[2])**2;
if ($d < $minDist) $minDist = $d;
}
$distances[] = $minDist;
}
// 概率正比於距離平方
$sum = array_sum($distances);
$rand = mt_rand() / mt_getrandmax() * $sum;
$cum = 0;
foreach ($distances as $idx => $d) {
$cum += $d;
if ($cum >= $rand) { $centroids[] = $pixels[$idx]; break; }
}
}
// 3. Lloyd 迭代
for ($iter = 0; $iter < $maxIter; $iter++) {
$clusters = array_fill(0, $k, []);
foreach ($pixels as $p) {
$best = 0; $bestDist = PHP_INT_MAX;
foreach ($centroids as $ci => $c) {
$d = ($p[0]-$c[0])**2 + ($p[1]-$c[1])**2 + ($p[2]-$c[2])**2;
if ($d < $bestDist) { $bestDist = $d; $best = $ci; }
}
$clusters[$best][] = $p;
}
$changed = false;
foreach ($clusters as $ci => $cluster) {
if (empty($cluster)) continue;
$newC = [0,0,0];
foreach ($cluster as $p) {
$newC[0] += $p[0]; $newC[1] += $p[1]; $newC[2] += $p[2];
}
$newC[0] = (int)round($newC[0] / count($cluster));
$newC[1] = (int)round($newC[1] / count($cluster));
$newC[2] = (int)round($newC[2] / count($cluster));
if ($newC !== $centroids[$ci]) $changed = true;
$centroids[$ci] = $newC;
}
if (!$changed) break;
}
// 4. 統計簇大小,按佔比排序
$result = [];
foreach ($clusters as $ci => $cluster) {
if (empty($cluster)) continue;
$c = $centroids[$ci];
$ratio = count($cluster) / $n;
$result[] = [
'r' => $c[0], 'g' => $c[1], 'b' => $c[2],
'hex' => sprintf('#%02X%02X%02X', $c[0], $c[1], $c[2]),
'ratio' => round($ratio, 4),
];
}
usort($result, fn($a, $b) => $b['ratio'] <=> $a['ratio']);
return $result;
}
<?php
// 需安裝: pecl install imagick
function getDominantColorsImagick(string $imagePath, int $topN = 5): array {
$imagick = new \Imagick($imagePath);
// 可選:縮小圖片加速 (直方圖統計不受縮放影響)
$imagick->scaleImage(150, 150, true);
// 獲取顏色直方圖: [ImagickPixel] => count
$histogram = $imagick->getImageHistogram();
if (!$histogram) return [];
// 轉換為可排序數組
$colors = [];
foreach ($histogram as $pixel => $count) {
$color = $pixel->getColor(); // ['r'=>,'g'=>,'b'=>,'a'=>]
// 忽略透明度極低的像素
if (($color['a'] ?? 255) < 128) continue;
$colors[] = [
'r' => $color['r'], 'g' => $color['g'], 'b' => $color['b'],
'hex' => sprintf('#%02X%02X%02X', $color['r'], $color['g'], $color['b']),
'count' => $count,
];
}
// 按像素數量降序
usort($colors, fn($a, $b) => $b['count'] <=> $a['count']);
// 計算佔比
$totalPixels = array_sum(array_column($colors, 'count'));
$result = [];
foreach (array_slice($colors, 0, $topN) as $c) {
$result[] = [
'r' => $c['r'], 'g' => $c['g'], 'b' => $c['b'],
'hex' => $c['hex'],
'ratio' => round($c['count'] / $totalPixels, 4),
];
}
return $result;
}
圖片: 白背景紅色 T恤模特圖 --- 平均法 --- RGB(210, 205, 200) #D2CDC8 (偏白灰,失敗) --- K-Means (k=5, 2萬採樣) --- 1. #E83E3E (紅色) 32.1% ← 視覺主色 ✓ 2. #F5F5F5 (白背景) 28.4% 3. #3A3A3A (深色陰影) 12.0% 4. #8B7355 (膚色) 9.5% 5. #C0C0C0 (灰邊框) 7.2% --- Imagick 直方圖 (Top 5) --- 1. #E83E3E (紅色) 31.8% ← 一致 ✓ 2. #F5F5F5 (白背景) 29.1% 3. #3A3A3A (深色陰影) 11.7% 4. #8B7355 (膚色) 9.8% 5. #C0C0C0 (灰邊框) 7.0%
<?php
function generatePalette(string $imagePath): array {
$colors = getDominantColorsImagick($imagePath, 8);
// 過濾近似顏色 (ΔE < 15 視為同色系)
$filtered = [];
foreach ($colors as $c) {
$isNew = true;
foreach ($filtered as $f) {
if (deltaE($c, $f) < 15) { $isNew = false; break; }
}
if ($isNew) $filtered[] = $c;
}
// 生成配色方案
$primary = $filtered[0]['hex'] ?? '#333333';
$secondary = $filtered[1]['hex'] ?? '#666666';
$accent = $filtered[2]['hex'] ?? '#999999';
return [
'primary' => $primary,
'secondary' => $secondary,
'accent' => $accent,
'palette' => array_map(fn($c) => $c['hex'], $filtered),
'ratios' => array_map(fn($c) => $c['ratio'], $filtered),
];
}
// CIE76 ΔE 簡易版
function deltaE(array $c1, array $c2): float {
return sqrt(
($c1['r']-$c2['r'])**2 +
($c1['g']-$c2['g'])**2 +
($c1['b']-$c2['b'])**2
);
}
「主色調」非單一數學定義,而是視覺感知產物。**全像素平均法本質是「背景色」,K-Means/直方圖才是真正的「主色調」。** 生產環境請直接使用 ImageMagick `getImageHistogram()`,配合降採樣、ΔE 去重、配色方案生成,即可構建專業級圖片色彩分析服務。