Source code for pyradi.ryscene

# -*- coding: utf-8 -*-

################################################################
# The contents of this file are subject to the BSD 3-Clause License.
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://directory.fsf.org/wiki/License:BSD_3Clause
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is part of the PyRadi toolkit.
#
# The Initial Developer of the Original Code is CJ Willers.
# Portions created by CJ Willers are Copyright (C) 2006-2012.
# All Rights Reserved.
################################################################
"""Scene orientation locus modelling using quaternion SLERP interpolation.

Provides the :class:`Scene` class for reading a time-stamped 6-DOF sensor
locus from a CSV file, converting Euler angles to quaternions (via the
``transformations`` library by Christoph Gohlke), and spherical-linear
(SLERP) interpolating orientation between time steps.

The ``transformations`` library follows the "column vectors on the right"
and "row major storage" (C contiguous) conventions.  Angles are in radians
unless stated otherwise.  Quaternions w+ix+jy+kz are represented as
``[w, x, y, z]``.

Euler angle sequence used throughout: yaw (z), pitch (y), roll (x) --
encoded as ``'rzyx'`` in the ``transformations`` API.  Do **not** change
this sequence without updating :attr:`Scene.sequence` and all callers.

See the ``__main__`` block for a worked usage example.

This package was partly developed to provide additional material in support
of students and readers of *Electro-Optical System Analysis and Design: A
Radiometry Perspective*, C. J. Willers, ISBN 9780819495693, SPIE Monograph
PM236, SPIE Press, 2013.  http://spie.org/x648.html?product_id=2021423
"""

__version__ = '0.1.0'
__author__ = 'pyradi team'
__all__ = ['Scene', 'df_euler2quat']


import numpy as np
import pandas as pd
import transformations as tr


[docs] def df_euler2quat(row): """Convert a DataFrame row's Euler angles to a quaternion. Intended to be called via ``DataFrame.apply(df_euler2quat, axis=1)``. The result is wrapped in a list so that pandas stores the entire quaternion array as a single cell value rather than expanding it into separate columns. Args: row: A ``pandas.Series`` with at minimum the keys ``'yawRad'``, ``'pitchRad'``, ``'rollRad'``, and ``'sequence'``. Returns: A one-element list whose sole item is a ``(4,)`` numpy array ``[w, x, y, z]`` representing the quaternion. The list wrapper is required by pandas -- do not remove it. """ q = [tr.quaternion_from_euler(row['yawRad'], row['pitchRad'], row['rollRad'], row['sequence'])] return q
[docs] class Scene: """Model a sensor's 6-DOF orientation locus and interpolate between poses. Reads a time-stamped position and orientation CSV file, converts Euler angles to quaternions, and provides SLERP-based interpolation so that the sensor orientation can be queried at arbitrary times. Coordinate system: Right-Hand, North (x), East (y), Down (z). * **Yaw / Azimuth** -- positive from x to y; clockwise when viewed along +z. * **Roll** -- positive from y to z; clockwise when viewed along +x. * **Pitch / Elevation** -- positive from z to x; clockwise when viewed along +y. The Euler sequence used to map camera axes into the world frame is yaw (z) -> pitch (y) -> roll (x), encoded as ``'rzyx'``. Attributes: dfLocus: ``pandas.DataFrame`` loaded by :meth:`readLocus`, or ``None`` before the first call. sequence: Euler-angle sequence string used by the ``transformations`` library (always ``'rzyx'``). """ def __init__(self): """Initialise an empty Scene with no locus data.""" self.dfLocus = None self.sequence = 'rzyx' # do not change -- see df_euler2quat()
[docs] def readLocus(self, filename): """Read a locus CSV file and compute derived angle and quaternion columns. The CSV must have no spaces and use the following column layout (time in seconds, x/y/z in metres, angles in degrees):: time,x,y,z,yawDeg,pitchDeg,rollDeg 0,0,0,0,0,0,0 1,0,0,0,30,0,0 2,0,0,0,30,15,0 3,0,0,0,30,15,20 4,0,0,0,0,0,0 After loading, the DataFrame gains four extra columns: * ``yawRad``, ``pitchRad``, ``rollRad`` -- angles converted to radians. * ``sequence`` -- the Euler sequence string (``'rzyx'``) on every row. * ``quat`` -- a list containing the ``(4,)`` quaternion array for that row. Args: filename: Path to the locus CSV file. """ print(filename) self.dfLocus = pd.read_csv(filename) self.dfLocus['yawRad'] = self.dfLocus['yawDeg'] * np.pi / 180. self.dfLocus['rollRad'] = self.dfLocus['rollDeg'] * np.pi / 180. self.dfLocus['pitchRad'] = self.dfLocus['pitchDeg'] * np.pi / 180. self.dfLocus['sequence'] = self.sequence self.dfLocus['quat'] = self.dfLocus.apply(df_euler2quat, axis=1)
[docs] def interpLocus(self, ltime): """Interpolate the sensor orientation at an arbitrary time using SLERP. Finds the two locus rows that bracket ``ltime``, computes the parametric fraction *t* between them, and spherically interpolates the quaternions. Args: ltime: Query time in seconds. Must lie strictly between the first and last times recorded in :attr:`dfLocus`. Returns: A 4-tuple ``(yaw, pitch, roll, q)`` where *yaw*, *pitch*, and *roll* are the interpolated Euler angles in radians and *q* is the interpolated ``(4,)`` quaternion array. """ # find the two rows spanning the current value for time self.dfLocus['uidx'] = np.where( np.sign(self.dfLocus['time'] - ltime).diff() > 0, True, False ) # uidx is the label index of the upper bound; uidx-1 is the lower bound uidx = self.dfLocus[self.dfLocus['uidx']].index.tolist()[0] # get parametric value between lower and upper, based on time t = ( (ltime - self.dfLocus.loc[uidx - 1]['time']) / (self.dfLocus.loc[uidx]['time'] - self.dfLocus.loc[uidx - 1]['time']) ) # get the two quats at lower and upper lq = self.dfLocus.loc[uidx - 1]['quat'] uq = self.dfLocus.loc[uidx]['quat'] # SLERP between the two quaternions # http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/ q = tr.quaternion_slerp(lq[0], uq[0], t) yaw, pit, rol = tr.euler_from_quaternion(q, axes=self.sequence) return yaw, pit, rol, q
################################################################ # Evaluation helpers -- not part of the public library API. # These functions are intended for use from the __main__ block. ################################################################
[docs] def unitxrotate(sc): """Rotate a unit vector along the x-axis through each locus pose. Prints the rotation quaternion and rotated point for every row in ``sc.dfLocus``. This is a diagnostic/visualisation helper; it is not part of the public API. Args: sc: A :class:`Scene` instance with :attr:`~Scene.dfLocus` loaded. """ # define a quaternion pointing at (1, 0, 0) pt = np.asarray([0, 1, 0, 0]) print(pt) for idx, item in sc.dfLocus.iterrows(): print(10 * '-') rts = tr.quaternion_from_euler( item['yawRad'], item['pitchRad'], item['rollRad'], sc.sequence ) print(f'rot quat={rts}') pts = tr.quaternion_multiply( rts, tr.quaternion_multiply(pt, tr.quaternion_inverse(rts)) ) print(f'point={pts}') print(f'yaw/pit/rol={tr.euler_from_quaternion(rts, sc.sequence)}') print('\n---------------------\n')
[docs] def applyInterpLocus(sc, t): """Interpolate the locus at time *t* and return a labelled Series. Intended to be used with ``DataFrame.apply`` when the time column is passed as the argument, with ``sc`` bound via a lambda or partial:: dfi['result'] = dfi['time'].apply(lambda t: applyInterpLocus(sc, t)) Args: sc: A :class:`Scene` instance with :attr:`~Scene.dfLocus` loaded. t: Query time in seconds. Returns: A ``pandas.Series`` with keys ``'yaw'``, ``'pit'``, ``'rol'``, and ``'quat'``. """ yaw, pit, rol, q = sc.interpLocus(t) return pd.Series({'yaw': yaw, 'pit': pit, 'rol': rol, 'quat': [q]})
[docs] def locusPlot(sc, ryplot): """Plot interpolated yaw, pitch, and roll over the full locus time span. Produces a ``ryscene-quatslerp.png`` figure showing the three Euler angles sampled at 50 uniformly-spaced times across the locus range, annotated with the raw locus table. Args: sc: A :class:`Scene` instance with :attr:`~Scene.dfLocus` loaded. ryplot: The ``pyradi.ryplot`` module (passed explicitly to avoid a module-level import in production code). """ dfi = pd.DataFrame() numSamples = 50 dfi['time'] = np.linspace( np.min(sc.dfLocus['time']), np.max(sc.dfLocus['time']), numSamples ) dfi = dfi.merge( dfi['time'].apply(lambda t: applyInterpLocus(sc, t)), left_index=True, right_index=True, ) p = ryplot.Plotter(1, 1, 1, 'Test slerp', figsize=(8, 5)) p.plot(1, dfi['time'], dfi['yaw'], label=['yaw']) p.plot(1, dfi['time'], dfi['pit'], label=['pit']) p.plot(1, dfi['time'], dfi['rol'], '', 'Time [s]', 'Angle [rad]', label=['rol']) yoffset = p.getYLim(1)[0] + 0.05 * (p.getYLim(1)[1] - p.getYLim(1)[0]) xoffset = p.getXLim(1)[0] + 0.05 * (p.getXLim(1)[1] - p.getXLim(1)[0]) pstr = sc.dfLocus[['time', 'yawRad', 'pitchRad', 'rollRad']].to_string() p.getSubPlot(1).text( xoffset, yoffset, pstr, horizontalalignment='left', verticalalignment='bottom', family='monospace', fontsize=10, ) p.saveFig('ryscene-quatslerp.png')
################################################################ if __name__ == '__main__': import pyradi.ryplot as ryplot # create a scene object and load locus from csv file sc = Scene() sc.readLocus('data/scene/sensorlocus.csv') print(sc.dfLocus) # rotate a unit vector along x-axis through each pose unitxrotate(sc) # interpolate and plot locus locusPlot(sc, ryplot) print('module ryscene done!')