-
Notifications
You must be signed in to change notification settings - Fork 3
/
fractal_analysis_fxns.py
176 lines (135 loc) · 5.88 KB
/
fractal_analysis_fxns.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# -----------------------------------------------------------------------------
# From https://en.wikipedia.org/wiki/Minkowski–Bouligand_dimension:
#
# In fractal geometry, the Minkowski–Bouligand dimension, also known as
# Minkowski dimension or box-counting dimension, is a way of determining the
# fractal dimension of a set S in a Euclidean space Rn, or more generally in a
# metric space (X, d).
# -----------------------------------------------------------------------------
# From https://github.com/rougier/numpy-100 (#87)
# From https://gist.github.com/rougier/e5eafc276a4e54f516ed5559df4242c0
import numpy as np
def boxcount(Z, k):
"""
returns a count of squares of size kxk in which there are both colours (black and white), ie. the sum of numbers
in those squares is not 0 or k^2
Z: np.array, matrix to be checked, needs to be 2D
k: int, size of a square
"""
S = np.add.reduceat(
np.add.reduceat(Z, np.arange(0, Z.shape[0], k), axis=0),
np.arange(0, Z.shape[1], k), axis=1) # jumps by powers of 2 squares
# We count non-empty (0) and non-full boxes (k*k)
return len(np.where((S > 0) & (S < k * k))[0])
def boxcount_grayscale(Z, k):
"""
find min and max intensity in the box and return their difference
Z - np.array, array to find difference in intensities in
k - int, size of a box
"""
S_min = np.fmin.reduceat(
np.fmin.reduceat(Z, np.arange(0, Z.shape[0], k), axis=0),
np.arange(0, Z.shape[1], k), axis=1)
S_max = np.fmax.reduceat(
np.fmax.reduceat(Z, np.arange(0, Z.shape[0], k), axis=0),
np.arange(0, Z.shape[1], k), axis=1)
return S_max - S_min
def fractal_dimension(Z, threshold=0.9):
"""
calculate fractal dimension of an object in an array defined to be above certain threshold as a count of squares
with both black and white pixels for a sequence of square sizes. The dimension is the a coefficient to a poly fit
to log(count) vs log(size) as defined in the sources.
:param Z: np.array, must be 2D
:param threshold: float, a thr to distinguish background from foreground and pick up the shape, originally from
(0, 1) for a scaled arr but can be any number, generates boolean array
:return: coefficients to the poly fit, fractal dimension of a shape in the given arr
"""
# Only for 2d image
assert (len(Z.shape) == 2)
# Transform Z into a binary array
Z = (Z < threshold)
# Minimal dimension of image
p = min(Z.shape)
# Greatest power of 2 less than or equal to p
n = 2 ** np.floor(np.log(p) / np.log(2))
# Extract the exponent
n = int(np.log(n) / np.log(2))
# Build successive box sizes (from 2**n down to 2**1)
sizes = 2 ** np.arange(n, 1, -1)
# Actual box counting with decreasing size
counts = []
for size in sizes:
counts.append(boxcount(Z, size))
# Fit the successive log(sizes) with log (counts)
coeffs = np.polyfit(np.log(sizes), np.log(counts), 1)
return -coeffs[0]
def fractal_dimension_grayscale(Z):
"""
works the same as fractal_dimension() just does not look at counts and does not require a binary arr rather is looks
at intensities (hence can be used for a grayscale image) and returns fractal dimensions D_B and D_M (based on sums
and means), as described in https://imagej.nih.gov/ij/plugins/fraclac/FLHelp/Glossary.htm#grayscale
:param Z: np. array, must be 2D
:return: D_B and D_M fractal dimensions based on polyfit to log(sum) or log(mean) resp. vs log(sizes)
"""
# Only for 2d image
assert (len(Z.shape) == 2)
# Minimal dimension of image
p = min(Z.shape)
# Greatest power of 2 less than or equal to p
n = 2 ** np.floor(np.log(p) / np.log(2))
# Extract the exponent
n = int(np.log(n) / np.log(2))
# Build successive box sizes (from 2**n down to 2**1)
sizes = 2 ** np.arange(n, 1, -1)
# Actual box counting with decreasing size
i_difference = []
for size in sizes:
i_difference.append(boxcount_grayscale(Z, size))
# D_B
d_b = [np.sum(x) for x in i_difference]
# D_M
d_m = [np.mean(x) for x in i_difference]
# Fit the successive log(sizes) with log (sum)
coeffs_db = np.polyfit(np.log(sizes), np.log(d_b), 1)
# Fit the successive log(sizes) with log (mean)
coeffs_dm = np.polyfit(np.log(sizes), np.log(d_m), 1)
return -coeffs_db[0], -coeffs_dm[0]
def fractal_dimension_grayscale_DBC(Z):
"""
Differential box counting method with implementation of appropriate box height selection.
:param Z: 2D np.array
:return: fd for a grayscale image
"""
# Only for 2d image
assert (len(Z.shape) == 2)
# Minimal dimension of image
p = min(Z.shape)
# Greatest power of 2 less than or equal to p
n = 2 ** np.floor(np.log(p) / np.log(2))
# Extract the exponent
n = int(np.log(n) / np.log(2))
# Build successive box sizes (from 2**n down to 2**1)
sizes = 2 ** np.arange(n, 1, -1)
# get the mean and standard deviation of the image intensity to rescale r
mu = np.mean(Z)
sigma = np.std(Z)
# total number of gray levels, used for computing s'
G = len(np.unique(Z))
# scaled scale of each block, r=size(s)
# TODO -- when to rescale, what should a be
# when to rescale -- either always or when the pixels in the selected box don't fall in +- 1 std, so far always
a = 1
r_prime = sizes / (1 + 2 * a * sigma)
# Actual box counting with decreasing size
i_difference = []
for size in sizes:
# rescale
n_r = np.ceil(boxcount_grayscale(Z, size) / r_prime)
# if max==min per the box, replace the 0 result with 1
n_r[n_r == 0] = 1
i_difference.append(n_r)
# contribution from all boxes
N_r = [np.sum(x) for x in i_difference]
# Fit the successive log(sizes) with log (sum)
coeffs = np.polyfit(np.log(sizes), np.log(N_r), 1)
return -coeffs[0]