def watch(self, dt): ''' Monitor watched directory. ''' for path in self.get_possible_fits(): if self.capturing: try: s = Image(path, check_image_data=True) except ImageNotReadyException as e: # give it another chance on next event cycle logger.debug('image not ready {:} ({:})'.format(path, e)) except Exception as e: logger.exception('other issue {:} ({:})'.format(path, e)) toast('Invalid fits file') move_to_dir(path, 'invalid') try: self.process_sub(s, path) except Exception as e: logger.exception('error processing sub ({:})'.format(e)) toast('error processing sub') move_to_dir(path, 'invalid') else: move_to_dir(path, 'unused')
def slew(self, RA=None, Dec=None): if self.connected(): # get RA/Dec from DSO RA, Dec = Component.get('DSO').current_object_coordinates() if RA is not None: self.device.slew(RA=RA, Dec=Dec) else: toast('Cannot slew without coordinates')
def change_filter(self, name, *args): try: self.device.select_position(position=self.filtermap()[name], name=name, success_action=partial( self.fw_changed, name), failure_action=self.fw_not_changed) except Exception as e: toast('problem moving EFW ({:})'.format(e)) if self.not_changed_action is not None: self.not_changed_action() return
def save_notes(self, *args): try: ol = {} for v in self.objects.values(): if v.get('Notes', '').strip(): ol[v['Name']] = v['Notes'] #ol = {v['Name']: v['Notes'] for v in self.objects.values() if v['Notes'].strip() != ''} with open(self.app.get_path('observing_notes.json'), 'w') as f: json.dump(ol, f, indent=1) except Exception as e: logger.exception('problem saving ({:})'.format(e)) toast('problem saving observing notes')
def save(self, im): # save, handling any format conversions try: if self.save_format == 'png': im.save(self.save_path) elif self.save_format == 'jpg': bg = Image.new('RGB', im.size, (255, 255, 255)) bg.paste(im, im) bg.save(self.save_path) toast('saved to {:}'.format(self.save_path), duration=2) except Exception as e: logger.exception('problem with _save ({:})'.format(e)) toast('error saving ({:})'.format(e), duration=2)
def stop_capture(self, message=None): # stops capture normally or abnormally logger.debug('stopping capture') Component.get('Camera').stop_capture() self.gui.set('capturing', False, update_property=True) self.gui.enable(capture_controls) self.gui.enable({'new_DSO'}) if Component.get('Stacker').is_empty(): self.gui.enable({'load_previous'}) if message is not None: Component.get('CaptureScript').reset_generator() logger.error('problem capturing ({:})'.format(message)) toast('Capture problem: {:}'.format(message)) self.info('')
def select_filter(self, name='L', changed_action=None, not_changed_action=None): logger.debug('trying to change to filter {:}'.format(name)) self.changed_action = changed_action self.not_changed_action = not_changed_action # no change of filter if self.current_filter == name: logger.debug('no need to change filter {:}'.format(name)) changed_action() return # filter not in wheel if name not in self.filtermap(): # if we want dark or we currently have dark, special case if name == 'dark' or self.current_filter == 'dark': if name == 'dark': title = 'Is scope capped for darks?' else: title = 'Is scope uncapped for lights?' self.dialog = MDDialog( auto_dismiss=False, text=title, buttons=[ MDFlatButton( text="DONE", text_color=self.app.theme_cls.primary_color, on_press=partial(self.confirmed_fw_changed, name)), MDFlatButton( text="CANCEL", text_color=self.app.theme_cls.primary_color, on_press=self.confirmed_fw_not_changed) ], ) self.dialog.open() else: toast('Cannot find filter {:}'.format(name)) logger.warning( 'Cannot find filter {:} in current filterwheel'.format( name)) if not_changed_action is not None: not_changed_action() else: # if we get here, we can go ahead self.change_filter(name)
def save_as_fits(self): stacker = Component.get('Stacker') im = stacker.get_stack() if im is not None: im = im * (2**16 - 1) logger.debug('min {:} max {:}'.format(np.min(im), np.max(im))) save_image(data=im.astype(np.uint16), path=self.save_path, exposure=stacker.describe().get('total_exposure', None), sub_type=unique_member( [s.sub_type for s in stacker.subs]), filt=unique_member([s.filter for s in stacker.subs])) toast('Saved fits to {:}'.format(os.path.basename(self.save_path)), duration=2.5) else: toast('no image to save')
def save_observing_list(self, *args): try: ''' bug here: if self.objects is changed (with new objects) then it won't have Added field. Temporary fix but needs migrationt to Catalogues ''' ol = {} for v in self.objects.values(): if v.get('Added', ''): ol[v['Name']] = v['Added'] # ol = {v['Name']: v['Added'] for v in self.objects.values() if v['Added'] != ''} with open(self.app.get_path('observing_list.json'), 'w') as f: json.dump(ol, f, indent=1) except Exception as e: logger.exception('problem saving ({:})'.format(e)) toast('problem saving observing list')
def save_capture(self, *args): ''' Called via camera when image is ready. ''' self.fps = 1 / (time.time() - self.start_capture_time) # temporarily added these 2 lines while sorting out camera stop issue # but think this thru as it might mean last thing doesn't get saved #if not self.capturing: # return im = Component.get('Camera').get_image() if im is None: toast('No image to save') return # we'll save as 16-bit int FITs im *= (2**16 - 1) # details = self.get_capture_details() if not hasattr(self, 'series_number') or self.series_number is None: self.series_number = 1 else: self.series_number += 1 sub_type = Component.get('CaptureScript').get_sub_type() filt = Component.get('FilterWheel').current_filter pref = sub_type if sub_type in {'flat', 'dark'} else filt name = '{:}_{:d}.fit'.format(pref, self.series_number) Component.get('ObjectIO').new_sub( data=im.astype(np.uint16), name=name, exposure=self.exposure, filt=filt, temperature=Component.get('Session').temperature, sub_type=sub_type) # ask for next capture immediately self.capture()
def add_sub(self, sub): ''' called by ObjectIO.new_sub ''' ''' check if sub is the same dims as existing stack, otherwise don't add; note dims are in reverse order to those of the numpy array ''' if not self.is_empty(): width, height = self.subs[0].image.shape if sub.shape[1] != width or sub.shape[0] != height: msg = 'sub dimensions {:} x {:} incompatible with those of current stack {:}'.format( width, height, self.subs[0].get_image().shape) toast(msg) logger.error(msg) return self.process(sub) self.subs.append(sub) self.sub_added() self.app.gui.has_changed('Stacker', not self.is_empty()) ''' I don't think we need to set the details here because it is done
def get_flat_exposure(self): # make a series of test exposures to get ADU just over half-way (0.5) logger.info('doing autoflat') min_exposure, max_exposure = 1, 2.5 min_ADU, max_ADU = .3, .8 expos = np.linspace(min_exposure, max_exposure, 5) adus = np.ones(len(expos)) # normalised ADUs actually # do shortest first in case no point adus[0] = self.compute_ADU(expos[0]) if adus[0] > max_ADU: toast('Too early to collect flats') return None # do longest in case too late adus[-1] = self.compute_ADU(expos[-1]) if adus[-1] < min_ADU: toast('Too late to collect flats') return None # do remaining ADUs adus[1:-1] = [self.compute_ADU(e) for e in expos[1:-1]] # interpolate to get things purrrrfect f = interp1d(expos, adus) adu_target = .7 xvals = np.linspace(min_exposure, max_exposure, 500) best = np.argmin(np.abs(adu_target - f(xvals))) best_exposure = xvals[best] mesg = 'best exposure for autoflat {:} with ADU {:}'.format( best_exposure, f(best_exposure)) toast(mesg) logger.info(mesg) return float(best_exposure)
def load(self, dt): # load DSOs try: ''' self.objects is not a copy, so when user objects are added in Catalogues.update_user_catalogue, they are also added to self.objects. This causes issues because new objects dont have all the augmented properties like Added let alone transit information. Should migrate some of the functionality of ObservingList and Catalogues to make it easier to maintain consistency. In fact, all the lookups should be done via Catalogues and not via ObservingList ''' self.objects = Component.get('Catalogues').get_basic_dsos() except Exception as e: logger.exception('problem loading DSOs ({:})'.format(e)) toast('problem loading DSOs') self.objects = {} # load observing list (maps names to date added) try: with open(self.app.get_path('observing_list.json'), 'r') as f: observing_list = json.load(f) except: observing_list = {} logger.info('{:} on observing list'.format(len(observing_list))) # load observing notes try: with open(self.app.get_path('observing_notes.json'), 'r') as f: observing_notes = json.load(f) except: observing_notes = {} logger.info('{:} observing notes'.format(len(observing_notes))) # load previous observations if necc and count previous try: obs = Component.get('Observations').get_observations() previous = dict( Counter( [v['Name'].lower() for v in obs.values() if 'Name' in v])) except Exception as e: logger.warning('problem loading previous DSOs ({:})'.format(e)) previous = {} logger.info('{:} unique previous observations found'.format( len(previous))) # augment/combine DSO info for v in self.objects.values(): name = v['Name'] v['Obs'] = previous.get(name.lower(), 0) v['Added'] = observing_list.get(name, '') v['List'] = 'Y' if name in observing_list else 'N' v['Notes'] = observing_notes.get(name, '') v['Other'] = v.get('Other', '') try: self.compute_transits() except Exception as e: logger.exception('problem computing transits {:}'.format(e))
def eyepiece_view(self): ox, oy = Metrics.get('origin') radius = Metrics.get('radius') # thanks to callump for the use of scrn_density scrn_density = KivyMetrics.density cx = int(Window.left + ox / scrn_density) cy = int(Window.top + Window.height / scrn_density - oy / scrn_density) r = int(radius / scrn_density - 40) w = 2 * r try: im = self.mss_grabber(bbox=(cx - r, cy - r, cx + r, cy + r)) except Exception as e: logger.exception('problem with screen grab ({:})'.format(e)) toast('screen grab error ({:})'.format(e)) return blur = 17 # radius of blurred edge in pixels # new from callump w = im.width r = w / 2 # construct alpha channel to composite eyepiece circle # quite likely some of these steps are redundant alpha = Image.new('L', (w, w), color=0) _alpha = np.ones((w, w)) grid_x, grid_y = np.meshgrid(np.arange(w), np.arange(w)) d = pow((grid_x - r)**2 + (grid_y - r)**2, .5) - (r - 30) db = d <= blur _alpha[db] = d[db] / blur alpha.putdata(_alpha.ravel(), scale=255) if self.dark_surround: grad_im = Image.new('RGBA', (w, w), color=(0, 0, 0)) else: grad_im = Image.new('RGBA', (w, w), color=(255, 255, 255)) grad_im.putalpha(alpha) # composite with scope image, converting if needed (to test) try: if im.mode != 'RGBA': im = im.convert('RGBA') im = Image.alpha_composite(im, grad_im) except Exception as e: logger.exception('problem compositing ({:})'.format(e)) toast('compositing error ({:})'.format(e)) return # annotate Draw object with required level of detail sf, sf_size, hh = self.small_font(w) lf, lf_size, hh = self.large_font(w) rowsep = sf_size + 6 if self.annotation != 'plain': draw = ImageDraw.Draw(im) w, h = im.size row = self.draw_aligned(draw, 0, 0, w, Component.get('DSO').Name, lf, align='left', rowsep=lf_size + 30) if self.annotation == 'full': self.draw_block(self.DSO_details(), draw, row, 0, w, sf, align='left', rowsep=rowsep) self.draw_block(self.processing_details()[::-1], draw, h - 1.5 * rowsep, 0, w, sf, align='right', rowsep=-rowsep) self.draw_block(self.kit_details(), draw, h - 1.5 * rowsep, 0, w, sf, align='left', rowsep=-rowsep) # neg builds from base self.draw_block(self.session_details(), draw, 5, 0, w, sf, align='right', rowsep=rowsep) # for high density screens, reduce size a little im = im.resize((int(im.width / self.imreduction), int(im.height / self.imreduction))) if self.return_image: return im self.save(im)
def rebuild_table(self, dt=None): self.observations_table.data = self.get_observations() self.observations_table.update() toast('rebuilt!')
def landscape_view(self): # to do: better scaling of fonts based on image size # v0.5: ensure tmp is in snapshots dir now that user can start from anywhere nm = os.path.join(self.app.get_path('snapshots'), '_tmp.png') im = Component.get('View').last_image.copy() try: imsave(nm, im[::-1]) orig_im = Image.open(nm) except Exception as e: logger.exception('load/save problem ({:})'.format(e)) toast('load/save error ({:})'.format(e)) return w, h = orig_im.size # NB this is indep of screen size, unlike eyepiece view sf, sf_size, small_text_height = self.small_font(w) lf, lf_size, large_text_height = self.large_font(w) rowgap = small_text_height / 2 obj_details = self.DSO_details() proc_details = self.processing_details() sesh_details = self.session_details() + self.kit_details() max_rows = max( [len(obj_details), len(proc_details), len(sesh_details)]) total_height = h if self.annotation != 'plain': total_height = total_height + 2 * rowgap + large_text_height if self.annotation == 'full': total_height = int(total_height + max_rows * small_text_height + (max_rows - 1) * rowgap) im = Image.new( 'RGBA', (w + 2 * self.framewidth, total_height + 2 * self.framewidth), color=(0, 0, 0) if self.dark_surround else (255, 255, 255)) im.paste(orig_im, (self.framewidth, self.framewidth)) draw = ImageDraw.Draw(im) w += 2 * self.framewidth h += 2 * self.framewidth # move row base to base of text w += large_text_height if self.annotation != 'plain': row = self.draw_aligned(draw, h, 0, w, Component.get('DSO').Name, lf, align='center', rowsep=large_text_height + 2 * rowgap) if self.annotation == 'full': row += rowgap draw.line((5, row, w - 5, row), fill=self.grey(), width=1) row += (rowgap + small_text_height) cols = [int(x) for x in orig_im.size[0] * np.linspace(0, 1, 4)] self.draw_block(sesh_details, draw, row, cols[0], cols[1], sf, align='left', rowsep=small_text_height + rowgap) self.draw_block(obj_details, draw, row, cols[1], cols[2], sf, align='center', rowsep=small_text_height + rowgap) self.draw_block(proc_details, draw, row, cols[2], cols[3], sf, align='right', rowsep=small_text_height + rowgap) # delete temporary file and save os.remove(nm) if self.return_image: return im self.save(im)
def do_save(self, content, *args): self.dialog.dismiss() metadata = Component.get('Metadata') subs = Component.get('Stacker').subs # save master if requested if content.save_master: Component.get('Calibrator').create_master( exposure=content.exposure, temperature=content.temperature, filt=unique_member([s.filter for s in subs]), sub_type=content.sub_type) # save info.json, handle rejects, and update observations table metadata.set({ 'exposure': content.exposure, 'sub_type': content.sub_type, 'temperature': content.temperature }) oldpath = self.current_object_dir Component.save_object() # check if name has been changed name = metadata.get('Name', default='') if name: session_dir, object_dir = os.path.split(self.current_object_dir) if name != object_dir: # user has changed name, so generate a (unique) new folder name new_name = generate_observation_name(session_dir, prefix=name) # change directory name; any problem, don't bother try: os.rename(self.current_object_dir, os.path.join(session_dir, new_name)) self.current_object_dir = os.path.join( session_dir, new_name) except Exception as e: logger.exception('cannot change name ({:})'.format(e)) # save metadata, and if successful update observations newpath = self.current_object_dir try: ''' kludge for now: temperature at least may have been changed by session during save_object so ensure metadata is reset with these new values ''' metadata.set({ 'exposure': content.exposure, 'sub_type': content.sub_type, 'temperature': content.temperature }) metadata.save(newpath, change_fits_headers=content.change_fits_headers) Component.get('Observations').update(oldpath, newpath) Component.get('ObservingList').new_observation() toast('Saved to {:}'.format(newpath)) except Exception as e: logger.exception('OSError saving info3.json to {:} ({:})'.format( newpath, e)) self.new_object()
def slew(self, RA=None, Dec=None): toast('Slewing not possible for manual scope')