Giải pháp dưới đây xử lý đầy đủ các vấn đề thực tế: lọc nhiễu sáng/tối, giảm kích thước ảnh để tối ưu hiệu năng, phân cụm màu thông minh, chấm điểm màu dựa trên độ bão hòa và trọng số, và cuối cùng là normalize để đảm bảo hiển thị tốt trên cả light/dark mode.
Hàm PHP hoàn chỉnh
<?php
/**
* Lấy màu nổi bật nhất từ ảnh bìa manga.
* Dùng median-cut quantization (K cụm) + vibrance scoring.
*
* @param string $image_path Đường dẫn tuyệt đối tới file ảnh
* @param int $k Số cụm màu (6-8 là sweet spot)
* @return string|false "R,G,B" hoặc false nếu lỗi
*/
function init_html_extract_average_color( $image_path, $k = 6 ) {
// ── 1. Load ảnh ──────────────────────────────────────────────
$info = @getimagesize( $image_path );
if ( ! $info ) return false;
switch ( $info['mime'] ) {
case 'image/jpeg': $img = @imagecreatefromjpeg( $image_path ); break;
case 'image/png': $img = @imagecreatefrompng( $image_path ); break;
case 'image/gif': $img = @imagecreatefromgif( $image_path ); break;
case 'image/webp':
$img = function_exists( 'imagecreatefromwebp' )
? @imagecreatefromwebp( $image_path ) : false;
break;
default: return false;
}
if ( ! $img ) return false;
// ── 2. Resize về 48×48 ───────────────────────────────────────
$size = 48;
$resized = imagecreatetruecolor( $size, $size );
imagefill( $resized, 0, 0, imagecolorallocate( $resized, 255, 255, 255 ) );
imagecopyresampled( $resized, $img, 0, 0, 0, 0,
$size, $size, imagesx( $img ), imagesy( $img ) );
imagedestroy( $img );
// ── 3. Thu thập pixel, bỏ quá tối/quá sáng/trong suốt ───────
$pixels = [];
for ( $x = 0; $x < $size; $x++ ) {
for ( $y = 0; $y < $size; $y++ ) {
$rgb = imagecolorat( $resized, $x, $y );
$r = ( $rgb >> 16 ) & 0xFF;
$g = ( $rgb >> 8 ) & 0xFF;
$b = $rgb & 0xFF;
$l = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
if ( $l < 25 || $l > 220 ) continue;
$pixels[] = [ $r, $g, $b ];
}
}
imagedestroy( $resized );
if ( count( $pixels ) < 20 ) return '120,120,120';
// ── 4. Median-cut quantization → K cụm màu ───────────────────
$buckets = [ $pixels ];
while ( count( $buckets ) < $k ) {
$best_idx = 0;
$best_range = -1;
foreach ( $buckets as $idx => $bucket ) {
if ( count( $bucket ) < 2 ) continue;
$r_vals = array_column( $bucket, 0 );
$g_vals = array_column( $bucket, 1 );
$b_vals = array_column( $bucket, 2 );
$range = max(
max( $r_vals ) - min( $r_vals ),
max( $g_vals ) - min( $g_vals ),
max( $b_vals ) - min( $b_vals )
);
if ( $range > $best_range ) {
$best_range = $range;
$best_idx = $idx;
}
}
if ( $best_range < 10 ) break;
$bucket = $buckets[ $best_idx ];
$r_vals = array_column( $bucket, 0 );
$g_vals = array_column( $bucket, 1 );
$b_vals = array_column( $bucket, 2 );
$r_range = max( $r_vals ) - min( $r_vals );
$g_range = max( $g_vals ) - min( $g_vals );
$b_range = max( $b_vals ) - min( $b_vals );
$max_range = max( $r_range, $g_range, $b_range );
if ( $max_range === $r_range ) {
usort( $bucket, fn( $a, $b ) => $a[0] - $b[0] );
} elseif ( $max_range === $g_range ) {
usort( $bucket, fn( $a, $b ) => $a[1] - $b[1] );
} else {
usort( $bucket, fn( $a, $b ) => $a[2] - $b[2] );
}
$mid = (int) floor( count( $bucket ) / 2 );
array_splice( $buckets, $best_idx, 1, [
array_slice( $bucket, 0, $mid ),
array_slice( $bucket, $mid ),
] );
}
// ── 5. Tính màu trung bình + weight của mỗi bucket ───────────
$clusters = [];
$total_px = count( $pixels );
foreach ( $buckets as $bucket ) {
if ( empty( $bucket ) ) continue;
$n = count( $bucket );
$clusters[] = [
'r' => array_sum( array_column( $bucket, 0 ) ) / $n,
'g' => array_sum( array_column( $bucket, 1 ) ) / $n,
'b' => array_sum( array_column( $bucket, 2 ) ) / $n,
'weight' => $n / $total_px,
];
}
// ── 6. Vibrance scoring ───────────────────────────────────────
$best_score = -999;
$best_cluster = $clusters[0];
foreach ( $clusters as $cluster ) {
$r = $cluster['r'];
$g = $cluster['g'];
$b = $cluster['b'];
$w = $cluster['weight'];
$max = max( $r, $g, $b ) / 255;
$min = min( $r, $g, $b ) / 255;
$l = ( $max + $min ) / 2;
$sat = ( $max === $min ) ? 0 : (
$l > 0.5
? ( $max - $min ) / ( 2 - $max - $min )
: ( $max - $min ) / ( $max + $min )
);
$sat *= 100;
$lum = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
$score = $sat * ( 0.4 + 0.6 * $w );
$penalty = 0;
if ( $sat < 15 ) $penalty += 50;
if ( $sat >= 15 && $sat < 35 ) $penalty += 20;
if ( $lum < 40 ) $penalty += 30;
if ( $lum > 180 ) $penalty += 40;
$score -= $penalty;
if ( $score > $best_score ) {
$best_score = $score;
$best_cluster = $cluster;
}
}
// ── 7. Normalize output ──────────────────────────────────────
$r = $best_cluster['r'];
$g = $best_cluster['g'];
$b = $best_cluster['b'];
$luminance = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
if ( $luminance < 70 ) {
$factor = 70 / max( $luminance, 1 );
$r *= $factor; $g *= $factor; $b *= $factor;
}
if ( $luminance > 160 ) {
$factor = 160 / $luminance;
$r *= $factor; $g *= $factor; $b *= $factor;
}
$r = (int) max( 0, min( 255, round( $r ) ) );
$g = (int) max( 0, min( 255, round( $g ) ) );
$b = (int) max( 0, min( 255, round( $b ) ) );
$r_n = $r / 255; $g_n = $g / 255; $b_n = $b / 255;
$mx = max( $r_n, $g_n, $b_n );
$mn = min( $r_n, $g_n, $b_n );
$lv = ( $mx + $mn ) / 2;
$sat_final = ( $mx === $mn ) ? 0 : (
$lv > 0.5
? ( $mx - $mn ) / ( 2 - $mx - $mn )
: ( $mx - $mn ) / max( $mx + $mn, 0.001 )
);
if ( $sat_final < 0.35 ) {
$avg = ( $r + $g + $b ) / 3;
$boost = 1.3;
$r = (int) max( 0, min( 255, round( $avg + ( $r - $avg ) * $boost ) ) );
$g = (int) max( 0, min( 255, round( $avg + ( $g - $avg ) * $boost ) ) );
$b = (int) max( 0, min( 255, round( $avg + ( $b - $avg ) * $boost ) ) );
}
return "$r,$g,$b";
}
?>
Cách hoạt động (tóm lược kỹ thuật)
Hàm này không đi theo hướng “average RGB” đơn giản — vốn dễ ra màu xám bẩn — mà xử lý theo pipeline chuẩn:
- Giảm kích thước ảnh về 48×48 để tối ưu CPU nhưng vẫn giữ phân bố màu
- Loại bỏ pixel nhiễu (quá tối, quá sáng)
- Dùng median-cut để chia màu thành K cụm đại diện
- Tính weight theo mật độ pixel mỗi cụm
- Chấm điểm bằng vibrance scoring (độ bão hòa + trọng số)
- Áp penalty cho màu xám, quá tối hoặc quá sáng
- Normalize để đảm bảo hiển thị ổn định trên UI
Ứng dụng thực tế
Bạn có thể dùng output dạng “R,G,B” để:
- Tạo gradient background cho trang chi tiết manga
- Set CSS variable (accent color)
- Dynamic theme theo từng truyện
- Tối ưu readability cho dark/light mode
Đây là một approach đủ “production-grade”, cân bằng giữa hiệu năng và độ chính xác, đặc biệt phù hợp với hệ thống có lượng ảnh lớn.
Bình luận