在PHP中把JPEG图像转换为《黑客帝国》配色方案

在PHP中将图像转换为《黑客帝国》的调色板风格

你有没有注意到电影《黑客帝国》里那微微泛绿的色彩?这部电影运用了各种各样的调色板,不过在《黑客帝国》的虚拟世界里,所有画面都带上了淡淡的绿色调。

这种调色板效果是通过色彩分级算法在后期处理中达成的。影片的原始色彩会经过一个滤镜处理,对模拟场景里的颜色做细微调整。

在本文中,我们就来探讨如何把像素转变为《黑客帝国》里那种绿色调效果,以及怎样在PHP里对图像运用这一系统。其实,在Drupal开发过程中,有时候也会涉及到对图像风格的调整,与我们这里要做的图像调色转变有一定的相通之处。

滤镜算法的原理是,从影片中提取红、绿、蓝三种颜色,并且把它们的数值范围转换为0到1。接着,将红色通道的值提升到7/5次方,蓝色通道的值提升到8/5次方。在这个过程中,绿色通道的值保持不变。

我们可以用下面这种方式来表示这个过程:

(r, g, b) => (r^7/5, g, b^8/5)

用十进制格式表示就是:

r, g, b => (r^1.4, g, b^1.6)

由于我们是把0到1之间的数值提升到大于1的幂,这会让这些通道的值稍微降低。我们只要对图像中的每个像素都应用这个算法就好了。

在PHP中,我们能够使用imagecolorat()函数提取像素的颜色值。这个函数会返回一个整数值,我们可以从这个值中提取出红、绿、蓝三种颜色的值。

$colour = imagecolorat($img, $x, $y);

// 提取红、绿、蓝值。
$r = ($colour >> 16) & 0xFF;
$g = ($colour >> 8) & 0xFF;
$b = $colour & 0xFF;

这里的红、绿、蓝值范围是0到255。这意味着在进行计算之前,我们得把它们转换为0到1的范围,这可以通过将数值除以255来实现。

// 将红色值从0 - 255的范围转换到0 - 1的范围。
$r = ($r / 255);
// 将值提升到1.4次方。
$r = min(pow($r, 1.4), 1);
// 将值转换回0 - 255的范围。
$r = 255 * ($r);

// 将蓝色值从0 - 255的范围转换到0 - 1的范围。
$b = ($b / 255);
// 将值提升到1.6次方。
$b = min(pow($b, 1.6), 1);
// 将值转换回0 - 255的范围。
$b = 255 * ($b);

然后,我们可以使用imagecolorallocate()函数创建新的颜色,再使用imagesetpixel()函数将新图像中相同位置的像素设置为该颜色。

// 创建新颜色。
$newColour = imagecolorallocate($newImage, $r, $g, $b);

// 设置新图像中的像素。
imagesetpixel($newImage, $x, $y, $newColour);

与其展示这个算法所产生的不同颜色,不如我们直接在一张图像上运行这个算法,这样就能更清楚地看到效果。我公司有一张在2024年苏格兰DrupalCamp上演讲的照片,这张照片目前是作者简介配图,所以很容易就能拿到。

将这张图像通过颜色转换算法处理后,现在照片里就像是在《黑客帝国》的虚拟世界里演讲一样。

画面确实变绿了。

通过降低红色和蓝色的数值,绿色以一种特殊的方式得到了增强,而且在转换过程中并没有损失太多信息。我们仍然保留了所有的基本颜色,唯一损失的信息是那些红色或蓝色值非常接近零的部分,因为这些值会被处理为零。

把上述所有内容整合起来,我们得到以下函数:

/**
 * 将JPEG图像文件转换为《黑客帝国》的色彩方案。
 *
 * 《黑客帝国》的色彩方案是通过从图像中提取红、绿、蓝值,
 * 并将它们转换到0 - 1的范围来计算的。然后使用公式r^7/5, g, b^8/5 
 * (十进制表示为r^1.4, g, b^1.6)对这些值进行处理。
 * 在使用新颜色创建图像之前,我们再将这些值转换回0 - 255的范围。
 *
 * 生成的图像文件会以 "matrix_" 为前缀并保存到磁盘。原始文件不会被修改。
 *
 * @param string $imgfile
 *   要转换的文件。
 */
function matrixFilter(string $imgfile) {
    // 将图像加载到内存中。
    $img = imagecreatefromjpeg($imgfile);

    // 获取图像的尺寸。
    $imagex = imagesx($img);
    $imagey = imagesy($img);

    // 创建一个具有相同尺寸的新图像。
    $newImage = imagecreatetruecolor($imagex, $imagey);

    // 遍历当前图像中的所有像素。
    for ($x = 0; $x < $imagex; $x++) {
        for ($y = 0; $y < $imagey; $y++) {
            $colour = imagecolorat($img, $x, $y);

            // 提取红、绿、蓝值。
            $r = ($colour >> 16) & 0xFF;
            $g = ($colour >> 8) & 0xFF;
            $b = $colour & 0xFF;

            // 将红色值从0 - 255的范围转换到0 - 1的范围。
            $r = ($r / 255);
            // 将值提升到1.4次方。
            $r = min(pow($r, 1.4), 1);
            // 将值转换回0 - 255的范围。
            $r = 255 * ($r);

            // 将蓝色值从0 - 255的范围转换到0 - 1的范围。
            $b = ($b / 255);
            // 将值提升到1.6次方。
            $b = min(pow($b, 1.6), 1);
            // 将值转换回0 - 255的范围。
            $b = 255 * ($b);

            // 创建新颜色。
            $newColour = imagecolorallocate($newImage, $r, $g, $b);
            // 设置新图像中的像素。
            imagesetpixel($newImage, $x, $y, $newColour);
        }
    }

    // 将新图像写入磁盘。
    imagejpeg($newImage, 'matrix_' . $imgfile);

    // 释放内存。
    imagedestroy($img);
    imagedestroy($newImage);
}

这个函数只接受JPEG图像,所以如果你尝试传入其他格式的图像,将会返回错误。在Drupal模块开发过程中,我们也需要注意函数对输入格式的要求,这有利于保证系统的稳定性。

可以用下面的方式使用这个函数:

matrixFilter('someimage.jpg');

你可以随意将此函数应用到自己的JPEG格式图像上进行颜色转换。

对这段代码的一个改进方向是让这个函数更通用,不再硬编码颜色缩放值,而是将它们作为参数传入,然后根据这些参数来调整颜色。随着Drupal11的逐步推进,代码的通用性和灵活性显得更加重要,这样的改进思路在Drupal开发中也同样适用。