Coverage for src/susi/base/config/cam.py: 97%
58 statements
« prev ^ index » next coverage.py v7.5.0, created at 2025-06-13 14:15 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2025-06-13 14:15 +0000
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3"""
4Module to specify cam specific calibration.
6@author: hoelken
7"""
8from __future__ import annotations
9from ..exceptions import MissConfigurationException
10from dataclasses import dataclass, field
13@dataclass
14class Cam:
15 C1 = "cam1"
16 C2 = "cam2"
17 C3 = "cam3"
19 #: Internal name of the camera
20 name: str = None
22 #: ID string of the camera
23 id: str = None
25 #: Names of the data shape entries. Supported: x, y, lambda and mod_state
26 shape_keys: list = None
28 #: Define the science ROI shape of the data within the full FOV
29 #: as a pair of slices in shape_keys order. Default (0, 2048) each.
30 data_shape: list = field(default_factory=lambda: [slice(0, 2048), slice(0, 2048)])
32 #: lid area of the camera
33 #: <pre>
34 #: ╔══════════════════════════════════════╗
35 #: ║ shielded area ║
36 #: ║ ╭──────────────────────────────╮ ║
37 #: ║ │ │ ║
38 #: ║ │ illuminated area │ ║
39 #: ║ │ │ ║
40 #: ║ │ │ ║
41 #: ║ ╰──────────────────────────────╯ ║
42 #: ╚══════════════════════════════════════╝
43 #: </pre>
44 lid_area: list = field(default_factory=lambda: [])
46 #: Borders that define thickness of usable area (no stray light) of the
47 #: shielded pixels in pixels distance from the frame border
48 #: in the order of [top, bottom, left, right]
49 #: <pre>
50 #: ╔════════════════════════════════════════╗
51 #: ║ shielded area ║
52 #: ║ ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┐ ║
53 #: ║ ┊╭──────────────────────────────╮┊ ║
54 #: ║ ┊│ │┊ ║
55 #: ║ ┊│ illuminated area │┊ ║
56 #: ║ ┊│ │┊ ║
57 #: ║ ┊│ │┊ ║
58 #: ║ ┊╰──────────────────────────────╯┊ ║
59 #: ║ └┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘ ║
60 #: ╚════════════════════════════════════════╝
61 #: </pre>
62 #: The area between `┊` and `║` will be used.
63 shielded_px: list = field(default_factory=lambda: [])
65 #: Mode for shielded px correction. Default is given in with_defaults below
66 #: Supported:
67 #: - 'N/A': Skip banding correction
68 #: - 'mean' for (frame) global mean correction
69 #: - 'linear_row' for a row-wise linear fit correction from left to right border
70 #: - 'median_col' for a column-wise median correction from top to bottom border
71 #: - 'linear_row_and_median_col' for both row and coll wise corrrction
72 shielded_px_mode: str = "linear_row_and_median_col"
74 #: Window size for the median filter used in cam_shielded_px_mode='mean' (must be an odd integer)
75 shielded_px_win: int = 9
77 #: Configuration of the modes used to filter hot pixels. Default is 'N/A' or not applied. See hot_pixels.py
78 hot_px_mode: str = "N/A"
80 # ====== Binning ======
81 #: Define the number of columns & rows to bin.
82 #: 1 (default) means no binning.
83 #: Negative values will bin everything to 1D in this direction.
84 #: (similar to the frames: keyword)
85 spacial_binning: list = field(default_factory=lambda: [1, 1])
87 #: Configures how many modulation cycles shall be averaged to a slice.
88 #: the number of frames thereby also defines the x-dimension
89 #: via (`= <num_files_in_folder>/<frames>`) of the resulting data cube.
90 #: The default `1` means no averaging, `2` would average over two frames of the same mod state, etc.
91 #: Set to `-1` to average over all frames in the input folder per mod state.
92 temporal_binning: int = 1
94 @staticmethod
95 def with_defaults(cam: str) -> Cam:
96 """
97 Load the class with the defaults suitable for the given camera
99 :param: cam: Cam identifier string ('cam1', 'cam2' or 'cam3')
100 """
101 if cam == Cam.C1:
102 lid_area = [slice(50, 1992), slice(70, 1992)]
103 shielded_px = [10, 20, 10, 10]
104 cam_id = "US550"
105 shielded_px_mode = "linear_row"
106 shape_keys = ["y", "lambda"]
107 elif cam == Cam.C2:
108 lid_area = [slice(50, 1992), slice(70, 1992)]
109 shielded_px = [10, 20, 10, 10]
110 cam_id = "US560"
111 shielded_px_mode = "linear_row"
112 shape_keys = ["y", "lambda"]
113 elif cam == Cam.C3:
114 lid_area = [slice(50, 1970), slice(100, 1840)]
115 shielded_px = [10, 20, 10, 10]
116 shielded_px_mode = "N/A"
117 cam_id = "US540"
118 shape_keys = ["y", "x"]
119 else:
120 raise MissConfigurationException(f'Cam id "cam" is unknown.')
122 return Cam(
123 name=cam,
124 id=cam_id,
125 lid_area=lid_area,
126 shielded_px=shielded_px,
127 shielded_px_mode=shielded_px_mode,
128 shape_keys=shape_keys,
129 )
131 def __repr__(self) -> str:
132 txt = f"== {self.__class__.__name__} ==\n"
133 txt += "\n".join(["{:<21} = {}".format(k, v) for k, v in self.__dict__.items()])
134 return txt
136 def amend_from_dict(self, data: dict):
137 """
138 Overwrites the parameters with the values from the given dictionary.
139 All instance variable names are supported as keywords.
140 All keywords are optional, if the keyword is not present the previous value will be kept.
142 There is a special key that must follow a specific syntax if given
144 - 'data_shape': The data shape must be given as a dictionary with keys 'x' and 'y'
145 for vertical and horizontal shape. The values of both keys must be lists
146 of integers of length 2, e.g. `{'x': [1, 2], 'y': [3, 4]}`
148 ### Params
149 - data: The dictionary to parse.
150 - c_name: The name of the camera to set defaults for.
152 ### Returns
153 the created Config
154 """
155 shape = data.pop("data_shape") if "data_shape" in data else None
156 self.shape_keys = shape["keys"] if shape and "keys" in shape else self.shape_keys
157 for k, v in data.items():
158 if not hasattr(self, k):
159 raise MissConfigurationException(f"{self.__class__.__name__} config has no attribute {k}")
160 setattr(self, k, v)
161 if shape:
162 self.data_shape = []
163 for key in self.shape_keys:
164 dim = slice(shape[key][0], shape[key][1]) if key in shape else slice(None, None)
165 self.data_shape.append(dim)