【问题标题】:How to apply an operation to every PIXEL (not every rgb component!) of a python image (either using numpy, opencv or PIL)?如何将操作应用于 python 图像的每个 PIXEL(不是每个 rgb 组件!)(使用 numpy、opencv 或 PIL)?
【发布时间】:2021-04-06 06:01:17
【问题描述】:

我正在尝试以有效的方式对图像的所有像素应用一个函数(在我的具体情况下,我想对每个像素进行一些颜色近似,但我认为这与问题无关)。问题是我找到了不同的方法来做到这一点,但它们都在像素的每个组件上应用一个函数,而我想要做的是应用一个函数 接收一个像素(不是像素分量),这是 3 个 rgb 分量(我猜是一个元组,但我不关心格式,只要我在函数中有 3 个分量作为参数)。

如果您对我拥有的东西感兴趣,这是解决我的问题的无效解决方案(工作正常,但速度太慢):

def closest_colour(pixel, colours):
    closest_colours = sorted(colours, key=lambda colour: colours_distance(colour, pixel))
    return closest_colours[0]

# reduces the colours of the image based on the results of KMean
# img is image open with opencv.imread()
# colours is an array of colours
def image_color_reduction(img, colours):
    start = time.time()
    print("Color reduction...")
    reduced_img = img.copy()[...,::-1]
    width = reduced_img.shape[0]
    height = reduced_img.shape[1]
    
    for x in range(width):
        for y in range(height):
            reduced_img[x,y] = closest_colour(reduced_img[x,y], colours)
    
    end = time.time()
    print(f"Successfully reduced in {end-start} seconds")
    return reduced_img

我已经关注了这篇文章:PIL - apply the same operation to every pixel,这似乎很清楚并且与我的问题一致。我尝试过使用任何类型的图像格式,我尝试过多线程(使用 pool.map 和 pool.imap),我尝试过 numpy.apply_along_axis,最后我尝试了 PIL.point(),我认为是与我正在寻找的最相似的解决方案。事实上,如果您查看他们的官方文档:.point(),它确切地说:该函数为每个可能的像素值调用一次。我发现这确实具有误导性,因为在尝试之后我意识到 pixel value 在这种情况下并不是指一个 rgb 元组,而是指 3 个 rgb 组件中的每一个(说真的,在什么世界里?)。

如果有人能分享一些他们的经验并让我对这个问题有所了解,我将不胜感激。提前谢谢你!

(更新)

根据您的要求,我添加了有关我正在处理的具体问题的更多信息:

给定

  • 大小为 1022*1080 的图像 M
  • 大小为 1 的颜色 N 数组

通过将每个像素的颜色替换为最 N中类似的一个(感谢您的回答,我知道这被定义为 最近邻颜色量化)

这是 colours_distance 的缺失实现:

def colours_distance(c1, c2):
    (r1,g1,b1) = c1
    (r2,g2,b2) = c2
    return math.sqrt((r1 - r2)**2 + (g1 - g2) ** 2 + (b1 - b2) **2)

这是运行此代码所需的导入:

import cv2
import time
import math

我的问题中显示的解决方案平均在不到 40 秒的时间内解决了所描述的问题。

【问题讨论】:

  • 在 numpy 和 pandas 中应用函数通常在底层运行 python 级别的 for 循环。它们甚至没有被远程矢量化。如果你想要快速的东西,展示你是如何实现你的功能的。
  • 另外,使用min 而不是每次都使用sorted 对整个数组进行排序:_, v = min((key(v), v) for v in blah)
  • 请出示colour_distance函数。
  • 如果您的问题不清楚,您往往得不到最佳答案。您似乎没有指定您的图像有多大(以像素为单位)、当前处理需要多长时间、您选择了多少种颜色或算法colours_distance 涉及什么......您也没有提供有代表性的图像和删除了您所有的 import 语句,因此没有人可以实际运行您的代码。
  • Python 不是实现循环图像算法的正确语言,但它是调用此类算法的正确语言。你可以试试 Numba 来加速你的代码。

标签: python numpy opencv image-processing python-imaging-library


【解决方案1】:

假设您的图像是一个(M, N, 3) numpy 数组,您的颜色表是(K, 3),并且您将颜色距离测量为一些理智的矢量规范,您可以使用 scipy 的cKDTree(或只是KDTree)进行优化并为您矢量化查找。

首先用你的颜色表制作一棵树:

colors = ... # K, 3 array
color_tree(colors)

现在你可以直接query树来获取输出图像:

_, output = color_tree.query(img)

output 将是(M, N) 索引数组到color_table。重要的是 KD 树经过优化,可以按像素执行 O(log K) 查找,而不是像您当前的实现中那样执行 O(K)O(K log K)。由于循环是在 C 中实现的,因此您也会从中获得很大的提升。

【讨论】:

  • 很高兴知道这存在。有没有办法自定义距离测量?
  • @CrisLuengo。您可以使用不同的规范,但据我所知,您不能指定任意函数
【解决方案2】:

关于“矢量化”的说明

对于 numpy,没有一种通用的有效方法可以在图像的某个轴上应用任意函数。为了有效地进行计算,numpy 需要能够在后端为您完成这些计算,而不是使用 Python。 OpenCV 也是如此。当你打电话时,例如np.mean()cv.meanStdDev() 或类似的,这些库在 C/C++/Fortran/etc 中遍历您的图像,因此需要执行的代码那里。但是,您想在这些值上应用您在 Python 中定义的函数,这意味着您需要直接对 Python 对象进行操作,这会降低在 numpy/OpenCV/ 中执行操作的所有效率。等等,这就是为什么没有立即快速的方法来进行这些计算。您在帖子中提到了来自 Pandas 的 df.apply() ——请注意,apply() 实际上很慢,它会像您目前正在做的那样在 Python 中进行循环,因此通常您不想使用它。 Numpy 和 OpenCV 不会公开像 apply() 这样的方法因为这并不是真正的好方法。

一般来说,为了有效地进行操作,您需要矢量化您的代码,这在 Python 领域意味着只使用可以对您的数据进行操作的内置 numpy/opencv/etc 函数一次,无需编写循环(或者没有在 Python 中隐式调用它们,例如 df.apply())。

请注意,这里没有什么是专门针对像素(或它们的单个组件)工作的,这是尝试在 Python 中实现快速计算的一个普遍问题。也就是说,即使您尝试过的任何解决方案都适用于像素(而不是组件),它仍然会很慢。

解决方案

您作为示例(最近邻颜色量化)给出的具体问题对于快速进行并非易事,因为对于每个像素,您需要确定您在颜色列表中最近的位置。如果你只有几种颜色,比如 8 种,只计算到所有颜色的距离并不可怕,但如果你试图将调色板减少到 256 种颜色或类似的颜色,那么计算量就很大。如果您只有几种颜色,那么您可以通过创建一个 3d 数组来对整个操作进行矢量化,该数组表示每个 x、y 位置的每种颜色的距离,并在颜色轴上取 argmin,然后您可以将其用于查找表。

这是一个示例实现,将图像减少到 8 种颜色。我们将从图像和一些定义的颜色开始

In [80]: img.shape
Out[80]: (90, 160, 3)

In [81]: colors
Out[81]:
array([[  0,   0,   0],
       [255,   0,   0],
       [  0, 255,   0],
       [  0,   0, 255],
       [255, 255,   0],
       [255,   0, 255],
       [  0, 255, 255],
       [255, 255, 255]])

现在我们想要,对于图像中的每个像素位置,到每种颜色的距离(我们将使用 abs diff 距离函数作为示例,但这里的任何可矢量化操作都可以)。在这里,我们可以利用广播来获得形状(h, w, n_colors)的结果数组:

In [83]: distances = np.sum(np.abs(img[..., np.newaxis] - colors.T), axis=2)

In [84]: distances.shape
Out[84]: (90, 160, 8)

现在您想知道哪种颜色导致每个像素的最小距离:

In [87]: nearest_colors = np.argmin(distances, axis=2)

In [88]: nearest_colors
Out[88]:
array([[7, 4, 3, ..., 2, 2, 4],
       [5, 7, 6, ..., 3, 2, 5],
       [5, 3, 7, ..., 3, 5, 7],
       ...,
       [6, 5, 0, ..., 7, 6, 1],
       [1, 6, 5, ..., 2, 0, 3],
       [0, 1, 0, ..., 7, 5, 4]])

所以在第一个像素,最接近的颜色是我的颜色列表中的最后一个(全白色),在右边的下一个像素,最接近的颜色是[255, 255, 0],依此类推。现在您可以使用查找表从这些映射到它们的实际颜色值。使用 numpy 执行此操作的方法是使用精美的索引:

In [91]: quantized = colors[nearest_colors]

In [92]: quantized.shape
Out[92]: (90, 160, 3)

这是您使用新量化颜色的图像。

解决这个问题的一个更有效的解决方案是使用 kd-tree,如MadPhysicist answered。但是,颜色距离函数可能是非线性的,并且这些距离可能无法很好地映射到空间数据结构,在这种情况下,通常有专门的实现或非常具体的方法可以使它们更快,但这更接近研究并且不适合所以。

对于其他颜色量化算法,这个问题有很多很好的例子:Fast color quantization in OpenCV

【讨论】:

  • 您可以对整个数组进行搜索排序,也可以进行索引 lexsort。您几乎不需要超过 N log N。
  • @MadPhysicist searchsorted 如何映射到 kd-tree?
  • 可能无法直接映射。当我说搜索排序时,我并没有想太多,虽然 kdtree 是多维排序的概括
【解决方案3】:

试试这个解决方案,如果它会更快:

img = cv2.imread(path)
result = np.zeros_like(img)
colors_arr = [[0, 0, 255], [255, 0, 0], [0, 255, 0], [0, 255, 255], [255, 0, 255], [255, 255, 0]]
#  Normalizing images and colors to 1.
colors = np.array(colors_arr, np.float32) / 255
img = img.astype(np.float32) / 255

#  For each color making an array of weights.
weights = []
for i in range(colors.shape[0]):
    weights.append(np.sum(np.square(img - colors[i]), axis=2))

weights = np.array(weights, np.float32)
#  Finding the index of minimum weight
weights = np.transpose(weights, axes=[1, 2, 0])
color_inds = np.argmin(weights, axis=2)

# Depending on minimum weight index assigning the color to the result
for i in range(len(colors_arr)):
    idx = np.where(color_inds == i)
    result[idx] = colors_arr[i]

cv2.imshow('', result)
cv2.waitKey()

【讨论】:

  • 这个解决方案可能没有你想象的那么快。
  • 这是正确的做法。对于非常大的图像,构建 3D 查找表会更有效,基本上将代码应用于所有可能颜色的列表(降低精度以减少计算量),然后使用每个图像像素的 RGB 值进行索引进入该表以查找映射的颜色。它涉及更多,但如果表格的元素少于图像的像素,则需要更少的距离计算。
  • @Mad Physicist 这里的瓶颈是 np.where 调用每个颜色索引。
  • 你真的不需要这些 for 循环;第一个您可以使用广播,第二个您可以直接使用精美的索引作为 LUT。
【解决方案4】:

我对使用 linalg.norm()cKDTree() 的相对性能感兴趣,您的数据集大小为 1022x1080 图像,N(调色板长度)在 1..16 范围内。

#!/usr/bin/env python3

import numpy as np
import cv2

def QuantizeToGivenPalette(im, palette):
    """Quantize image to a given palette.
    
    The input image is expected to be a Numpy array.
    The palette is expected to be a list of R,G,B values."""

    # Calculate the distance to each palette entry from each pixel
    distance = np.linalg.norm(im[:,:,None].astype(np.float) - palette[None,None,:].astype(np.float), axis=3)

    # Now choose whichever one of the palette colours is nearest for each pixel
    palettised = np.argmin(distance, axis=2).astype(np.uint8)

    return palettised

################################################################################
# main
################################################################################

# Let's get some repeatable randomness
np.random.seed(42)

# Open a colorwheel, resize to match dimensions of question
M = 1022, 1080
im = cv2.imread('colorwheel.png', cv2.IMREAD_COLOR)
im = cv2.resize(im, M, interpolation = cv2.INTER_AREA)

# Make a full 256-entry palette of random colours, but we'll just use the first N
palette = np.random.randint(0,256,(256,3),dtype=np.uint8)

# Try quantizing with linalg.norm, for various palette lengths
pLengths = [4,8,12,16]
for pLength in pLengths:

    indices = QuantizeToGivenPalette(im, palette[:pLength])

    # Write image of just palette indices
    cv2.imwrite(f'DEBUG-indices-linalg{pLength}.png', indices)

    # Look up each pixel in the palette and revert to BGR and save
    BGR = palette[indices]
    cv2.imwrite(f'DEBUG-result-linalg{pLength}.png', BGR)

################################################################################
# NOW DO SAME THING BUT WITH KDTREE
################################################################################

from scipy.spatial import cKDTree

# Try quantizing with cKDTree, for various palette lengths
for pLength in pLengths:

    # Build our tree from the palette, only necessary once for any given palette
    treeFromPalette = cKDTree(palette[:pLength])

    # Lookup nearest indices for each pixel in image
    _, indices = treeFromPalette.query(im)

    # Write image of just palette indices
    cv2.imwrite(f'DEBUG-indices-cKDTree{pLength}.png', indices)

    # Look up each pixel in the palette and revert to BGR and save
    BGR = palette[indices]
    cv2.imwrite(f'DEBUG-result-cKDTree{pLength}.png', BGR)

我将此图像用作输入并将其调整为您指定的尺寸:

两种方法的结果相同:

4 种颜色:

8 种颜色:

16 种颜色:


有趣的是时间 - 以毫秒为单位:

N      norm()  cKDTree()
4      147     485
8      307     308
12     449     530
16     601     542

如果我们绘制这些图,您会看到 cKDTree() 仅在您的 N 值的较高端真正发挥作用:

关键字:Python、图像处理、KDTree、linalg.norm、调色板、量化、素数。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-12-28
    • 1970-01-01
    • 2022-11-25
    • 2012-06-05
    • 1970-01-01
    • 2016-08-19
    • 1970-01-01
    相关资源
    最近更新 更多