forked from citrusvanilla/multiplewavetracking_py
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mwt_objects.py
412 lines (314 loc) · 12.9 KB
/
mwt_objects.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
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
##
## Near-shore Wave Tracking
## mwt_objects.py
##
## Created by Justin Fung on 9/1/17.
## Copyright 2017 justin fung. All rights reserved.
##
## ====================================================================
"""Objects for implementing wave tracking."""
from __future__ import division
import math
from collections import deque
import cv2
import numpy as np
# Pixel height to buffer a sections's search region for other sections:
SEARCH_REGION_BUFFER = 15
# Length of Deque to keep track of displacement of the wave.
TRACKING_HISTORY = 21
# Width of frame in analysis steps (not original width):
ANALYSIS_FRAME_WIDTH = 320
# Height of frame in analysis steps (not original height):
ANALYSIS_FRAME_HEIGHT = 180
# The minimum orthogonal displacement to be considered an actual wave:
DISPLACEMENT_THRESHOLD = 10
# The minimum mass to be considered an actual wave:
MASS_THRESHOLD = 200
# The axis of major waves in the scene, counter clockwise from horizon:
GLOBAL_WAVE_AXIS = 5.0
# Integer global variable seed for naming detected waves by number:
NAME_SEED = 0
class Section(object):
"""Filtered contours become "sections" with the following
attributes. Dynamic attributes are updated in each frame through
tracking routine.
"""
# pylint: disable=too-many-instance-attributes
def __init__(self, points, birth):
self.name = _generate_name()
self.points = points
self.birth = birth
self.axis_angle = GLOBAL_WAVE_AXIS
self.centroid = _get_centroid(self.points)
self.centroid_vec = deque([self.centroid],
maxlen=TRACKING_HISTORY)
self.original_axis = _get_standard_form_line(self.centroid,
self.axis_angle)
self.searchroi_coors = _get_searchroi_coors(self.centroid,
self.axis_angle,
SEARCH_REGION_BUFFER,
ANALYSIS_FRAME_WIDTH)
self.boundingbox_coors = np.int0(cv2.boxPoints(
cv2.minAreaRect(points)))
self.displacement = 0
self.max_displacement = self.displacement
self.displacement_vec = deque([self.displacement],
maxlen=TRACKING_HISTORY)
self.mass = len(self.points)
self.max_mass = self.mass
self.recognized = False
self.death = None
def update_searchroi_coors(self):
"""Method that adjusts the search roi for tracking a wave in
future Frames.
Args:
NONE
Returns:
NONE: updates self.searhroi_coors
"""
self.searchroi_coors = _get_searchroi_coors(self.centroid,
self.axis_angle,
SEARCH_REGION_BUFFER,
ANALYSIS_FRAME_WIDTH)
def update_death(self, frame_number):
"""Checks to see if wave has died, which occurs when no pixels
are found in the wave's search roi. "None" indicates wave is
alive, while an integer represents the frame number of death.
Args:
frame_number: number of frame in a video sequence
Returns:
NONE: sets wave death to wave.death attribute
"""
if self.points is None:
self.death = frame_number
def update_points(self, frame):
"""Captures all positive pixels the search roi based on
measurement of the wave's position in the previous frame by
using a mask.
Docs:
https://stackoverflow.com/questions/17437846/
https://stackoverflow.com/questions/10469235/
Args:
frame: frame in which to obtain new binary representation of
the wave
Returns:
NONE: returns all positive points as an array to the
self.points attribute
"""
# make a polygon object of the wave's search region
rect = self.searchroi_coors
poly = np.array([rect], dtype=np.int32)
# make a zero valued image on which to overlay the roi polygon
img = np.zeros((ANALYSIS_FRAME_HEIGHT, ANALYSIS_FRAME_WIDTH),
np.uint8)
# fill the polygon roi in the zero-value image with ones
img = cv2.fillPoly(img, poly, 255)
# bitwise AND with the actual image to obtain a "masked" image
res = cv2.bitwise_and(frame, frame, mask=img)
# all points in the roi are now expressed with ones
points = cv2.findNonZero(res)
# update points
self.points = points
def update_centroid(self):
"""Calculates the center of mass of all positive pixels that
represent the wave, using first-order moments.
See _get_centroid.
Args:
NONE
Returns:
NONE: updates wave.centroid
"""
self.centroid = _get_centroid(self.points)
# Update centroid vector.
self.centroid_vec.append(self.centroid)
def update_boundingbox_coors(self):
"""Finds minimum area rectangle that bounds the points of the
wave. Returns four coordinates of the bounding box. This is
primarily for visualization purposes.
Args:
NONE
Returns:
NONE: updates self.boundingbox_coors attribute
"""
boundingbox_coors = None
if self.points is not None:
# Obtain the moments of the object from its points array.
X = [p[0][0] for p in self.points]
Y = [p[0][1] for p in self.points]
mean_x = np.mean(X)
mean_y = np.mean(Y)
std_x = np.std(X)
std_y = np.std(Y)
# We only capture points without outliers for display
# purposes.
points_without_outliers = np.array(
[p[0] for p in self.points
if np.abs(p[0][0]-mean_x) < 3*std_x
and np.abs(p[0][1]-mean_y) < 3*std_y])
rect = cv2.minAreaRect(points_without_outliers)
box = cv2.boxPoints(rect)
boundingbox_coors = np.int0(box)
self.boundingbox_coors = boundingbox_coors
def update_displacement(self):
"""Evaluates orthogonal displacement compared to original axis.
Updates self.max_displacement if necessary. Appends new
displacement to deque.
Args:
NONE
Returns:
NONE: updates self.displacement and self.max_displacement
attributes
"""
if self.centroid is not None:
self.displacement = _get_orthogonal_displacement(
self.centroid,
self.original_axis)
# Update max displacement of the wave if necessary.
if self.displacement > self.max_displacement:
self.max_displacement = self.displacement
# Update displacement vector.
self.displacement_vec.append(self.displacement)
def update_mass(self):
"""Calculates mass of the wave by weighting each pixel in a
search roi equally and performing a simple count. Updates
self.max_mass attribute if necessary.
Args:
wave: a Section object
Returns:
NONE: updates self.mass and self.max_mass attributes
"""
self.mass = _get_mass(self.points)
# Update max_mass for the wave if necessary.
if self.mass > self.max_mass:
self.max_mass = self.mass
def update_recognized(self):
"""Updates the boolean self.recognized to True if wave mass and
wave displacement exceed user-defined thresholds. Once a wave
is recognized, the wave is not checked again.
Args:
wave: a wave object
Returns:
NONE: changes self.recognized boolean to True if conditions
are met.
"""
if self.recognized is False:
if self.max_displacement >= DISPLACEMENT_THRESHOLD \
and self.max_mass >= MASS_THRESHOLD:
self.recognized = True
## ====================================================================
def _get_mass(points):
"""Simple function to calculate mass of an array of points with
equal weighting of the points.
Args:
points: an array of non-zero points
Returns:
mass: "mass" of the points
"""
mass = 0
if points is not None:
mass = len(points)
return mass
def _get_orthogonal_displacement(point, standard_form_line):
"""Helper function to calculate the orthogonal distance of a point
to a line.
Args:
point: 2-element array representing a point as [x,y]
standard_form_line: 3-element array representing a line in
standard form coordinates as [A,B,C]
Returns:
ortho_disp: distance of point to line in pixels
"""
ortho_disp = 0
# Retrieve standard form coefficients of original axis.
a = standard_form_line[0]
b = standard_form_line[1]
c = standard_form_line[2]
# Retrieve current location of the wave.
x0 = point[0]
y0 = point[1]
# Calculate orthogonal distance from current postion to
# original axis.
ortho_disp = np.abs(a*x0 + b*y0 + c) / math.sqrt(a**2 + b**2)
return int(ortho_disp)
def _get_standard_form_line(point, angle):
"""Helper function returning a 3-element array corresponding to
coefficients of the standard form for a line of Ax+By=C.
Requires one point in [x,y], and a counterclockwise angle from the
horizion in degrees.
Args:
point: a two-element array in [x,y] representing a point
angle: a float representing counterclockwise angle from horizon
of a line
Returns:
coefficients: a three-element array as [A,B,C]
"""
coefficients = [None, None, None]
coefficients[0] = np.tan(np.deg2rad(-angle))
coefficients[1] = -1
coefficients[2] = (point[1] - np.tan(np.deg2rad(-angle))*point[0])
return coefficients
def _get_centroid(points):
"""Helper function for getting the x,y coordinates of the center of
mass of an object that is represented by positive pixels in a
bilevel image.
Args:
points: array of points
Returns:
centroid: 2 element array as [x,y] if points is not empty
"""
centroid = None
if points is not None:
centroid = [int(sum([p[0][0] for p in points]) / len(points)),
int(sum([p[0][1] for p in points]) / len(points))]
return centroid
def _get_searchroi_coors(centroid, angle, searchroi_buffer, frame_width):
"""Helper function for returning the four coordinates of a
polygonal search region- a region in which we would want to merge
several independent wave objects into one wave object because they
are indeed one wave. Creates a buffer based on searchroi_buffer
and the polygon (wave) axis angle.
Args:
centroid: a two-element array representing center of mass of
a wave
angle: counterclosewise angle from horizon of a wave's axis
searchroi_buffer: a buffer, in pixels, in which to generate
a search region buffer
frame_width: the width of the frame, to establish left and
right bounds of a polygon
Returns:
polygon_coors: a four element array representing the top left,
top right, bottom right, and bottom left
coordinates of a search region polygon
"""
polygon_coors = [[None, None],
[None, None],
[None, None],
[None, None]]
delta_y_left = np.round(centroid[0] * np.tan(np.deg2rad(angle)))
delta_y_right = np.round((frame_width - centroid[0])
* np.tan(np.deg2rad(angle)))
upper_left_y = int(centroid[1] + delta_y_left - searchroi_buffer)
upper_left_x = 0
upper_right_y = int(centroid[1] - delta_y_right - searchroi_buffer)
upper_right_x = frame_width
lower_left_y = int(centroid[1] + delta_y_left + searchroi_buffer)
lower_left_x = 0
lower_right_y = int(centroid[1] - delta_y_right + searchroi_buffer)
lower_right_x = frame_width
polygon_coors = [[upper_left_x, upper_left_y],
[upper_right_x, upper_right_y],
[lower_right_x, lower_right_y],
[lower_left_x, lower_left_y]]
return polygon_coors
def _generate_name():
"""Name generator for identifying waves by simple incremental
numeric sequence.
Args:
None
Returns:
NAME_SEED: next integer in a sequence seeded by the "NAME_SEED"
global variable
"""
global NAME_SEED
NAME_SEED += 1
return NAME_SEED