英文:
Fractal dimension with the Mass-radius method
问题
我有一些图像,我想计算质量-半径维度以确定图像中的分形特性。这是其中之一:
Ottawa.png
:
质量维度定义了位于特定半径内的区域与该半径(或框)的大小之间的关系。这是从质心开始的各种半径上执行的。可以通过区域(非黑色像素的数量)与半径(以像素为单位)的对数-对数图来估算质量维度。
我的逐步操作:
- 找到质心
- 创建半径 r 的圆并找到非黑色像素的数量
- 将这些值的对数放入两个列表中(R,N(R))
- 迭代,直到分析图像完成
- 在(R,N(R))之间执行线性回归
- ax+b 中的系数 a 就是维度
我使用以下代码来计算分形维度:
from scipy.stats import linregress
from skimage import io
import matplotlib.pyplot as plt
import numpy as np
# 验证像素是否为黑色
def test_pixel(l):
return (l == np.array([0, 0, 0])).all()
# 找到图像的质心
def center_of_mass(img):
i, j, f = img.shape
x, y, p = 0, 0, 0
for k1 in range(1, i - 1):
for k2 in range(1, j - 1):
if not test_pixel(img[k1][k2]):
p += 1
x += k2
y += k1
return int(x / p), int(y / p)
# 返回两个对数列表:半径和半径处的像素数
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # 当分析图像时停止
X, Y, A, p = [], [], 0, 0
for t in range(1, k):
for k1 in range(-t, t + 1):
for k2 in range(-t, t + 1):
# 验证 (x+k2, y+k1) 是否在图像内
if -1 < (x + k2) < j and -1 < (y + k1) < i:
# 测试 (x+k2, y+k1) 是否不是黑色并且位于半径为 t 的圆内
if A == 0 and k1 ** 2 + k2 ** 2 < t ** 2 and not test_pixel(img[k1 + y][k2 + x]):
p += 1
elif A ** 2 <= k1 ** 2 + k2 ** 2 < t ** 2 and not test_pixel(img[k1 + y][k2 + x]):
p += 1
A = t
if p != 0:
Y.append(np.log(p))
X.append(np.log(t))
return X, Y
I = io.imread('Ottawa.png')
X, Y = D_R(I)
a, b, r, p_value, std_err = linregress(X, Y)
print(a) # 对应维度
print(b)
print(r**2)
然而,这段代码运行速度非常慢。
我想知道如何使这段代码更有效率。
英文:
I have some images for which I want to calculate the Mass-Radius dimension to determine the fractal characteristics in the image. Here is one of them :
Ottawa.png
:
The mass dimension defines the relationship between the area located within a certain radius and
the size of this radius (or box). This is performed for various radiuses beginning at the center of mass. The mass dimension can be estimated from the log-log plot of the area (the number of non black pixel) as a function of the radius (in pixel).
My Step-by-Step:
- Find the center of mass
- Create a circle of radius r and find the number of non black pixels
- Put the log of those values inside 2 lists (R, N(R))
- Iterate until we finished analyzing the image
- Do a linear regression between (R, N(R))
- The coefficient a from ax+b is the dimension
I'm using the following code to calculate the fractal dimension:
from scipy.stats import linregress
from skimage import io
import matplotlib.pyplot as plt
import numpy as np
# Verify if a pixel is black
def test_pixel(l):
return (l == np.array([0, 0, 0])).all()
# Find the center of the image
def center_of_mass(img):
i, j, f = img.shape
x,y,p = 0,0,0
for k1 in range(1, i - 1):
for k2 in range(1, j - 1):
if not test_pixel(img[k1][k2]):
p += 1
x += k2
y += k1
return int(x / p), int(y / p)
# Return two lists of logs : the radius and the number of pixel at the radius
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # Stop when the image is analyzed
X, Y, A, p = [], [], 0, 0
for t in range(1, k):
for k1 in range(-t, t+1):
for k2 in range(-t, t+1):
# Verify if (x+k2, y+k1) is in the image
if -1 < (x + k2) < j and -1 < (y + k1) < i:
# Test if (x+k2, y+k1) is not black and is in the circle of radius t
if A == 0 and k1 ** 2 + k2 ** 2 < t ** 2 and not test_pixel(img[k1+y][k2+x]):
p += 1
elif A ** 2 <= k1 ** 2 + k2 ** 2 < t ** 2 and not test_pixel(img[k1+y][k2+x]):
p += 1
A = t
if p != 0:
Y.append(np.log(p))
X.append(np.log(t))
return X, Y
I = io.imread('Ottawa.png')
X, Y = D_R(I)
a, b, r, p_value, std_err = linregress(X, Y)
print(a) # Corresponds to the Dimension
print(b)
print(r**2)
However, this code is really slow.
I want to know how can I make this code more efficient.
答案1
得分: 1
你的代码运行速度很慢的原因是由于你的三层嵌套循环。我想你可能已经自己意识到了这一点,但在Python中,循环(尤其是嵌套循环)并不是一个好主意。相反,你应该尝试使用Numpy的广播功能。
只需注意一个细节:因为你正在处理PNG格式的图像,你可能会得到四个通道(其中一个是alpha通道),这可能不是你想要的。
如果你有一个循环,其输出仅依赖于迭代变量,并且该变量是一个简单的range
,通常可以用np.arange
替代。如果你有嵌套循环,你可以将np.arange
进行广播,以生成2D(或nD)数组,模拟你的for循环。在这种情况下,你可以很容易地用k1
和k2
来做到这一点,消除了两个内部循环:
k1 = np.arange(-t, t+1)[:, np.newaxis]
k2 = np.arange(-t, t+1)
然后,你可以将大部分的if/elif部分转换为布尔数学运算:
r = k1**2 + k2**2
x_k2 = x + k2
y_k1 = y + k1
p_mat = (-1 < x_k2) & (x_k2 < j) & (-1 < y_k1) & (y_k1 < i) & ((A == 0) | (A**2 <= r)) & (r < t**2)
请注意,在这里r
现在是一个2D数组,并导致所有其他操作/数组广播到相同的形状。
这种方法的唯一真正复杂的部分实际上是处理图像,因为图像的索引不总是产生一致大小的输出,尤其是在图像的边缘。由于边缘情况,这有点复杂,但是实际情况如下:
cropped_img = img[max(y-t, 0):y+t+1,max(x-t, 0):x+t+1]
offset_y = max(-(y-t), 0)
offset_x = max(-(x-t), 0)
p_mat = p_mat[offset_y:offset_y+cropped_img.shape[0], offset_x:offset_x+cropped_img.shape[1]] & (np.any(cropped_img != 0, axis=-1))
如果你只保留t
循环,这已经显著提高了性能:
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # 当图像分析完毕时停止
X, Y, A, p = [], [], 0, 0
for t in range(1, k):
k1 = np.arange(-t, t+1)[:, np.newaxis]
k2 = np.arange(-t, t+1)
r = k1**2 + k2**2
x_k2 = x + k2
y_k1 = y + k1
p_mat = (-1 < x_k2) & (x_k2 < j) & (-1 < y_k1) & (y_k1 < i) & ((A == 0) | (A**2 <= r)) & (r < t**2)
cropped_img = img[max(y-t, 0):y+t+1,max(x-t, 0):x+t+1]
offset_y = max(-(y-t), 0)
offset_x = max(-(x-t), 0)
p_mat = p_mat[offset_y:offset_y+cropped_img.shape[0], offset_x:offset_x+cropped_img.shape[1]] & (np.any(cropped_img != 0, axis=-1))
p += p_mat.sum()
A = t
if p != 0:
Y.append(np.log(p))
X.append(np.log(t))
return X, Y
请注意,你的center_of_mass
函数仍然非常慢。你可以尝试应用我之前提到的关于用np.arange
替换循环的方法。
还有一个进一步的改进你可以尝试,完全消除t
循环:如果你仔细考虑,你的t
循环基本上是在质心周围定义一个不断增大的圆,并对其边缘执行某些计算。如果将t
视为第三维度,这将产生一个圆锥的外壳。圆锥中的每个值都与底层图像中的值进行&
操作,然后进行累积求和。类似这样:
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # 当图像分析完毕时停止
X, Y, A, p = [], [], 0, 0
cone = k - 1 - np.sqrt((np.arange(i)[:, np.newaxis] - y) ** 2 + (np.arange(j) - x) ** 2)
cone = np.maximum(cone.astype(np.int64), 0)
cone = cone >= (k - 1 - np.arange(k).reshape((-1, 1, 1)))
cone = cone[1:]
cone[1:] &= ~cone[:-1]
p_vec = np.any(img != 0, axis=-1) & cone
p_vec = np.cumsum(np.sum(p_vec, (1, 2)))
return np.log(np.arange(1, k)[p_vec != 0]).tolist(), np.log(p_vec[p_vec != 0]).tolist()
然而,尽管这与原始实现得到了类似的结果,但它并不绝对准确,所以我假设我漏掉了一些细节。不过,这很接近,所
英文:
The reason your code is so slow is because of your three nested loops. I imagine you've worked this out yourself, but loops (and especially nested loops) are a bad idea in Python. Instead, you should try to use Numpy broadcasting.
Just one detail to watch out for: because you're working with a PNG, you may end up with four channels (one of them being the alpha channel), which may not be what you intended.
If you have a loop whose output only depends on the iteration variable, and that variable is a simple range
, it is frequently possible to replace this with np.arange
. If you have nested loops, you can broadcast np.arange
s together to produce 2D (or nD) arrays which replicate your for loops. In this case, it's fairly easy to do this with k1
and k2
, eliminating the two inner loops:
k1 = np.arange(-t, t+1)[:, np.newaxis]
k2 = np.arange(-t, t+1)
You can then convert most of your if/elif section to boolean maths:
r = k1**2 + k2**2
x_k2 = x + k2
y_k1 = y + k1
p_mat = (-1 < x_k2) & (x_k2 < j) & (-1 < y_k1) & (y_k1 < i) & ((A == 0) | (A**2 <= r)) & (r < t**2)
Note that here r
is now a 2D array and causes all other operations/arrays to broadcast to the same shape.
The only real complication of this approach is actually dealing with the image, since the indexing of the image doesn't always produce a consistently sized output, such as at the edge of the image. It's a bit complicated because of the edge cases, but this is how it turns out:
cropped_img = img[max(y-t, 0):y+t+1,max(x-t, 0):x+t+1]
offset_y = max(-(y-t), 0)
offset_x = max(-(x-t), 0)
p_mat = p_mat[offset_y:offset_y+cropped_img.shape[0], offset_x:offset_x+cropped_img.shape[1]] & (np.any(cropped_img != 0, axis=-1))
If you just leave the t
loop, this already significantly improves performance:
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # Stop when the image is analyzed
X, Y, A, p = [], [], 0, 0
for t in range(1, k):
k1 = np.arange(-t, t+1)[:, np.newaxis]
k2 = np.arange(-t, t+1)
r = k1**2 + k2**2
x_k2 = x + k2
y_k1 = y + k1
p_mat = (-1 < x_k2) & (x_k2 < j) & (-1 < y_k1) & (y_k1 < i) & ((A == 0) | (A**2 <= r)) & (r < t**2)
cropped_img = img[max(y-t, 0):y+t+1,max(x-t, 0):x+t+1]
offset_y = max(-(y-t), 0)
offset_x = max(-(x-t), 0)
p_mat = p_mat[offset_y:offset_y+cropped_img.shape[0], offset_x:offset_x+cropped_img.shape[1]] & (np.any(cropped_img != 0, axis=-1))
p += p_mat.sum()
A = t
if p != 0:
Y.append(np.log(p))
X.append(np.log(t))
return X, Y
Note that your center_of_mass
function is still really slow. You could try to apply what I said earlier about replacing loops with np.arange
to improve it.
There is a further improvement you could work on, completely removing the t
loop: if you think about it, your t
loop is basically defining an ever growing circle centred around the centre of mass and performing certain computations on its edge. If you think of t
as being a 3rd dimension, this produces the shell of a cone. Each value in the cone is combined with the value in the underlying image with an &
, and the result is cumulatively summed. Something like this:
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # Stop when the image is analyzed
X, Y, A, p = [], [], 0, 0
cone = k - 1 - np.sqrt((np.arange(i)[:, np.newaxis] - y) ** 2 + (np.arange(j) - x) ** 2)
cone = np.maximum(cone.astype(np.int64), 0)
cone = cone >= (k - 1 - np.arange(k).reshape((-1, 1, 1)))
cone = cone[1:]
cone[1:] &= ~cone[:-1]
p_vec = np.any(img != 0, axis=-1) & cone
p_vec = np.cumsum(np.sum(p_vec, (1, 2)))
return np.log(np.arange(1, k)[p_vec != 0]).tolist(), np.log(p_vec[p_vec != 0]).tolist()
However, while this gets similar results to the original, it isn't absolutely accurate, so I assume I've missed some detail. It's close though, so you could work on it.
Building the function in this way has great potential for applying other interesting things like np.einsum
, since an &
operation is equivalent to a boolean multiplication and an np.any
operation is equivalent to a sum along that axis, the two operations that np.einsum
is built for.
Obviously in all these cases there is a tradeoff between compute time and memory. The original implementation leans heavily on the former, while my last version requires a lot of the latter. Though if you rewrite it using np.einsum
you could get fairly good performance with low memory usage.
PS: In this line:
k = int(max(j - x, i - y, x, y)) # Stop when the image is analyzed
Are you sure this is correct? It seems to me you intended to cover the entire image, however because you're working with circles, this misses all the corners of the image. Maybe this is what you intended, maybe not. If not, I suggest you mean:
k = int((max(j-x, x)**2 + max(i-y, y)**2)**0.5)
But I'm only guessing.
Edit: Regarding my suggestion of using einsum
, turns out this doesn't really work: the problem is that the array that uses most memory is the cone, which can't be converted to einsum
(at least, I can't see any way).
However, there are other other methods to reduce memory and time: instead of converting the first step of the cone to a boolean array, just use the values directly. This is a slight compromise in terms of accuracy (compared to your original implementation) but a massive leap in performance and memory usage (I can't even see the memory usage and it takes a quarter of the time of the previous best). It uses a function called np.bincount
:
def D_R(img):
(x, y) = center_of_mass(img)
i, j, f = img.shape
k = int(max(j - x, i - y, x, y)) # Stop when the image is analyzed
cone = k - 1 - \
np.sqrt((np.arange(i)[:, np.newaxis] - y)
** 2 + (np.arange(j) - x) ** 2)
cone = np.maximum(cone.round().astype(np.int64), 0)
p_vec = np.any(img != 0, axis=-1) * cone
p_vec = np.bincount(p_vec.ravel(), minlength=k)[1:][::-1]
p_vec = np.cumsum(p_vec)
return np.log(np.arange(1, k)[p_vec != 0]).tolist(), np.log(p_vec[p_vec != 0]).tolist()
The reason this is different is again probably due to the exact thickness of the "circles" you're testing. In this approach, the circles are very thin, since they can never overlap.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论