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

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 temporal_binning: int = 1 

93 

94 @staticmethod 

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

96 """ 

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

98 

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.') 

121 

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 ) 

130 

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 

135 

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. 

141 

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

143 

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]}` 

147 

148 ### Params 

149 - data: The dictionary to parse. 

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

151 

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)