使用 PHP 从图像生成调色板:分步指南

在PHP中从图像生成调色板

在网页设计里,一种常用的设计模式是把图像融入页面设计中,这样能让图像与页面的其他部分更紧密地整合在一起。

围绕图像设计页面时,主要的问题在于页面的颜色必须和图像相匹配。否则,会使图像与网站风格之间产生不协调的感觉。

在本文中,我将探讨如何从图像里提取调色板,以便将其运用到网页设计中。在实际的Drupal开发Drupal模块开发以及Drupal升级过程中,这种从图像生成调色板的技术也可能会发挥重要作用,比如在Drupal11的主题设计里,可以根据页面中的图像生成调色板来统一页面颜色风格。

一、颜色类

首先,我们要创建一个颜色类,用于存储图像的颜色,并且提供一些方法来呈现颜色和计算亮度值。

class Colour {
  public function __construct(public int $red = 0,
                              public int $green = 0,
                              public int $blue = 0,
                              public int $alpha = 0) {}

  /**
   * 将颜色渲染为十六进制值。
   *
   * @return string
   *   颜色的十六进制表示。
   */
  public function toHex():string {
    $return = str_pad(dechex($this->red), 2, '0', STR_PAD_LEFT);
    $return .= str_pad(dechex($this->green), 2, '0', STR_PAD_LEFT);
    $return .= str_pad(dechex($this->blue), 2, '0', STR_PAD_LEFT);
    return $return;
  }

  /**
   * 计算颜色的亮度值。
   *
   * @return float
   *   亮度值。
   */
  public function calculateLightness():float {
    $red = $this->red / 255;
    $green = $this->green / 255;
    $blue = $this->blue / 255;

    $chroma_min = min($red, $green, $blue);
    $chroma_max = max($red, $green, $blue);

    return (float) ($chroma_max + $chroma_min) / 2;
  }
}

亮度值可用于对从图像中提取的颜色进行排序,这样我们就能从调色板中挑选出“最亮”和“最暗”的颜色。

二、从图像中提取颜色

一开始,你可能觉得我们可以遍历图像的所有像素来提取颜色。但问题是,这样最终可能会得到成千上万种颜色,从中提取调色板会很困难。

这对计算机来说也是一个耗时的过程,因为它至少需要访问每个像素一次。要是图像大小为1000x1000像素,那就意味着我们要从100万个像素中提取颜色。

解决这个问题其实有个取巧的办法。我们不用从每个像素提取颜色,而是把图像缩小到15x15像素。这样,我们只需从225个像素中提取颜色信息。缩小图像实际上会将颜色归为一组,所以新图像包含了原始图像中最重要的颜色。

在PHP中缩小图像需要几个步骤,下面我们来详细介绍。

首先,我们要把原始图像加载到内存中。假设原始图像是JPEG格式,我们可以使用imagecreatefromjpeg()方法。

$imgfile = 'image.jpg';
$image = imagecreatefromjpeg($imgfile);

现在,我们需要计算新图像的尺寸。如果新图像的宽度为15像素,那么高度将是15除以宽度与高度的比例。这样能确保新图像的形状与原始图像相同。

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

// 计算新图像的尺寸。
$ratio = $imagex/$imagey;
$newX = 15;
$newY = round($newX/$ratio);

然后,我们使用imagecreatetruecolor()函数创建一个新的(空白)图像资源,该函数接受新的尺寸作为参数。

$newImage = imagecreatetruecolor($newX, $newY);

imagecreatetruecolor()函数在这里很重要,因为使用它可以在不改变原始图像颜色的情况下调整图像大小。虽然也有imagecreate()函数,但它创建的是一个调色板图像,其颜色数量比真彩色格式要少。

最后,我们需要使用imagecopyresampled()函数将原始图像复制到新图像中,并在这个过程中调整图像大小。

imagecopyresampled($newImage, $image, 0, 0, 0, 0, $newX, $newY, $imagex, $imagey);

新图像(存储在$newImage资源中)现在比原始图像小很多,所以需要检查的像素数量也大大减少了。

要提取颜色,我们只需遍历新调整大小的图像的像素并从中提取颜色。我们可以使用展开运算符将颜色数组快速转换为颜色类构造函数中的属性。

// 通过遍历小图像并收集找到的颜色来填充颜色列表。
$coloursList = [];
for ($x = 0; $x < $newX; $x++) {
  for ($y = 0; $y < $newY; $y++) {
    $rgb = imagecolorat($newImage, $x, $y);
    $colours = imagecolorsforindex($newImage, $rgb);
    $colourObj = new Colour(...$colours);
    $hex = $colourObj->toHex();
    if (!isset($coloursList[$hex])) {
      // 将颜色添加到调色板中,使用十六进制值作为索引。
      $coloursList[$hex] = $colourObj;
    }
  }
}

由于我们将图像缩小到15像素高,所以$coloursList数组中的颜色数量不会比225种多太多(假设输入的是正方形图像)。这是最坏的情况,意味着图像中的每种颜色都是独一无二的,但这种情况并不常见。

提取出来的颜色并没有特定的顺序,所以我们使用usort()函数简单地根据颜色的亮度值对它们进行排序。

// 按亮度排序。
usort($coloursList, function ($a, $b) {
  return $a->calculateLightness() <=> $b->calculateLightness();
});

然后,我们就可以从排序后的颜色数组中提取所需的颜色。

以下是一个完整的函数,它接受一个文件名作为参数,并返回一个(有序的)颜色对象数组,详细列出找到的颜色。

/**
 * 从图像生成调色板。
 *
 * @param string $imgfile
 *   图像文件。
 *
 * @return array<Colour>
 *   颜色数组。
 */
function generatePalette($imgfile):array {
  $image = imagecreatefromjpeg($imgfile);

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

  // 计算新图像的尺寸。
  $ratio = $imagex/$imagey;
  $newX = 15;
  $newY = round($newX/$ratio);

  // 创建一个真彩色目标图像以防止颜色失真。
  $newImage = imagecreatetruecolor($newX, $newY);

  // 在采样颜色之前缩小图像。
  imagecopyresampled($newImage, $image, 0, 0, 0, 0, $newX, $newY, $imagex, $imagey);

  // 通过遍历小图像并收集找到的颜色来填充颜色列表。
  $coloursList = [];
  for ($x = 0; $x < $newX; $x++) {
    for ($y = 0; $y < $newY; $y++) {
      $rgb = imagecolorat($newImage, $x, $y);
      $colours = imagecolorsforindex($newImage, $rgb);
      $colourObj = new Colour(...$colours);
      $hex = $colourObj->toHex();
      if (!isset($coloursList[$hex])) {
        // 将颜色添加到调色板中,使用十六进制值作为索引。
        $coloursList[$hex] = $colourObj;
      }
    }
  }

  // 按亮度排序。
  usort($coloursList, function ($a, $b) {
    return $a->calculateLightness() <=> $b->calculateLightness();
  });

  return $coloursList;
}

可以这样调用这个函数。

$coloursList = generatePalette($imgFile);

现在,我们就可以使用这些颜色了。

三、使用调色板

一旦得到排序后的颜色列表,我们就能够轻松地从数组中提取出最暗、中间和最亮的颜色。

$darkest = $coloursList[0];
$middle = $coloursList[round(count($coloursList)/2)];
$lightest = $coloursList[count($coloursList) - 1];

使用这些颜色可以让我们创建出与图像现有颜色紧密匹配的设计元素,这在Drupal开发Drupal模块开发里,有助于打造出更符合用户需求和视觉体验的页面。

如果你愿意,还可以使用以下代码将整个调色板以HTML形式输出。

foreach ($coloursList as $color) {
  $htmlString .= '<div style="float:left;width:100px;height:100px;background-color:#' . $color->toHex() . '">#' . $color->toHex() . '</div>' . PHP_EOL;
}

四、改进建议

将原始图像缩放到15像素这个值或多或少是随意的。我对图像缩放进行了一些实验,以了解减少像素数量会产生什么影响。15像素似乎能得到不错的最暗、中间和最亮的颜色值,并且提取后剩余的颜色数量不会太多。

如果使用小于15像素的值,新图像会丢失太多信息,在提取颜色时用处不大,所以调色板可能与原始图像不太匹配。而将调整大小后的图像设置为更大的尺寸,提取颜色会花费更长时间,还可能只会选择图像中最暗的颜色,这通常与调色板不太协调。

我们可以通过在算法中引入颜色差异来改进颜色提取。这样做意味着我们可以通过舍弃与已收集颜色相似的颜色来减少收集到的颜色数量。这可能会产生更好(或更一致)的结果,但颜色差异计算会比较耗时,也比较难以理解。在Drupal升级和Drupal11的开发中,如果能优化颜色提取算法,可以为页面设计带来更好的视觉效果。

你可以使用这个函数在自己的图像上进行实验。