Python - 计算图像的直方图

7

我正在学习计算机图像处理的基础知识,并且同时自学Python。

给定一个尺寸为2048x1354,具有3个通道的图像x,高效地计算像素强度的直方图。

import numpy as np, cv2 as cv

img = cv.imread("image.jpg")
bins = np.zeros(256, np.int32)

for i in range(0, img.shape[0]):
    for j in range(0, img.shape[1]):

        intensity = 0
        for k in range(0, len(img[i][j])):
            intensity += img[i][j][k]

        bins[intensity/3] += 1

print bins

我的问题是这段代码运行得比较慢,大约需要30秒钟。我应该如何加速并使其更符合Pythonic风格?


1
也许这会有所帮助。 - iMom0
2
通过3个嵌套的for循环,您的算法执行时间为O(n^3),非常缓慢。 - geoff
这并非与您原来的问题直接相关,但请考虑使用更好的算法生成直方图。由于您可能对所感知的颜色感兴趣,因此可以尝试使用亮度计算:https://dev59.com/QHRB5IYBdhLWcg3wiHpl - akirilov
1
@geoff 第三个循环只运行固定次数,可能是3次。它与图像的大小无关。 - Mark Ransom
一个小的改进是替换内部循环:for k in img[i][j]: intensity += k。循环range(len(...))永远不是一个好兆头。更好的方法是使用sum(img[i][j])并完全消除循环。 - Mark Ransom
5个回答

16
你可以使用更新的OpenCV Python接口,该接口本身使用numpy数组,并使用matplotlib hist绘制像素强度的直方图。在我的电脑上只需要不到一秒钟。
import matplotlib.pyplot as plt
import cv2

im = cv2.imread('image.jpg')
# calculate mean value from RGB channels and flatten to 1D array
vals = im.mean(axis=2).flatten()
# plot histogram with 255 bins
b, bins, patches = plt.hist(vals, 255)
plt.xlim([0,255])
plt.show()

输入图像描述

更新: 以上指定的箱子数量并不总是提供所需的结果,因为最小值和最大值是从实际值计算出来的。此外,值为254和255的计数在最后一个箱子中合并。这里是更新后的代码,它总是正确地绘制直方图,并使条形居中于值0..255。

import numpy as np
import matplotlib.pyplot as plt
import cv2

# read image
im = cv2.imread('image.jpg')
# calculate mean value from RGB channels and flatten to 1D array
vals = im.mean(axis=2).flatten()
# calculate histogram
counts, bins = np.histogram(vals, range(257))
# plot histogram centered on values 0..255
plt.bar(bins[:-1] - 0.5, counts, width=1, edgecolor='none')
plt.xlim([-0.5, 255.5])
plt.show()

enter image description here


请问您能否解释一下 plt.bar(bins[:-1] - 0.5, counts, width=1, edgecolor='none') 的动机是什么? - Beginner
很好,应该使用 plt.bar(bins[:-1], counts, width=1, edgecolor='none') 来绘制以值0..255为中心的直方图。 - Ondro

3
如果您只想计算数组中每个值出现的次数,可以使用 numpynumpy.bincount。在您的情况下:
arr  = numpy.asarray(img)
flat = arr.reshape(numpy.prod(arr.shape[:2]),-1)
bins = numpy.bincount(np.sum(flat,1)/flat.shape[1],minsize=256)

我在这里使用 `numpy.asarray` 来确保 `img` 是一个 numpy 数组,这样我就可以将其展平为一维数组 `bincount` 所需的格式。如果 `img` 已经是一个数组,你可以跳过这一步。计数本身会非常快速。大部分时间可能会花费在将 cv 矩阵转换为数组上。
修改:根据 这个答案,你可能需要使用 `numpy.asarray(img[:,:])`(或者可能是 `img[:,:,:]`)才能成功地将图片转换为数组。另一方面,根据这个答案,从较新版本的 openCV 中得到的内容已经是一个 numpy 数组。所以在那种情况下,你可以完全跳过 `asarray`。

这是最好的答案。使用CV2就像用大炮打蚊子一样。当有纯numpy或基于numpy的库,如scikit-image时,不需要为所有事情都使用openCV。 - lesolorzanov

2

纯Python无法做到这一点(即不删除for循环)。Python的for循环结构有太多的东西要处理,因此速度不够快。如果您确实想保留for循环,唯一的解决方案是使用numba或cython,但这些工具也有其自身的问题。通常,这类循环会使用C/C++编写(在我看来是最直接的方式),然后从Python中调用,其主要作用是一个脚本语言。

虽然如此,opencv + numpy提供了足够的有用程序,可以在90%的情况下简单地使用内置函数,而无需编写自己的像素级代码。

这里是一个使用numba的解决方案,而不改变您的循环代码。在我的电脑上,它比纯Python快大约150倍。

import numpy as np, cv2 as cv

from time import time
from numba import jit,int_,uint8 

@jit(argtypes=(uint8[:,:,:],int_[:]),
    locals=dict(intensity=int_),
    nopython=True
    )
def numba(img,bins):
    for i in range(0, img.shape[0]):
        for j in range(0, img.shape[1]):
            intensity = 0
            for k in range(0, len(img[i][j])):
                intensity += img[i][j][k]
            bins[intensity/3] += 1


def python(img,bins):
    for i in range(0, img.shape[0]):
        for j in range(0, img.shape[1]):
            intensity = 0
            for k in range(0, len(img[i][j])):
                intensity += img[i][j][k]
            bins[intensity/3] += 1

img = cv.imread("image.jpg")
bins = np.zeros(256, np.int32)

t0 = time()
numba(img,bins)
t1 = time()
#print bins
print t1 - t0

bins[...]=0
t0 = time()
python(img,bins)
t1 = time()
#print bins
print t1 - t0    

1

请看MatPlotLib。这个教程可以帮助你完成你想做的一切,而且不需要使用for循环。


1
我知道已经存在工具。然而,我想将其作为语言和算法的学习机会。 - Jason
Python的一个重要部分是学习可用的工具,而matplotlib是一个我几乎在所有代码中都使用的大型库。我理解你想要学习这门语言,但Python的实用性在于有很多工具可以让你轻松高效地完成各种任务。 - Wesley Bowman
另一种加速的方法是使用numpy,但这里你仍然需要使用库来帮助你。Python不是最适合使用'for'循环的语言。你可以使用Numpy将代码向量化,或者使用Matplotlib以更简单的方式完成它。 - Wesley Bowman
2
Python之所以伟大的主要原因不是语言本身(虽然这也很好(但速度较慢))。它的巨大标准库是最重要的,如果您不使用它们,就会让Python失去功能。 - amaurea
1
同意。这非常快。有趣的是,使用matplotlibcv2导入图像的方式会导致img变量的不同值。如果使用cv2导入,则像素的值将在[0,255]之间。如果使用matplotlib导入图像,则值将在[0,1]之间。 - alpha_989

0

来自OpenCV文档

单通道直方图(图像转换为灰度):

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg',0)
plt.hist(img.ravel(),256,[0,256]); plt.show()

RGB直方图(每个通道分别)

import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
color = ('b','g','r')
for i,col in enumerate(color):
    histr = cv.calcHist([img],[i],None,[256],[0,256])
    plt.plot(histr,color = col)
    plt.xlim([0,256])
plt.show()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接