diff --git a/ex_commands.csv b/ex_commands.csv new file mode 100644 index 0000000..b7ed21d --- /dev/null +++ b/ex_commands.csv @@ -0,0 +1 @@ +0,auto_offset,FALSE,offsetX,0,offsetY,0 diff --git a/pypogs/gui.py b/pypogs/gui.py index f1f233c..35e4612 100644 --- a/pypogs/gui.py +++ b/pypogs/gui.py @@ -155,7 +155,7 @@ def __del__(self): @property def debug_folder(self): - """pathlib.Path: Get or set the path for debug logging. Will create folder if not existing.""" + """pathlib.Path: Get or set the path for debug logging. Will create folder if not exsisting.""" return self._debug_folder @debug_folder.setter def debug_folder(self, path): @@ -502,9 +502,12 @@ def __init__(self, master, pypogs_system, logger): ttk.Radiobutton(self.bottom_frame1, text='Fine (FCL)', variable=self.camera_variable, value=FINE_FCL) \ .grid(row=0, column=5, padx=(5,0)) self.logger.debug('Creating entry and checkbox for image max value control') - ttk.Label(self.bottom_frame1, text='Max Value:').grid(row=0, column=6, padx=(30,0)) + ttk.Label(self.bottom_frame1, text='Max Value coarse:').grid(row=0, column=6, padx=(30,0)) self.max_entry = ttk.Entry(self.bottom_frame1, width=10) self.max_entry.grid(row=0, column=7, padx=(5,0)) + ttk.Label(self.bottom_frame1, text='Max Value fine:').grid(row=1, column=6, padx=(30,0)) + self.max_entry_fine = ttk.Entry(self.bottom_frame1, width=10) + self.max_entry_fine.grid(row=1, column=7, padx=(5,0)) self.auto_max_variable = tk.BooleanVar() self.auto_max_variable.set(True) ttk.Checkbutton(self.bottom_frame1, variable=self.auto_max_variable, text='Auto')\ @@ -571,7 +574,7 @@ def clear_tracker_callback(self): self.logger.debug('No camera selected in clear tracker callback') def clear_offset_callback(self): - """Clear the offset *of the preceding tracker*.""" + """Clear the offset *of the preceeding tracker*.""" self.logger.debug('Clicked on clear offset') cam = self.camera_variable.get() if cam == COARSE_CCL: @@ -767,7 +770,10 @@ def update(self): except: self.logger.debug('Failed', exc_info=True) - if img is not None: + if img is not None and img.size == 0: + print('_____________________Image size 0') + + if (img is not None) and (img.size != 0): zoom = self.zoom_variable.get() (height, width) = img.shape offs_x = round(width / 2 * (1 - 1/zoom)) @@ -780,20 +786,37 @@ def update(self): self.logger.warning('Failed to zoom image') self.logger.debug('Setting image to: ' + str(img)) - if img is not None: - if self.auto_max_variable.get(): #Auto set max scaling - maxval = int(np.max(img)) - self.max_entry.delete(0, 'end') - self.max_entry.insert(0, str(maxval)) - self.logger.debug('Using auto max scaling with maxval {}'.format(maxval)) + if (img is not None) and (img.size != 0): + if self.auto_max_variable.get(): # Auto set max scaling + try: + maxval = int(np.max(img)) + if cam == FINE_FCL: + self.max_entry_fine.delete(0, 'end') + self.max_entry_fine.insert(0, str(maxval)) + else: + self.max_entry.delete(0, 'end') + self.max_entry.insert(0, str(maxval)) + self.logger.debug('Using auto max scaling with maxval {}'.format(maxval)) + except: + self.logger.debug('Failed to convert max entry value {} to int') + maxval = 255 else: try: - maxval = int(self.max_entry.get()) - self.logger.debug('Using manual maxval {}'.format(maxval)) + # Max white mapping: Get max pixel value for coarse and for fine cameras seperatly + # Max white mapping: for coarse and star cam + if cam == STAR_OL or cam == COARSE_CCL: + maxval = int(self.max_entry.get()) + self.logger.debug('Using manual maxval for coars/star {}'.format(maxval)) + # Max white mapping: for fine cam + else: + maxval = int(self.max_entry_fine.get()) + self.logger.debug('Using manual maxval for finecam {}'.format(maxval)) except: - self.logger.debug('Failed to convert max entry value {} to int'\ - .format(self.max_entry.get())) + self.logger.debug('Failed to convert max entry value {} to int' \ + .format(self.max_entry.get())) maxval = 255 + + # Scale and convert to 8-bit img_scaled = (img / max(1, int(.8 * maxval / 255))).clip(0, 255).astype('uint8') pil_img = Image.fromarray(img_scaled) @@ -1398,6 +1421,7 @@ def manual_button_callback(self): def file_button_callback(self): try: raise NotImplementedError('Feature coming soon!') + except Exception as err: ErrorPopup(self, err, self.logger) @@ -1420,11 +1444,11 @@ def __init__(self, master): radec_frame = ttk.Frame(self) radec_frame.grid(row=1, column=0, padx=(10,0), pady=10) - ttk.Label(radec_frame, text='Set target from RA/Dec:').grid(row=0, column=0, columnspan=2) - ttk.Label(radec_frame, text='RA: (deg)').grid(row=1, column=0, sticky=tk.E) + ttk.Label(radec_frame, text='Set target from RA/Dec hhmmss :').grid(row=0, column=0, columnspan=2) + ttk.Label(radec_frame, text='RA:(023344)').grid(row=1, column=0, sticky=tk.E) self.ra_entry = ttk.Entry(radec_frame, width=25, font='TkFixedFont') self.ra_entry.grid(row=1, column=1) - ttk.Label(radec_frame, text='Dec: (deg)').grid(row=2, column=0, sticky=tk.E) + ttk.Label(radec_frame, text='Dec:(891533)').grid(row=2, column=0, sticky=tk.E) self.dec_entry = ttk.Entry(radec_frame, width=25, font='TkFixedFont') self.dec_entry.grid(row=2, column=1) ttk.Button(radec_frame, text='Set', command=self.radec_callback) \ @@ -1460,8 +1484,19 @@ def update(self): if isinstance(target, SkyCoord): ra = target.ra.to_value('deg') dec = target.dec.to_value('deg') - self.ra_entry.insert(0, str(ra)) - self.dec_entry.insert(0, str(dec)) + # Ra Dec convertion from degree back to deg:arcmin:arcsec + ra = ra/15 + ra_min_ss = (ra-np.trunc(ra))*60 + ra_h = str(int(np.trunc(ra))) + if (ra<10): + ra_h = ('0' + str(int(np.trunc(ra)))) + + ra_string = (ra_h + str(int(np.trunc(ra_min_ss))) + str(int((ra_min_ss- np.trunc(ra_min_ss))*60))) + dec_min_ss = (dec-np.trunc(dec))*60 + dec_string = (str(int(np.trunc(dec))) + str(int(np.trunc(dec_min_ss))) + str(int((dec_min_ss- np.trunc(dec_min_ss))*60))) + + self.ra_entry.insert(0, str(ra_string)) + self.dec_entry.insert(0, str(dec_string)) elif isinstance(target, EarthSatellite): (l1, l2) = self.master.sys.target.get_tle_raw() self.tle_line1_entry.insert(0, l1) diff --git a/pypogs/hardware.py b/pypogs/hardware.py index 0f47cc3..9df6fe1 100644 --- a/pypogs/hardware.py +++ b/pypogs/hardware.py @@ -1,7 +1,7 @@ """Hardware interfaces ====================== -Current hardware support: +Current harware support: - :class:`pypogs.Camera`: 'ptgrey' for FLIR (formerly Point Grey) machine vision cameras. Requires Spinnaker API and PySpin, see the installation instructions. Tested with Blackfly S USB3 model BFS-U3-31S4M. @@ -33,13 +33,15 @@ from pathlib import Path import logging from time import sleep, time as timestamp -from datetime import datetime +from datetime import datetime, time from threading import Thread, Event from struct import pack as pack_data +import threading # External imports: import numpy as np import serial +from harvesters.core import Harvester, TimeoutException # type: ignore class Camera: """Control acquisition and receive images from a camera. @@ -49,7 +51,7 @@ class Camera: auto_init=False is passed). Manually initialise with a call to Camera.initialize(); release hardware with a call to Camera.deinitialize(). - After the Camera is initialised, acquisition properties (e.g. exposure_time and frame_rate) may be set and images + After the Camera is intialised, acquisition properties (e.g. exposure_time and frame_rate) may be set and images received. The Camera also supports event-driven acquisition, see Camera.add_event_callback(), where new images are automatically passed on to the desired functions. @@ -83,7 +85,7 @@ class Camera: # Release the hardware cam.deinitialize() """ - _supported_models = ('ptgrey',) + _supported_models = ('ptgrey','phfocus') def __init__(self, model=None, identity=None, name=None, auto_init=True, debug_folder=None): """Create Camera instance. See class documentation.""" @@ -125,6 +127,11 @@ def __init__(self, model=None, identity=None, name=None, auto_init=True, debug_f self._ptgrey_camera = None self._ptgrey_camlist = None self._ptgrey_system = None + #Only used for phfocus + self._phfocus_dev = None + self._phfocus_ia = None + self._phfocus_on_frame_ready = None + self._phfocus_is_running = False #Callbacks on image event self._call_on_image = set() self._got_image_event = Event() @@ -140,8 +147,11 @@ def __init__(self, model=None, identity=None, name=None, auto_init=True, debug_f if name is not None: self.name = name if auto_init and not None in (model, identity): - self._logger.debug('Trying to auto-initialise') - self.initialize() + try: + self._logger.debug('Trying to auto-initialise') + self.initialize() + except: + self._logger.info('Camera initialization failed') self._logger.debug('Registering destructor') # TODO: Should we register deinitialisor instead? (probably yes...) import atexit, weakref @@ -196,6 +206,30 @@ def _ptgrey_release(self): self._ptgrey_system = None self._log_debug('Hardware released') + def _phfocus_release(self): + """PRIVATE: Release PhotonFocus hardware resources.""" + self._log_debug('Photon Focus hardware release called') + if self._phfocus_ia: + self._phfocus_ia.stop_acquisition() + try: + self._phfocus_ia.destroy() + except: + pass + del (self._phfocus_ia) + self._phfocus_ia = None + if self._phfocus_dev: + try: + self._phfocus_dev.reset() + except: + pass + del (self._phfocus_dev) + self._phfocus_dev = None + self._log_debug('PhotonFocus Hardware released') + + def set_on_frame_ready(self, on_frame_ready): + """Add a method to be called on new frame arrival""" + self._phfocus_on_frame_ready = on_frame_ready + @property def debug_folder(self): """pathlib.Path: Get or set the path for debug logging. Will create folder if not existing.""" @@ -286,6 +320,8 @@ def identity(self, identity): self._ptgrey_camera = None self._identity = identity self._ptgrey_camlist.Clear() + elif self.model.lower() == 'phfocus': + self._identity = identity else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -298,6 +334,10 @@ def is_init(self): if self.model.lower() == 'ptgrey': init = self._ptgrey_camera is not None and self._ptgrey_camera.IsInitialized() return init + if self.model.lower() == 'phfocus': + #TODO: DM finish this + init = self._phfocus_dev is not None + return init else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -374,6 +414,86 @@ def OnImageEvent(self, img_ptr): self._ptgrey_camera.RegisterEventHandler( self._ptgrey_event_handler ) self._log_debug('Registered ptgrey image event handler') self._log_info('Camera successfully initialised') + elif self.model.lower() == 'phfocus': + self._log_debug('Using GenICam, try to initialise') + h = Harvester() + locs = [ + r"~/tools/mvImpact/lib/x86_64/mvGenTLProducer.cti", + r"C:\Program Files\MATRIX VISION\mvIMPACT Acquire\bin\x64\mvGenTLProducer.cti" + ] + cti = "" + for loc in locs: + if Path(loc).expanduser().exists(): + cti = loc + if not cti: + raise FileNotFoundError("Could not locate cti file: mvGenTLProducer.cti") + + cti = str(Path(cti).expanduser()) + h.add_file(cti) + h.update() + # len(h.device_info_list) #For debug purpose + # h.device_info_list[0] + ia = h.create_image_acquirer(0) + self._phfocus_dev = h + self._phfocus_ia = ia + + #print('Nodes:', dir(self._phfocus_ia.remote_device.node_map)) # get all available properties of genicam + # BASIC SETUP + self._log_debug('Setting pixel format to mono12') + self._log_debug('Setting acquisition mode to continuous') + self._phfocus_ia.remote_device.node_map.PixelFormat.value = 'Mono12' + self._phfocus_ia.remote_device.node_map.AcquisitionMode.value = 'Continuous' + self._phfocus_ia.remote_device.node_map.EnAcquisitionFrameRate.value = 'True' + + from harvesters.core import Callback + class CallbackOnNewBuffer(Callback): + """Barebones event handler for phfocus, just pass along the event to the Camera class.""" + def __init__(self, parent): + super().__init__() + self.parent = parent + + def emit(self, context): + """Read out the image and a timestamp, reshape to array, pass to parent""" + with self.parent._phfocus_ia.fetch_buffer() as buffer: + # Work with the fetched buffer. + """Read out the image and a timestamp, reshape to array, pass to cam object""" + self.parent._log_debug('Image event! Unpack ') + self.parent._image_timestamp = datetime.utcnow() + try: + component = buffer.payload.components[0] + data = component.data.reshape(component.height, component.width) + img = data.copy() + if self.parent._flipX: + img = np.fliplr(img) + if self.parent._flipY: + img = np.flipud(img) + if self.parent._rot90: + img = np.rot90(img, self.parent._rot90) + self.parent._image_data = img + except: + self.parent._log_warning('Failed to read image', exc_info=True) + self.parent._image_data = None + self.parent._got_image_event.set() + self.parent._log_debug('Time: ' + str(self.parent._image_timestamp) \ + + ' Size:' + str(self.parent._image_data.shape) \ + + ' Type:' + str(self.parent._image_data.dtype)) + for func in self.parent._call_on_image: + try: + self.parent._log_debug('Calling back to: ' + str(func)) + func(self.parent._image_data, self.parent._image_timestamp) + except: + self.parent._log_warning('Failed image callback', exc_info=True) + self.parent._imgs_since_start += 1 + self.parent._log_debug('Image event handler finished.') + self.parent._phfocus_is_running = True + + # Add method to the callback method for camera NEW_BUFFER event. PhotonFocus + on_new_buffer = CallbackOnNewBuffer(self) + ia.add_callback( + ia.Events.NEW_BUFFER_AVAILABLE, + on_new_buffer + ) + else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -402,6 +522,10 @@ def deinitialize(self): self._log_exception('Failed to close task') self._log_debug('Trying to release PtGrey hardware resources') self._ptgrey_release() + + elif self._phfocus_dev: + self._log_debug('Found PhtonFocus camera, deinitialising') + self._phfocus_release() else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -413,6 +537,10 @@ def available_properties(self): if self.model.lower() == 'ptgrey': return ('flip_x', 'flip_y', 'rotate_90', 'plate_scale', 'rotation', 'binning', 'size_readout', 'frame_rate_auto',\ 'frame_rate', 'gain_auto', 'gain', 'exposure_time_auto', 'exposure_time') + elif self.model.lower() == 'phfocus': + #TODO: check whether its vailidity + return ('flip_x', 'flip_y', 'rotate_90', 'plate_scale', 'rotation', 'size_readout',\ + 'frame_rate', 'gain', 'exposure_time') else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -425,6 +553,9 @@ def flip_x(self): if self.model.lower() == 'ptgrey': self._log_debug('Using PtGrey camera. Will flip the received image array ourselves: ' +str(self._flipX)) return self._flipX + elif self.model.lower() == 'phfocus': + self._log_debug('Using phfocus camera. Will flip the received image array ourselves: ' +str(self._flipX)) + return self._flipX else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -437,6 +568,10 @@ def flip_x(self, flip): self._log_debug('Using PtGrey camera. Will flip the received image array ourselves.') self._flipX = flip self._log_debug('_flipX set to: '+str(self._flipX)) + elif self.model.lower() == 'phfocus': + self._log_debug('Using phfocus camera. Will flip the received image array ourselves.') + self._flipX = flip + self._log_debug('_flipX set to: '+str(self._flipX)) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -447,7 +582,10 @@ def flip_y(self): self._log_debug('Get flip-Y called') assert self.is_init, 'Camera must be initialised' if self.model.lower() == 'ptgrey': - self._log_debug('Using PtGrey camera. Will flip the received image array ourselves: ' +str(self._flipX)) + self._log_debug('Using PtGrey camera. Will flip the received image array ourselves: ' + str(self._flipY)) + return self._flipY + elif self.model.lower() == 'phfocus': + self._log_debug('Using Phfocus camera. Will flip the received image array ourselves: ' + str(self._flipY)) return self._flipY else: self._log_warning('Forbidden model string defined.') @@ -461,6 +599,10 @@ def flip_y(self, flip): self._log_debug('Using PtGrey camera. Will flip the received image array ourselves.') self._flipY = flip self._log_debug('_flipY set to: '+str(self._flipY)) + elif self.model.lower() == 'phfocus': + self._log_debug('Using PhotonFocus camera. Will flip the received image array ourselves.') + self._flipY = flip + self._log_debug('_flipY set to: '+str(self._flipY)) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -470,7 +612,7 @@ def rotate_90(self): """int: Get or set how many times the image should be rotated by 90 degrees. Applied *after* flip_x and flip_y. """ assert self.is_init, 'Camera must be initialised' - if self.model.lower() == 'ptgrey': + if self.model.lower() == 'ptgrey' or self.model.lower() == 'phfocus': return self._rot90 else: self._log_warning('Forbidden model string defined.') @@ -480,8 +622,8 @@ def rotate_90(self, k): self._log_debug('Set rot90 called with: '+str(k)) assert self.is_init, 'Camera must be initialised' k = int(k) - if self.model.lower() == 'ptgrey': - self._log_debug('Using PtGrey camera. Will rotate the received image array ourselves.') + if self.model.lower() == 'ptgrey' or self.model.lower() == 'phfocus': + self._log_debug('Using PtGrey or Phfocus camera. Will rotate the received image array ourselves.') self._rot90 = k self._log_debug('rot90 set to: '+str(self._rot90)) else: @@ -534,6 +676,9 @@ def frame_rate_auto(self): val = node.GetValue() self._log_debug('Returning not '+str(val)) return not val + elif self.model.lower() == 'phfocus': + self._log_debug('Phfocus camera will not use auto frame rate') + return False else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -555,6 +700,8 @@ def frame_rate_auto(self, auto): else: self._log_debug('Setting frame rate') node.SetValue(not auto) + elif self.model.lower() == 'ptgrey': + self._log_debug('PhFocus auto frame rate will not be used') else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -579,6 +726,11 @@ def frame_rate_limit(self): val = (node1.GetValue(), node2.GetValue()) self._log_debug('Returning '+str(val)) return val + elif self.model.lower() == 'phfocus': + self._log_debug('Using phfocus frame rate limits') + frame_rate_max = self._phfocus_ia.remote_device.node_map.AcquisitionFrameRateMax.value + frame_rate_min = 2.1 #Hz + return frame_rate_min, frame_rate_max else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -601,6 +753,9 @@ def frame_rate(self): val = node.GetValue() self._log_debug('Returning '+str(val)) return val + elif self.model.lower() == 'phfocus': + if self._phfocus_ia: + return self._phfocus_ia.remote_device.node_map.AcquisitionFrameRate.value else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -631,6 +786,18 @@ def frame_rate(self, frame_rate_hz): raise AssertionError('The commanded value is outside the allowed range. See frame_rate_limit') else: raise #Rethrows error + elif self.model.lower() == 'phfocus': + if self._phfocus_ia: + try: + if frame_rate_hz< self._phfocus_ia.remote_device.node_map.AcquisitionFrameRateMax.value: + self._phfocus_ia.remote_device.node_map.AcquisitionFrameRate.value = frame_rate_hz + else: + self._log_warning('Frame rate above max limit. Change exposure time.') + except self._phfocus_ia as e: + if 'OutOfRangeException' in e.message: + raise AssertionError('The commanded value is outside the allowed range. See frame_rate_limit') + else: + raise #Rethrows error else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -662,9 +829,13 @@ def gain_auto(self): else: self._log_debug('Unexpected return value') raise RuntimeError('Unknow response from camera') + elif self.model.lower() == 'phfocus': + self._log_debug('Using phfocus auto gain is always disable') + return False else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) + @gain_auto.setter def gain_auto(self, auto): self._log_debug('Set gain called with: '+str(auto)) @@ -688,6 +859,8 @@ def gain_auto(self, auto): else: self._log_debug('Setting gain') node.SetIntValue(node.GetEntryByName(set_to).GetValue()) + elif self.model.lower() == 'phfocus': + self._log_debug('Using phfocus auto gain is always disable') else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -712,6 +885,11 @@ def gain_limit(self): val = (node1.GetValue(), node2.GetValue()) self._log_debug('Returning '+str(val)) return val + elif self.model.lower() == 'phfocus': + #TODO: find pffocus gain limits + min_g = 0.1 + max_g = 16 + return min_g, max_g else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -734,9 +912,14 @@ def gain(self): val = node.GetValue() self._log_debug('Returning '+str(val)) return val + elif self.model.lower() == 'phfocus': + gain_val = self._phfocus_ia.remote_device.node_map.Gain.value + self._log_debug('Returning '+str(gain_val)) + return gain_val else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) + @gain.setter def gain(self, gain_db): self._log_debug('Set gain called with: '+str(gain_db)) @@ -764,6 +947,9 @@ def gain(self, gain_db): raise AssertionError('The commanded value is outside the allowed range. See gain_limit') else: raise #Rethrows error + elif self.model.lower() == 'phfocus': + self._log_debug('Setting gain phfocus'+str(gain_db)) + self._phfocus_ia.remote_device.node_map.Gain.value = gain_db else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -795,6 +981,9 @@ def exposure_time_auto(self): else: self._log_debug('Unexpected return value') raise RuntimeError('Unknow response from camera') + elif self.model.lower() == 'phfocus': + self._log_debug('Phfocus auto exposure always disable') + return False else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -821,6 +1010,8 @@ def exposure_time_auto(self, auto): else: self._log_debug('Setting exposure auto to: '+set_to) node.SetIntValue(node.GetEntryByName(set_to).GetValue()) + elif self.model.lower() == 'phfocus': + self._log_debug('Phfocus auto exposure always disable') else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -845,6 +1036,11 @@ def exposure_time_limit(self): val = (node1.GetValue()/1000, node2.GetValue()/1000) self._log_debug('Returning '+str(val)) return val + elif self.model.lower() == 'phfocus': + #TODO: get exposure limits from camera + min_ex = 0.01 + max_ex = 671 + return min_ex, max_ex else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -867,6 +1063,11 @@ def exposure_time(self): val = node.GetValue() / 1000 #microseconds used in PtGrey self._log_debug('Returning '+str(val)) return val + elif self.model.lower() == 'phfocus': + factor = 1000 + if self._phfocus_ia: + return int(self._phfocus_ia.remote_device.node_map.ExposureTime.value / factor) #out is in ms + return 0.0 else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -898,6 +1099,11 @@ def exposure_time(self, exposure_ms): +' See exposure_time_limit') else: raise #Rethrows error + elif self.model.lower() == 'phfocus': + factor = 1000 #convert to ms + if self._phfocus_ia: + self._phfocus_ia.remote_device.node_map.ExposureTime.value = exposure_ms * factor + self._log_debug('Setting exposure time phfocus to (ms): ' + str(exposure_ms)) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -925,6 +1131,16 @@ def binning(self): return val_horiz except PySpin.SpinnakerException: self._log_warning('Failed to read', exc_info=True) + elif self.model.lower() == 'phfocus': + try: + val_horiz = self._phfocus_ia.remote_device.node_map.BinningHorizontal.value + val_vert = self._phfocus_ia.remote_device.node_map.BinningVertical.value + self._log_debug('Got '+str(val_horiz)+' '+str(val_vert)) + if val_horiz != val_vert: + self._log_warning('Horzontal and vertical binning is not equal.') + return val_horiz + except: + self._log_warning('Failed to read', exc_info=True) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -958,6 +1174,12 @@ def binning(self, binning): raise AssertionError('Not allowed to change binning now.') else: raise #Rethrows error + elif self.model.lower() == 'phfocus': + try: + self._phfocus_ia.remote_device.node_map.BinningHorizontal.value = binning + self._phfocus_ia.remote_device.node_map.BinningVertical.value = binning + except: + self._log_warning('Failure setting binning of phfocus', exc_info=True) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -996,6 +1218,15 @@ def size_max(self): self._log_debug('Failure reading', exc_info=True) raise return (val_w, val_h) + elif self.model.lower() == 'phfocus': + try: + val_w = self._phfocus_ia.remote_device.node_map.WidthMax.value + val_h = self._phfocus_ia.remote_device.node_map.HeightMax.value + self._log_debug('Got '+str(val_w)+' '+str(val_h)) + except: + self._log_debug('Failure reading', exc_info=True) + raise + return (val_w, val_h) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -1024,6 +1255,15 @@ def size_readout(self): self._log_debug('Failure reading', exc_info=True) raise return (val_w, val_h) + elif self.model.lower() == 'phfocus': + try: + val_w = self._phfocus_ia.remote_device.node_map.Width.value + val_h = self._phfocus_ia.remote_device.node_map.Height.value + self._log_debug('Got '+str(val_w)+' '+str(val_h)) + return (val_w, val_h) + except: + self._log_debug('Failure reading', exc_info=True) + raise else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -1081,6 +1321,37 @@ def size_readout(self, size): node_offs_y.SetValue(new_offset[1]) except PySpin.SpinnakerException: self._log_warning('Failure centering readout', exc_info=True) + elif self.model.lower() == 'phfocus': + nodemap = self._phfocus_ia.remote_device.node_map + self._log_debug('Got nodes for offset and size') + try: + self._log_debug('Set offsets to zero') + nodemap.Width.value = size[0] + nodemap.Height.value = size[1] + self._log_debug('Set desired size') + except: + self._log_debug('Failure setting', exc_info=True) + raise #Rethrows error + self._log_debug('Read max size and set offsets to center.') + try: + max_size = self.size_max + except: + self._log_warning('Failure reading max size to center', exc_info=True) + new_offset = None + try: + actual_w = nodemap.Width.value #Read what we set before to be sure + actual_h = nodemap.Height.value + self._log_debug('Actual and max, w,h {} {} {} {}'.format(actual_w, actual_h, max_size[0], max_size[1])) + new_offset = (round((max_size[0] - actual_w) / 2), round((max_size[1] - actual_h) / 2)) + self._log_debug('Neccessary offset: ' + str(new_offset)) + except: + self._log_warning('Failure centering readout', exc_info=True) + if new_offset is not None: + try: + nodemap.OffsetX.value = new_offset[0] + nodemap.OffsetY.value = new_offset[1] + except: + self._log_warning('Failure centering readout', exc_info=True) else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -1123,6 +1394,13 @@ def is_running(self): if not self.is_init: return False if self.model.lower() == 'ptgrey': return self._ptgrey_camera is not None and self._ptgrey_camera.IsStreaming() + elif self.model.lower() == 'phfocus': + #TODO: DM finish it + if self._image_timestamp is not None: + time_since_last_image = (datetime.utcnow() - self._image_timestamp).total_seconds() + if time_since_last_image >0.5: #If last image was 1sec ago + self._phfocus_is_running = False + return self._phfocus_dev is not None and self._phfocus_is_running else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -1146,6 +1424,14 @@ def start(self): self._log_warning('The camera was already streaming...') else: raise RuntimeError('Failed to start camera acquisition') from e + elif self.model.lower() == 'phfocus': + #TODO: DM finish it, add logs and protections + try: + self._phfocus_ia.stop_acquisition() + self._phfocus_ia.start_acquisition(run_in_background=True) + except: + self._log_warning('Phfocus camera cant start, trying reconnect..') + raise else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -1164,6 +1450,13 @@ def stop(self): except: self._log_debug('Could not stop:', exc_info=True) raise RuntimeError('Failed to stop camera acquisition') + elif self.model.lower() == 'phfocus': + try: + self._phfocus_ia.stop_acquisition() + self._phfocus_is_running = False + except: + self._log_debug('Could not stop PhFocus:', exc_info=True) + raise RuntimeError('Failed to stop PhFocus camera acquisition') else: self._log_warning('Forbidden model string defined.') raise RuntimeError('An unknown (forbidden) model is defined: '+str(self.model)) @@ -2437,4 +2730,4 @@ def _create_data_file(self): +'FORMAT: ' + 'STRUCT_PACK_FLOAT32' + ';\n' self._logger.debug('Header: ' + header) with open(self._data_file, 'a') as file: - file.write(header) + file.write(header) \ No newline at end of file diff --git a/pypogs/system.py b/pypogs/system.py index 9bf84bc..fec45b9 100644 --- a/pypogs/system.py +++ b/pypogs/system.py @@ -32,6 +32,7 @@ import logging from threading import Thread from csv import writer as csv_write +from csv import reader as csv_reader # External imports: import numpy as np @@ -124,20 +125,21 @@ class System: sys.coarse_track_thread.spot_tracker.bg_subtract_mode = 'global_median' sys.control_loop_thread.CCL_P = 1 - Set location and alignment: - :attr:`alignment` references the :class:`pypogs.Alignment` instance (auto created) used to - determine and calibrate the location, alignment, and mount corrections. See the class - documentation for how to set location and alignments. To do auto-alignment, use the - :meth:`do_auto_star_alignment` method (requires a star camera). Plate solving for the auto - alignment is performed via the tetra3 package. You can set a custom instance via - :attr:`tetra3`, otherwise a default instance will be created for you. - - If your mount has built in alignment (and/or is physically aligned to the earth) you may - call :meth:`alignment.set_alignment_enu` to set the telescope alignment to East, North, Up - (ENU) coordinates, which will also disable the corrections done in pypogs. ENU is the - traditional astronomical coordinate system for altitude (elevation) and azimuth telescopes, - measured as degrees above the horizon and degrees away from north (towards east) - respectively. + Set location and alignment: + :attr:`alignment` references the :class:`pypogs.Alignment` instance (auto created) used to + determine and calibrate the location, alignment, and mount corrections. See the class + documentation for how to set location and alignments. To do auto-alignment, use the + :meth:`do_auto_star_alignment` method (requires a star camera). Plate solving for the auto + alignment is performed via the tetra3 package. You can set a custom instance via + :attr:`tetra3`, otherwise a default instance will be created for you. + + If your mount has built in alignment (and/or is physically aligned to the earth) you may + call :meth:`alignment.set_alignment_enu` to set the telescope alignment to East, North, Up + (ENU) coordinates, which will also disable the corrections done in pypogs. ENU is the + traditional astronomical coordinate system for altitude (elevation) and azimuth telescopes, + measured as degrees above the horizon and degrees away from north (towards east) + respectively. + Example: :: @@ -183,7 +185,7 @@ class System: @staticmethod def update_databases(): """Download and update Skyfield and Astropy databases (of earth rotation).""" - sf_api.Loader(_system_data_dir).download('finals2000A.all') + sf_api.Loader(_system_data_dir).download('finals2000A.all') apy_util.data.download_file(apy_util.iers.IERS_A_URL, cache='update') def __init__(self, data_folder=None, debug_folder=None): @@ -229,7 +231,7 @@ def __init__(self, data_folder=None, debug_folder=None): self._mount = None self._alignment = Alignment() self._target = Target() - # tetra3 instance used for plate solving + # tetra3 instance used for plate solving self._tetra3 = None # Variable to stop system thread self._stop_loop = True @@ -764,26 +766,26 @@ def add_mount(self, *args, **kwargs): def clear_mount(self): """Set the mount to None.""" self.mount = None - - @property - def tetra3(self): - """tetra3.Tetra3: Get or set the tetra3 instance used for plate solving star images. Will - create an instance with 'default_database' if none is set. - """ - if self._tetra3 is None: - self._logger.debug('Loading default database tetra3') - self._tetra3 = Tetra3('default_database') - return self._tetra3 - - @tetra3.setter - def tetra3(self, tetra3): - self._logger.debug('Got set tetra3 instance with' + str(tetra3)) - assert isinstance(tetra3, Tetra3), 'Must be tetra3 instance' - self._tetra3 = tetra3 - delf._logger.debug('Set tetra3 instace') + + @property + def tetra3(self): + """tetra3.Tetra3: Get or set the tetra3 instance used for plate solving star images. Will + create an instance with 'default_database' if none is set. + """ + if self._tetra3 is None: + self._logger.debug('Loading default database tetra3') + self._tetra3 = Tetra3('default_database') + return self._tetra3 + + @tetra3.setter + def tetra3(self, tetra3): + self._logger.debug('Got set tetra3 instance with' + str(tetra3)) + assert isinstance(tetra3, Tetra3), 'Must be tetra3 instance' + self._tetra3 = tetra3 + delf._logger.debug('Set tetra3 instace') def do_auto_star_alignment(self, max_trials=1, rate_control=True): - """Do the auto star alignment procedure by taking eight star images across the sky. + """Do the auto star aliginment procedure by taking eight star images across the sky. Will call System.Alignment.set_alignment_from_observations() with the captured images. @@ -830,11 +832,12 @@ def run(): assert not self._stop_loop, 'Thread stop flag is set' img = self.star_camera.get_next_image() timestamp = apy_time.now() - # TODO: Test - fov_estimate = self.star_camera.plate_scale * img.shape[1] / 3600 - solve = self.tetra3.solve_from_image(img, fov_estimate=fov_estimate, - fov_max_error=.1) + # TODO: Test + fov_estimate = self.star_camera.plate_scale * img.shape[1] / 3600 + solve = self.tetra3.solve_from_image(img, fov_estimate=fov_estimate, + fov_max_error=.1) self._logger.debug('TIME: ' + timestamp.iso) + # Save image tiff_write(self.data_folder / (start_time.strftime('%Y-%m-%dT%H%M%S') + '_Alt' + str(alt) + '_Azi' + str(azi) @@ -868,6 +871,84 @@ def run(): self._stop_loop = False self._thread.start() + def get_alignment_list_from_csv(self, star_align): + """Read data from csv and put in list for alignment + Args: + star_align (path, must): csv file with RA,DE,0,0,0,apy_time, az,al. Blank lines - no difference. + Returns: + list for method (set_alignment_from_observations(alignment_list)) + """ + alignment_list = [] + parsed_list = [] + with open(star_align, newline='') as csvfile: + reader = csv_reader(csvfile, delimiter=',') + for row in reader: + try: + parsed_list = (0, 0, 0, 0, 0) + apy_time_value = apy_time(row[5], format='iso') + try: + parsed_list = (float(row[0]), + float(row[1]), + apy_time_value, + int(float(row[6])), + int(float(row[7]))) + alignment_list.append(parsed_list) + except: + parsed_list = (0, 0, + apy_time_value, + int(float(row[6])), + int(float(row[7]))) + alignment_list.append(parsed_list) + except: + pass + return alignment_list + + def get_external_commands_from_csv(self, file_path): + """Read data from csv file for OL offsets + Args: + file_path (path, must): csv file with offset values + Returns: + none + """ + parsed_list = [] + with open(file_path, newline='') as csvfile: + reader = csv_reader(csvfile, delimiter=',') + for row in reader: + try: + parsed_list = (int(float(row[0])), row[1], row[2], row[3], int(float(row[4])), row[5], + int(float(row[6]))) + except: + pass + + if parsed_list[0]==1: + y = list(parsed_list) + y[0] = 0 + parsed_list = tuple(y) + with open(file_path, 'w+') as file: + writer = csv_write(file) + writer.writerow(parsed_list) + return parsed_list + + return None + + def execute_commands_from_csv(self, cmd_list): + """Read data from csv and put in list for alignment + Args: + file_path (path, must): csv file with RA,DE,0,0,0,apy_time, az,al. Blank lines - no difference. + Returns: + list for method (set_alignment_from_observations(alignment_list)) + """ + _offsetX, _offsetY = 0, 0 + if cmd_list is not None: + if cmd_list[2] == "true": + _auto_offset = True + else: + _offsetX = cmd_list[4] + _offsetY = cmd_list[6] + # self.sys.control_loop_thread.OL_goal_offset_x_y = [_offsetX, _offsetY] + return [_offsetX, _offsetY] + return [_offsetX, _offsetY] + def get_alt_az_of_target(self, times=None, time_step=.1): """Get the corrected altitude and azimuth angles and rates of the target from the current alignment. @@ -1009,6 +1090,7 @@ def do_alignment_test(self, max_trials=2, rate_control=True): assert self.is_init, 'System not initialized' assert not self.is_busy, 'System is busy' + self._logger.info('Starting alignment test, 2x20 positions.') pos_LH = [(53, -16), (71, -23), (80, -9), (44, -114), (56, -135), (50, -100), (65, -65), (26, -72), (23, -30), (59, -37), (35, -177), (47, -142), (20, -86), (38, -79), @@ -1056,9 +1138,9 @@ def do_alignment_test(self, max_trials=2, rate_control=True): for trial in range(max_trials): img = self.star_camera.get_next_image() timestamp = apy_time.now() - # TODO: Test - fov_estimate = self.star_camera.plate_scale * img.shape[1] / 3600 - solve = self.tetra3.solve_from_image(img, fov_estimate=fov_estimate, fov_max_error=.1) + # TODO: Test + fov_estimate = self.star_camera.plate_scale * img.shape[1] / 3600 + solve = self.tetra3.solve_from_image(img, fov_estimate=fov_estimate, fov_max_error=.1) self._logger.debug('TIME: ' + timestamp.iso) # Save image tiff_write(self.data_folder / (test_time.strftime('%Y-%m-%dT%H%M%S') + '_Alt' @@ -1203,7 +1285,7 @@ def __init__(self, data_folder=None, debug_folder=None): self._telescope_ITRF = None # Tel. location ITRF (xyz) in metres self._location = None # Telescope location astropy EarthLocation # Transformation matrices - self._MX_itrf2enu = None # Matrix transforming vectors in ITRF-xyz to ENU-xyz + self._MX_itrf2enu = None # Matrix tranforming vectors in ITRF-xyz to ENU-xyz self._MX_enu2itrf = None # Inverse of above self._MX_itrf2mnt = None # Matrix transforming vectors in ITRF-xyz to MNT-xyz self._MX_mnt2itrf = None # Inverse of above @@ -1766,7 +1848,7 @@ class Target: You may also give a start and end time (e.g. useful for satellite rise and set times) when creating the target or by the method Target.set_start_end_time(). - With a target set, get the ITRF_xyz coordinates at your preferred times with + With a target set, get the ITRF_xyz coordinates at your prefered times with Target.get_target_itrf_xyz(). Note: @@ -1812,11 +1894,26 @@ def set_target_from_ra_dec(self, ra, dec, start_time=None, end_time=None): """Create an Astropy *SkyCoord* and set as the target. Args: - ra (float): Right ascension in decimal degrees. - dec (float): Declination in decimal degrees. + ra (float): Right ascension must be 6 digits ex:030031 or -030031 + dec (float): Declination must be 6 digits ex:891516 or -021500 start_time (astropy *Time*, optional): The start time to set. end_time (astropy *Time*, optional): The end time to set. """ + + if ((ra[0:1]) == "-"): + ra = ra[1:7] + ra = int(ra[0:2]) + int(ra[2:4]) / 60 + int(ra[4:6]) / 3600 + ra = ra * 15 * (-1) + else: + ra = int(ra[0:2]) + int(ra[2:4]) / 60 + int(ra[4:6]) / 3600 + ra = ra * 15 + + if ((dec[0:1]) == "-"): + dec = dec[1:7] + dec = (-1) * (int(dec[0:2]) + int(dec[2:4]) / 60 + int(dec[4:6]) / 3600) + else: + dec = int(dec[0:2]) + int(dec[2:4]) / 60 + int(dec[4:6]) / 3600 + self.target_object = apy_coord.SkyCoord(ra, dec, unit='deg') self.set_start_end_time(start_time, end_time) diff --git a/pypogs/tracking.py b/pypogs/tracking.py index e0ad3cc..6c003c3 100644 --- a/pypogs/tracking.py +++ b/pypogs/tracking.py @@ -36,6 +36,7 @@ from time import sleep, perf_counter as precision_timestamp from datetime import datetime from csv import writer as csv_write +from os import path # External imports: from astropy.time import Time as apy_time @@ -434,6 +435,7 @@ def OL_goal_offset_x_y(self, offset): self._log_debug('OL goal offset set got: ' + str(offset)) self._OL_goal_offset_x_y = np.array(offset) / 3600 # deg internally self._log_debug('OL goal offset set to: ' + str(self.OL_goal_offset_x_y)) + self._log_info('OL goal offset set to: ' + str(self.OL_goal_offset_x_y)) @property def CCL_enable(self): @@ -741,8 +743,9 @@ def _run(self): 'COARSE_EXIST', 'COARSE_TRACK', 'COARSE_ALT_TRACK', 'COARSE_ALT_MEAN', 'COARSE_AZ_TRACK', 'COARSE_AZ_MEAN', 'COARSE_SD', 'COARSE_RMSE', 'FINE_EXIST', 'FINE_TRACK', 'FINE_ALT_TRACK', 'FINE_ALT_MEAN', - 'FINE_AZ_TRACK', 'FINE_AZ_MEAN', 'FINE_SD', 'FINE_RMSE', 'FB_ERR_ALT', - 'FB_ERR_AZ', 'FB_INT_ALT', 'FB_INT_AZ', 'FB_ANGVEL_ALT', + 'FINE_AZ_TRACK', 'FINE_AZ_MEAN', 'FINE_SD', 'FINE_RMSE', 'FB_ERR_ALT', 'FB_ERR_AZ', + 'P_term_ALT', 'P_term_AZ', 'I_term_ALT', 'I_term_AZ', + 'FB_INT_ALT', 'FB_INT_AZ', 'FB_ANGVEL_ALT', 'FB_ANGVEL_AZ', 'FB_SATURATED', 'FB_COMMAND_ALT', 'FB_COMMAND_AZ', 'FB_KP', 'FB_KI', 'FF_ALT', 'FF_AZ', 'REC_EXIST', 'REC_POWER', 'REC_SMOOTH']) @@ -779,6 +782,18 @@ def seconds_since_start(): return precision_timestamp() - start_timestamp while not self._stop_loop and (loop_utctime < end_time if end_time is not None else True): # CONTROL LOOP # + #DM external commands + # TODO: test and finish + try: + csv_file = r'C:\Pypogs\pypogs-master\pypogs\ex_commands.csv' + if path.exists(csv_file): + command_list = self._parent.get_external_commands_from_csv(csv_file) + if command_list is not None: + ex_offsets = self._parent.execute_commands_from_csv(command_list) + if ex_offsets is not None: + self.OL_goal_offset_x_y = ex_offsets + except: + self._log_info('EXT csv read error') # Time info: loop_timestamp = seconds_since_start() loop_utctime = apy_time.now() # Astropy timestamp in UTC @@ -1046,6 +1061,8 @@ def seconds_since_start(): return precision_timestamp() - start_timestamp err_integral += add_integral self._log_debug('Integral calculated to: ' + str(err_integral)) # Calculate correction term + p_term = fb_kp * (err_alt_az) + i_term = fb_kp * (fb_ki * err_integral) angvel_correction = fb_kp * (err_alt_az + fb_ki * err_integral) (angvel_correction, saturated) = self._clip_feedback_rates(angvel_correction, speed_limit) @@ -1138,7 +1155,7 @@ def seconds_since_start(): return precision_timestamp() - start_timestamp 'ft_search_x': ft_search_pos[0], 'ft_search_y': ft_search_pos[1]} # Save to file - with open(data_file, 'a') as file: + with open(data_file, 'a', newline='') as file: writer = csv_write(file) writer.writerow([loop_utctime.isot, loop_timestamp, loop_index, mode, @@ -1150,6 +1167,7 @@ def seconds_since_start(): return precision_timestamp() - start_timestamp ft_exists, ft_has_track, ft_track_alt_az[0], ft_mean_alt_az[0], ft_track_alt_az[1], ft_mean_alt_az[1], ft_track_sd, ft_rmse, err_alt_az[0], err_alt_az[1], + p_term[0],p_term[1],i_term[0],i_term[1], err_integral[0], err_integral[1], angvel_correction[0], angvel_correction[1], saturated, angvel_total[0], angvel_total[1], fb_kp, fb_ki, ff_alt, ff_azi, rec_exists, @@ -1529,6 +1547,7 @@ def _run(self): dt = loop_timestamp - old_timestamp old_timestamp = loop_timestamp self._actual_freq = 1/dt + # Feedforward ff_step_abs = np.sqrt(np.sum(np.array(self._feedforward_rate)**2)) self._log_debug('Feedforward step magnitude: ' + str(ff_step_abs)) @@ -1538,6 +1557,7 @@ def _run(self): self.spot_tracker.change_mean_relative(*ff_step) else: ff_step = np.array([0, 0]) + # Update the goal offset offs_step = np.array(self._goal_offset_rate) * dt self._log_debug('Adding step to goal offset: (x,y): ' + str(offs_step)) @@ -1578,7 +1598,7 @@ def _run(self): ff_rate = self._feedforward_rate offs_rate = self._goal_offset_rate # Saving log - with open(data_file, 'a') as file: + with open(data_file, 'a', newline='') as file: writer = csv_write(file) writer.writerow([loop_datetime_utc.isoformat(), loop_timestamp, loop_index, int(img_saved), int(img_used), int(has_track), rmse, x, mx, @@ -1587,6 +1607,7 @@ def _run(self): offs_step[0], offs_step[1]]) loop_index += 1 self._process_image.clear() # Clear processing flag, used to sync + except BaseException: self._log_debug('Worker loop threw exception', exc_info=True) try: @@ -1922,13 +1943,13 @@ class SpotTracker: reasonable maximum search radius (default 1000 arcsec).* This will also be used to initialise the position SD estimate. *It is recommended to set a reasonable minimum search radius (default None)* to at least one pixel's width. It is also possible to set maximum and minimum - limits on the signal sum and area SD estimates, otherwise it will be limited to between zero + limits on the signal sum and area SD estimates, otherwise it will be limitied to between zero and the mean value of the respective signal. You may define a goal position against which the output should be measured. By default this is (0,0) which is the centre of the image. The units for all positions will be in the units you provide via plate_scale (default 1 arcsec/pixel) to SpotTracker.update_from_image(). The goal - and properties ending in _absolute are always measured relative to image centre. + and properties ending in _absolute are allways measured relative to image centre. The estimators use exponentially weighted moving mean and variance. SpotTracker.smoothing_parameter (default 10) defines how samples are weighted and roughly @@ -1940,7 +1961,7 @@ class SpotTracker: SpotTracker.failure_sd_penalty (default 25%) to account for drift in the mean. A performance metric, the root-mean-squared error (RMSE), is provided which measures the - position error relative to the goal (instead of the mean, as the SD does). By default this uses + position error relative to the goal (insted of the mean, as the SD does). By default this uses SpotTracker.smoothing_parameter with exponential averaging, but SpotTracker.rmse_smoothing_parameter may be defined to control this individually. @@ -2994,6 +3015,7 @@ def update_from_image(self, img, plate_scale=1): bg_sub_mode=self.bg_subtract_mode, return_moments=True, sigma=self.image_sigma_th, centroid_window=self.centroid_window) + if ret[0][:, 0].size > 0: self._success_count += 1 self._fail_count = 0