当迭代次数达到 40,000 时,图像分析速度加快 "for-loop"
Speeding-up "for-loop" in image analysis when iterations are up to 40,000
这段代码的先决条件细节很长,所以我会尽量总结一下。 WB/RG/BYColor 是基本图像,FIDO 是应用于此基本图像的叠加层。 S_wb/rg/by 是最终的输出图像。 WB/RG/BYColor 与 FIDO 大小相同。
对于 FIDO 中的每个独特元素,我们要计算基础图像中该区域的平均颜色。下面的 代码执行此操作,但是 由于 numFIDOs 非常大(最多 40,000),这需要 很长的时间 。
计算三个独立 RGB 通道的平均值。
sX=200
sY=200
S_wb = np.zeros((sX, sY))
S_rg = np.zeros((sX, sY))
S_by = np.zeros((sX, sY))
uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True)
numFIDOs = uniqueFIDOs.shape
for i in np.arange(0,numFIDOs[0]):
Lookup = FIDO==uniqueFIDOs[i]
# Get average of color signals for this FIDO
S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
S_rg[Lookup] = np.sum(RGColor[Lookup])/unique_counts[i]
S_by[Lookup] = np.sum(BYColor[Lookup])/unique_counts[i]
运行 大约需要 7.89 秒,不会太长,但这将包含在另一个循环中,因此它会累积!
我尝试过矢量化(如下所示),但我做不到
FIDOsize = unique_counts[0:numFIDOs[0]:1]
Lookup = FIDO ==uniqueFIDOs[0:numFIDOs[0]:1]
S_wb[Lookup] = np.sum(WBColor[Lookup])/FIDOsize
S_rg[Lookup] = np.sum(RGColor[Lookup])/FIDOsize
S_by[Lookup] = np.sum(BYColor[Lookup])/FIDOsize
数组大小匹配错误
简而言之:python 中的循环很慢。您应该执行以下操作之一:
- 向量化(你试过了,但你声称"it does not work"),你的意思是什么但不起作用?矢量化(如果可能)始终有效
- 切换到Cython,声明迭代器值为
int
以上两种方法都是基于将瓶颈循环转换为C循环。
根据我的时间安排,这比您原来的方法快 10 倍左右。我用这些数组进行了测试:
import numpy as np
sX=200
sY=200
FIDO = np.random.randint(0, sX*sY, (sX, sY))
WBColor = np.random.randint(0, sX*sY, (sX, sY))
RGColor = np.random.randint(0, sX*sY, (sX, sY))
BYColor = np.random.randint(0, sX*sY, (sX, sY))
这是我计时的部分:
import collections
colors = {'wb': WBColor, 'rg': RGColor, 'by': BYColor}
planes = colors.keys()
S = {plane: np.zeros((sX, sY)) for plane in planes}
for plane in planes:
counts = collections.defaultdict(int)
sums = collections.defaultdict(int)
for (i, j), f in np.ndenumerate(FIDO):
counts[f] += 1
sums[f] += colors[plane][i, j]
for (i, j), f in np.ndenumerate(FIDO):
S[plane][i, j] = sums[f]/counts[f]
可能是因为尽管 Python 中的循环很慢,但它遍历的数据较少。
请注意,如果 FIDO
中有少量唯一值,原始版本会变得更快。对于大多数情况,这花费的时间大致相同。
您的代码不是最优的,因为您扫描了 FIDO
中每个区域的所有图像。更好的方法是对每个区域的像素进行分组,然后先计算均值。 pandas
为此类计算提供不错的工具(此处仅在一条运河上)。然后你跨越区域的平均值:
import numpy as np
import pandas as pd
sX=200
sY=200
Nreg=sX*sY
WBColor=np.random.randint(0,256,(sX,sY))
FIDO=np.random.randint(0,Nreg,(sX,sY))
def oldloop():
S_wb = np.zeros((sX, sY))
uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True)
numFIDOs = uniqueFIDOs.shape
for i in np.arange(0,numFIDOs[0]):
Lookup = FIDO==uniqueFIDOs[i]
S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
return S_wb
def newloop():
index=pd.Index(FIDO.flatten(),name='region')
means= pd.DataFrame(WBColor.flatten(),index).groupby(level='region').mean()
lookup=np.zeros(Nreg)
lookup[means.index]=means.values
return lookup[FIDO]
在这种情况下,速度快了大约 200 倍:
In [32]: np.allclose(oldloop(),newloop())
Out[32]: True
In [33]: %timeit -n1 oldloop()
1 loops, best of 3: 3.92 s per loop
In [34]: %timeit -n100 newloop()
100 loops, best of 3: 20.5 ms per loop
编辑
另一种很酷的现代方法是使用 numba
。您以接近 C 的速度编写(非常)基本的 python 代码 运行 :
from numba import jit
@jit
def numbaloops():
counts=np.zeros(Nreg)
sums=np.zeros(Nreg)
S = np.empty((sX, sY))
for x in range(sX):
for y in range(sY):
region=FIDO[x,y]
value=WBColor[x,y]
counts[region]+=1
sums[region]+=value
for x in range(sX):
for y in range(sY):
region=FIDO[x,y]
S[x,y]=sums[region]/counts[region]
return S
你现在大约快了 4000 倍:
In [45]: np.allclose(oldloop(),numbaloops())
Out[45]: True
In [46]: %timeit -n1000 numbaloops()
1000 loops, best of 3: 1.06 ms per loop
正如@lejlot 之前建议的那样,代码很难向量化。不能运行并联,除非你事先知道每个FIDO有哪些像素属于哪个像素。不知道你是不是对超像素调用FIDO,不过我经常遇到这类问题,目前我找到的最好的解决方案如下:
扁平化数据:
data = data.reshape(-1, 3)
labels = FIDO.copy()
这里 data
是您的 (Width, Height, 3)
图像,而不是您拥有的单独的 3 个矢量。它被压扁为 (Width * Height, 3)
.
将 FIDO 重新标记为 0..N-1
范围,其中 N
=num unique FIDO:
from skimage.segmentation import relabel_sequential
labels = relabel_sequential(labels)[0]
labels -= labels.min()
以上,从 scikit-image
开始,将您的 FIDO 数组转换为 [0, N-1]
范围,以后使用起来更容易。
最后,在 cython 中编写一个简单的函数来计算每个 FIDO;s 的平均值(因为它们是从 0 到 N 排序的,你可以在长度为 N 的一维数组中完成):
def fmeans(double[:, ::1] data, long[::1] labels, long nsp):
cdef long n, N = labels.shape[0]
cdef int K = data.shape[1]
cdef double[:, ::1] F = np.zeros((nsp, K), np.float64)
cdef int[::1] sizes = np.zeros(nsp, np.int32)
cdef long l, b
cdef double t
for n in range(N):
l = labels[n]
sizes[l] += 1
for z in range(K):
t = data[n, z]
F[l, z] += t
for n in range(nsp):
for z in range(K):
F[n, z] /= sizes[n]
return np.asarray(F)
您可以稍后调用该函数(一旦用 cython 编译),就像:
mean_colors = fmeans(data, labels.flatten(), labels.max()+1) # labels.max()+1 == N
平均颜色的图像可以恢复为:
mean_img = mean_colors[labels]
如果您不想在 cython 中编写代码,scikit-image
也通过使用图结构和 networkx
提供绑定,但速度要慢得多:
http://scikit-image.org/docs/dev/auto_examples/plot_rag_mean_color.html
以上示例包含您需要的函数调用,以获取每个超像素的平均颜色为 labels1
(您的 FIDO)的图像。
注意:cython 方法要快得多,因为它不是迭代唯一 FIDO N
的数量,而是对每个 FIDO 扫描图像(大小 M = Width x Height
) 这只会迭代图像一次。因此,计算成本的顺序是 O(M+N)
,而不是原始方法的 O(M*N)
。
示例测试:
import numpy as np
from skimage.segmentation import relabel_sequential
sX=200
sY=200
FIDO = np.random.randint(0, sX*sY, (sX, sY))
data = np.random.rand(sX, sY, 3) # Your image
展平并重新标记:
data = data.reshape(-1, 3)
labels = relabel_sequential(FIDO)[0]
labels -= labels.min()
获取均值:
>>> %timeit color_means = fmeans(data, labels.flatten(), labels.max()+1)
1000 loops, best of 3: 520 µs per loop
一张 200x200 的图像需要 0.5 毫秒(半毫秒):
print labels.max()+1 # --> 25787 unique FIDO
print color_means.shape # --> (25287, 3), the mean color of each FIDO
您可以使用智能索引恢复平均颜色的图像:
mean_image = color_means[labels]
print mean_image.shape # --> (200, 200, 3)
我怀疑你能否通过原始 python 方法获得那样的速度(或者至少,我没有找到方法)。
这已在 Scipy 中实施,因此您可以:
from scipy.ndimage.measurements import mean as labeled_mean
labels = np.arange(FIDO.max()+1, dtype=int)
S_wb = labeled_mean(WBColor, FIDO, labels)[FIDO]
S_rg = labeled_mean(RGColor, FIDO, labels)[FIDO]
S_by = labeled_mean(BYColor, FIDO, labels)[FIDO]
这是假设 FIDO
包含相对较小的整数。如果不是这种情况,您可以通过 np.unique(FIDO, return_inverse=True)
.
对其进行转换
对于 200x200 图像和包含从 0 到 40,000 的随机整数的FIDO
,这个简单代码比原始代码快大约 1000 倍。
这段代码的先决条件细节很长,所以我会尽量总结一下。 WB/RG/BYColor 是基本图像,FIDO 是应用于此基本图像的叠加层。 S_wb/rg/by 是最终的输出图像。 WB/RG/BYColor 与 FIDO 大小相同。
对于 FIDO 中的每个独特元素,我们要计算基础图像中该区域的平均颜色。下面的 代码执行此操作,但是 由于 numFIDOs 非常大(最多 40,000),这需要 很长的时间 。
计算三个独立 RGB 通道的平均值。
sX=200
sY=200
S_wb = np.zeros((sX, sY))
S_rg = np.zeros((sX, sY))
S_by = np.zeros((sX, sY))
uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True)
numFIDOs = uniqueFIDOs.shape
for i in np.arange(0,numFIDOs[0]):
Lookup = FIDO==uniqueFIDOs[i]
# Get average of color signals for this FIDO
S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
S_rg[Lookup] = np.sum(RGColor[Lookup])/unique_counts[i]
S_by[Lookup] = np.sum(BYColor[Lookup])/unique_counts[i]
运行 大约需要 7.89 秒,不会太长,但这将包含在另一个循环中,因此它会累积!
我尝试过矢量化(如下所示),但我做不到
FIDOsize = unique_counts[0:numFIDOs[0]:1]
Lookup = FIDO ==uniqueFIDOs[0:numFIDOs[0]:1]
S_wb[Lookup] = np.sum(WBColor[Lookup])/FIDOsize
S_rg[Lookup] = np.sum(RGColor[Lookup])/FIDOsize
S_by[Lookup] = np.sum(BYColor[Lookup])/FIDOsize
数组大小匹配错误
简而言之:python 中的循环很慢。您应该执行以下操作之一:
- 向量化(你试过了,但你声称"it does not work"),你的意思是什么但不起作用?矢量化(如果可能)始终有效
- 切换到Cython,声明迭代器值为
int
以上两种方法都是基于将瓶颈循环转换为C循环。
根据我的时间安排,这比您原来的方法快 10 倍左右。我用这些数组进行了测试:
import numpy as np
sX=200
sY=200
FIDO = np.random.randint(0, sX*sY, (sX, sY))
WBColor = np.random.randint(0, sX*sY, (sX, sY))
RGColor = np.random.randint(0, sX*sY, (sX, sY))
BYColor = np.random.randint(0, sX*sY, (sX, sY))
这是我计时的部分:
import collections
colors = {'wb': WBColor, 'rg': RGColor, 'by': BYColor}
planes = colors.keys()
S = {plane: np.zeros((sX, sY)) for plane in planes}
for plane in planes:
counts = collections.defaultdict(int)
sums = collections.defaultdict(int)
for (i, j), f in np.ndenumerate(FIDO):
counts[f] += 1
sums[f] += colors[plane][i, j]
for (i, j), f in np.ndenumerate(FIDO):
S[plane][i, j] = sums[f]/counts[f]
可能是因为尽管 Python 中的循环很慢,但它遍历的数据较少。
请注意,如果 FIDO
中有少量唯一值,原始版本会变得更快。对于大多数情况,这花费的时间大致相同。
您的代码不是最优的,因为您扫描了 FIDO
中每个区域的所有图像。更好的方法是对每个区域的像素进行分组,然后先计算均值。 pandas
为此类计算提供不错的工具(此处仅在一条运河上)。然后你跨越区域的平均值:
import numpy as np
import pandas as pd
sX=200
sY=200
Nreg=sX*sY
WBColor=np.random.randint(0,256,(sX,sY))
FIDO=np.random.randint(0,Nreg,(sX,sY))
def oldloop():
S_wb = np.zeros((sX, sY))
uniqueFIDOs, unique_counts = np.unique(FIDO, return_counts=True)
numFIDOs = uniqueFIDOs.shape
for i in np.arange(0,numFIDOs[0]):
Lookup = FIDO==uniqueFIDOs[i]
S_wb[Lookup] = np.sum(WBColor[Lookup])/unique_counts[i]
return S_wb
def newloop():
index=pd.Index(FIDO.flatten(),name='region')
means= pd.DataFrame(WBColor.flatten(),index).groupby(level='region').mean()
lookup=np.zeros(Nreg)
lookup[means.index]=means.values
return lookup[FIDO]
在这种情况下,速度快了大约 200 倍:
In [32]: np.allclose(oldloop(),newloop())
Out[32]: True
In [33]: %timeit -n1 oldloop()
1 loops, best of 3: 3.92 s per loop
In [34]: %timeit -n100 newloop()
100 loops, best of 3: 20.5 ms per loop
编辑
另一种很酷的现代方法是使用 numba
。您以接近 C 的速度编写(非常)基本的 python 代码 运行 :
from numba import jit
@jit
def numbaloops():
counts=np.zeros(Nreg)
sums=np.zeros(Nreg)
S = np.empty((sX, sY))
for x in range(sX):
for y in range(sY):
region=FIDO[x,y]
value=WBColor[x,y]
counts[region]+=1
sums[region]+=value
for x in range(sX):
for y in range(sY):
region=FIDO[x,y]
S[x,y]=sums[region]/counts[region]
return S
你现在大约快了 4000 倍:
In [45]: np.allclose(oldloop(),numbaloops())
Out[45]: True
In [46]: %timeit -n1000 numbaloops()
1000 loops, best of 3: 1.06 ms per loop
正如@lejlot 之前建议的那样,代码很难向量化。不能运行并联,除非你事先知道每个FIDO有哪些像素属于哪个像素。不知道你是不是对超像素调用FIDO,不过我经常遇到这类问题,目前我找到的最好的解决方案如下:
扁平化数据:
data = data.reshape(-1, 3) labels = FIDO.copy()
这里
data
是您的(Width, Height, 3)
图像,而不是您拥有的单独的 3 个矢量。它被压扁为(Width * Height, 3)
.将 FIDO 重新标记为
0..N-1
范围,其中N
=num unique FIDO:from skimage.segmentation import relabel_sequential labels = relabel_sequential(labels)[0] labels -= labels.min()
以上,从
scikit-image
开始,将您的 FIDO 数组转换为[0, N-1]
范围,以后使用起来更容易。最后,在 cython 中编写一个简单的函数来计算每个 FIDO;s 的平均值(因为它们是从 0 到 N 排序的,你可以在长度为 N 的一维数组中完成):
def fmeans(double[:, ::1] data, long[::1] labels, long nsp): cdef long n, N = labels.shape[0] cdef int K = data.shape[1] cdef double[:, ::1] F = np.zeros((nsp, K), np.float64) cdef int[::1] sizes = np.zeros(nsp, np.int32) cdef long l, b cdef double t for n in range(N): l = labels[n] sizes[l] += 1 for z in range(K): t = data[n, z] F[l, z] += t for n in range(nsp): for z in range(K): F[n, z] /= sizes[n] return np.asarray(F)
您可以稍后调用该函数(一旦用 cython 编译),就像:
mean_colors = fmeans(data, labels.flatten(), labels.max()+1) # labels.max()+1 == N
平均颜色的图像可以恢复为:
mean_img = mean_colors[labels]
如果您不想在 cython 中编写代码,scikit-image
也通过使用图结构和 networkx
提供绑定,但速度要慢得多:
http://scikit-image.org/docs/dev/auto_examples/plot_rag_mean_color.html
以上示例包含您需要的函数调用,以获取每个超像素的平均颜色为 labels1
(您的 FIDO)的图像。
注意:cython 方法要快得多,因为它不是迭代唯一 FIDO N
的数量,而是对每个 FIDO 扫描图像(大小 M = Width x Height
) 这只会迭代图像一次。因此,计算成本的顺序是 O(M+N)
,而不是原始方法的 O(M*N)
。
示例测试:
import numpy as np
from skimage.segmentation import relabel_sequential
sX=200
sY=200
FIDO = np.random.randint(0, sX*sY, (sX, sY))
data = np.random.rand(sX, sY, 3) # Your image
展平并重新标记:
data = data.reshape(-1, 3)
labels = relabel_sequential(FIDO)[0]
labels -= labels.min()
获取均值:
>>> %timeit color_means = fmeans(data, labels.flatten(), labels.max()+1)
1000 loops, best of 3: 520 µs per loop
一张 200x200 的图像需要 0.5 毫秒(半毫秒):
print labels.max()+1 # --> 25787 unique FIDO
print color_means.shape # --> (25287, 3), the mean color of each FIDO
您可以使用智能索引恢复平均颜色的图像:
mean_image = color_means[labels]
print mean_image.shape # --> (200, 200, 3)
我怀疑你能否通过原始 python 方法获得那样的速度(或者至少,我没有找到方法)。
这已在 Scipy 中实施,因此您可以:
from scipy.ndimage.measurements import mean as labeled_mean
labels = np.arange(FIDO.max()+1, dtype=int)
S_wb = labeled_mean(WBColor, FIDO, labels)[FIDO]
S_rg = labeled_mean(RGColor, FIDO, labels)[FIDO]
S_by = labeled_mean(BYColor, FIDO, labels)[FIDO]
这是假设 FIDO
包含相对较小的整数。如果不是这种情况,您可以通过 np.unique(FIDO, return_inverse=True)
.
对于 200x200 图像和包含从 0 到 40,000 的随机整数的FIDO
,这个简单代码比原始代码快大约 1000 倍。