pyopenrivercam 0.8.6__py3-none-any.whl → 0.8.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyopenrivercam-0.8.6.dist-info → pyopenrivercam-0.8.8.dist-info}/METADATA +4 -4
- {pyopenrivercam-0.8.6.dist-info → pyopenrivercam-0.8.8.dist-info}/RECORD +16 -16
- pyorc/__init__.py +1 -1
- pyorc/api/cameraconfig.py +140 -37
- pyorc/api/cross_section.py +139 -34
- pyorc/api/frames.py +19 -58
- pyorc/api/plot.py +94 -36
- pyorc/cli/cli_utils.py +7 -9
- pyorc/cli/main.py +12 -3
- pyorc/cv.py +111 -24
- pyorc/helpers.py +23 -0
- pyorc/plot_helpers.py +21 -6
- pyorc/service/velocimetry.py +238 -176
- {pyopenrivercam-0.8.6.dist-info → pyopenrivercam-0.8.8.dist-info}/WHEEL +0 -0
- {pyopenrivercam-0.8.6.dist-info → pyopenrivercam-0.8.8.dist-info}/entry_points.txt +0 -0
- {pyopenrivercam-0.8.6.dist-info → pyopenrivercam-0.8.8.dist-info}/licenses/LICENSE +0 -0
pyorc/service/velocimetry.py
CHANGED
|
@@ -9,7 +9,6 @@ import subprocess
|
|
|
9
9
|
from typing import Dict, Optional
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
|
-
import geopandas as gpd
|
|
13
12
|
import numpy as np
|
|
14
13
|
import xarray as xr
|
|
15
14
|
import yaml
|
|
@@ -19,6 +18,7 @@ from matplotlib.colors import Normalize
|
|
|
19
18
|
import pyorc
|
|
20
19
|
from pyorc import CameraConfig, CrossSection, Video, const
|
|
21
20
|
from pyorc.cli import cli_utils
|
|
21
|
+
from pyorc.helpers import read_shape_safe_crs
|
|
22
22
|
|
|
23
23
|
__all__ = ["velocity_flow", "velocity_flow_subprocess"]
|
|
24
24
|
|
|
@@ -45,8 +45,13 @@ def get_water_level(
|
|
|
45
45
|
da_frames = video.get_frames(method=method)[n_start:n_end]
|
|
46
46
|
# preprocess
|
|
47
47
|
da_frames = apply_methods(da_frames, "frames", logger=logger, skip_args=["to_video"], **frames_options)
|
|
48
|
+
# if preprocessing still results in a time dim, average in time
|
|
49
|
+
if "time" in da_frames.dims:
|
|
50
|
+
da_mean = da_frames.mean(dim="time")
|
|
51
|
+
else:
|
|
52
|
+
da_mean = da_frames
|
|
48
53
|
# extract the image
|
|
49
|
-
img = np.uint8(
|
|
54
|
+
img = np.uint8(da_mean.values)
|
|
50
55
|
h_a = cross_section.detect_water_level(img, **water_level_options)
|
|
51
56
|
return h_a
|
|
52
57
|
|
|
@@ -214,6 +219,7 @@ class VelocityFlowProcessor(object):
|
|
|
214
219
|
output: str,
|
|
215
220
|
h_a: Optional[float] = None,
|
|
216
221
|
cross: Optional[str] = None,
|
|
222
|
+
cross_wl: Optional[str] = None,
|
|
217
223
|
update: bool = False,
|
|
218
224
|
concurrency: bool = True,
|
|
219
225
|
fn_piv: str = "piv.nc",
|
|
@@ -238,6 +244,9 @@ class VelocityFlowProcessor(object):
|
|
|
238
244
|
h_a : float, optional
|
|
239
245
|
Current water level in meters
|
|
240
246
|
cross : str, optional
|
|
247
|
+
path to cross-section coordinates and crs file in GeoJSON or other readible by geopandas
|
|
248
|
+
to be used for discharge estimation.
|
|
249
|
+
cross_wl : str, optional
|
|
241
250
|
path to cross-section coordinates and crs file in GeoJSON or other readible by geopandas
|
|
242
251
|
to be used for water level detection.
|
|
243
252
|
update : bool, optional
|
|
@@ -254,7 +263,8 @@ class VelocityFlowProcessor(object):
|
|
|
254
263
|
reference to logger instance
|
|
255
264
|
|
|
256
265
|
"""
|
|
257
|
-
|
|
266
|
+
logger.debug("Initializing Velocity Flow Processor")
|
|
267
|
+
cross_section_wl = None
|
|
258
268
|
camera_config = CameraConfig(**cameraconfig)
|
|
259
269
|
if h_a is not None:
|
|
260
270
|
if abs(h_a - cameraconfig["gcps"]["h_ref"]) > const.WATER_LEVEL_MAX_DIFF:
|
|
@@ -271,17 +281,26 @@ class VelocityFlowProcessor(object):
|
|
|
271
281
|
)
|
|
272
282
|
if h_a is not None:
|
|
273
283
|
recipe["video"]["h_a"] = h_a
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
logger.info(f"Water level provided as argument: h = {h_a} m.") # cross section wl will NOT be used!
|
|
285
|
+
elif cross_wl is not None:
|
|
286
|
+
logger.info(
|
|
287
|
+
"Cross section for water level detection provided, and no water level set, "
|
|
288
|
+
" water level will be estimated optically."
|
|
289
|
+
)
|
|
290
|
+
gdf = read_shape_safe_crs(cross_wl)
|
|
291
|
+
cross_section_wl = pyorc.CrossSection(camera_config=camera_config, cross_section=gdf)
|
|
278
292
|
if "water_level" not in recipe:
|
|
279
293
|
# make sure water_level is represented
|
|
280
294
|
recipe["water_level"] = {}
|
|
295
|
+
elif recipe["video"].get("h_a") is not None:
|
|
296
|
+
logger.info(f"Water level provided in recipe: h = {recipe['video']['h_a']} m.")
|
|
281
297
|
else:
|
|
282
|
-
logger.
|
|
283
|
-
"No water level provided on CLI and no cross section provided.
|
|
298
|
+
logger.error(
|
|
299
|
+
"No water level provided on CLI and no cross section provided. If you only process one video, use the "
|
|
300
|
+
"same value as h_ref in your camera config, by changing your command as follows: "
|
|
301
|
+
f"pyorc velocimetry ... <repeat as before> --h_a {camera_config.gcps['h_ref']}"
|
|
284
302
|
)
|
|
303
|
+
raise click.Abort()
|
|
285
304
|
# check what projection method is used, use throughout
|
|
286
305
|
self.proj_method = "cv"
|
|
287
306
|
proj = recipe["frames"].get("project")
|
|
@@ -294,7 +313,7 @@ class VelocityFlowProcessor(object):
|
|
|
294
313
|
self.concurrency = concurrency
|
|
295
314
|
self.prefix = prefix
|
|
296
315
|
# set cross section for water levels
|
|
297
|
-
self.cross_section_wl =
|
|
316
|
+
self.cross_section_wl = cross_section_wl
|
|
298
317
|
# for now also provide a cross section for flow extraction, use the same.
|
|
299
318
|
self.cross_section_fn = cross # TODO use this property to extract velocity transects.
|
|
300
319
|
self.fn_piv = os.path.join(self.output, prefix + fn_piv)
|
|
@@ -310,7 +329,7 @@ class VelocityFlowProcessor(object):
|
|
|
310
329
|
self.cam_config = camera_config
|
|
311
330
|
self.logger = logger
|
|
312
331
|
# TODO: perform checks, minimum steps required
|
|
313
|
-
self.logger.info("
|
|
332
|
+
self.logger.info("Velocity Flow Processor initialized")
|
|
314
333
|
|
|
315
334
|
@property
|
|
316
335
|
def output(self):
|
|
@@ -357,6 +376,7 @@ class VelocityFlowProcessor(object):
|
|
|
357
376
|
|
|
358
377
|
The process also checks for compulsory steps.
|
|
359
378
|
"""
|
|
379
|
+
self.logger.info("Starting velocimetry processing pipeline")
|
|
360
380
|
if not self.concurrency:
|
|
361
381
|
import dask
|
|
362
382
|
|
|
@@ -390,6 +410,7 @@ class VelocityFlowProcessor(object):
|
|
|
390
410
|
delattr(self, "velocimetry_obj")
|
|
391
411
|
delattr(self, "velocimetry_mask_obj")
|
|
392
412
|
delattr(self, "da_frames")
|
|
413
|
+
self.logger.info("Velocimetry processing pipeline completed :-)")
|
|
393
414
|
return None
|
|
394
415
|
# TODO .get_transect and check if it contains data,
|
|
395
416
|
|
|
@@ -401,33 +422,49 @@ class VelocityFlowProcessor(object):
|
|
|
401
422
|
# -y is provided, do with user intervention if stale file is present or -y is not provided
|
|
402
423
|
|
|
403
424
|
def video(self, **kwargs):
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
425
|
+
try:
|
|
426
|
+
self.logger.debug(f"Reading video {self.fn_video} from file")
|
|
427
|
+
self.video_obj = Video(self.fn_video, camera_config=self.cam_config, **kwargs)
|
|
428
|
+
# some checks ...
|
|
429
|
+
self.logger.info(f"Video successfully read from {self.fn_video}")
|
|
430
|
+
except Exception as e:
|
|
431
|
+
self.logger.error(f"Could not read video from {self.fn_video}. Error: {e}")
|
|
432
|
+
raise Exception(f"Could not read video from {self.fn_video}. Error: {e}")
|
|
407
433
|
|
|
408
434
|
def water_level(self, **kwargs):
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
435
|
+
try:
|
|
436
|
+
self.logger.debug("Estimating water level from video by crossing water line with cross section.")
|
|
437
|
+
h_a = get_water_level(self.video_obj, cross_section=self.cross_section_wl, **kwargs)
|
|
438
|
+
if h_a is None:
|
|
439
|
+
self.logger.error("Water level could not be estimated from video. Please set a water level with --h_a.")
|
|
440
|
+
raise click.Abort()
|
|
441
|
+
# set the found water level on video
|
|
442
|
+
self.logger.info("Water level estimated optically h = {:1.3f} m. in local datum.".format(h_a))
|
|
443
|
+
self.video_obj.h_a = h_a
|
|
444
|
+
except Exception as e:
|
|
445
|
+
self.logger.error(f"Could not estimate water level from video. Error: {e}")
|
|
446
|
+
raise Exception(f"Could not estimate water level from video. Error: {e}")
|
|
417
447
|
|
|
418
448
|
def frames(self, **kwargs):
|
|
419
449
|
# start with extracting frames
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
450
|
+
try:
|
|
451
|
+
self.logger.debug("Retrieving frames from video.")
|
|
452
|
+
self.da_frames = self.video_obj.get_frames()
|
|
453
|
+
self.logger.debug(f"Retrieved {len(self.da_frames)} from video.")
|
|
454
|
+
if "project" not in kwargs:
|
|
455
|
+
kwargs["project"] = {}
|
|
456
|
+
# iterate over steps in processing
|
|
457
|
+
self.da_frames = apply_methods(
|
|
458
|
+
self.da_frames, "frames", logger=self.logger, skip_args=["to_video"], **kwargs
|
|
459
|
+
)
|
|
460
|
+
if "to_video" in kwargs:
|
|
461
|
+
kwargs_video = kwargs["to_video"]
|
|
462
|
+
self.logger.info(f"Writing video of processed frames to {kwargs_video['fn']}")
|
|
463
|
+
self.da_frames.frames.to_video(**kwargs_video)
|
|
464
|
+
self.logger.info("Frames retrieved and preprocessed.")
|
|
465
|
+
except Exception as e:
|
|
466
|
+
self.logger.error(f"Could not extract frames from video. Error: {e}")
|
|
467
|
+
raise Exception(f"Could not extract frames from video. Error: {e}")
|
|
431
468
|
|
|
432
469
|
@run_func_hash_io(
|
|
433
470
|
attrs=["velocimetry_obj"],
|
|
@@ -437,23 +474,29 @@ class VelocityFlowProcessor(object):
|
|
|
437
474
|
outputs=["fn_piv"],
|
|
438
475
|
)
|
|
439
476
|
def velocimetry(self, method="get_piv", write=False, **kwargs):
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
delayed_obj.compute
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
477
|
+
self.logger.debug(f"Performing velocimetry with {method}.")
|
|
478
|
+
try:
|
|
479
|
+
if len(kwargs) > 1:
|
|
480
|
+
raise OverflowError(f"Too many arguments under velocimetry, only one allowed, but {len(kwargs)} given.")
|
|
481
|
+
kwargs[method] = kwargs.get(method, {}) if len(kwargs) == 0 else kwargs[method]
|
|
482
|
+
# get velocimetry results
|
|
483
|
+
self.velocimetry_obj = apply_methods(self.da_frames, "frames", logger=self.logger, **kwargs)
|
|
484
|
+
m = list(kwargs.keys())[0]
|
|
485
|
+
parameters = kwargs[m]
|
|
486
|
+
self.logger.info(f"Velocimetry derived with method {m} with parameters {parameters}")
|
|
487
|
+
if write:
|
|
488
|
+
delayed_obj = self.velocimetry_obj.to_netcdf(self.fn_piv, compute=False)
|
|
489
|
+
with ProgressBar():
|
|
490
|
+
delayed_obj.compute()
|
|
491
|
+
del delayed_obj
|
|
492
|
+
self.logger.info(f"Velocimetry written to {self.fn_piv}")
|
|
493
|
+
# Load the velocimetry into memory to prevent re-writes in next steps
|
|
494
|
+
delattr(self, "velocimetry_obj")
|
|
495
|
+
self.velocimetry_obj = xr.open_dataset(self.fn_piv)
|
|
496
|
+
self.logger.info("Velocimetry successfully derived.")
|
|
497
|
+
except Exception as e:
|
|
498
|
+
self.logger.error(f"Could not derive velocimetry from frames. Error: {e}")
|
|
499
|
+
raise Exception(f"Could not derive velocimetry from frames. Error: {e}")
|
|
457
500
|
|
|
458
501
|
@run_func_hash_io(
|
|
459
502
|
attrs=["velocimetry_mask_obj"],
|
|
@@ -463,84 +506,97 @@ class VelocityFlowProcessor(object):
|
|
|
463
506
|
outputs=["fn_piv_mask"],
|
|
464
507
|
)
|
|
465
508
|
def mask(self, write=False, **kwargs):
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
509
|
+
try:
|
|
510
|
+
self.logger.debug("Applying masks to velocimetry.")
|
|
511
|
+
self.velocimetry_mask_obj = copy.deepcopy(self.velocimetry_obj)
|
|
512
|
+
for mask_name, mask_grp in kwargs.items():
|
|
513
|
+
self.logger.debug(f'Applying "{mask_name}" with parameters {mask_grp}')
|
|
514
|
+
masks = get_masks(self.velocimetry_mask_obj, **mask_grp)
|
|
515
|
+
# apply found masks on velocimetry object
|
|
516
|
+
self.velocimetry_mask_obj.velocimetry.mask(masks, inplace=True)
|
|
517
|
+
self.logger.info("Velocimetry masks applied")
|
|
518
|
+
# set the encoding to a good compression level
|
|
519
|
+
self.velocimetry_mask_obj.velocimetry.set_encoding()
|
|
520
|
+
# store results to file
|
|
521
|
+
if write:
|
|
522
|
+
delayed_obj = self.velocimetry_mask_obj.to_netcdf(self.fn_piv_mask, compute=False)
|
|
523
|
+
with ProgressBar():
|
|
524
|
+
delayed_obj.compute()
|
|
525
|
+
del delayed_obj
|
|
526
|
+
self.logger.info(f"Velocimetry masked written to {self.fn_piv_mask}.")
|
|
527
|
+
except Exception as e:
|
|
528
|
+
self.logger.error(f"Could not apply masks to velocimetry. Error: {e}")
|
|
529
|
+
raise Exception(f"Could not apply masks to velocimetry. Error: {e}")
|
|
482
530
|
|
|
483
531
|
@run_func_hash_io(check=False, configs=["transect"], inputs=["fn_piv_mask"])
|
|
484
532
|
def transect(self, write=False, **kwargs):
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
f'"geojson". Please add "shapefile" in the recipe file or provide a '
|
|
496
|
-
f'shapefile or geojson file with the "--cross" option.'
|
|
497
|
-
)
|
|
498
|
-
# read geojson or shapefile (as alternative
|
|
499
|
-
if "geojson" in transect_grp:
|
|
500
|
-
# read directly from geojson
|
|
501
|
-
coords, crs = cli_utils.read_shape(geojson=transect_grp["geojson"])
|
|
502
|
-
elif "shapefile" in transect_grp:
|
|
503
|
-
coords, crs = cli_utils.read_shape(fn=transect_grp["shapefile"])
|
|
504
|
-
self.logger.debug(f"Coordinates read for transect {transect_name}")
|
|
505
|
-
# check if coords have z coordinates
|
|
506
|
-
if len(coords[0]) == 2:
|
|
507
|
-
raise click.UsageError(
|
|
508
|
-
f'Transect in {os.path.abspath(transect_grp["shapefile"])} only contains x, y, but no '
|
|
509
|
-
f'z-coordinates.'
|
|
510
|
-
)
|
|
511
|
-
x, y, z = zip(*coords)
|
|
512
|
-
self.logger.debug(f"Sampling transect {transect_name}")
|
|
513
|
-
# sample the coordinates
|
|
514
|
-
if "get_transect" not in transect_grp:
|
|
515
|
-
transect_grp["get_transect"] = {}
|
|
516
|
-
if transect_grp["get_transect"] is None:
|
|
517
|
-
transect_grp["get_transect"] = {}
|
|
518
|
-
self.transects[transect_name] = self.velocimetry_mask_obj.velocimetry.get_transect(
|
|
519
|
-
x=x, y=y, z=z, crs=crs, **transect_grp["get_transect"]
|
|
520
|
-
)
|
|
521
|
-
if "get_q" in transect_grp:
|
|
522
|
-
if transect_grp["get_q"] is None:
|
|
523
|
-
transect_grp["get_q"] = {}
|
|
524
|
-
# add q
|
|
525
|
-
self.transects[transect_name] = self.transects[transect_name].transect.get_q(**transect_grp["get_q"])
|
|
526
|
-
if "get_river_flow" in transect_grp:
|
|
527
|
-
if "get_q" not in transect_grp:
|
|
533
|
+
try:
|
|
534
|
+
self.logger.debug("Deriving transects from velocimetry.")
|
|
535
|
+
self.transects = {}
|
|
536
|
+
# keep integrity of original kwargs
|
|
537
|
+
_kwargs = copy.deepcopy(kwargs)
|
|
538
|
+
for transect_name, transect_grp in _kwargs.items():
|
|
539
|
+
self.logger.debug(f'Processing transect "{transect_name}"')
|
|
540
|
+
# check if there are coordinates provided
|
|
541
|
+
|
|
542
|
+
if not ("shapefile" in transect_grp or "geojson" in transect_grp):
|
|
528
543
|
raise click.UsageError(
|
|
529
|
-
f'
|
|
530
|
-
f' "
|
|
544
|
+
f'Transect with name "{transect_name}" does not have a "shapefile" or '
|
|
545
|
+
f'"geojson". Please add "shapefile" in the recipe file or provide a '
|
|
546
|
+
f'shapefile or geojson file with the "--cross" option.'
|
|
531
547
|
)
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
548
|
+
# read geojson or shapefile (as alternative
|
|
549
|
+
if "geojson" in transect_grp:
|
|
550
|
+
# read directly from geojson
|
|
551
|
+
coords, crs = cli_utils.read_shape(geojson=transect_grp["geojson"])
|
|
552
|
+
elif "shapefile" in transect_grp:
|
|
553
|
+
coords, crs = cli_utils.read_shape(fn=transect_grp["shapefile"])
|
|
554
|
+
self.logger.debug(f"Coordinates read for transect {transect_name}")
|
|
555
|
+
# check if coords have z coordinates
|
|
556
|
+
if len(coords[0]) == 2:
|
|
557
|
+
raise click.UsageError(
|
|
558
|
+
f'Transect in {os.path.abspath(transect_grp["shapefile"])} only contains x, y, but no '
|
|
559
|
+
f'z-coordinates.'
|
|
560
|
+
)
|
|
561
|
+
x, y, z = zip(*coords)
|
|
562
|
+
self.logger.debug(f"Sampling transect {transect_name}")
|
|
563
|
+
# sample the coordinates
|
|
564
|
+
if "get_transect" not in transect_grp:
|
|
565
|
+
transect_grp["get_transect"] = {}
|
|
566
|
+
if transect_grp["get_transect"] is None:
|
|
567
|
+
transect_grp["get_transect"] = {}
|
|
568
|
+
self.transects[transect_name] = self.velocimetry_mask_obj.velocimetry.get_transect(
|
|
569
|
+
x=x, y=y, z=z, crs=crs, **transect_grp["get_transect"]
|
|
570
|
+
)
|
|
571
|
+
if "get_q" in transect_grp:
|
|
572
|
+
if transect_grp["get_q"] is None:
|
|
573
|
+
transect_grp["get_q"] = {}
|
|
574
|
+
# add q
|
|
575
|
+
self.transects[transect_name] = self.transects[transect_name].transect.get_q(
|
|
576
|
+
**transect_grp["get_q"]
|
|
577
|
+
)
|
|
578
|
+
if "get_river_flow" in transect_grp:
|
|
579
|
+
if "get_q" not in transect_grp:
|
|
580
|
+
raise click.UsageError(
|
|
581
|
+
f'"get_river_flow" found in {transect_name} but no '
|
|
582
|
+
f'"get_q" found, which is a requirement for "get_river_flow"'
|
|
583
|
+
)
|
|
584
|
+
if transect_grp["get_river_flow"] is None:
|
|
585
|
+
transect_grp["get_river_flow"] = {}
|
|
586
|
+
# add q
|
|
587
|
+
self.transects[transect_name].transect.get_river_flow(**transect_grp["get_river_flow"])
|
|
588
|
+
if write:
|
|
589
|
+
# output file
|
|
590
|
+
fn_transect = os.path.abspath(self.fn_transect_template(transect_name))
|
|
591
|
+
self.logger.debug(f'Writing transect "{transect_name}" to {fn_transect}')
|
|
592
|
+
delayed_obj = self.transects[transect_name].to_netcdf(fn_transect, compute=False)
|
|
593
|
+
with ProgressBar():
|
|
594
|
+
delayed_obj.compute()
|
|
595
|
+
self.logger.info(f'Transect "{transect_name}" written to {fn_transect}')
|
|
596
|
+
self.logger.info("Transects derived.")
|
|
597
|
+
except Exception as e:
|
|
598
|
+
self.logger.error(f"Could not derive transects from velocimetry. Error: {e}")
|
|
599
|
+
raise Exception(f"Could not derive transects from velocimetry. Error: {e}")
|
|
544
600
|
|
|
545
601
|
@run_func_hash_io(
|
|
546
602
|
check=False,
|
|
@@ -549,61 +605,67 @@ class VelocityFlowProcessor(object):
|
|
|
549
605
|
outputs=[],
|
|
550
606
|
)
|
|
551
607
|
def plot(self, **plot_recipes):
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
f =
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
reducer = plot_params["reducer"] if "reducer" in plot_params else "mean"
|
|
578
|
-
reducer_params = plot_params["reducer_params"] if "reducer_params" in plot_params else {}
|
|
579
|
-
# reduce the velocimetry over time for plotting purposes
|
|
580
|
-
velocimetry_reduced = getattr(self.velocimetry_mask_obj, reducer)(
|
|
581
|
-
dim="time", keep_attrs=True, **reducer_params
|
|
582
|
-
)
|
|
583
|
-
p = velocimetry_reduced.velocimetry.plot(ax=ax, mode=mode, **opts)
|
|
584
|
-
ax = p.axes
|
|
585
|
-
del velocimetry_reduced
|
|
586
|
-
if "transect" in plot_params:
|
|
587
|
-
for transect_name, opts in plot_params["transect"].items():
|
|
608
|
+
try:
|
|
609
|
+
self.logger.debug("Plotting velocimetry.")
|
|
610
|
+
_plot_recipes = copy.deepcopy(plot_recipes)
|
|
611
|
+
for name, plot_params in _plot_recipes.items():
|
|
612
|
+
self.logger.debug(f'Processing plot "{name}"')
|
|
613
|
+
fn_jpg = os.path.join(self.output, self.prefix + name + ".jpg")
|
|
614
|
+
mode = plot_params["mode"]
|
|
615
|
+
ax = None
|
|
616
|
+
# look for inputs
|
|
617
|
+
if "frames" in plot_params:
|
|
618
|
+
if "frame_number" in plot_params:
|
|
619
|
+
n = plot_params["frame_number"]
|
|
620
|
+
else:
|
|
621
|
+
n = 0
|
|
622
|
+
opts = plot_params["frames"] if plot_params["frames"] is not None else {}
|
|
623
|
+
f = self.video_obj.get_frames(method="rgb")
|
|
624
|
+
if mode != "camera":
|
|
625
|
+
f = f[n : n + 1].frames.project(method=self.proj_method)[0]
|
|
626
|
+
else:
|
|
627
|
+
f = f[n]
|
|
628
|
+
p = f.frames.plot(ax=ax, mode=mode, **opts)
|
|
629
|
+
# continue with axes of p
|
|
630
|
+
ax = p.axes
|
|
631
|
+
if "velocimetry" in plot_params:
|
|
632
|
+
opts = plot_params["velocimetry"]
|
|
588
633
|
opts = vmin_vmax_to_norm(opts)
|
|
589
|
-
#
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
#
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
p =
|
|
634
|
+
# select the time reducer. If not defined, choose mean
|
|
635
|
+
reducer = plot_params["reducer"] if "reducer" in plot_params else "mean"
|
|
636
|
+
reducer_params = plot_params["reducer_params"] if "reducer_params" in plot_params else {}
|
|
637
|
+
# reduce the velocimetry over time for plotting purposes
|
|
638
|
+
velocimetry_reduced = getattr(self.velocimetry_mask_obj, reducer)(
|
|
639
|
+
dim="time", keep_attrs=True, **reducer_params
|
|
640
|
+
)
|
|
641
|
+
p = velocimetry_reduced.velocimetry.plot(ax=ax, mode=mode, **opts)
|
|
597
642
|
ax = p.axes
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
643
|
+
del velocimetry_reduced
|
|
644
|
+
if "transect" in plot_params:
|
|
645
|
+
for transect_name, opts in plot_params["transect"].items():
|
|
646
|
+
opts = vmin_vmax_to_norm(opts)
|
|
647
|
+
# read file
|
|
648
|
+
fn_transect = self.fn_transect_template(transect_name)
|
|
649
|
+
ds_trans = xr.open_dataset(fn_transect)
|
|
650
|
+
# default quantile is 2 (50%), otherwise choose from list
|
|
651
|
+
quantile = 2 if ("quantile") not in opts else opts["quantile"]
|
|
652
|
+
ds_trans_q = ds_trans.isel(quantile=quantile)
|
|
653
|
+
# add to plot
|
|
654
|
+
p = ds_trans_q.transect.plot(ax=ax, mode=mode, **opts)
|
|
655
|
+
ax = p.axes
|
|
656
|
+
# done with transect, remove from memory
|
|
657
|
+
ds_trans.close()
|
|
658
|
+
del ds_trans
|
|
659
|
+
# if mode == "camera":
|
|
660
|
+
# ax.axis("equal")
|
|
661
|
+
write_pars = plot_params["write_pars"] if "write_pars" in plot_params else {}
|
|
662
|
+
self.logger.debug(f'Writing plot "{name}" to {fn_jpg}')
|
|
663
|
+
ax.figure.savefig(fn_jpg, **write_pars)
|
|
664
|
+
self.logger.info(f'Plot "{name}" written to {fn_jpg}')
|
|
665
|
+
self.logger.info("Plot procedure done.")
|
|
666
|
+
except Exception as e:
|
|
667
|
+
self.logger.error(f"Could not plot velocimetry. Error: {e}")
|
|
668
|
+
raise Exception(f"Could not plot velocimetry. Error: {e}")
|
|
607
669
|
|
|
608
670
|
|
|
609
671
|
def velocity_flow(**kwargs):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|