Commit 6a4ed2c2c06ecfe730948620a9abed2d75c3bd2d
1 parent
1cff4249
Exists in
dev
Start the AgentImagesCalibrator coding.
Showing
5 changed files
with
407 additions
and
15 deletions
Show diff stats
privatedev/config/tnc/observatory_tnc.yml
... | ... | @@ -322,6 +322,11 @@ OBSERVATORY: |
322 | 322 | computer: MainComputer |
323 | 323 | protocol: private/plugin/agent/AgentScheduler.py |
324 | 324 | |
325 | + # SF10-CAL (for AKz) | |
326 | + - AGENT: | |
327 | + name: AgentImagesCalibrator | |
328 | + computer: MainComputer | |
329 | + | |
325 | 330 | # SF11-IPC (for AKz) |
326 | 331 | - AGENT: |
327 | 332 | name: AgentImagesProcessor | ... | ... |
pyros.py
... | ... | @@ -61,6 +61,7 @@ AGENTS = { |
61 | 61 | "AgentSP": "scientific_programs", |
62 | 62 | "AgentScheduler": "scheduler", |
63 | 63 | "AgentImagesProcessor": "observation_manager", |
64 | + "AgentImagesCalibrator": "observation_manager", | |
64 | 65 | "agentSST": "agent", |
65 | 66 | "Agent": "agent", |
66 | 67 | "Agent2": "agent", | ... | ... |
src/core/pyros_django/obsconfig/obsconfig_class.py
... | ... | @@ -33,8 +33,8 @@ class OBSConfig: |
33 | 33 | "AgentM":None |
34 | 34 | |
35 | 35 | } |
36 | - | |
37 | - | |
36 | + | |
37 | + | |
38 | 38 | def verify_if_pickle_needs_to_be_updated(self, observatory_config_file) -> bool: |
39 | 39 | """ |
40 | 40 | |
... | ... | @@ -159,7 +159,7 @@ class OBSConfig: |
159 | 159 | |
160 | 160 | Args: |
161 | 161 | yaml_file (str): Path to the config_file to be validated |
162 | - schema_file (str): Path to the schema file | |
162 | + schema_file (str): Path to the schema file | |
163 | 163 | |
164 | 164 | Returns: |
165 | 165 | dict: dictionary of the config file (with values) |
... | ... | @@ -195,7 +195,7 @@ class OBSConfig: |
195 | 195 | |
196 | 196 | Args: |
197 | 197 | yaml_file (str): Path to the config_file to be validated |
198 | - schema_file (str): Path to the schema file | |
198 | + schema_file (str): Path to the schema file | |
199 | 199 | |
200 | 200 | Returns: |
201 | 201 | any: boolean (True) if the configuration is valid according the schema or a list of error otherwise |
... | ... | @@ -333,7 +333,7 @@ class OBSConfig: |
333 | 333 | |
334 | 334 | def get_devices_names_and_file(self) -> dict: |
335 | 335 | """ |
336 | - Return a dictionary giving the device file name by the device name | |
336 | + Return a dictionary giving the device file name by the device name | |
337 | 337 | Returns: |
338 | 338 | dict: key is device name, value is file name |
339 | 339 | """ |
... | ... | @@ -549,7 +549,7 @@ class OBSConfig: |
549 | 549 | self.unit_name = self.get_units_name()[0] |
550 | 550 | else: |
551 | 551 | self.unit_name = unit_name |
552 | - # call get_agents so the class will check if mandatory agents are in the obsconfig | |
552 | + # call get_agents so the class will check if mandatory agents are in the obsconfig | |
553 | 553 | self.get_agents(self.unit_name) |
554 | 554 | |
555 | 555 | def get_obs_name(self) -> str: |
... | ... | @@ -630,7 +630,7 @@ class OBSConfig: |
630 | 630 | |
631 | 631 | def get_agents(self, unit_name) -> dict: |
632 | 632 | """ |
633 | - return dictionary of agents | |
633 | + return dictionary of agents | |
634 | 634 | |
635 | 635 | Args: |
636 | 636 | unit_name (str): name of the unit |
... | ... | @@ -695,7 +695,7 @@ class OBSConfig: |
695 | 695 | Return information of the given channel name of a unit |
696 | 696 | |
697 | 697 | Args: |
698 | - unit_name (str): Name of the unit | |
698 | + unit_name (str): Name of the unit | |
699 | 699 | channel_name (str): name of the channel |
700 | 700 | |
701 | 701 | Returns: |
... | ... | @@ -727,6 +727,32 @@ class OBSConfig: |
727 | 727 | topology[key] = branch |
728 | 728 | return topology |
729 | 729 | |
730 | + def get_image_calibrations(self, unit_name: str, channel_name: str, category: str) -> dict: | |
731 | + """ | |
732 | + Return dictionary of image calibrations | |
733 | + | |
734 | + Args: | |
735 | + unit_name (str): name of the unit | |
736 | + channel_name (str): name of the channel | |
737 | + category (str): category name of image calibration (BI, DA, FL) | |
738 | + | |
739 | + Returns: | |
740 | + dict: dictionary of image calibrations | |
741 | + """ | |
742 | + unit = self.get_unit_by_name(unit_name) | |
743 | + info = {} | |
744 | + info["image_calibrations"] = {} | |
745 | + for kserie in range(len(unit["IMAGE_CALIBRATIONS"]["SERIES"])): | |
746 | + scategory = unit["IMAGE_CALIBRATIONS"]["SERIES"][kserie]["category"] | |
747 | + if category == scategory: | |
748 | + for kchannel in range(len(unit["IMAGE_CALIBRATIONS"]["SERIES"][kserie]["CHANNELS"])): | |
749 | + schannel = unit["IMAGE_CALIBRATIONS"]["SERIES"][kserie]["CHANNELS"][kchannel]["name"] | |
750 | + if channel_name == schannel: | |
751 | + dico = unit["IMAGE_CALIBRATIONS"]["SERIES"][kserie]["CHANNELS"][kchannel] | |
752 | + for key, val in dico: | |
753 | + info[key] = val | |
754 | + return info | |
755 | + | |
730 | 756 | def get_active_agents(self, unit_name: str) -> list: |
731 | 757 | """ |
732 | 758 | Return the list of active agents (i.e. agents that have an association with a device) |
... | ... | @@ -780,7 +806,7 @@ class OBSConfig: |
780 | 806 | |
781 | 807 | def get_units_name(self) -> list: |
782 | 808 | """ |
783 | - Return list of units names | |
809 | + Return list of units names | |
784 | 810 | |
785 | 811 | Returns: |
786 | 812 | [list]: names of units |
... | ... | @@ -890,7 +916,7 @@ class OBSConfig: |
890 | 916 | |
891 | 917 | def get_device_information(self, device_name: str) -> dict: |
892 | 918 | """ |
893 | - Give the dictionary of the attributes of the device | |
919 | + Give the dictionary of the attributes of the device | |
894 | 920 | |
895 | 921 | Args: |
896 | 922 | device_name (str): device name |
... | ... | @@ -1144,7 +1170,7 @@ class OBSConfig: |
1144 | 1170 | # default with docker should be /home/pyros_user/app |
1145 | 1171 | path_data_root = os.environ['PROJECT_ROOT_PATH'] |
1146 | 1172 | return path_data_root |
1147 | - | |
1173 | + | |
1148 | 1174 | def get_agent_path_data_tree(self, agent_name:str, makedirs: bool=False) -> dict: |
1149 | 1175 | """ |
1150 | 1176 | Return a dictionary containing all paths to store data. |
... | ... | @@ -1171,11 +1197,38 @@ class OBSConfig: |
1171 | 1197 | for path in data_paths.values(): |
1172 | 1198 | os.makedirs(path, exist_ok = True) |
1173 | 1199 | return data_paths |
1174 | - | |
1200 | + | |
1201 | + def get_image_calibrations(self, unit_name: str) -> dict: | |
1202 | + """ | |
1203 | + Return a dictionary containing all strategies of image calibration. | |
1204 | + | |
1205 | + Args: | |
1206 | + agent_name (str): _description_ | |
1207 | + | |
1208 | + Returns: | |
1209 | + All | |
1210 | + | |
1211 | + """ | |
1212 | + path_data_root = self.get_agent_path_data_root(agent_name) | |
1213 | + data_paths = {} | |
1214 | + data_paths['data_root'] = path_data_root | |
1215 | + data_paths['ima_incoming'] = os.path.join(path_data_root,"data/images/incoming") | |
1216 | + data_paths['ima_processed'] = os.path.join(path_data_root,"data/images/processed") | |
1217 | + data_paths['ima_tmp'] = os.path.join(path_data_root,"data/images/tmp") | |
1218 | + data_paths['ima_darks'] = os.path.join(path_data_root,"data/images/darks") | |
1219 | + data_paths['ima_flats'] = os.path.join(path_data_root,"data/images/flats") | |
1220 | + data_paths['ima_bias'] = os.path.join(path_data_root,"data/images/bias") | |
1221 | + data_paths['triton_input'] = os.path.join(path_data_root,"data/triton/input") | |
1222 | + data_paths['triton_output'] = os.path.join(path_data_root,"data/triton/out") | |
1223 | + if makedirs == True: | |
1224 | + for path in data_paths.values(): | |
1225 | + os.makedirs(path, exist_ok = True) | |
1226 | + return data_paths | |
1227 | + | |
1175 | 1228 | def get_agent_sst_of_computer(self,computer:str)->str: |
1176 | 1229 | """ |
1177 | 1230 | Return agent SST config name |
1178 | - The agentSST's name in obsconfig must contains AgentSST | |
1231 | + The agentSST's name in obsconfig must contains AgentSST | |
1179 | 1232 | |
1180 | 1233 | Args: |
1181 | 1234 | computer (str): _description_ |
... | ... | @@ -1196,7 +1249,7 @@ class OBSConfig: |
1196 | 1249 | if agent.startswith(base_agent_name): |
1197 | 1250 | return agent |
1198 | 1251 | except: |
1199 | - # do nothing, only goes to in except condition when launching pyros tests because we're simulating obsconfig | |
1252 | + # do nothing, only goes to in except condition when launching pyros tests because we're simulating obsconfig | |
1200 | 1253 | pass |
1201 | 1254 | |
1202 | 1255 | def get_dependencies(self): | ... | ... |
src/core/pyros_django/observation_manager/AgentImagesCalibrator.py
0 โ 100644
... | ... | @@ -0,0 +1,333 @@ |
1 | +#!/usr/bin/env python3 | |
2 | +# | |
3 | +# To launch this agent from the root of Pyros: | |
4 | +# cd /srv/develop/pyros | |
5 | +# .\PYROS -t start AgentImagesCalibrator -o tnc -fg | |
6 | +# | |
7 | +# Edit the file pyros/pyros.py, | |
8 | +# Add a new entry in the dict AGENT: | |
9 | +# "AgentImagesCalibrator": "observation_manager", | |
10 | +# --------------------------------------------------- | |
11 | + | |
12 | +import sys | |
13 | +import time | |
14 | + | |
15 | +import os | |
16 | +pwd = os.environ['PROJECT_ROOT_PATH'] | |
17 | +if pwd not in sys.path: | |
18 | + sys.path.append(pwd) | |
19 | + | |
20 | +short_paths = ['src', 'src/core/pyros_django'] | |
21 | +for short_path in short_paths: | |
22 | + path = os.path.join(pwd, short_path) | |
23 | + if path not in sys.path: | |
24 | + sys.path.insert(0, path) | |
25 | + | |
26 | +from src.core.pyros_django.agent.Agent import Agent, build_agent, log | |
27 | + | |
28 | +# = Specials | |
29 | +import glob | |
30 | +import shutil | |
31 | +import guitastro | |
32 | + | |
33 | +class AgentImagesCalibrator(Agent): | |
34 | + | |
35 | + # - All possible running states | |
36 | + RUNNING_NOTHING = 0 | |
37 | + RUNNING_ONE_IMAGE_PROCESSING = 1 | |
38 | + RUNNING_COMPUTE_RON_GAIN = 2 | |
39 | + | |
40 | + # TODO: Redefine valid timeout | |
41 | + _AGENT_SPECIFIC_COMMANDS = [ | |
42 | + ("do_create_test_images_1",60, 0), # self.EXEC_MODE.SEQUENTIAL | |
43 | + ("do_create_test_images_2",60, 0), # self.EXEC_MODE.THREAD | |
44 | + ("do_stop_current_processing",60, 0), # self.EXEC_MODE.PROCESS | |
45 | + ] | |
46 | + | |
47 | + # Scenario to be executed | |
48 | + # "self do_stop_current_processing" | |
49 | + # AgentCmd.CMD_STATUS_CODE.CMD_EXECUTED | |
50 | + _TEST_COMMANDS_LIST = [ | |
51 | + # Format : ("self cmd_name cmd_args", timeout, "expected_result", expected_status), | |
52 | + (True, "self do_create_test_images_1", 200, '', Agent.CMD_STATUS.CMD_EXECUTED), | |
53 | + (True, "self do_exit", 500, "STOPPING", Agent.CMD_STATUS.CMD_EXECUTED), | |
54 | + ] | |
55 | + | |
56 | + """ | |
57 | + ================================================================= | |
58 | + Methods running inside main thread | |
59 | + ================================================================= | |
60 | + """ | |
61 | + | |
62 | + def __init__(self, name:str=None): | |
63 | + if name is None: | |
64 | + name = self.__class__.__name__ | |
65 | + super().__init__() | |
66 | + | |
67 | + def _init(self): | |
68 | + super()._init() | |
69 | + log.debug("end super init()") | |
70 | + log.info(f"self.TEST_MODE = {self.TEST_MODE}") | |
71 | + | |
72 | + # === Get config infos | |
73 | + agent_alias = self.name | |
74 | + log.info(f"agent_alias = {agent_alias}") | |
75 | + # === Get the config object | |
76 | + self.config = self._oc['config'] | |
77 | + # === Get self._path_data_root | |
78 | + self._path_data_root = self.config.get_agent_path_data_root(agent_alias) | |
79 | + # === Get self._home of current unit | |
80 | + self._home = self.config.getHome() | |
81 | + # === Get self._paths the directories for all data (images). See obsconfig_class.py to know keys | |
82 | + self._paths = self.config.get_agent_path_data_tree(agent_alias, True) | |
83 | + # === Get bias strategies | |
84 | + unit_name = "TNC" | |
85 | + channel_name = "OpticalChannel_up1" | |
86 | + category = "BI" | |
87 | + self_strategy_bias = self.config.get_image_calibrations(unit_name, channel_name, category) | |
88 | + log.debug(f"self_strategy_bias={self_strategy_bias}") | |
89 | + | |
90 | + # === Instanciate an object Ima to make image processing | |
91 | + self._ima = guitastro.Ima() | |
92 | + home = guitastro.Home(self._home) | |
93 | + | |
94 | + # === Instanciate an object Filenames to manage file names | |
95 | + self._filename_manager = guitastro.Filenames() | |
96 | + self._filename_manager.naming("PyROS.1") | |
97 | + | |
98 | + # === Set longitude to ima object to generate the night yyyymmdd and subdirectories yyyy/mm/dd | |
99 | + longitude = home.longitude | |
100 | + log.info(f"Longitude={longitude}") | |
101 | + self._ima.longitude(longitude) | |
102 | + log.info("Init done with success") | |
103 | + | |
104 | + # === Status of routine processing | |
105 | + self._routine_running = self.RUNNING_NOTHING | |
106 | + log.debug("end init()") | |
107 | + | |
108 | + # Note : called by _routine_process() in Agent | |
109 | + # @override | |
110 | + def _routine_process_iter_start_body(self): | |
111 | + log.debug("in routine_process_before_body()") | |
112 | + | |
113 | + # Note : called by _routine_process() in Agent | |
114 | + # @override | |
115 | + def _routine_process_iter_end_body(self): | |
116 | + log.debug("in routine_process_after_body()") | |
117 | + if self._routine_running == self.RUNNING_NOTHING: | |
118 | + # Get files to process | |
119 | + fitsfiles = self.glob_images_to_process() | |
120 | + n = len(fitsfiles) | |
121 | + log.info(f"There are {n} image{self._plural(n)} to process") | |
122 | + if n > 0: | |
123 | + # - We select the oldest image | |
124 | + fitsfile = fitsfiles[0] | |
125 | + log.info(f"Process the file {fitsfile}") | |
126 | + # - Thread TODO | |
127 | + self._routine_running = self.RUNNING_ONE_IMAGE_PROCESSING | |
128 | + self.process_one_image(fitsfile) | |
129 | + | |
130 | + """ | |
131 | + ================================================================= | |
132 | + Methods of specific commands | |
133 | + ================================================================= | |
134 | + """ | |
135 | + | |
136 | + def do_stop_current_processing(self): | |
137 | + pass | |
138 | + | |
139 | + def do_create_test_images_1(self): | |
140 | + self._create_test_images_1() | |
141 | + | |
142 | + def do_create_test_images_2(self): | |
143 | + self._create_test_images_2() | |
144 | + | |
145 | + """ | |
146 | + ================================================================= | |
147 | + Methods called by commands or routine. Overload these methods | |
148 | + ================================================================= | |
149 | + """ | |
150 | + | |
151 | + def glob_images_to_process(self): | |
152 | + | |
153 | + # - glob the incoming directory: | |
154 | + fitsfiles = glob.glob(f"{self._paths['ima_incoming']}/BI_*.fit") | |
155 | + # - Please sort list of files in increasing dates (TODO) | |
156 | + return fitsfiles | |
157 | + | |
158 | + def bias_correction(self): | |
159 | + | |
160 | + # - Search the bias | |
161 | + path_bias = os.path.join( self._paths['ima_bias'], self._date_night ) | |
162 | + fitsbiasfiles = glob.glob(f"{path_bias}/*.fit") | |
163 | + log.info(f"fitsbiasfiles = {fitsbiasfiles}") | |
164 | + if len(fitsbiasfiles) > 0: | |
165 | + | |
166 | + # - Select the bias | |
167 | + pass | |
168 | + | |
169 | + def dark_correction(self): | |
170 | + | |
171 | + # - Search the dark | |
172 | + path_darks = os.path.join( self._paths['ima_darks'], self._date_night ) | |
173 | + fitsdarkfiles = glob.glob(f"{path_darks}/*.fit") | |
174 | + log.info(f"fitsdarkfiles = {fitsdarkfiles}") | |
175 | + if len(fitsdarkfiles) > 0: | |
176 | + | |
177 | + # - Select two darks and compute the therm using exposure | |
178 | + # - Correction of dark | |
179 | + pass | |
180 | + | |
181 | + def flat_correction(self): | |
182 | + | |
183 | + # - Search the flat | |
184 | + path_flats = os.path.join( self._paths['ima_flats'], self._date_night ) | |
185 | + fitsflatfiles = glob.glob(f"{path_flats}/*.fit") | |
186 | + log.info(f"fitsflatfiles = {fitsflatfiles}") | |
187 | + if len(fitsflatfiles) > 0: | |
188 | + | |
189 | + # - Select the flat (with the filter) | |
190 | + # - Correction of flat | |
191 | + pass | |
192 | + | |
193 | + def inversion_correction(self): | |
194 | + pass | |
195 | + | |
196 | + def cosmetic_correction(self): | |
197 | + pass | |
198 | + | |
199 | + def wcs_calibration(self): | |
200 | + return 0 | |
201 | + | |
202 | + def process_one_image(self, fitsfile: str): | |
203 | + """This is the general algorithm of processing | |
204 | + | |
205 | + The processing consists to make corrections of dark, flat, inversions, cosmetic | |
206 | + and perform WCS calibration. | |
207 | + | |
208 | + Args: | |
209 | + fitsfile: The file of the FITS file to process. | |
210 | + | |
211 | + """ | |
212 | + | |
213 | + # - Load file in memory | |
214 | + log.info("Load the file in memory") | |
215 | + #self.set_infos("Load the file in memory") | |
216 | + f = self._ima.genename(self._ima.load(fitsfile)) | |
217 | + # log.info(f"f={f}") | |
218 | + | |
219 | + # - Save as tmp | |
220 | + self._ima.path(self._paths['ima_tmp']) | |
221 | + log.info("Save the temporary file as tmp name") | |
222 | + self._ima.save("tmp") | |
223 | + | |
224 | + # - Load tmp and get infos | |
225 | + self._ima.load("tmp") | |
226 | + date_obs = self._ima.getkwd("DATE-OBS") | |
227 | + self._date_night = self._ima.get_night(date_obs) | |
228 | + log.info(f"Date_obs = {date_obs}") | |
229 | + log.info(f"Night = {self._date_night}") | |
230 | + exposure = self._ima.getkwd("EXPOSURE") | |
231 | + log.info(f"Exposure = {exposure}") | |
232 | + | |
233 | + # - Bias correction | |
234 | + self.bias_correction() | |
235 | + | |
236 | + # - Dark correction | |
237 | + self.dark_correction() | |
238 | + | |
239 | + # - Flat correction | |
240 | + self.flat_correction() | |
241 | + | |
242 | + # - Save tmp corrected by dark and flat | |
243 | + self._ima.path(self._paths['ima_tmp']) | |
244 | + self._ima.save("tmp") | |
245 | + | |
246 | + # - Inversion of mirrors or mirorxy | |
247 | + self.inversion_correction() | |
248 | + | |
249 | + # - Cosmetic correction | |
250 | + self.cosmetic_correction() | |
251 | + | |
252 | + # - WCS calibration | |
253 | + nmatched = self.wcs_calibration() | |
254 | + | |
255 | + # - Prepare the output file name | |
256 | + log.info("Decode the filename") | |
257 | + fgen_in = f['genename'] + f['sep'] + f['indexes'][0] + f['suffix'] | |
258 | + fext_in = f['file_extension'] | |
259 | + fext_out = ".fits" | |
260 | + | |
261 | + # - Save in processed | |
262 | + yyyy = self._date_night[0:4] | |
263 | + mm = self._date_night[4:6] | |
264 | + dd = self._date_night[6:8] | |
265 | + path_processed = os.path.join( self._paths['ima_processed'], yyyy, mm, dd ) | |
266 | + self._ima.path(path_processed) | |
267 | + fname_out = fgen_in + fext_out | |
268 | + fname = self._ima.save(fname_out) | |
269 | + log.info(f"Save the processed image {fname}") | |
270 | + | |
271 | + # - Delete the file in incoming directory | |
272 | + os.remove(fitsfile) | |
273 | + log.info(f"Delete the raw image {fitsfile}") | |
274 | + | |
275 | + # - Update the running state | |
276 | + self._routine_running = self.RUNNING_NOTHING | |
277 | + | |
278 | + time.sleep(5) | |
279 | + print("\n ...End of image calibration\n") | |
280 | + | |
281 | + """ | |
282 | + ================================================================= | |
283 | + Internal methods | |
284 | + ================================================================= | |
285 | + """ | |
286 | + | |
287 | + def _create_test_images_1(self): | |
288 | + try: | |
289 | + # === Define an image to test the processing and copy it in incoming directory | |
290 | + self._file_ima_test = os.path.join(self._path_data_root,"vendor/guitastro/tests/data/m57.fit") | |
291 | + file_in = self._file_ima_test | |
292 | + file_out = f"{self._paths['ima_incoming']}/m57.fit" | |
293 | + shutil.copyfile(file_in, file_out) | |
294 | + self._filename_manager.naming("") | |
295 | + except: | |
296 | + raise | |
297 | + | |
298 | + def _create_test_images_2(self): | |
299 | + try: | |
300 | + self._ima.etc.camera("Kepler 4040") | |
301 | + self._ima.etc.optics("Takahashi_180ED") | |
302 | + self._ima.etc.params("msky",18) | |
303 | + ra = 132.84583 | |
304 | + dec = 11.81333 | |
305 | + at = self._ima.simulation("GAIA", "PHOTOM", shutter_mode="closed", t=50) | |
306 | + file_out = os.path.join(self._paths['ima_tmp'], "m67.ecsv") | |
307 | + print(f"STEP TOTO 1 = {at}") | |
308 | + at.t.write(file_out, format='astrotable', overwrite=True) | |
309 | + print(f"STEP TOTO 2") | |
310 | + date_obs = self.getkwd("DATE-OBS") | |
311 | + except: | |
312 | + raise | |
313 | + | |
314 | + def _plural(self, n: int) -> str: | |
315 | + """Return "s" if n>1 for plurals. | |
316 | + | |
317 | + Args: | |
318 | + n: Number of entities | |
319 | + | |
320 | + Returns: | |
321 | + The string "s" or "" | |
322 | + """ | |
323 | + if n > 1: | |
324 | + s = "s" | |
325 | + else: | |
326 | + s = "" | |
327 | + return s | |
328 | + | |
329 | +if __name__ == "__main__": | |
330 | + | |
331 | + agent = build_agent(AgentImagesCalibrator) | |
332 | + print(agent) | |
333 | + agent.run() | ... | ... |
src/core/pyros_django/observation_manager/AgentImagesProcessor.py