-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathImageAnalysis.m
More file actions
399 lines (327 loc) · 15.5 KB
/
ImageAnalysis.m
File metadata and controls
399 lines (327 loc) · 15.5 KB
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
%IMAGEANALYSIS Image processing for Hurunui hapua timelapse imagery
% This script runs the main image processing workflow to analyse fixed
% camera timelapse imagery from the Hurunui Hapua.
%
% Key outputs from the script are:
% - 'outputs\PhotoDatabase.mat' a database of photos including quality
% metrics.
% - 'outputs\ShortlistPhotos.mat' a database of quality image pairs
% including calculated lagoon waters edge location and width at
% transects.
%
% The workflow involves:
% - Cataloging the photos (genPhotoDataTable).
% - Assessing photo quality to screen out low quality images
% (photoQuality).
% - Sorting images into time-matched pairs (timeMatchPhotos).
% - Reading in timeseries data and assigning lagoon level to each
% image.
% - Identifying fixed objects in images to correct for camera
% movement, identifying waters edge, and projecting onto a flat
% plane at the elevation of the lagoon water surface
% (lagoonEdgePosition).
% - Filtering to remove inconsistent waters edge or camera orientation
% measurements (propDistLT).
% - Measuring lagoon width at specific measurement transects based on
% the identified waters edge location (measureLagoonWidth).
%
% Pre-requisits to run this script are:
% - Configuration parameters in HurunuiAnalysisConfig set up correctly
% - Photos labeled by capture time and saved within a single directory
% (sub-directories are allowed). The directory structure created by
% the OrganiseImages script is ideal but other structures would also
% work.
% - Timeseries data processed by the TimeseriesAnalysis script (the
% script stores output to 'outputs\LagoonTS.csv' which is then read
% in by ImageAnalysis).
%
% See Also: HURUNUIANALYSISCONFIG, ORGANISEIMAGES, GENPHOTODATATABLE,
% PHOTOQUALITY, TIMEMATCHPHOTOS, LAGOONEDGEPOSITION,
% PROPDISTLT, MEASURELAGOONWIDTH, TIMESERIESANALYSIS.
%% Setup
% Add required directories (and subdirectories)
addpath(genpath('functions'))
addpath(genpath('inputs'))
% Read input parameters
Config = HurunuiAnalysisConfig;
% get screensize for plot setups
ScrSz = get(groot, 'ScreenSize');
%% Read in timeseries data
% Read lagoon time series (already processed by TimeseriesAnalysis)
LagoonTS = readtable('outputs\LagoonTS.csv');
LagoonTS.DateTime = datetime(LagoonTS.DateTime);
%% Find available images and extract key information
AllPhotos = genPhotoDataTable(fullfile(Config.DataFolder,Config.PhotoFolder));
%% Make cut down list (30min rather than 15min)
[~ ,~ ,~ ,~ ,CaptureMins ,~ ] = datevec(AllPhotos.CaptureTime);
Photos = AllPhotos(CaptureMins==30 | CaptureMins==0,:);
clear AllPhotos CaptureMins
%% Generate quality metrics and assess quality
% See if there are any previously calculated
if exist('outputs\PhotoDatabase.mat','file')
% Re-use previously calculated metrics to avoid duplication of effort
PhotosPrevious = load('outputs\PhotoDatabase.mat');
PhotosPrevious = PhotosPrevious.Photos;
Photos = photoQuality(fullfile(Config.DataFolder,Config.PhotoFolder), ...
Photos,PhotosPrevious);
else
% generate metrics from scratch
Photos = photoQuality(fullfile(Config.DataFolder,Config.PhotoFolder), ...
Photos);
end
% assess quality
Photos.QualityOk = Photos.Sharpness > Config.SharpThresh & ...
Photos.Contrast > Config.ContrastThresh & ...
Photos.Brightness > Config.BrightThresh;
% tidy up
clear PhotosPrevious
% Save photo database as generating metrics takes a long time
save('outputs\PhotoDatabase.mat','Photos','-v7.3')
% load('outputs\PhotoDatabase.mat');
%% Summarise number of quality images per month as a check
BinEdges = dateshift(min(Photos.CaptureTime),'start','month'): ...
caldays(1): ...
dateshift(max(Photos.CaptureTime),'end','month');
figure
plot(BinEdges(1:end-1)', ...
[histcounts(Photos.CaptureTime(Photos.CameraNo==1),BinEdges)', ...
histcounts(Photos.CaptureTime(Photos.CameraNo==2),BinEdges)', ...
histcounts(Photos.CaptureTime(Photos.CameraNo==1&Photos.QualityOk),BinEdges)', ...
histcounts(Photos.CaptureTime(Photos.CameraNo==2&Photos.QualityOk),BinEdges)'])
legend({'Camera 1 images', 'Camera 2 images', ...
'Camera 1 quality images', 'Camera 2 quality images'})
ylabel('Number of images/day')
%% Sort images into mtched pairs
[TimeMatchedPhotos] = timeMatchPhotos(Photos);
%% Assign lagoon level info to image times
TimeMatchedPhotos.LagoonLevel = interp1(LagoonTS.DateTime, ...
LagoonTS.WL, ...
TimeMatchedPhotos.UniqueTime, ...
'linear', nan);
%% Create timelapse
animatePhotos('outputs/HurunuiTimeLapse', ...
fullfile(Config.DataFolder, Config.PhotoFolder), Photos, ...
TimeMatchedPhotos, LagoonTS, 24, true);
%% Make a cut down list of times with quality images from both cameras
% remove times with missing level data
ShortlistPhotos = TimeMatchedPhotos(~isnan(TimeMatchedPhotos.LagoonLevel),:);
% remove low quality photos
for ii=1:size(ShortlistPhotos,1)
if ~isnan(ShortlistPhotos.Cam1Photo(ii))
if ~Photos.QualityOk(ShortlistPhotos.Cam1Photo(ii))
ShortlistPhotos.Cam1Photo(ii) = nan;
end
end
if ~isnan(ShortlistPhotos.Cam2Photo(ii))
if ~Photos.QualityOk(ShortlistPhotos.Cam2Photo(ii))
ShortlistPhotos.Cam2Photo(ii) = nan;
end
end
end
% remove times with 1 missing or low quality image
ShortlistPhotos = ShortlistPhotos(~isnan(ShortlistPhotos.Cam1Photo) & ...
~isnan(ShortlistPhotos.Cam2Photo), :);
clear ii
%% Loop through images, measure twist and extract waters edge
% Create variables to hold outputs.
% (first check if they exist in ShortlistPhotos table)
if ~ismember('Twist', ShortlistPhotos.Properties.VariableNames)
ShortlistPhotos.Twist = nan(height(ShortlistPhotos),3);
end
if ~ismember('WetBdy', ShortlistPhotos.Properties.VariableNames)
ShortlistPhotos.WetBdy = cell(height(ShortlistPhotos),1);
end
% Import previous data if available
if exist('outputs\ShortlistPhotos.mat','file')
OldShortlistPhotos = load('outputs\ShortlistPhotos.mat');
OldShortlistPhotos = OldShortlistPhotos.ShortlistPhotos;
% find matching data
[~,IA,IB] = intersect(OldShortlistPhotos.UniqueTime, ...
ShortlistPhotos.UniqueTime,'stable');
ShortlistPhotos(IB,{'Twist','WetBdy'}) = OldShortlistPhotos(IA,{'Twist','WetBdy'});
clear OldShortlistPhotos
end
TimeNoToProcess = find(cellfun(@isempty,ShortlistPhotos.WetBdy));
NoToProcess = size(TimeNoToProcess,1);
% limit the number of timesteps processed at a time so that if I need to
% break the process I can get the results out... Pareval might be better?
IterationLimit = 70;
ii = 1;
while ii <= NoToProcess
ThisLoop = TimeNoToProcess(ii : min(ii+IterationLimit-1, NoToProcess))';
ii = min(ii+IterationLimit-1, NoToProcess) + 1;
Cam1Photos = ShortlistPhotos.Cam1Photo(ThisLoop);
Cam2Photos = ShortlistPhotos.Cam2Photo(ThisLoop);
WL = ShortlistPhotos.LagoonLevel(ThisLoop);
Photo1FileName = fullfile(Config.DataFolder, Config.PhotoFolder, ...
Photos.FileSubDir(Cam1Photos), ...
strcat(Photos.FileName(Cam1Photos), '.jpg'));
Photo2FileName = fullfile(Config.DataFolder, Config.PhotoFolder, ...
Photos.FileSubDir(Cam2Photos), ...
strcat(Photos.FileName(Cam2Photos), '.jpg'));
Twist1 = ShortlistPhotos.Twist(ThisLoop,:);
WetBdy1 = ShortlistPhotos.WetBdy(ThisLoop);
WetBdy2 = ShortlistPhotos.WetBdy(ThisLoop);
fprintf('Camera1: ')
[Twist1, WetBdy1] = lagoonEdgePosition(Photo1FileName, ...
WL, Config.Cam1, ...
Config.FgBgMask1, ...
Config.SeedPixel1, ...
Twist1, WetBdy1);
Twist2 = [Twist1(:,1),-Twist1(:,2),-Twist1(:,3)];
fprintf('Camera2: ')
[ ~ , WetBdy2] = lagoonEdgePosition(Photo2FileName, ...
WL, Config.Cam2, ...
Config.FgBgMask2, ...
Config.SeedPixel2, ...
Twist2, WetBdy2);
% Put variables into ShortlistPhotos table
ShortlistPhotos.Twist(ThisLoop,:) = Twist1;
ShortlistPhotos.WetBdy(ThisLoop) = ...
cellfun(@(a,b) [a;nan(1,2);b], WetBdy1, WetBdy2, ...
'UniformOutput', false);
% ShortlistPhotos.WetBdy(isempty(WetBdy1) && isempty(WetBdy2)) = [];
% Report progress
fprintf('Extracting waters edge. %i out of %i completed\n', ...
ii-1, NoToProcess)
end
% Clean up
clear Cam1Photos Cam2Photos WL Photo1FileName Photo2FileName Twist1 ...
Twist2 WetBdy1 WetBdy2 IterationLimit NoToProcess ThisLoop ii ...
TimeNoToProcess
% Save
save('outputs\ShortlistPhotos.mat','ShortlistPhotos','-v7.3')
% load('outputs\ShortlistPhotos.mat')
%% QA on twist results
% calculate proportion of surrounding points within threshold tolerance
WindowSize = 15;
PixelThreshold = 15;
PropThreshold = 0.35;
PropDistLT = propDistLT([ShortlistPhotos.Twist(:,1), ...
ShortlistPhotos.Twist(:,2)*2], ...
WindowSize, PixelThreshold, true);
ShortlistPhotos.TwistOK = PropDistLT > PropThreshold;
% view filtering results
figure('Position', [(ScrSz(3)/2)-600, ScrSz(4)/2-200, 1200, 400]);
orient landscape
plot(ShortlistPhotos.UniqueTime(ShortlistPhotos.TwistOK), ...
ShortlistPhotos.Twist(ShortlistPhotos.TwistOK,1), 'bx', ...
ShortlistPhotos.UniqueTime(~ShortlistPhotos.TwistOK), ...
ShortlistPhotos.Twist(~ShortlistPhotos.TwistOK,1), 'rx', ...
ShortlistPhotos.UniqueTime(ShortlistPhotos.TwistOK), ...
ShortlistPhotos.Twist(ShortlistPhotos.TwistOK,2), 'b+', ...
ShortlistPhotos.UniqueTime(~ShortlistPhotos.TwistOK), ...
ShortlistPhotos.Twist(~ShortlistPhotos.TwistOK,2), 'r+');
legend('valid TwistX','outlier TwistX','valid TwistY', 'outlier TwistY')
ylabel('Twist (pixels)')
export_fig 'outputs\TwistQA.pdf' -pdf
clear WindowSize PixelThreshold PropDistLT
%% simple QA on WetBdy results
WetBdySize = cellfun(@(x) size(x,1), ShortlistPhotos.WetBdy);
ShortlistPhotos.WetBdyOK = WetBdySize>1000;
clear WetBdySize
%% Extract cross-section barrier backshore position
% create column in ShortlistPhotos table to hold outputs if not already present
% note: allows up to 5 intersection points per transect
MaxIntersections = 5;
if ~any(strcmp('Offsets', ShortlistPhotos.Properties.VariableNames))
ShortlistPhotos.Offsets = nan(size(ShortlistPhotos,1), ...
size(Config.Transects,1), ...
MaxIntersections);
end
% only process timesteps which pass quality checks
TimesToProcess = ShortlistPhotos.TwistOK & ShortlistPhotos.WetBdyOK;
% Calculate the offsets for all times and transects
[ShortlistPhotos.Offsets(TimesToProcess,:,:)] = ...
measureLagoonWidth(ShortlistPhotos(TimesToProcess,:), ...
Config.Transects, false);
save('outputs\ShortlistPhotos.mat','ShortlistPhotos','-v7.3')
% tidy up
clear TimesToProcess
%% QA on Offsets
% calculate proportion of surrounding points within threshold tolerance
ShortlistPhotos.OffsetOK = nan(size(ShortlistPhotos.Offsets));
OffsetOK = false(size(ShortlistPhotos.Offsets));
WindowSize = 100;
OffsetThreshold = 6; % in metres
PropThreshold = 0.5; % proportion which must be within threshold distance
for TransectNo = 1:size(ShortlistPhotos.Offsets,2)
ProportionOK = propDistLT(permute(ShortlistPhotos.Offsets(:,TransectNo,:), [1,3,2]), ...
WindowSize, OffsetThreshold, false);
OffsetOK(:,TransectNo,:) = permute(ProportionOK > PropThreshold,[1,3,2]);
end
ShortlistPhotos.OffsetOK(OffsetOK) = ShortlistPhotos.Offsets(OffsetOK);
save('outputs\ShortlistPhotos.mat','ShortlistPhotos','-v7.3')
% ShortlistPhotos.WetBdy = []; save('ShortlistPhotos_small.mat','ShortlistPhotos');
clear WindowSize OffsetThreshold PropThreshold ProportionOK OffsetOK
%% plot the filtered Offset time series
for TransectNo = 1:13 %size(ShortlistPhotos.Offsets,2);
figure('Position', [(ScrSz(3)/2)-660+10*(TransectNo-1), ...
ScrSz(4)/2-100+10*(TransectNo-1), 1200, 400],...
'PaperOrientation', 'landscape');
AllDataH = plot(ShortlistPhotos.UniqueTime, ...
permute(ShortlistPhotos.Offsets(:,TransectNo,:),[1,3,2]), ...
'x', 'color', [0.85,0.85,0.85], 'MarkerSize', 4);
hold on
QADataH = plot(ShortlistPhotos.UniqueTime, ...
permute(ShortlistPhotos.OffsetOK(:,TransectNo,:),[1,3,2]), ...
'bx', 'MarkerSize', 6);
hold off
title(sprintf('Transect %i',TransectNo))
ylabel('Lagoon width (m)')
ylim([0,250])
set(gca, 'YTick', 0:50:250)
legend([AllDataH(1), QADataH(1)], ...
{'All width data','Width data passing QA/consistency check'})
% if TransectNo == 1
% export_fig 'outputs\Offsets.pdf' -pdf
% else
% export_fig 'outputs\Offsets.pdf' -pdf -append
% end
% close
end
clear TransectNo AllDataH QADataH
%% Make a list of daily high tide and low tide images
% Loop through days and identify high tide and low tide images
PhotoDates = dateshift(ShortlistPhotos.UniqueTime,'start','day');
UniquePhotoDates = unique(PhotoDates);
HT = nan(size(UniquePhotoDates));
LT = nan(size(UniquePhotoDates));
ST = nan(size(UniquePhotoDates));
for ii = 1:size(UniquePhotoDates)
TodaysPhotos = PhotoDates == UniquePhotoDates(ii);
% ID high tide image
[~,HT(ii)] = max(ShortlistPhotos.LagoonLevel +100 * TodaysPhotos);
% ID low tide image
[~,LT(ii)] = min(ShortlistPhotos.LagoonLevel -100 * TodaysPhotos);
% ID standard tide image
[~,ST(ii)] = min(abs(ShortlistPhotos.LagoonLevel - Config.StandardWL) + ...
999 * ~TodaysPhotos);
end
HighTidePhotos = ShortlistPhotos(HT,:);
LowTidePhotos = ShortlistPhotos(LT,:);
StdTidePhotos = ShortlistPhotos(ST,:);
save('outputs\HighTidePhotos.mat',...
'HighTidePhotos');
save('outputs\LowTidePhotos.mat',...
'LowTidePhotos');
save('outputs\StdTidePhotos.mat',...
'StdTidePhotos');
clear PhotoDates UniquePhotoDates HT LT ST TodaysPhotos
% load('outputs\LowTidePhotos.mat')
% load('outputs\HighTidePhotos.mat')
% load('outputs\StdTidePhotos.mat')
%% Plot offsets for std tide
figure('Position', [1, ScrSz(4)/2, ScrSz(3), 300]);
plot(ShortlistPhotos.UniqueTime, ShortlistPhotos.OffsetOK, 'x')
ylabel('Offset to barrier backshore (m)')
%% Animate Lowtide and high tide images
animatePhotos('outputs\DailyLowTide', ...
fullfile(Config.DataFolder,Config.PhotoFolder), Photos, ...
LowTidePhotos, Config, LagoonTS, 2, true);
animatePhotos('outputs\DailyHighTide', ...
fullfile(Config.DataFolder,Config.PhotoFolder), Photos, ...
HighTidePhotos, Config, LagoonTS, 2, true);
animatePhotos('outputs\DailyStdTide', ...
fullfile(Config.DataFolder,Config.PhotoFolder), Photos, ...
StdTidePhotos, Config, LagoonTS, 2, true);