pyopenrivercam 0.8.6__py3-none-any.whl → 0.8.7__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.
@@ -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(da_frames.mean(dim="time").values)
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
- cross_section = None
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
- elif cross is not None:
275
- logger.info("Cross section provided, and no water level set, water level will be estimated optically.")
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
+ )
276
290
  gdf = gpd.read_file(cross)
277
- cross_section = pyorc.CrossSection(camera_config=camera_config, cross_section=gdf)
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.warning(
283
- "No water level provided on CLI and no cross section provided. Using default water level in recipe."
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 = cross_section
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("pyorc velocimetry processor initialized")
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
- self.video_obj = Video(self.fn_video, camera_config=self.cam_config, **kwargs)
405
- # some checks ...
406
- self.logger.info(f"Video successfully read from {self.fn_video}")
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
- self.logger.debug("Estimating water level from video by crossing water line with cross section.")
410
- h_a = get_water_level(self.video_obj, cross_section=self.cross_section_wl, **kwargs)
411
- if h_a is None:
412
- self.logger.error("Water level could not be estimated from video. Please set a water level with --h_a.")
413
- raise click.Abort()
414
- # set the found water level on video
415
- self.logger.info("Water level estimated to be {:1.3f} meters in local datum.".format(h_a))
416
- self.video_obj.h_a = h_a
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
- self.da_frames = self.video_obj.get_frames()
421
- self.logger.debug(f"{len(self.da_frames)} frames retrieved from video")
422
- if "project" not in kwargs:
423
- kwargs["project"] = {}
424
- # iterate over steps in processing
425
- self.da_frames = apply_methods(self.da_frames, "frames", logger=self.logger, skip_args=["to_video"], **kwargs)
426
- if "to_video" in kwargs:
427
- kwargs_video = kwargs["to_video"]
428
- self.logger.info(f"Writing video of processed frames to {kwargs_video['fn']}")
429
- self.da_frames.frames.to_video(**kwargs_video)
430
- self.logger.info("Frames are preprocessed")
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
- if len(kwargs) > 1:
441
- raise OverflowError(f"Too many arguments under velocimetry, only one allowed, but {len(kwargs)} given.")
442
- kwargs[method] = kwargs.get(method, {}) if len(kwargs) == 0 else kwargs[method]
443
- # get velocimetry results
444
- self.velocimetry_obj = apply_methods(self.da_frames, "frames", logger=self.logger, **kwargs)
445
- m = list(kwargs.keys())[0]
446
- parameters = kwargs[m]
447
- self.logger.info(f"Velocimetry derived with method {m} with parameters {parameters}")
448
- if write:
449
- delayed_obj = self.velocimetry_obj.to_netcdf(self.fn_piv, compute=False)
450
- with ProgressBar():
451
- delayed_obj.compute()
452
- del delayed_obj
453
- self.logger.info(f"Velocimetry written to {self.fn_piv}")
454
- # Load the velocimetry into memory to prevent re-writes in next steps
455
- delattr(self, "velocimetry_obj")
456
- self.velocimetry_obj = xr.open_dataset(self.fn_piv)
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
- # TODO go through several masking groups
467
- self.velocimetry_mask_obj = copy.deepcopy(self.velocimetry_obj)
468
- for mask_name, mask_grp in kwargs.items():
469
- self.logger.debug(f'Applying "{mask_name}" with parameters {mask_grp}')
470
- masks = get_masks(self.velocimetry_mask_obj, **mask_grp)
471
- # apply found masks on velocimetry object
472
- self.velocimetry_mask_obj.velocimetry.mask(masks, inplace=True)
473
- self.logger.info("Velocimetry masks applied")
474
- # set the encoding to a good compression level
475
- self.velocimetry_mask_obj.velocimetry.set_encoding()
476
- # store results to file
477
- if write:
478
- delayed_obj = self.velocimetry_mask_obj.to_netcdf(self.fn_piv_mask, compute=False)
479
- with ProgressBar():
480
- delayed_obj.compute()
481
- del delayed_obj
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
- self.transects = {}
486
- # keep integrity of original kwargs
487
- _kwargs = copy.deepcopy(kwargs)
488
- for transect_name, transect_grp in _kwargs.items():
489
- self.logger.debug(f'Processing transect "{transect_name}"')
490
- # check if there are coordinates provided
491
-
492
- if not ("shapefile" in transect_grp or "geojson" in transect_grp):
493
- raise click.UsageError(
494
- f'Transect with name "{transect_name}" does not have a "shapefile" or '
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'"get_river_flow" found in {transect_name} but no "get_q" found, which is a requirement for'
530
- f' "get_river_flow"'
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
- if transect_grp["get_river_flow"] is None:
533
- transect_grp["get_river_flow"] = {}
534
- # add q
535
- self.transects[transect_name].transect.get_river_flow(**transect_grp["get_river_flow"])
536
- if write:
537
- # output file
538
- fn_transect = os.path.abspath(self.fn_transect_template(transect_name))
539
- self.logger.debug(f'Writing transect "{transect_name}" to {fn_transect}')
540
- delayed_obj = self.transects[transect_name].to_netcdf(fn_transect, compute=False)
541
- with ProgressBar():
542
- delayed_obj.compute()
543
- self.logger.info(f'Transect "{transect_name}" written to {fn_transect}')
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
- _plot_recipes = copy.deepcopy(plot_recipes)
553
- for name, plot_params in _plot_recipes.items():
554
- self.logger.debug(f'Processing plot "{name}"')
555
- fn_jpg = os.path.join(self.output, self.prefix + name + ".jpg")
556
- mode = plot_params["mode"]
557
- ax = None
558
- # look for inputs
559
- if "frames" in plot_params:
560
- if "frame_number" in plot_params:
561
- n = plot_params["frame_number"]
562
- else:
563
- n = 0
564
- opts = plot_params["frames"] if plot_params["frames"] is not None else {}
565
- f = self.video_obj.get_frames(method="rgb")
566
- if mode != "camera":
567
- f = f[n : n + 1].frames.project(method=self.proj_method)[0]
568
- else:
569
- f = f[n]
570
- p = f.frames.plot(ax=ax, mode=mode, **opts)
571
- # continue with axes of p
572
- ax = p.axes
573
- if "velocimetry" in plot_params:
574
- opts = plot_params["velocimetry"]
575
- opts = vmin_vmax_to_norm(opts)
576
- # select the time reducer. If not defined, choose mean
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
- # read file
590
- fn_transect = self.fn_transect_template(transect_name)
591
- ds_trans = xr.open_dataset(fn_transect)
592
- # default quantile is 2 (50%), otherwise choose from list
593
- quantile = 2 if ("quantile") not in opts else opts["quantile"]
594
- ds_trans_q = ds_trans.isel(quantile=quantile)
595
- # add to plot
596
- p = ds_trans_q.transect.plot(ax=ax, mode=mode, **opts)
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
- # done with transect, remove from memory
599
- ds_trans.close()
600
- del ds_trans
601
- # if mode == "camera":
602
- # ax.axis("equal")
603
- write_pars = plot_params["write_pars"] if "write_pars" in plot_params else {}
604
- self.logger.debug(f'Writing plot "{name}" to {fn_jpg}')
605
- ax.figure.savefig(fn_jpg, **write_pars)
606
- self.logger.info(f'Plot "{name}" written to {fn_jpg}')
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):