Source code for pyplis.utils

# -*- coding: utf-8 -*-
#
# Pyplis is a Python library for the analysis of UV SO2 camera data
# Copyright (C) 2017 Jonas Gliss (jonasgliss@gmail.com)
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License a
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Pyplis module containing low level utilitiy methods and classes."""
from __future__ import (absolute_import, division)
import os
from numpy import (vstack, asarray, ndim, round, hypot, linspace, sum, zeros,
                   complex, angle, array, cos, sin, arctan, dot, int32, pi,
                   isnan, nan, mean, ndarray)

from numpy.linalg import norm
from scipy.ndimage import map_coordinates


from matplotlib.pyplot import subplot, subplots, tight_layout, draw
from matplotlib.patches import Polygon, Rectangle

from pandas import Series
from cv2 import cvtColor, COLOR_BGR2GRAY, fillPoly

from pyplis import logger
from .helpers import (map_coordinates_sub_img, same_roi, map_roi, roi2rect)
from .inout import get_cam_ids
from .glob import DEFAULT_ROI

import six


[docs]def identify_camera_from_filename(filepath): """Identify camera based on image filepath convention. Parameters ---------- filepath : str valid image file path Returns ------- str ID of Camera that matches best Raises ------ IOError Exception is raised if no match can be found """ from pyplis.camera_base_info import CameraBaseInfo if not os.path.exists(filepath): logger.warning("Invalid file path") cam_id = None all_ids = get_cam_ids() max_match_num = 0 for cid in all_ids: cam = CameraBaseInfo(cid) cam.get_img_meta_from_filename(filepath) matches = sum(list(cam._fname_access_flags.values())) if matches > max_match_num: max_match_num = matches cam_id = cid if max_match_num == 0: raise IOError("Camera type could not be identified based on input" "file name {}".format(os.path.basename(filepath))) return cam_id
[docs]class LineOnImage(object): """Class representing a line on an image Main purpose is data extraction along this line on a discrete image grid. This is done using spline interpolation. Parameters ---------- x0 : int start x coordinate y0 : int start y coordinate x1 : int stop x coordinate y1 : int stop y coordinate normal_orientation : str orientation of normal vector, choose from left or right (left means in negative x direction for a vertical line) roi_abs_def : list ROI specifying image sub coordinate system in which the line coordinates are defined (is used to convert to other image shape settings) pyrlevel_def : int pyramid level of image for which start /stop coordinates are defined line_id : str string for identification (optional) Note ---- The input coordinates correspond to relative image coordinates with respect to the input ROI (``roi_def``) and pyramid level (``pyrlevel_def``) """
[docs] def __init__(self, x0=0, y0=0, x1=1, y1=1, normal_orientation="right", roi_abs_def=DEFAULT_ROI, pyrlevel_def=0, line_id="", color="lime", linestyle="-"): self.line_id = line_id # string ID of line self.color = color self.linestyle = linestyle if x0 > x1: x0, y0, x1, y1 = x1, y1, x0, y0 elif x0 == x1 and y0 > y1: y0, y1 = y1, y0 self.x0 = x0 # start x coordinate self.y0 = y0 # start y coordinate self.x1 = x1 # stop x coordinate self.y1 = y1 # stop y coordinate self._roi_abs_def = roi_abs_def self._pyrlevel_def = pyrlevel_def self._rect_roi_rot = None self._line_roi_abs = DEFAULT_ROI self._last_rot_roi_mask = None self.profile_coords = None self._dir_idx = {"left": 0, "right": 1} self.normal_vecs = [None, None] self._velo_glob = nan self._velo_glob_err = nan self._plume_props = None self.check_coordinates() self.normal_orientation = normal_orientation self.prepare_coords()
@property def start(self): """x, y coordinates of start point (``[x0, y0]``).""" return [self.x0, self.y0] @start.setter def start(self, val): try: if len(val) == 2: self.x0 = val[0] self.y0 = val[1] except BaseException: logger.warning("Start coordinates could not be set") @property def stop(self): """x, y coordinates of stop point (``[x1, y1]``).""" return [self.x1, self.y1] @stop.setter def stop(self, val): try: if len(val) == 2: self.x1 = val[0] self.y1 = val[1] except BaseException: logger.warning("Stop coordinates could not be set") @property def center_pix(self): """Return coordinate of center pixel.""" dx, dy = self._delx_dely() xm = self.x0 + dx / 2. ym = self.y0 + dy / 2. return xm, ym @property def normal_orientation(self): """Get / set value for orientation of normal vector.""" return self._normal_orientation @normal_orientation.setter def normal_orientation(self, val): if val not in ["left", "right"]: raise ValueError("Invalid input for attribute orientation, please" " choose from left or right") dx, dy = self._delx_dely() if dx * dy < 0: self._dir_idx["left"] = 1 self._dir_idx["right"] = 0 self._normal_orientation = val @property def line_frame(self): """ROI framing the line (in line coordinate system).""" return map_roi(self._line_roi_abs, self.pyrlevel_def) @property def line_frame_abs(self): """ROI framing the line (in absolute coordinate system).""" return self._line_roi_abs @property def roi_def(self): """ROI in which line is defined (at current ``pyrlevel``).""" return map_roi(self.roi_abs_def, pyrlevel_rel=self.pyrlevel_def) @property def roi_abs_def(self): """Return current ROI (in absolute detector coordinates).""" return self._roi_abs_def @roi_abs_def.setter def roi_abs_def(self): raise AttributeError("This attribute is not supposed to be changed, " "please use method convert() to create a new " "LineOnImage object " "corresponding to other image shape settings") # Redundancy (after renaming attribute in v0.10) @property def pyrlevel(self): """Pyramid level at which line coords are defined.""" logger.warning("This method was renamed in version 0.10. " "Please use pyrlevel_def") return self._pyrlevel_def @pyrlevel.setter def pyrlevel(self): raise AttributeError("This attribute is not supposed to be changed, " "please use method convert() to create a new " "LineOnImage object " "corresponding to other image shape settings") @property def roi_abs(self): """Return current ROI (in absolute detector coordinates).""" logger.warning("This method was renamed in version 0.10. Please use roi_abs_def") return self._roi_abs_def @roi_abs.setter def roi_abs(self): raise AttributeError("This attribute is not supposed to be changed, " "please use method convert() to create a new " "LineOnImage object " "corresponding to other image shape settings") @property def pyrlevel_def(self): """Pyramid level at which line coords are defined.""" return self._pyrlevel_def @pyrlevel_def.setter def pyrlevel_def(self): """Raise AttributeError.""" raise AttributeError("This attribute is not supposed to be changed, " "please use method convert() to create a new " "LineOnImage object " "corresponding to other image shape settings") @property def coords(self): """Return coordinates as ROI list.""" return [self.x0, self.y0, self.x1, self.y1] @property def rect_roi_rot(self): """Rectangle specifying coordinates of ROI aligned with line normal.""" try: if not self._rect_roi_rot.shape == (5, 2): raise Exception except BaseException: logger.info("Rectangle for rotated ROI was not set and is not being " "set to default depth of +/- 30 pix around line. Use " "method set_rect_roi_rot to change the rectangle") self.set_rect_roi_rot() return self._rect_roi_rot @property def velo_glob(self): """Global velocity in m/s, assigned to this line. Raises ------ AttributeError if current value is not of type float """ if not isinstance(self._velo_glob, float) or isnan(self._velo_glob): raise AttributeError("Global velocity not assigned to line") return self._velo_glob @velo_glob.setter def velo_glob(self, val): try: val = float(val) if isnan(val): raise Exception except BaseException: raise ValueError("Invalid input, need float or int...") if val < 0: raise ValueError("Velocity must be larger than 0") elif val > 40: logger.warning("Large value warning: input velocity exceeds 40 m/s") self._velo_glob = val if self._velo_glob_err is None or isnan(self._velo_glob_err): logger.warning("Global velocity error not assigned, assuming 50% of " "velocity") self.velo_glob_err = val * 0.50 @property def velo_glob_err(self): """Error of global velocity in m/s, assigned to this line. Raises ------ AttributeError if current value is not of type float """ if not isinstance(self._velo_glob_err, float) or\ isnan(self._velo_glob_err): raise AttributeError("Global velocity error not assigned to line") return self._velo_glob_err @velo_glob_err.setter def velo_glob_err(self, val): try: val = float(val) if isnan(val): raise Exception except BaseException: raise ValueError("Invalid input, need float or int...") self._velo_glob_err = val @property def plume_props(self): """:class:`LocalPlumeProperties` object assigned to this list.""" from pyplis import LocalPlumeProperties if not isinstance(self._plume_props, LocalPlumeProperties): raise AttributeError("Local plume properties not assigned to line") return self._plume_props @plume_props.setter def plume_props(self, val): from pyplis import LocalPlumeProperties if not isinstance(val, LocalPlumeProperties): raise ValueError("Invalid input, need class LocalPlumeProperties") self._plume_props = val
[docs] def dist_other(self, other): """Determine the distance to another line. Note ---- 1. The offset is applied in relative coordinates, i.e. it does not consider the pyramide level or ROI. #. The two lines need to be parallel Parameters ---------- other : LineOnImage the line to which the distance is retrieved Returns ------- float retrieved distance in pixel coordinates Raises ------ ValueError if the two lines are not parallel """ dx0, dy0 = other.x0 - self.x0, other.y0 - self.y0 dx1, dy1 = other.x1 - self.x1, other.y1 - self.y1 if dx1 != dx0 or dy1 != dy0: logger.warning("Lines are not parallel...") return mean([norm([dx0, dy0]), norm([dx1, dy1])])
[docs] def offset(self, pixel_num=20, line_id=None): """Return a new line shifted within normal direction. Note ---- 1. The offset is applied in relative coordinates, i.e. it does not consider the pyramide level or ROI 2. The determined required displacement (dx, dy) is converted into integers Parameters ---------- pixel_num : int shift length in pixels line_id : str string ID of new line, if None (default) it is set automatically Returns ------- LineOnImage shifted line """ if line_id is None: line_id = self.line_id + "_shifted_%dpix" % pixel_num dx, dy = self.normal_vector * pixel_num x0, x1 = self.x0 + int(dx), self.x1 + int(dx) y0, y1 = self.y0 + int(dy), self.y1 + int(dy) return LineOnImage(x0, y0, x1, y1, normal_orientation=self.normal_orientation, line_id=line_id, color=self.color, linestyle=self.linestyle, pyrlevel_def=self.pyrlevel_def)
[docs] def convert(self, to_pyrlevel=0, to_roi_abs=DEFAULT_ROI): """Convert to other image preparation settings.""" if to_pyrlevel == self.pyrlevel_def and same_roi(self.roi_abs_def, to_roi_abs): logger.info("Same shape settings, returning current line object""") return self # first convert to absolute coordinates ((x0, x1), (y0, y1)) = map_coordinates_sub_img([self.x0, self.x1], [self.y0, self.y1], roi_abs=self._roi_abs_def, pyrlevel=self._pyrlevel_def, inverse=True) # now convert from absolute into specified coords (x0, x1), (y0, y1) = map_coordinates_sub_img([x0, x1], [y0, y1], roi_abs=to_roi_abs, pyrlevel=to_pyrlevel, inverse=False) new_line = LineOnImage(x0, y0, x1, y1, roi_abs_def=to_roi_abs, pyrlevel_def=to_pyrlevel, normal_orientation=self.normal_orientation, line_id=self.line_id, color=self.color, linestyle=self.linestyle) try: new_line.velo_glob = self.velo_glob except BaseException: pass try: new_line.velo_glob_err = self.velo_glob_err except BaseException: pass try: new_line.plume_props = self.plume_props except BaseException: pass return new_line
[docs] def check_coordinates(self): """Check line coordinates. Checks if coordinates are in the right order and exchanges start / stop points if not Raises ------ ValueError if any of the current coordinates is smaller than zero """ if any([x < 0 for x in self.coords]): raise ValueError("Invalid value encountered, all coordinates of " "line must exceed zero, current coords: %s" % self.coords) if self.start[0] > self.stop[0]: logger.info("x coordinate of start point is larger than of stop point: " "start and stop will be exchanged") self.start, self.stop = self.stop, self.start
[docs] def in_image(self, img_array): """Check if this line is within the coordinates of an image array. Parameters ---------- img_array : array image data Returns ------- bool True if point is in image, False if not """ if not all(self.point_in_image(p, img_array) for p in [self.start, self.stop]): return False return True
[docs] def point_in_image(self, x, y, img_array): """Check if a given coordinate is within image. Parameters ---------- x : int x coordinate of point y : int y coordinate of point img_array : array image data Returns ------- bool True if point is in image, False if not """ h, w = img_array.shape[:2] if not 0 < x < w: logger.info("x coordinate out of image") return False if not 0 < y < h: logger.info("y coordinate out of image") return False return True
[docs] def get_roi_abs_coords(self, img_array, add_left=5, add_right=5, add_bottom=5, add_top=5): """Get a rectangular ROI covering this line. Parameters ---------- add_left : int expand range to left of line (in pix) add_right : int expand range to right of line (in pix) add_bottom : int expand range to bottom of line (in pix) add_top : int expand range to top of line (in pix) Returns ------- list ROI around this line """ x0, x1 = self.start[0] - add_left, self.stop[0] + add_right # y start must not be smaller than y stop y_arr = [self.start[1], self.stop[1]] y_min, y_max = min(y_arr), max(y_arr) y0, y1 = y_min - add_top, y_max + add_bottom roi = self.check_roi_borders([x0, y0, x1, y1], img_array) roi_abs = map_roi(roi, pyrlevel_rel=-self.pyrlevel_def) self._line_roi_abs = roi_abs return roi_abs
[docs] def integrate_profile(self, input_img, pix_step_length=None): """Integrate the line profile on input image. Parameters ---------- input_img : Img input image data for """ try: # in case input is an Img input_img = input_img.img except: pass vals = self.get_line_profile(input_img) if pix_step_length is None: logger.warning("No information about integration step lengths provided " "Integration is performed in units of pixels") return sum(vals) try: pix_step_length = pix_step_length.img except: pass if isinstance(pix_step_length, ndarray): if not pix_step_length.shape == input_img.shape: raise ValueError("Shape mismatch between input image and " "pixel") pix_step_length = self.get_line_profile(pix_step_length) return sum(vals * pix_step_length)
def _roi_from_rot_rect(self): """Set current ROI from current rotated rectangle coords.""" r = self._rect_roi_rot xc = asarray([x[0] for x in r]) xc[xc < 0] = 0 yc = asarray([x[1] for x in r]) yc[yc < 0] = 0 roi = [xc.min(), yc.min(), xc.max(), yc.max()] self._line_roi_abs = map_roi(roi, pyrlevel_rel=-self.pyrlevel_def) return roi
[docs] def set_rect_roi_rot(self, depth=None): """Get rectangle for rotated ROI based on current tilting. Note ---- This function also changes the current ``roi_abs`` attribute Parameters ---------- depth : int depth of rotated ROI (in normal direction of line) in pixels Returns ------- list rectangle coordinates """ dx, dy = self._delx_dely() if depth is None: depth = norm((dx, dy)) * 0.10 n = self.normal_vecs[1] dx0, dy0 = n * depth / 2.0 # ============================================================================== # # if sign(dx0) == sign(dy0): # dx0 = -dx0 # dy0 = -dy0 # ============================================================================== x0 = self.x0 + int(dx0) y0 = self.y0 + int(dy0) offs = array([x0, y0]) w = self.length() r = array([(0, 0), (w, 0), (w, depth), (0, depth), (0, 0)]) dx, dy = self._delx_dely() try: theta = arctan(dy / dx) except ZeroDivisionError: theta = pi / 2 # rotation matrix (account for neg. y direction) m_rot = array([[cos(theta), sin(theta)], [-sin(theta), cos(theta)]]) r = dot(r, m_rot) + offs self._rect_roi_rot = r self._roi_from_rot_rect() return r
# ============================================================================== # def set_rect_roi_rot_v0(self, depth=None): # """Get rectangle for rotated ROI based on current tilting # # Note # ---- # This function also changes the current ``roi_abs`` attribute # # Parameters # ---------- # depth : int # depth of rotated ROI (in normal direction of line) in pixels # # Returns # ------- # list # rectangle coordinates # """ # if depth is None: # depth = self.length() * 0.10 # dx0, dy0 = self.normal_vector * depth / 2.0 # # if sign(dx0) == sign(dy0): # dx0 = -dx0 # dy0 = -dy0 # x0 = self.x0 + int(dx0) # y0 = self.y0 + int(dy0) # offs = array([x0, y0]) # # w = self.length() # r = array([(0, 0), (w, 0), (w, depth), (0, depth), (0, 0)]) # # dx, dy = self._delx_dely() # try: # theta = arctan(dy / dx) # except ZeroDivisionError: # theta = pi / 2 # #rotation matrix (account for neg. y direction) # m_rot = array([[cos(theta), sin(theta)], # [-sin(theta), cos(theta)]]) # r = dot(r, m_rot) + offs # self._rect_roi_rot = r # self._roi_from_rot_rect() # return r # ==============================================================================
[docs] def get_rotated_roi_mask(self, shape): """Return pixel access mask for rotated ROI. Parameters ---------- shape : tuple shape of image for which the mask is supposed to be used Returns ------- array bool array that can be used to access pixels within the ROI """ try: if not self._last_rot_roi_mask.shape == shape: raise Exception mask = self._last_rot_roi_mask except BaseException: mask = zeros(shape) rect = self.rect_roi_rot poly = array([rect], dtype=int32) fillPoly(mask, poly, 1) mask = mask.astype(bool) self._last_rot_roi_mask = mask return mask
[docs] def check_roi_borders(self, roi, img_array): """Check if all points of ROI are within image borders. Parameters ---------- roi : list ROI rectangle ``[x0,y0,x1,y1]`` img_array : array exemplary image data for which the ROI is checked Returns ------- list roi within image coordinates (unchanged, if input is ok, else image borders) """ x0, y0 = roi[0], roi[1] x1, y1 = roi[2], roi[3] h, w = img_array.shape if not x0 >= 0: x0 = 1 if not x1 < w: x1 = w - 2 if not y0 >= 0: y0 = 1 if not y1 < h: y1 = h - 2 return [x0, y0, x1, y1]
[docs] def prepare_coords(self): """Prepare the analysis mesh. Note ---- The number of analysis points on this object correponds to the physical length of this line in pixel coordinates. """ length = self.length() x0, y0 = self.start x1, y1 = self.stop x = linspace(x0, x1, length) y = linspace(y0, y1, length) self.profile_coords = vstack((y, x)) self.det_normal_vecs() self.set_rect_roi_rot()
[docs] def length(self): """Determine the length in pixel coordinates.""" return int(round(hypot(*self._delx_dely())))
[docs] def get_line_profile(self, array, order=1, **kwargs): """Retrieve the line profile along pixels in input array. Parameters ---------- array : array 2D data array (e.g. image data). Color images are converted into gray scale using :func:`cv2.cvtColor`. order : int order of spline interpolation used to retrieve the values along input coordinates (passed to :func:`map_coordinates`) **kwargs additional keword args passed to interpolation method :func:`map_coordinates` Returns ------- array profile """ try: array = array.img # if input is Img object except BaseException: pass if ndim(array) != 2: if ndim(array) != 3: logger.info("Error retrieving line profile, invalid dimension of " "input array: %s" % (ndim(array))) return if array.shape[2] != 3: logger.info("Error retrieving line profile, invalid dimension of " "input array: %s" % (ndim(array))) return "Input in BGR, conversion into gray image" array = cvtColor(array, COLOR_BGR2GRAY) # Extract the values along the line, using interpolation zi = map_coordinates(array, self.profile_coords, order=order, **kwargs) if sum(isnan(zi)) != 0: logger.warning("Retrieved NaN for one or more pixels along line on input " "array") return zi
"""Plotting / visualisation etc... """
[docs] def plot_line_on_grid(self, img_arr=None, ax=None, include_normal=False, include_roi_rot=False, include_roi=False, annotate_normal=False, **kwargs): """Draw this line on the image. Parameters ---------- img_arr : ndarray if specified, the array is plotted using :func:`imshow` and onto that axes, the line is drawn ax : matplotlib axes object. Is created if unspecified. Leave :param:`img_arr` empty if you want the line to be drawn onto an already existing image (plotted in ax) include_normal : bool if True, the line normal vector is drawn include_roi_rot : bool if True, a line-orientation specific ROI is drawn include_roi : bool if True, an ROI is drawn which spans the i,j range of the image covered by the line annotate_normal : bool if True, the normal vector is annotated (only if include_normal is set True) **kwargs : additional keyword arguments for plotting of line (please use following keys: marker for marker style, mec for marker edge color, c for line color and ls for line style) Returns ------- Axes matplotlib axes instance """ new_ax = False keys = kwargs.keys() if "mec" not in keys: kwargs["mec"] = "none" if "color" not in keys: kwargs["color"] = self.color if "ls" not in keys: kwargs["ls"] = self.linestyle if "marker" not in keys: kwargs["marker"] = "o" if "label" not in keys: kwargs["label"] = self.line_id if ax is None: new_ax = True ax = subplot(111) else: xlim = ax.get_xlim() ylim = ax.get_ylim() c = kwargs["color"] if img_arr is not None: ax.imshow(img_arr, cmap="gray") p = ax.plot([self.start[0], self.stop[0]], [self.start[1], self.stop[1]], **kwargs) if img_arr is not None: ax.set_xlim([0, img_arr.shape[1]]) ax.set_ylim([img_arr.shape[0], 0]) if include_normal: mag = self.norm * 0.06 # 3 % of length of line n = self.normal_vector * mag xm, ym = self.center_pix epx, epy = n[0], n[1] c = p[0].get_color() ax.arrow(xm, ym, epx, epy, head_width=mag / 2, head_length=mag, fc=c, ec=c) if annotate_normal: ax.text(xm + epx * 2, ym + epy * 3, r'$\hat{n}$', color=c, fontweight='bold', fontsize=18) if include_roi: x0, y0, w, h = roi2rect(self.roi) ax.add_patch(Rectangle((x0, y0), w, h, fc="none", ec=c)) if include_roi_rot: self.plot_rotated_roi(color=c, ax=ax) # axis('image') if new_ax: ax.set_title("Line " + str(self.line_id)) else: ax.set_xlim(xlim) ax.set_ylim(ylim) draw() return ax
[docs] def plot_rotated_roi(self, color=None, ax=None): """Plot current rotated ROI into axes. Parameters ---------- color optional, color information. If None (default) then the current line color is used ax : :obj:`Axes`, optional matplotlib axes object, if None, a figure with one subplot will be created Returns ------- Axes axes instance """ if ax is None: ax = subplot(111) if color is None: color = self.color r = self.rect_roi_rot p = Polygon(r, fc=color, alpha=0.2) ax.add_patch(p) return ax
[docs] def plot_line_profile(self, img_arr, ax=None): """Plot the line profile.""" if ax is None: ax = subplot(111) p = self.get_line_profile(img_arr) ax.set_xlim([0, self.length()]) ax.grid() ax.plot(p, label=self.line_id) ax.set_title("Profile") return ax
[docs] def plot(self, img_arr): """Create two subplots showing line on image and corresponding profile. Parameters ---------- img_arr : array the image data Returns ------- Figure figure containing the supblots """ fig, axes = subplots(1, 2) self.plot_line_on_grid(img_arr, axes[0]) self.plot_line_profile(img_arr, axes[1]) tight_layout() return fig
def _delx_dely(self): """Length of x and y range covered by line.""" return float(self.x1) - float(self.x0), float(self.y1) - float(self.y0) @property def norm(self): """Return length of line in pixels.""" dx, dy = self._delx_dely() return norm([dx, dy])
[docs] def det_normal_vecs(self): """Get both normal vectors.""" dx, dy = self._delx_dely() v1, v2 = array([-dy, dx]), array([dy, -dx]) self.normal_vecs = [v1 / norm(v1), v2 / norm(v2)] return self.normal_vecs
@property def normal_vector(self): """Get normal vector corresponding to current orientation setting.""" return self.normal_vecs[self._dir_idx[self.normal_orientation]] @property def complex_normal(self): """Return current normal vector as complex number.""" dx, dy = self.normal_vector return complex(-dy, dx) @property def normal_theta(self): """Return orientation of normal vector in degrees. The angles correspond to: 1. 0 => to the top (neg. y direction) 2. 90 => to the right (pos. x direction) 3. 180 => to the bottom (pos. y direction) 4. 270 => to the left (neg. x direction) """ return angle(self.complex_normal, True) % 360
[docs] def to_list(self): """Return line coordinates as 4-list.""" return [self.x0, self.y0, self.x1, self.y1]
[docs] def to_dict(self): """Write relevant parameters to dictionary.""" return {"class": "LineOnImage", "line_id": self.line_id, "color": self.color, "linestyle": self.linestyle, "x0": self.x0, "y0": self.y0, "x1": self.x1, "y1": self.y1, "_normal_orientation": self._normal_orientation, "_pyrlevel_def": self._pyrlevel_def, "_roi_abs_def": self._roi_abs_def}
[docs] def from_dict(self, settings_dict): """Load line parameters from dictionary. Parameters ---------- settings_dict : dict dictionary containing line parameters (cf. :func:`to_dict`) """ for k, v in six.iteritems(settings_dict): if k in self.__dict__: self.__dict__[k] = v self.check_coordinates() self.prepare_coords()
@property def orientation_info(self): """Return string about orientation of line and normal.""" dx, dy = self._delx_dely() s = "delx, dely = %s, %s\n" % (dx, dy) s += "normal orientation: %s\n" % self.normal_orientation s += "normal vector: %s\n" % self.normal_vector s += "Theta normal: %s\n" % self.normal_theta return s def __str__(self): s = ("Line %s: [%d, %d, %d, %d], @pyrlevel %d, @ROI: %s" % (self.line_id, self.x0, self.y0, self.x1, self.y1, self.pyrlevel_def, self.roi_abs_def)) return s
[docs]class Filter(object): """Object representing an interference filter. A low level helper class to store information of interference filters. """
[docs] def __init__(self, id=None, type="on", acronym="default", meas_type_acro=None, center_wavelength=nan): """Initialize of object. :param str id ("on"): string identification of this object for working environment :param str type ("on"): Type of object (choose from "on" and "off") :param str acronym (""): acronym for identification in filename :param str meas_type_acro (""): acronym for meastype identification in filename :param str center_wavelength (nan): center transmission wavelength of filter """ if type not in ["on", "off"]: raise ValueError("Invalid type specification for filter: %s, " "please use on or off as type") if id is None: id = type if meas_type_acro is None: meas_type_acro = acronym self.id = id self.type = type # filter acronym (e.g. F01, i.e. as used in filename) self.acronym = acronym self.meas_type_acro = meas_type_acro # filter central wavelength self.center_wavelength = center_wavelength self.trans_curve = None # filter peak transmission if self.id is None: self.id = self.type
[docs] def to_list(self): """Return filter info as list.""" return [self.id, self.type, self.acronym, self.meas_type_acro, self.center_wavelength]
[docs] def set_trans_curve(self, data, wavelengths=None): """Assign transmission curve to this filter. :param ndarray data: transmission data :param ndarray wavelengths: corresponding wavelength array :returns: :class:`pandas.Series` object .. note:: Also accepts :class:`pandas.Series` as input using input param data and leaving wavelength empty, in this case, the Series index is assumed to be the wavelenght data """ if isinstance(data, Series): self.trans_curve = data else: try: self.trans_curve = Series(data, wavelengths) except BaseException: logger.info("Failed to set transmission curve in Filter %s" % self.id)
def __str__(self): s = ("\nFilter\n-----------\n" "id: {}\n" "type: {}\n" "acronym: {}\n" "meas_type_acro: {}\n" "center_wavelength: {}\n" .format(self.id, self.type, self.acronym, self.meas_type_acro, self.center_wavelength)) return s def __repr__(self): s = ("Filter {}; type: {}; acronym: {}; meas_type_acro: {}; " "center_wavelength: {}" .format(self.id, self.type, self.acronym, self.meas_type_acro, self.center_wavelength)) return s
[docs] def print_specs(self): """Print __str__.""" logger.info(self.__str__())
[docs]class DarkOffsetInfo(object): """Base class for storage of dark offset information. Similar to :class:`Filter`. This object can be used to store relevant information of different types of dark and offset images. The attribute "read_gain" is set 0 by default. For some camera types (e.g. Hamamatsu c8484 16c as used in the ECII SO2 camera), the signal can be enhancened with an electronic read_gain (measured in dB) on read. This can be helpful in low light conditions. However, it significantly increases the noise in the images and therefore also the dark image signal. """
[docs] def __init__(self, id="dark", type="dark", acronym="", meas_type_acro=None, read_gain=0): """Initialize object. :param str id: string identification of this object for working environment (default: "dark") :param str type: Type of object (e.g. dark or offset, default: "dark") :param str acronym: acronym for identification in filename :param str meas_type_acro: acronym for meastype identification in filename :param str read_gain: string specifying read_gain mode of this object (use 0 or 1, default is 0) """ if type not in ["dark", "offset"]: raise ValueError("Invalid type specification for " "DarkOffsetInfo: %s, please use dark or offset " "as type") self.id = id self.type = type self.acronym = acronym if meas_type_acro is None: meas_type_acro = acronym self.meas_type_acro = meas_type_acro self.read_gain = read_gain
[docs] def to_list(self): """Return parameters as list.""" return [self.id, self.type, self.acronym, self.meas_type_acro, self.read_gain]
def __str__(self): """Get string representation.""" s = ("\nDarkOffsetInfo\n---------------------------------\n" "ID: %s\n" "Type: %s\n" "Acronym: %s\n" "Meas type acronym: %s\n" "Read gain: %s\n" % (self.id, self.type, self.acronym, self.meas_type_acro, self.read_gain)) return s