CelestialSurveyor
747 строк · 30.8 Кб
1import datetime
2import json
3import numpy as np
4import os.path
5import threading
6import wx
7import wx.lib.scrolledpanel as scrolled
8
9from dataclasses import dataclass
10from decimal import Decimal
11from ObjectListView import ObjectListView, ColumnDefn
12from PIL import Image
13from threading import Event
14from typing import Optional, Callable, Any
15
16from backend.find_asteroids import predict_asteroids, save_results, annotate_results
17from backend.progress_bar import ProgressBarFactory
18from backend.source_data_v2 import SourceDataV2
19from logger.logger import get_logger
20
21
22logger = get_logger()
23
24
25@dataclass(frozen=True)
26class MyDataObject:
27"""
28Represents an object within file list.
29
30Attributes:
31file_path (str): The path to the file.
32timestamp (datetime.datetime): The timestamp of the object.
33exposure (Decimal): The exposure value of the object.
34checked (bool): The status of the object if it is checked or not in the list, default is False.
35"""
36file_path: str
37timestamp: datetime.datetime
38exposure: Decimal
39checked: bool = False
40
41
42class ImagePanel(scrolled.ScrolledPanel):
43"""
44A panel for displaying images with zoom and scrolling capability.
45Note: Scrolling doesn't work for now.
46"""
47def __init__(self, parent: wx.Window, image_array: Optional[np.ndarray] = None):
48super().__init__(parent, style=wx.BORDER_SIMPLE)
49self.image_array = image_array
50self.Bind(wx.EVT_PAINT, self.on_paint)
51self.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)
52self.scale_factor = None
53self.default_scale_factor = 1.0
54self.scroll_x = 0
55self.scroll_y = 0
56self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False)
57
58def on_paint(self, event: Event) -> None:
59"""
60Callback function for when the panel needs to paint its content.
61
62Parameters:
63event (wx.Event): A wxPython event object.
64
65Returns:
66None
67"""
68if self.image_array is not None:
69display_width, display_height = self.GetSize()
70image_height, image_width = self.image_array.shape[:2]
71dc = wx.PaintDC(self)
72if self.scale_factor is None:
73self.scale_factor = display_width / image_width
74self.default_scale_factor = self.scale_factor
75
76dc.SetUserScale(self.scale_factor, self.scale_factor)
77image = self.convert_array_to_bitmap(self.image_array)
78dc.DrawBitmap(image, 0, 0)
79
80def on_scroll(self, event: Event) -> None:
81"""
82Callback function for when the mouse wheel is scrolled.
83It's zooming.
84
85Parameters:
86event (wx.Event): A wxPython event object.
87
88Returns:
89None
90"""
91
92rotation = event.GetWheelRotation()
93
94# Calculate the new scale factor based on the mouse wheel rotation
95if rotation > 0:
96self.scale_factor *= 1.1
97else:
98self.scale_factor /= 1.1
99
100self.Refresh(eraseBackground=False)
101
102@staticmethod
103def convert_array_to_bitmap(array: np.ndarray, display_width: int = None, display_height: int = None) -> wx.Bitmap:
104"""
105Convert a NumPy array to a wx.Bitmap to be displayed on the image panel.
106
107Parameters:
108array (np.ndarray): The input NumPy array.
109display_width (int): The width for displaying the bitmap.
110display_height (int): The height for displaying the bitmap.
111
112Returns:
113wx.Bitmap: The converted wx.Bitmap object.
114"""
115# Create a PIL Image from the NumPy array
116array = array.reshape(array.shape[:2])
117image = Image.fromarray(array)
118
119# Convert PIL Image to wx.Image
120pil_image = image.convert("RGB")
121if display_width is not None and display_height is not None:
122pil_image = pil_image.resize((display_width, display_height))
123
124width, height = pil_image.size
125image = wx.Image(width, height)
126image.SetData(pil_image.tobytes())
127
128# Convert wx.Image to wx.Bitmap
129bitmap = image.ConvertToBitmap()
130
131return bitmap
132
133
134class ProgressFrame(wx.Dialog):
135"""Progress dialog to display progress of the current operation"""
136
137def __init__(self, parent: wx.Frame, title: str, stop_event: Event):
138super().__init__(parent=parent, title=title, size=(500, 200))
139self.parent = parent
140self.stop_event = stop_event
141panel = wx.Panel(self)
142self.label = wx.StaticText(panel, label="Working...")
143self.progress = wx.Gauge(panel, range=100, size=(450, 30))
144self.cancel_button = wx.Button(panel, label='Cancel')
145self.Bind(wx.EVT_BUTTON, self.on_cancel, self.cancel_button)
146sizer = wx.BoxSizer(wx.VERTICAL)
147sizer.Add(self.label, 0, wx.ALL | wx.LEFT, 5)
148sizer.Add(self.progress, 0, wx.ALL | wx.CENTER, 5)
149sizer.Add(self.cancel_button, 0, wx.ALL | wx.CENTER, 5)
150panel.SetSizer(sizer)
151
152self.failed = False
153
154def set_failed(self, value: bool) -> None:
155"""
156Sets the 'failed' attribute of the ProgressFrame instance.
157Used to indicate that the operation has failed and display an error message.
158
159Args:
160value (bool): The value to set the 'failed' attribute to.
161
162Returns:
163None
164"""
165self.failed = value
166
167def on_cancel(self, event: Event) -> None:
168"""
169Callback function for when the cancel button is clicked.
170It's stopping the current operation.
171
172Parameters:
173event (wx.Event): A wxPython event object.
174
175Returns:
176None
177"""
178if self.failed:
179self.Close(force=True)
180self.set_failed(False)
181else:
182self.label.SetLabel("Stopping...")
183self.stop_event.set()
184
185
186class MyFrame(wx.Frame):
187"""Main frame of the application"""
188
189def __init__(self, *args, **kw):
190super(MyFrame, self).__init__(*args, **kw)
191
192self.source_data: SourceDataV2 = None
193
194# Create the main panel
195self.panel = wx.Panel(self)
196panel_sizer = wx.BoxSizer(wx.VERTICAL)
197
198# Create a horizontal box sizer to organize the elements
199hbox = wx.BoxSizer(wx.HORIZONTAL)
200
201# Create the controls area (1/6 of window width)
202controls_sizer = wx.BoxSizer(wx.VERTICAL)
203
204controls_label = wx.StaticText(self.panel)
205flat_label = wx.StaticText(self.panel, label="Select folder with flat frames")
206self.flat_path_picker = wx.DirPickerCtrl(self.panel, style=wx.DIRP_USE_TEXTCTRL)
207dark_flat_label = wx.StaticText(self.panel, label="Select folder with dark flat frames")
208self.dark_flat_path_picker = wx.DirPickerCtrl(self.panel, style=wx.DIRP_USE_TEXTCTRL)
209dark_label = wx.StaticText(self.panel, label="Select folder with dark frames")
210self.dark_path_picker = wx.DirPickerCtrl(self.panel, style=wx.DIRP_USE_TEXTCTRL)
211self.btn_add_files = wx.Button(self.panel, label="Add files")
212self.Bind(wx.EVT_BUTTON, self.on_add_files, self.btn_add_files)
213self.chk_to_align = wx.CheckBox(self.panel, label="Align images")
214self.chk_to_align.SetValue(True)
215self.chk_non_linear = wx.CheckBox(self.panel, label="Non-linear")
216self.chk_non_linear.SetValue(False)
217self.to_debayer = wx.CheckBox(self.panel, label="Debayer")
218self.to_debayer.SetValue(True)
219self.btn_load_files = wx.Button(self.panel, label="Load files")
220self.progress_frame = None
221self.process_thread = None
222self.stop_event = None
223
224self.Bind(wx.EVT_BUTTON, self.on_load_files, self.btn_load_files)
225self.results_label = wx.StaticText(self.panel, label="Select folder to store results")
226self.results_path_picker = wx.DirPickerCtrl(self.panel, style=wx.DIRP_USE_TEXTCTRL)
227self.magnitude_label = wx.StaticText(self.panel, label="Annotation magnitude limit")
228self.magnitude_input = wx.TextCtrl(self.panel)
229self.magnitude_input.SetValue("18.0")
230self.magnitude_input.Bind(wx.EVT_TEXT, self.on_text_change)
231self.btn_process = wx.Button(self.panel, label="Process")
232self.Bind(wx.EVT_BUTTON, self.on_process, self.btn_process)
233self.btn_start_again = wx.Button(self.panel, label="Start again")
234self.Bind(wx.EVT_BUTTON, self.on_start_again, self.btn_start_again)
235
236controls_sizer.Add(controls_label, 0, wx.EXPAND | wx.ALL, 5)
237controls_sizer.Add(flat_label, 0, wx.EXPAND | wx.ALL, 5)
238controls_sizer.Add(self.flat_path_picker, 0, wx.EXPAND | wx.ALL, 5)
239controls_sizer.Add(dark_flat_label, 0, wx.EXPAND | wx.ALL, 5)
240controls_sizer.Add(self.dark_flat_path_picker, 0, wx.EXPAND | wx.ALL, 5)
241controls_sizer.Add(dark_label, 0, wx.EXPAND | wx.ALL, 5)
242controls_sizer.Add(self.dark_path_picker, 0, wx.EXPAND | wx.ALL, 5)
243controls_sizer.Add(self.btn_add_files, 0, wx.EXPAND | wx.ALL, 5)
244controls_sizer.Add(self.to_debayer, 0, wx.EXPAND | wx.ALL, 5)
245controls_sizer.Add(self.chk_to_align, 0, wx.EXPAND | wx.ALL, 5)
246controls_sizer.Add(self.chk_non_linear, 0, wx.EXPAND | wx.ALL, 5)
247controls_sizer.Add(self.btn_load_files, 0, wx.EXPAND | wx.ALL, 5)
248controls_sizer.Add(self.results_label, 0, wx.EXPAND | wx.ALL, 5)
249controls_sizer.Add(self.results_path_picker, 0, wx.EXPAND | wx.ALL, 5)
250controls_sizer.Add(self.magnitude_label, 0, wx.EXPAND | wx.ALL, 5)
251controls_sizer.Add(self.magnitude_input, 0, wx.EXPAND | wx.ALL, 5)
252controls_sizer.Add(self.btn_process, 0, wx.EXPAND | wx.ALL, 5)
253controls_sizer.Add(self.btn_start_again, 0, wx.EXPAND | wx.ALL, 5)
254
255hbox.Add(controls_sizer, 1, wx.EXPAND | wx.ALL, 5)
256
257# Create the checkbox list area (2/6 of window width) using ObjectListView
258checkbox_label = wx.StaticText(self.panel)
259self.checkbox_list = ObjectListView(self.panel, style=wx.LC_REPORT | wx.SUNKEN_BORDER)
260self.checkbox_list.Bind(wx.EVT_LIST_ITEM_SELECTED, self.on_item_selected)
261columns = [
262ColumnDefn("Item", "left", 350, "file_path"),
263ColumnDefn("Timestamp", "left", 130, "timestamp"),
264ColumnDefn("Exposure", "left", 60, "exposure"),
265]
266
267self.checkbox_list.SetColumns(columns)
268self.checkbox_list.CreateCheckStateColumn()
269hbox.Add(checkbox_label, 0, wx.EXPAND | wx.ALL, 5)
270hbox.Add(self.checkbox_list, 2, wx.EXPAND | wx.ALL, 5)
271
272# Create the picture representation area (3/6 of window width)
273self.draw_panel = ImagePanel(self.panel)
274hbox.Add(self.draw_panel, 3, wx.EXPAND | wx.ALL, 5)
275
276self.log_text = wx.TextCtrl(self.panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
277logger.text_ctrl = self.log_text
278panel_sizer.Add(hbox, 3, wx.EXPAND | wx.ALL, 5)
279panel_sizer.Add(self.log_text, 1, wx.EXPAND | wx.ALL, 5)
280
281# Set the sizer for the main panel
282self.panel.SetSizer(panel_sizer)
283
284# Create the menu bar
285menubar = wx.MenuBar()
286
287# Create a File menu
288file_menu = wx.Menu()
289file_menu.Append(wx.ID_EXIT, "Exit", "Exit the application")
290menubar.Append(file_menu, "&File")
291self.set_startup_states()
292
293# Set the menu bar for the frame
294self.SetMenuBar(menubar)
295# Set the frame properties
296self.SetSize((1900, 1000))
297self.SetTitle("CelestialSurveyor")
298self.Centre()
299self.Maximize(True)
300
301# Bind events
302self.Bind(wx.EVT_MENU, self.on_exit, id=wx.ID_EXIT)
303logger.log.info("App initialized")
304
305def on_text_change(self, event: Event) -> None:
306"""
307Event handler for text change in the magnitude limit input field.
308If the input value is greater than 25, an error message is shown.
309
310Args:
311event: The event object that triggered this function.
312
313Returns:
314None
315"""
316input_value = self.magnitude_input.GetValue()
317try:
318float_value = float(input_value)
319if float_value > 25:
320wx.MessageBox("Input value must be less than 25", "Error", wx.OK | wx.ICON_ERROR)
321except ValueError:
322wx.MessageBox("Input value must be a float", "Error", wx.OK | wx.ICON_ERROR)
323
324@staticmethod
325def __get_previous_settings() -> dict:
326"""
327Retrieves the previous settings from the 'settings.json' file.
328Used to populate calibration folder paths. It's too annoying to type them every time.
329
330Returns:
331dict: The previous settings loaded from the file, or an empty dictionary if the file does not exist.
332"""
333settings_file = "settings.json"
334if os.path.exists(settings_file):
335with open(settings_file, 'r') as fileo:
336return json.load(fileo)
337else:
338return {}
339
340@staticmethod
341def __write_previous_settings(settings: dict) -> None:
342"""Writes the previous settings to the 'settings.json' file.
343
344Args:
345settings (dict): The settings to be written to the file.
346
347Returns:
348None
349"""
350with open("settings.json", 'w') as fileo:
351json.dump(settings, fileo)
352
353def set_startup_states(self):
354"""
355This function sets the startup states of the UI elements taking in account the previous settings.
356"""
357prev_settings = self.__get_previous_settings()
358dark_path = prev_settings.get('dark_path', '')
359dark_path = "" if dark_path is None else dark_path
360self.dark_path_picker.SetPath(dark_path)
361flat_path = prev_settings.get('flat_path', '')
362flat_path = "" if flat_path is None else flat_path
363self.flat_path_picker.SetPath(flat_path)
364dark_flat_path = prev_settings.get('dark_flat_path', '')
365dark_flat_path = "" if dark_flat_path is None else dark_flat_path
366self.dark_flat_path_picker.SetPath(dark_flat_path)
367self.chk_to_align.Enable(False)
368self.to_debayer.Enable(False)
369self.chk_non_linear.Enable(False)
370self.btn_load_files.Enable(False)
371self.results_path_picker.Enable(False)
372self.results_label.Enable(False)
373self.magnitude_label.Enable(False)
374self.magnitude_input.Enable(False)
375self.btn_process.Enable(False)
376self.checkbox_list.SetObjects([])
377self.checkbox_list.Refresh(eraseBackground=False)
378self.results_path_picker.SetPath('')
379
380def on_exit(self, event: Event) -> None:
381"""
382Event handler for the exit button.
383
384Args:
385event: The event object that triggered this function.
386
387Returns:
388None
389"""
390logger.log.info("App exiting")
391self.Close()
392
393@staticmethod
394def _gen_short_file_names(file_list: list[str]) -> list[str]:
395"""
396Generates short file names from a list of full file paths.
397Cuts off the common part of the file paths to make representation shorter.
398
399Args:
400file_list (list[str]): List of full file paths.
401
402Returns:
403list[str]: List of short file names.
404"""
405folders = {os.path.split(fp)[0] for fp in file_list}
406split = os.path.split
407if len(folders) == 1:
408short_file_names = [split(fp)[1] for fp in file_list]
409else:
410short_file_names = [os.path.join(split(split(fp)[0])[1], split(fp)[1]) for fp in file_list]
411return short_file_names
412
413def on_header_loaded(self) -> None:
414"""
415Event handler for the header loaded event.
416Displays the header information in the file list.
417Enables elements responsible for the next step: image loading, alignment and processing.
418
419Returns:
420None
421"""
422self.source_data.stop_event.clear()
423self.progress_frame.Close()
424self.checkbox_list.SetObjects([])
425short_file_paths = self._gen_short_file_names([item.file_name for item in self.source_data.headers])
426self.checkbox_list.SetObjects([
427MyDataObject(
428fp, header.timestamp, header.exposure, checked=True
429) for fp, header in zip(short_file_paths, self.source_data.headers)
430])
431objects = self.checkbox_list.GetObjects()
432for obj in objects:
433self.checkbox_list.SetCheckState(obj, True)
434self.checkbox_list.RefreshObjects(objects)
435paths = [item.file_name for item in self.source_data.headers]
436logger.log.debug(f"Added the following files: {paths}")
437if paths:
438self.btn_load_files.Enable(True)
439self.chk_to_align.Enable(True)
440self.to_debayer.Enable(True)
441self.chk_non_linear.Enable(True)
442
443def handle_load_error(self, func: Callable, message: str, *args: Any, **kwargs: Any) -> Any:
444try:
445return func(*args, **kwargs)
446except Exception:
447self.progress_frame.label.SetLabel(message)
448self.progress_frame.label.Update()
449self.progress_frame.progress.SetValue(0)
450self.progress_frame.progress.SetRange(0)
451self.progress_frame.set_failed(True)
452raise
453
454def on_add_files(self, event: Event) -> None:
455"""
456Event handler for the add files button.
457Opens dialog to select FIT(s) or XISF files.
458
459Args:
460event: The event object that triggered this function.
461
462Returns:
463None
464"""
465wildcard = "Fits files (*.fits)|*.fits;*.FITS;*.fit;*.FIT|XISF files (*.xisf)|*.xisf;*.XISF|All files (*.*)|*.*"
466
467dialog = wx.FileDialog(self, message="Choose files to load",
468wildcard=wildcard,
469style=wx.FD_OPEN | wx.FD_MULTIPLE | wx.FD_FILE_MUST_EXIST)
470
471paths = []
472if dialog.ShowModal() == wx.ID_OK:
473paths = dialog.GetPaths()
474if self.source_data is None:
475self.source_data: SourceDataV2 = SourceDataV2()
476self.progress_frame = ProgressFrame(self, "Loading headers...", stop_event=self.source_data.stop_event)
477
478def load_headers():
479self.handle_load_error(
480self.source_data.extend_headers, "Failed to load headers", file_list=paths,
481progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
482wx.CallAfter(self.on_header_loaded)
483self.process_thread = threading.Thread(target=load_headers)
484self.progress_frame.Show()
485self.process_thread.start()
486dialog.Destroy()
487
488def on_load_files(self, event: Event) -> None:
489"""
490Event handler for loading files.
491Performs loading images, calibration, alignment, cropping and further processing.
492"""
493
494self.progress_frame = ProgressFrame(self, "Loading progress", stop_event=self.source_data.stop_event)
495self.progress_frame.progress.SetValue(0)
496selected_headers = []
497objects = self.checkbox_list.GetObjects()
498for header, obj in zip(self.source_data.headers, objects):
499checked = self.checkbox_list.GetCheckState(obj)
500if checked:
501selected_headers.append(header)
502else:
503logger.log.debug(f"Excluding {header.file_name}")
504self.source_data.set_headers(selected_headers)
505short_file_paths = self._gen_short_file_names([item.file_name for item in self.source_data.headers])
506self.checkbox_list.SetObjects([
507MyDataObject(
508fp, header.timestamp, header.exposure, checked=True
509) for fp, header in zip(short_file_paths, self.source_data.headers)
510])
511objects = self.checkbox_list.GetObjects()
512for obj in objects:
513self.checkbox_list.SetCheckState(obj, True)
514self.checkbox_list.RefreshObjects(objects)
515
516to_align = self.chk_to_align.GetValue()
517
518logger.log.info(f"Loading {len(self.source_data.headers)} images...")
519dark_path = self.dark_path_picker.GetPath()
520dark_path = dark_path if dark_path else None
521flat_path = self.flat_path_picker.GetPath()
522flat_path = flat_path if flat_path else None
523dark_flat_path = self.dark_flat_path_picker.GetPath()
524dark_flat_path = dark_flat_path if dark_flat_path else None
525self.__write_previous_settings({
526'dark_path': dark_path,
527'flat_path': flat_path,
528'dark_flat_path': dark_flat_path
529})
530self.source_data.to_debayer = self.to_debayer.GetValue()
531self.source_data.linear = not self.chk_non_linear.GetValue()
532self.stop_event = Event()
533
534def load_images_calibrate_and_align() -> None:
535"""
536This function loads, calibrates, and aligns images.
537It's run within the thread.
538"""
539self.progress_frame.label.SetLabel("Loading images...")
540self.handle_load_error(
541self.source_data.load_images, "Failed to load images",
542progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
543master_dark = None
544master_dark_flat = None
545master_flat = None
546if not self.source_data.stop_event.is_set():
547if dark_path:
548if os.path.exists(dark_path):
549self.progress_frame.label.SetLabel("Loading dark...")
550master_dark = self.handle_load_error(
551self.source_data.make_master_dark, "Failed to make master dark",
552self.source_data.make_file_paths(dark_path),
553progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
554else:
555logger.log.warning(f"Dark path {dark_path} does not exist. Skipping make master dark...")
556if not self.source_data.stop_event.is_set():
557if dark_flat_path:
558if os.path.exists(dark_flat_path):
559self.progress_frame.label.SetLabel("Loading dark flat...")
560master_dark_flat = self.handle_load_error(
561self.source_data.make_master_dark, "Failed to make master dark flat",
562self.source_data.make_file_paths(dark_flat_path),
563progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
564else:
565logger.log.warning(f"Dark flat path {dark_flat_path} does not exist. "
566f"Skipping make master dark flat...")
567
568if not self.source_data.stop_event.is_set():
569if flat_path:
570if os.path.exists(flat_path):
571self.progress_frame.label.SetLabel("Loading flats...")
572flats = self.handle_load_error(
573self.source_data.load_flats, "Failed to load flats",
574self.source_data.make_file_paths(flat_path),
575progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
576if master_dark_flat is not None:
577for flat in flats:
578flat -= master_dark_flat
579master_flat = np.average(flats, axis=0)
580if master_dark is not None:
581self.source_data.original_frames -= master_dark
582if master_flat is not None:
583self.source_data.original_frames /= master_flat
584if to_align and not self.source_data.stop_event.is_set():
585self.progress_frame.label.SetLabel("Plate solving...")
586self.handle_load_error(
587self.source_data.plate_solve_all, "Failed to plate solve images",
588progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
589if to_align and not self.source_data.stop_event.is_set():
590self.progress_frame.label.SetLabel("Aligning images...")
591self.handle_load_error(
592self.source_data.align_images_wcs, "Failed to align images",
593progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
594if not self.source_data.stop_event.is_set():
595self.progress_frame.label.SetLabel("Cropping images...")
596self.handle_load_error(self.source_data.crop_images, "Failed to crop images")
597if not self.source_data.stop_event.is_set() and not self.chk_non_linear.GetValue():
598self.progress_frame.label.SetLabel("Stretching images... ")
599self.handle_load_error(
600self.source_data.stretch_images, "Failed to stretch images",
601progress_bar=ProgressBarFactory.create_progress_bar(self.progress_frame.progress))
602if not self.source_data.stop_event.is_set():
603self.source_data.images_from_buffer()
604else:
605self.source_data.original_frames = None
606self.source_data.stop_event.clear()
607self.progress_frame.Close()
608wx.CallAfter(self.on_load_finished)
609self.process_thread = threading.Thread(target=load_images_calibrate_and_align)
610self.progress_frame.Show()
611self.process_thread.start()
612
613def on_load_finished(self) -> None:
614"""
615Event handler for the images loaded event.
616Displays the selected (first) image on image panel.
617Enables elements responsible for the next step: searching for moving objects and annotation.
618
619Returns:
620None
621"""
622self.source_data.stop_event.clear()
623if self.source_data.images is not None and len(self.source_data.images) > 0:
624short_file_paths = self._gen_short_file_names([item.file_name for item in self.source_data.headers])
625self.checkbox_list.SetObjects([
626MyDataObject(
627fp, header.timestamp, header.exposure, checked=True
628) for fp, header in zip(short_file_paths, self.source_data.headers)
629])
630objects = self.checkbox_list.GetObjects()
631for obj in objects:
632self.checkbox_list.SetCheckState(obj, True)
633self.checkbox_list.SelectObject(objects[0])
634self.checkbox_list.RefreshObjects(objects)
635img_to_draw = self.source_data.images[0]
636img_to_draw = (img_to_draw * 255).astype('uint8')
637self.draw_panel.image_array = img_to_draw
638self.draw_panel.Refresh(eraseBackground=False)
639self.results_label.Enable(True)
640self.magnitude_label.Enable(True)
641self.magnitude_input.Enable(True)
642self.results_path_picker.Enable(True)
643self.btn_process.Enable(True)
644
645def on_item_selected(self, event: Event) -> None:
646"""
647Event handler for when an item is selected.
648Renders the selected item's image on the draw panel.
649"""
650if self.source_data.images is not None and len(self.source_data.images) > 0:
651obj = self.checkbox_list.GetSelectedObject()
652file_paths = [item.file_name for item in self.source_data.headers]
653for num, item in enumerate(file_paths):
654if item.endswith(obj.file_path):
655image_idx = num
656break
657else:
658raise ValueError(f"{'obj.file_path'} is not in file list")
659img_to_draw = self.source_data.images[image_idx]
660img_to_draw = (img_to_draw * 255).astype('uint8')
661self.draw_panel.image_array = img_to_draw
662self.draw_panel.Refresh(eraseBackground=False)
663
664def on_process(self, event: Event) -> None:
665"""
666Event handler for the 'Process' button click.
667Initiates the process of finding moving objects by AI model.
668
669Args:
670event (Event): The event object that triggered this function.
671
672Returns:
673None
674"""
675self.progress_frame = ProgressFrame(self, "Finding moving objects", stop_event=self.source_data.stop_event)
676# self.progress_frame.label.SetLabel("Finding moving objects...")
677self.progress_frame.progress.SetValue(0)
678output_folder = self.results_path_picker.GetPath()
679objects = self.checkbox_list.GetObjects()
680use_img_mask = []
681for obj in objects:
682checked = self.checkbox_list.GetCheckState(obj)
683if checked:
684use_img_mask.append(True)
685else:
686use_img_mask.append(False)
687self.source_data.usage_map = np.array(use_img_mask, dtype=bool)
688
689def find_asteroids():
690"""
691Predict asteroids in the given source data using AI model.
692The function to be run in processing thread.
693"""
694
695self.progress_frame.label.SetLabel("Finding moving objects...")
696results = predict_asteroids(self.source_data, progress_bar=ProgressBarFactory.create_progress_bar(
697self.progress_frame.progress))
698if not self.source_data.stop_event.is_set():
699self.progress_frame.label.SetLabel("Saving results...")
700image_to_annotate = save_results(
701source_data=self.source_data, results=results, output_folder=output_folder)
702if not self.source_data.stop_event.is_set():
703self.progress_frame.label.SetLabel("Annotating results...")
704magnitude_limit = float(self.magnitude_input.GetValue())
705annotate_results(self.source_data, image_to_annotate, output_folder, magnitude_limit=magnitude_limit)
706self.source_data.stop_event.clear()
707self.progress_frame.Close()
708
709self.progress_frame.Show()
710self.process_thread = threading.Thread(target=find_asteroids)
711self.process_thread.start()
712
713def on_process_finished(self) -> None:
714"""
715Event handler for the process finished event.
716Closes the progress frame and resets the usage map in source data.
717"""
718self.progress_frame.Close()
719self.source_data.usage_map = None
720
721def on_start_again(self, event: Event) -> None:
722"""
723Event handler for the 'Start again' button click.
724Resets the source data and sets the startup states.
725
726Args:
727event (Event): The event object that triggered this function.
728
729Returns:
730None
731"""
732self.source_data = None
733self.set_startup_states()
734
735
736def start_ui():
737"""
738Initializes the user interface and runs the main application loop.
739"""
740app = wx.App(False)
741frame = MyFrame(None, wx.ID_ANY, "CelestialSurveyor", style=wx.DEFAULT_FRAME_STYLE)
742frame.Show()
743app.MainLoop()
744
745
746if __name__ == '__main__':
747start_ui()
748