Thuật toán trích xuất màu chủ đạo từ ảnh bằng PHP (Median-Cut + Vibrance Scoring)

Trong các hệ thống UI hiện đại, đặc biệt là các nền tảng nội dung như manga, việc tự động lấy màu chủ đạo từ ảnh bìa để đồng bộ giao diện (gradient, theme, accent color) là một nâng cấp UX rất đáng giá. Bài viết này trình bày một hàm PHP có độ chính xác cao, kết hợp median-cut quantizationvibrance scoring để chọn ra màu nổi bật nhất — không chỉ là trung bình RGB đơn giản.

Thuật toán trích xuất màu chủ đạo từ ảnh bằng PHP (Median-Cut + Vibrance Scoring)

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


  • Không có bình luận.

Init Toolbox

Nhấn Ctrl + \ trên máy tính, hoặc vuốt sang trái ở bất kỳ đâu trên mobile.

Đăng nhập





Đang tải...