Coverage for src/susi/base/config/cam.py: 97%

58 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2025-08-11 10:03 +0000

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3""" 

4Module to specify cam specific calibration. 

5 

6@author: hoelken 

7""" 

8from __future__ import annotations 

9from ..exceptions import MissConfigurationException 

10from dataclasses import dataclass, field 

11 

12 

13@dataclass 

14class Cam: 

15 C1 = "cam1" 

16 C2 = "cam2" 

17 C3 = "cam3" 

18 

19 #: Internal name of the camera 

20 name: str = None 

21 

22 #: ID string of the camera 

23 id: str = None 

24 

25 #: Names of the data shape entries. Supported: x, y, lambda and mod_state 

26 shape_keys: list = None 

27 

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)]) 

31 

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: []) 

45 

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: []) 

64 

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" 

73 

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 

76 

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" 

79 

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]) 

86 

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 #: NOTE: This also controls the chunk size returned by the 'Chunker' even if block B is not used. 

93 temporal_binning: int = 1 

94 

95 @staticmethod 

96 def with_defaults(cam: str) -> Cam: 

97 """ 

98 Load the class with the defaults suitable for the given camera 

99 

100 :param: cam: Cam identifier string ('cam1', 'cam2' or 'cam3') 

101 """ 

102 if cam == Cam.C1: 

103 lid_area = [slice(50, 1992), slice(70, 1992)] 

104 shielded_px = [10, 20, 10, 10] 

105 cam_id = "US550" 

106 shielded_px_mode = "linear_row" 

107 shape_keys = ["y", "lambda"] 

108 elif cam == Cam.C2: 

109 lid_area = [slice(50, 1992), slice(70, 1992)] 

110 shielded_px = [10, 20, 10, 10] 

111 cam_id = "US560" 

112 shielded_px_mode = "linear_row" 

113 shape_keys = ["y", "lambda"] 

114 elif cam == Cam.C3: 

115 lid_area = [slice(50, 1970), slice(100, 1840)] 

116 shielded_px = [10, 20, 10, 10] 

117 shielded_px_mode = "N/A" 

118 cam_id = "US540" 

119 shape_keys = ["y", "x"] 

120 else: 

121 raise MissConfigurationException(f'Cam id "cam" is unknown.') 

122 

123 return Cam( 

124 name=cam, 

125 id=cam_id, 

126 lid_area=lid_area, 

127 shielded_px=shielded_px, 

128 shielded_px_mode=shielded_px_mode, 

129 shape_keys=shape_keys, 

130 ) 

131 

132 def __repr__(self) -> str: 

133 txt = f"== {self.__class__.__name__} ==\n" 

134 txt += "\n".join(["{:<21} = {}".format(k, v) for k, v in self.__dict__.items()]) 

135 return txt 

136 

137 def amend_from_dict(self, data: dict): 

138 """ 

139 Overwrites the parameters with the values from the given dictionary. 

140 All instance variable names are supported as keywords. 

141 All keywords are optional, if the keyword is not present the previous value will be kept. 

142 

143 There is a special key that must follow a specific syntax if given 

144 

145 - 'data_shape': The data shape must be given as a dictionary with keys 'x' and 'y' 

146 for vertical and horizontal shape. The values of both keys must be lists 

147 of integers of length 2, e.g. `{'x': [1, 2], 'y': [3, 4]}` 

148 

149 ### Params 

150 - data: The dictionary to parse. 

151 - c_name: The name of the camera to set defaults for. 

152 

153 ### Returns 

154 the created Config 

155 """ 

156 shape = data.pop("data_shape") if "data_shape" in data else None 

157 self.shape_keys = shape["keys"] if shape and "keys" in shape else self.shape_keys 

158 for k, v in data.items(): 

159 if not hasattr(self, k): 

160 raise MissConfigurationException(f"{self.__class__.__name__} config has no attribute {k}") 

161 setattr(self, k, v) 

162 if shape: 

163 self.data_shape = [] 

164 for key in self.shape_keys: 

165 dim = slice(shape[key][0], shape[key][1]) if key in shape else slice(None, None) 

166 self.data_shape.append(dim)