-
Notifications
You must be signed in to change notification settings - Fork 12
/
rolling_ball.py
215 lines (184 loc) · 7.13 KB
/
rolling_ball.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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
import math
import numpy
"""
Ported to Python from ImageJ's Background Subtractor.
Only works for 8-bit greyscale images currently.
Does not perform shrinking/enlarging for larger radius sizes.
Based on the concept of the rolling ball algorithm described
in Stanley Sternberg's article,
"Biomedical Image Processing", IEEE Computer, January 1983.
Imagine that the 2D grayscale image has a third (height) dimension by the image
value at every point in the image, creating a surface. A ball of given radius
is rolled over the bottom side of this surface; the hull of the volume
reachable by the ball is the background.
http://rsbweb.nih.gov/ij/developer/source/ij/plugin/filter/BackgroundSubtracter.java.html
"""
def smooth(array, window=3.0):
"""
Applies a 3x3 mean filter to specified array.
"""
dx, dy = array.shape
new_array = numpy.copy(array)
edgex = int(math.floor(window / 2.0))
edgey = int(math.floor(window / 2.0))
for i in range(dx):
for j in range(dy):
window_array = array[max(i - edgex, 0):min(i + edgex + 1, dx),
max(j - edgey, 0):min(j + edgey + 1, dy)]
new_array[i, j] = window_array.mean()
return new_array
def rolling_ball_background(array, radius, light_background=True,
smoothing=True):
"""
Calculates and subtracts background from array.
Arguments:
array - uint8 numpy array representing image
radius - radius of the rolling ball creating the background
light_background - Does image have light background
smoothing - Whether the image should be smoothed before creating the
background.
"""
invert = False
if light_background:
invert = True
ball = RollingBall(radius)
float_array = array
float_array = rolling_ball_float_background(float_array, radius, invert,
smoothing, ball)
background_pixels = float_array.flatten()
if invert:
offset = 255.5
else:
offset = 0.5
pixels = numpy.int8(array.flatten())
for p in range(len(pixels)):
value = (pixels[p] & 0xff) - (background_pixels[p] + 255) + offset
if value < 0:
value = 0
if value > 255:
value = 255
pixels[p] = numpy.int8(value)
return numpy.reshape(pixels, array.shape)
def rolling_ball_float_background(float_array, radius, invert, smoothing,
ball):
"""
Create background for a float image by rolling a ball over the image
"""
pixels = float_array.flatten()
shrink = ball.shrink_factor > 1
if invert:
for i in range(len(pixels)):
pixels[i] = -pixels[i]
if smoothing:
smoothed_pixels = smooth(numpy.reshape(pixels, float_array.shape))
pixels = smoothed_pixels.flatten()
pixels = roll_ball(ball, numpy.reshape(pixels, float_array.shape))
if invert:
for i in range(len(pixels)):
pixels[i] = -pixels[i]
return numpy.reshape(pixels, float_array.shape)
def roll_ball(ball, array):
"""
Rolls a filtering object over an image in order to find the
image's smooth continuous background. For the purpose of explaining this
algorithm, imagine that the 2D grayscale image has a third (height)
dimension defined by the intensity value at every point in the image. The
center of the filtering object, a patch from the top of a sphere having
radius 'radius', is moved along each scan line of the image so that the
patch is tangent to the image at one or more points with every other point
on the patch below the corresponding (x,y) point of the image. Any point
either on or below the patch during this process is considered part of the
background.
"""
height, width = array.shape
pixels = numpy.float32(array.flatten())
z_ball = ball.data
ball_width = ball.width
radius = ball_width / 2
cache = numpy.zeros(width * ball_width)
for y in range(-radius, height + radius):
next_line_to_write_in_cache = (y + radius) % ball_width
next_line_to_read = y + radius
if next_line_to_read < height:
src = next_line_to_read * width
dest = next_line_to_write_in_cache * width
cache[dest:dest + width] = pixels[src:src + width]
p = next_line_to_read * width
for x in range(width):
pixels[p] = -float('inf')
p += 1
y_0 = y - radius
if y_0 < 0:
y_0 = 0
y_ball_0 = y_0 - y + radius
y_end = y + radius
if y_end >= height:
y_end = height - 1
for x in range(-radius, width + radius):
z = float('inf')
x_0 = x - radius
if x_0 < 0:
x_0 = 0
x_ball_0 = x_0 - x + radius
x_end = x + radius
if x_end >= width:
x_end = width - 1
y_ball = y_ball_0
for yp in range(y_0, y_end + 1):
cache_pointer = (yp % ball_width) * width + x_0
bp = x_ball_0 + y_ball * ball_width
for xp in range(x_0, x_end + 1):
z_reduced = cache[cache_pointer] - z_ball[bp]
if z > z_reduced:
z = z_reduced
cache_pointer += 1
bp += 1
y_ball += 1
y_ball = y_ball_0
for yp in range(y_0, y_end + 1):
p = x_0 + yp * width
bp = x_ball_0 + y_ball * ball_width
for xp in range(x_0, x_end + 1):
z_min = z + z_ball[bp]
if pixels[p] < z_min:
pixels[p] = z_min
p += 1
bp += 1
y_ball += 1
return numpy.reshape(pixels, array.shape)
class RollingBall(object):
"""
A rolling ball (or actually a square part thereof).
"""
def __init__(self, radius):
if radius <= 10:
self.shrink_factor = 1
arc_trim_per = 24
elif radius <= 30:
self.shrink_factor = 2
arc_trim_per = 24
elif radius <= 100:
self.shrink_factor = 4
arc_trim_per = 32
else:
self.shrink_factor = 8
arc_trim_per = 40
self.build(radius, arc_trim_per)
def build(self, ball_radius, arc_trim_per):
small_ball_radius = ball_radius / self.shrink_factor
if small_ball_radius < 1:
small_ball_radius = 1
rsquare = small_ball_radius * small_ball_radius
xtrim = int(arc_trim_per * small_ball_radius) / 100
half_width = int(round(small_ball_radius - xtrim))
self.width = (2 * half_width) + 1
self.data = [0.0] * (self.width * self.width)
p = 0
for y in range(self.width):
for x in range(self.width):
xval = x - half_width
yval = y - half_width
temp = rsquare - (xval * xval) - (yval * yval)
if temp > 0:
self.data[p] = float(math.sqrt(temp))
p += 1