Source code for view

"""
This module includes two functions:
    - An interactive viewer for time-series data ("view.ts")
    - An animation of 3D orientations, expressed as quaternions ("view.orientation")

For the time-series viewer, variable types that can in principle be plotted are:
    * np.ndarray
    * pd.core.frame.DataFrame
    * pd.core.series.Series

Viewer can be used to inspect a single variable, or to select one from the current workspace.

Notable aspects:
    - Based on Tkinter, to ensure that it runs on all Python installations.
    - Resizable window.
    - Keyboard-based interaction.
    - Logging of marked events.
"""

# author:       Thomas Haslwanter
# date:         April-2021

import sys
import tkinter as tk

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib.lines import Line2D
from sys import _getframe
from os.path import expanduser, join

from mpl_toolkits.mplot3d import axes3d
import matplotlib.animation as animation

# The following construct is required since I want to run the module as a script
# inside the skinematics-directory
import os
import sys

file_dir = os.path.dirname(__file__)
if file_dir not in sys.path:
    sys.path.insert(0, file_dir)

import vector, quat
from sensors.xsens import XSens

# For Orientation_Viewers
# import pygame
# To avoid the annoying "Hello from the pygame community":
import contextlib
with contextlib.redirect_stdout(None):
    import pygame


# Since 2021, MacOS no longer supports OpenGL :(
try:
    import OpenGL.GL as gl
    import OpenGL.GLU as glu
    openGL_installed = True
except ModuleNotFoundError:
    openGL_installed = False


# List if plottable datatypes
plottable = [np.ndarray, pd.core.frame.DataFrame, pd.core.series.Series]

class Orientation_OGL:
    """Orientation viewer utilizing OpenGL

    In the "zero" orientation, the pointer indicating the 3D orientation
    will point towards the lower right. In the display, the (x/y/z)-axes point
    in the (lower_right/lower_left/up) direction, respectively.

    Parameters
    ----------
    quat_in : (Nx3) or (Nx4) array
            Quaternion containing the orientation
    win_width : integer
            Pixel-width of the display window.
    win_height : integer
            Pixel-height of the display window.

    Examples
    --------
    >>> in_file = r'.\tests\data\data_xsens.txt'
    >>> from skinematics.sensors.xsens import XSens
    >>> data = XSens(in_file)
    >>> viewer = Orientation_OGL(quat_in=data.quat)
    >>> viewer.run(looping=False, rate=100)
    """

    def __init__(self, quat_in=None, rate=50, looping=False, win_width = 800, win_height = 600):
        """Initialize the OpenGL-viewer"""

        if openGL_installed:
            # Camera
            self.cam_pos = [0.2, 0.2, 0]
            self.cam_target = [0, 0, -1]
            self.cam_up = [0, 1, 0]

            # OpenGL to my convention
            x = [1, 0, 0]
            y = [0, 0, -1]
            z = [0, 1, 0]
            self.openGL2skin = np.column_stack( (x,y,z) )

            # Initialize the pygame grafics setup
            pygame.init()
            self.display = (win_width, win_height)
            pygame.display.set_mode(self.display, pygame.DOUBLEBUF|pygame.OPENGL)

            self.define_elements()
            self.quat = quat_in
        else:
            raise ModuleNotFoundError('Sorry, OpenGL is not installed on your computer.')


    def define_elements(self):
        """Define the visual components"""

        # Define the pointer
        delta = 0.01
        self.vertices = (
            (0, -0.2, delta),
            (0, 0.2, delta),
            (0.6, 0, delta),
            (0, -0.2, -delta),
            (0, 0.2, -delta),
            (0.6, 0, -delta),
            )

        self.edges = (
            (0,1),
            (0,2),
            (0,3),
            (1,2),
            (1,4),
            (2,5),
            (3,4),
            (3,5),
            (4,5) )

        self.colors = (
            (0.8,0,0),
            (0.7,0.7,0.6),
            (1,1,1) )

        self.surfaces = (
            (0,1,2),
            (3,4,5),
            (0,1,3,4),
            (1,4,2,5),
            (0,3,2,5) )

        # Define the axes
        self.axes_endpts = np.array(
            [[-1,  0,  0],
             [ 1,  0,  0],
             [ 0, -1,  0],
             [ 0,  1,  0],
             [ 0,  0, -1],
             [ 0,  0,  1]])

        self.axes = (
            (0,1),
            (2,3),
            (4,5) )

    def draw_axes(self):
        """Draw the axes. """

        gl.glBegin(gl.GL_LINES)
        gl.glColor3fv(self.colors[2])

        # Here I have a difficulty with making the axes thicker
        #glLineWidth(1.5)

        for line in self.axes:
            for vertex in line:
                gl.glVertex3fv(self.axes_endpts[vertex])

        gl.glEnd()


    def draw_pointer(self, vertices):
        """Draw the triangle that indicates 3D orientation."""

        gl.glBegin(gl.GL_TRIANGLES)

        for (color, surface) in zip(self.colors[:2], self.surfaces[:2]):
            for vertex in surface:
                gl.glColor3fv(color)
                gl.glVertex3fv(vertices[vertex])
        gl.glEnd()

        gl.glBegin(gl.GL_LINES)
        gl.glColor3fv(self.colors[2])

        for edge in self.edges:
            for vertex in edge:
                gl.glVertex3fv(vertices[vertex])
        gl.glEnd()


    def run(self, rate=100, looping=True):
        """Run the viewer

        Parameters
        ----------
        rate : integer
            Sample rate for the display [Hz]. Lower numbers result in slower display.
        looping : boolean
            If set to "True", the display will loop until the window is closed.
        """

        dt = int(1/rate*1000)  # [msec]

        # Camera properties, e.g. focal length etc
        gl.glMatrixMode(gl.GL_PROJECTION)
        gl.glLoadIdentity()

        glu.gluPerspective(45, (self.display[0]/self.display[1]), 0.1, 50.0)
        gl.glTranslatef(0.0,0.0, -3)

        counter = 0
        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    quit()

            counter = np.mod(counter+1, self.quat.shape[0])
            if not looping and counter == self.quat.shape[0]-1:
                break

            gl.glClear(gl.GL_COLOR_BUFFER_BIT|gl.GL_DEPTH_BUFFER_BIT)
            gl.glEnable(gl.GL_DEPTH_TEST)

            # Camera position
            gl.glMatrixMode(gl.GL_MODELVIEW)
            gl.glLoadIdentity()
            glu.gluLookAt(
                self.cam_pos[0], self.cam_pos[1], self.cam_pos[2],
                self.cam_target[0], self.cam_target[1], self.cam_target[2],
                self.cam_up[0], self.cam_up[1], self.cam_up[2] )

            # Scene elements
            gl.glPushMatrix()
            cur_corners = vector.rotate_vector(self.vertices, self.quat[counter]) @ self.openGL2skin.T
            #cur_corners = cur_corners * np.r_[1, 1, -1] # This seems to be required
                        ##to get things right - but I don't understand OpenGL at this point

            self.draw_pointer(cur_corners)
            gl.glPopMatrix()
            self.draw_axes()

            pygame.display.flip()
            pygame.time.wait(dt)


[docs]def orientation(quats, out_file=None, title_text=None, deltaT=100): """Calculates the orienation of an arrow-patch used to visualize a quaternion. Uses "_update_func" for the display. Parameters ---------- quats : array [(N,3) or (N,4)] Quaterions describing the orientation. out_file : string Path- and file-name of the animated out-file (".mp4"). [Default=None] title_text : string Name of title of animation [Default=None] deltaT : int interval between frames [msec]. Smaller numbers make faster animations. Example ------- To visualize a rotation about the (vertical) z-axis: >>> # Set the parameters >>> omega = np.r_[0, 10, 10] # [deg/s] >>> duration = 2 >>> rate = 100 >>> q0 = [1, 0, 0, 0] >>> out_file = 'demo_patch.mp4' >>> title_text = 'Rotation Demo' >>> >>> # Calculate the orientation >>> num_rep = duration*rate >>> omegas = np.tile(omega, [num_rep, 1]) >>> q = skin.quat.calc_quat(omegas, q0, rate, 'sf') >>> >>> orientation(q, out_file, 'Well done!', deltaT=1000./rate) Note ---- Seems to be slow. So unless you need a movie, better use "Orientation_OGL". """ # Initialize the 3D-figure fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # Define the arrow-shape and the top/bottom colors delta = 0.01 # "Thickness" of arrow corners = [[0, 0, 0.6], [0.2, -0.2, 0], [0, 0, 0]] colors = ['r', 'b'] # Calculate the arrow corners corner_array = np.column_stack(corners) corner_arrays = [] corner_arrays.append( corner_array + np.r_[0., 0., delta] ) corner_arrays.append( corner_array - np.r_[0., 0., delta] ) # Calculate the new orientations, given the quaternion orientation all_corners = [] for quat in quats: all_corners.append([vector.rotate_vector(corner_arrays[0], quat), vector.rotate_vector(corner_arrays[1], quat)]) # Animate the whole thing, using 'update_func' num_frames = len(quats) ani = animation.FuncAnimation(fig, _update_func, num_frames, fargs=[all_corners, colors, ax, title_text], interval=deltaT) # If requested, save the animation to a file if out_file is not None: try: ani.save(out_file) print('Animation saved to {0}'.format(out_file)) except ValueError: print('Sorry, no animation saved!') print('You probably have to install "ffmpeg", and add it to your PATH.') plt.show() return
def _update_func(num, all_corners, colors, ax, title=None): """For 3D plots it seems to be impossible to only re-set the data values, so the plot has to be cleared and re-generated for each frame """ # Clear previous plot ax.clear() # Plot coordinate axes ax.plot([-1, 1], [0, 0], [0, 0]) ax.plot([0, 0], [-1, 1], [0, 0]) ax.plot([0, 0], [0, 0], [-1, 1]) # Format the plot plt.xlim(-1, 1) plt.ylim(-1, 1) plt.xlabel('x') plt.ylabel('y') try: # Plot and color the top- and bottom-arrow for up_down in range(2): corners = all_corners[num][up_down] ph = ax.plot_trisurf(corners[:,0], corners[:,1], corners[:,2]) ph.set_color(colors[up_down]) if title is not None: plt.title(title) except RuntimeError: # When the triangle is exactly edge-on "plot_trisurf" seems to have a numerical problem print('Cannot show triangle edge-on!') return class Display: def __init__(self, master, data=None): """Create all frames, buttons and labels""" if type(data) not in plottable: self.inDictionary = data data = None self.master = master if data is None: data = np.arange(1) if data.ndim == 1: data = np.atleast_2d(data).T self.numData = data.shape[1] # Generate the figure ------------------------------------------- fig, self.axs = plt.subplots(nrows=self.numData, sharex=True, sharey=False) if self.numData == 1: self.axs = [self.axs] self.lines = [] self.rects = [] self.zeros = [] for ii in range(self.numData): self.lines.append(self.axs[ii].plot(data[:,ii])) # Zero line self.zeros.append(self.axs[ii].hlines(0,0,len(data), linestyle='dotted')) # Zoom box self.epsilon = 5 (x0,x1,y0,y1) = (0,0,0,0) self.rects.append(Line2D([x0,x1,x1,x0,x0], [y0,y0,y1,y1,y0], linestyle='dotted')) self.axs[ii].add_line(self.rects[-1]) # Create the canvas self.canvas = FigureCanvasTkAgg(fig,master=master) #self.canvas.show() self.canvas.get_tk_widget().pack(side='top', fill='both', expand=1) # Keyboard and mouse control self.button = False self.marks = [] self.canvas.mpl_connect('key_press_event', self.on_key_event) self.canvas.mpl_connect('button_press_event', self.onclick) self.canvas.mpl_connect('button_release_event', self.onrelease) self.canvas.mpl_connect('motion_notify_event', self.onmotion) # Create and pack the widgets self.createWidgets() self.showAll() if 'inDictionary' in dir(self): self.selectPlotVar() def createWidgets(self): """Create frames, buttons, text, etc.""" # Frame for the slider frame_0_top = tk.Frame(self.master) self.scale = tk.Scale(frame_0_top, from_=0, to=1, resolution=0.01, orient=tk.HORIZONTAL, length=500, sliderlength=50, showvalue=False, command=self.position) self.scale.set(0.0) self.scale.pack(fill=tk.X, ipadx=10) frame_0_bottom = tk.Frame(self.master) # Frame for Load/Exit buttons ----------------------- frame_1 = tk.Frame(frame_0_bottom) # Create 2 buttons self.button_exit = tk.Button(frame_1,text="Exit", foreground='red', command=self.exit) self.button_exit.pack(side="right") self.button_load = tk.Button(frame_1,text="Load", fore='green', command=self.selectPlotVar) self.button_load.pack(side="right") self.button_showAll = tk.Button(frame_1,text="ShowAll", foreground='blue', command=self.showAll) self.button_showAll.pack(side="right") # Frame for navigation buttons ---------------------- frame_2 = tk.Frame(frame_0_bottom) # Add navigation buttons self.button_ff = tk.Button(frame_2,text=">>", command=self.fforward) self.button_ff.pack(side="right") self.button_ff = tk.Button(frame_2,text=">", command=self.forward) self.button_ff.pack(side="right") self.button_ff = tk.Button(frame_2,text="<", command=self.backward) self.button_ff.pack(side="right") self.button_ff = tk.Button(frame_2,text="<<", command=self.fbackward) self.button_ff.pack(side="right") # Frame for entering text ----------------------------- frame_3 = tk.Frame(frame_0_bottom) # Subframe for upper/lower limit frame_3_lim = tk.Frame(frame_3) # Subsubframe for upper limit frame_upper = tk.Frame(frame_3_lim) frame_lower = tk.Frame(frame_3_lim) label_upper = tk.Label(frame_upper, text="Upper Limit") label_upper.pack(side='left') label_lower = tk.Label(frame_lower, text="Lower Limit") label_lower.pack(side='left') self.text_upper = tk.Entry(frame_upper) self.text_upper.pack(side='right') self.text_lower = tk.Entry(frame_lower) self.text_lower.pack(side='right') # Subframe for rate frame_3_rate = tk.Frame(frame_3) label_rate = tk.Label(frame_3_rate, text='Rate') self.text_rate = tk.Entry(frame_3_rate) self.text_rate.insert(0, '1') # Checkbutton for loggin frame_3_log = tk.Frame(frame_3) label_log = tk.Label(frame_3_log, text='Log') self.chkVar = tk.IntVar() log_check = tk.Checkbutton(frame_3_log, variable=self.chkVar, command=self.log) # Pack the elements, and assign key bindings --------------------- frame_0_top.pack() frame_0_bottom.pack() frame_1.pack(side="right") frame_2.pack(side="right", expand=1) frame_3.pack(side="left", expand=1) frame_3_lim.pack(side="left", padx=5, expand=1) frame_upper.pack() frame_lower.pack() label_upper.pack(side='left') self.text_upper.bind('<Return>', self.setUpperLimit) self.text_upper.pack(side='left') label_lower.pack(side='left') self.text_lower.bind('<Return>', self.setLowerLimit) self.text_lower.pack(side='left') frame_3_rate.pack(side='left', padx=5, expand=1) label_rate.pack() self.text_rate.bind('<Return>', self.setRate) self.text_rate.pack() #self.text_rate.event_generate('<Return>') self.setRate('<Return>') frame_3_log.pack(side='left', padx=5, expand=1) label_log.pack() log_check.pack() def log(self): """Log right mouse clicks""" #print('logging is {0}'.format(self.chkVar.get())) if self.chkVar.get() == 1 and 'logFile' not in dir(self): home = expanduser('~') self.logFile = join(home, 'default.log') print('right-Mouse clicks are logged into {0}'.format(self.logFile)) def onmotion(self, event): """Event for mouse dragging""" if self.button: self._stop = (event.x, event.y, event.xdata, event.ydata) x = [self._start[2], self._stop[2]] y = [self._start[3], self._stop[3]] for ii in range(self.numData): self.rects[ii].set_xdata([x[0],x[1],x[1],x[0],x[0]]) self.rects[ii].set_ydata([y[0],y[0],y[1],y[1],y[0]]) #print('button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%( #event.button, event.x, event.y, event.xdata, event.ydata)) self.canvas.draw() def onclick(self, event): """Select the button-down position""" if event.button == 1: # left mouse click self._start = (event.x, event.y, event.xdata, event.ydata) self.button = True if event.button == 3: # right mouse click if self.chkVar.get() == 1: # right mouse click self.marks.append(event.xdata) for ii in range(self.numData): self.axs[ii].vlines(event.xdata, self.range[2], self.range[3]) self.canvas.draw() def onrelease(self, event): """Select the button-up position, and zoom in on the selected range""" if self.button == True: # only for left mouse clicks self._stop = (event.x, event.y, event.xdata, event.ydata) self.button = False # Only zoom in if a "reasonably" large area has been selected start = np.r_[self._start[:2]] stop = np.r_[self._stop[:2]] x,y = stop-start curDist = np.hypot(x,y) if curDist > self.epsilon: # Zoom in for ii in range(self.numData): self.axs[ii].set_xlim([min(self._start[2], self._stop[2]), max(self._start[2], self._stop[2])]) self.axs[ii].set_ylim([min(self._start[3], self._stop[3]), max(self._start[3], self._stop[3])]) xLim = self.axs[0].get_xlim() self.xRange = np.diff(xLim)[0] self.sliderMax = self.range[1]-self.xRange self.scale.set(xLim[0]/self.sliderMax) (x,y) = ([0,0], [0,0]) for ii in range(self.numData): self.rects[ii].set_xdata([x[0],x[1],x[1],x[0],x[0]]) self.rects[ii].set_ydata([y[0],y[0],y[1],y[1],y[0]]) self.canvas.draw() def on_key_event(self, event): """Keyboard interaction""" #print('you pressed %s'%event.key) key = event.key # In Python 2.x, the key gets indicated as "alt+[key]" # Bypass this bug: if key.find('alt') == 0: key = key.split('+')[1] if key == 'f': self.forward() elif key == 'n': self.fforward() elif key == 'b': self.backward() elif key == 'p': self. fbackward() elif key == 'x': self.exit() elif key == 'a': self.showAll() elif key == 'z': self.zoom() def setRate(self, event): """Set the rate. Also use this to initialize a number of default values, and the values for the limit-boxes.""" rate = float(self.text_rate.get()) minVal = 0 maxVal = 0 for line in self.lines: x,y = line[0].get_data() pnts = np.arange(len(x)) time = pnts/rate line[0].set_xdata(time) minVal = min(minVal, np.min(y)) maxVal = max(maxVal, np.max(y)) # Initially, show all data for ii in range(self.numData): # to avoid some spurious UserWarning max_range = np.max(time) if max_range == 0.0: max_range += 0.001 self.axs[ii].set_xlim([0, max_range]) # Make sure small numbers are nicely formatted if max(np.abs([minVal, maxVal])) < 0.01: strMin = '{0:.2e}'.format(minVal) strMax = '{0:.2e}'.format(maxVal) else: strMin = '{0:.2f}'.format(minVal) strMax = '{0:.2f}'.format(maxVal) self.text_lower.delete(0, tk.END) self.text_lower.insert(0, strMin) self.text_upper.delete(0, tk.END) self.text_upper.insert(0, strMax) # Set limit and range parameters curLim = self.axs[0].get_xlim() xMin = 0 xMax = np.max(time) self.xRange, = np.diff(curLim) self.sliderMax = xMax-self.xRange self.range = [xMin, xMax, minVal, maxVal] # Draw canvas self.canvas.draw() self.canvas._tkcanvas.focus_set() def zoom(self): """ Show all the data on the y-axis, and 10% of all on the x-axis. """ for ii in range(self.numData): self.axs[ii].set_xlim([0, 0.1*self.range[1]]) self.axs[ii].set_ylim(self.range[2:]) self.xRange = 0.1*self.range[1] self.sliderMax = self.range[1]-self.xRange self.scale.set(0) self.canvas.draw() def showAll(self): """ Show all the data """ for ii in range(self.numData): # The "if" is to avoid some spurious "UserWarning" if np.diff(self.range[:2])== 0.: self.axs[ii].set_xlim([-0.001, 0.001]) else: self.axs[ii].set_xlim(self.range[:2]) if np.diff(self.range[2:])== 0.: self.axs[ii].set_ylim([-0.001, 0.001]) else: self.axs[ii].set_ylim(self.range[2:]) self.xRange = self.range[1] self.scale.set(0) self.canvas.draw() def setUpperLimit(self, event): """Set the "Upper Limit" """ UpperLimit = float(self.text_upper.get()) self.text_lower.delete(0, tk.END) self.text_lower.insert(0, str(-UpperLimit)) for ii in range(self.numData): self.axs[ii].set_ylim([-UpperLimit, UpperLimit]) self.canvas.draw() self.canvas._tkcanvas.focus_set() def setLowerLimit(self, event): """Set the "Lower Limit" """ UpperLimit = float(self.text_upper.get()) LowerLimit = float(self.text_lower.get()) for ii in range(self.numData): self.axs[ii].set_ylim([LowerLimit, UpperLimit]) self.canvas.draw() self.canvas._tkcanvas.focus_set() def update_xPos(self, xLim): """ Set the x-range, and check that the limits are within the possible range. """ xMin = self.range[0] xMax = self.range[1] # Check the minimum position if xLim[0] < xMin: xLim = [xMin, xMin+self.xRange] # Check the maximum position if xLim[1] > xMax: xLim = [xMax-self.xRange, xMax] # Update xlimits, and redraw the screen if self.sliderMax == 0: self.scale.set(0) else: self.scale.set(float(xLim[0])/self.sliderMax) for ii in range(self.numData): self.axs[ii].set_xlim(xLim) self.canvas.draw() def position(self, event): """Position window according to slider """ sliderPos = self.scale.get() newLim = np.r_[0, self.xRange] + sliderPos*self.sliderMax self.update_xPos(newLim) def forward(self): """Move data forward by half the visible distance""" for ii in range(self.numData): curLim = self.axs[ii].get_xlim() newLim = curLim + self.xRange/2 self.update_xPos(newLim) def fforward(self): """Move data forward by one visible distance""" for ii in range(self.numData): curLim = self.axs[ii].get_xlim() newLim = curLim + self.xRange self.update_xPos(newLim) def backward(self): """Move data backward by half the visible distance""" for ii in range(self.numData): curLim = self.axs[ii].get_xlim() newLim = curLim - self.xRange/2 self.update_xPos(newLim) def fbackward(self): """Move data back by one visible distance""" for ii in range(self.numData): curLim = self.axs[ii].get_xlim() newLim = curLim - self.xRange self.update_xPos(newLim) def exit(self): """Close the window, and - if necessary - save the right-clicked marks.""" if self.chkVar.get() == 1: np.savetxt(self.logFile, self.marks) print('right-Mouse clicks are saved into {0}'.format(self.logFile)) self.master.quit() self.master.destroy() # If you don't use both, Python crashes under Python 2.x def updatePlot(self): """update the figure""" for ii in range(self.numData): for line in self.lines[ii]: # Remove the old lines line.remove() self.zeros[ii].remove() # plot the new data #self.axs[ii].set_color_cycle(None) self.lines[ii] = self.axs[ii].plot(self.varValues) self.zeros[ii] = self.axs[ii].hlines(0,0,len(self.varValues), linestyle='dotted') self.master.title(self.varName) self.range = [0, len(self.varValues), np.min(self.varValues), np.max(self.varValues)] self.showAll() self.text_upper.delete(0, tk.END) self.text_upper.insert(0, str(self.range[3])) self.text_lower.delete(0, tk.END) self.text_lower.insert(0, str(self.range[2])) self.canvas.draw() self.master.call('wm', 'attributes', '.', '-topmost', '1') def selectPlotVar(self): """ Select a plottable variable from those in the workspace. """ if 'inDictionary' not in dir(self): print('No additional variables available!') else: # Create a new window self.loadWindow = tk.Toplevel(self.master) varSelector = VarSelector(self.loadWindow, self) varSelector.master.title('Selection') class VarSelector(): """Class for the GUI-display of plottable items Analyze the current workspace for variables that can be plotted, and let the user select one. Variable types that can in principle be plotted are: - np.ndarray - pd.core.frame.DataFrame - pd.core.series.Series """ def __init__(self, selectionWindow, mainApp): varList = mainApp.inDictionary.keys() plotList = [] for curType in plottable: plotList += [var for var in varList if type(mainApp.inDictionary[var])==curType] self.master = selectionWindow self.frame = tk.Frame(selectionWindow) self.frame.grid() self.createWidgets(plotList) self.mainApp = mainApp def selectAndQuit(self): """Grab the selected item, update the main plot, and close the VarSelector-GUI.""" try: selected = self.items[int(self.listbox.curselection()[0])] self.mainApp.varName = selected self.mainApp.varValues = self.mainApp.inDictionary[selected] self.mainApp.updatePlot() home = expanduser('~') self.mainApp.logFile = join(home, selected + '.log') if self.mainApp.chkVar.get() == 1: print('right-Mouse clicks are logged into {0}'.format(self.mainApp.logFile)) except IndexError: # No selection made self.selected = '' self.master.destroy() def quitFun(self): """Quit VarSelector-GUI with no further action.""" self.selected = '' self.master.destroy() def createWidgets(self, items): """Create the List, and the Quit-button for the VarSelector-GUI.""" self.listbox = tk.Listbox(self.frame, name='varSelection', font=('times',13)) # Populate the list with the items provided self.items = items for item in items: self.listbox.insert(tk.END,item) # Place it on the grid self.listbox.grid(row=0, columnspan=2) # Create and place the Quit-button self.quitButton = tk.Button(self.frame, text='Select', command=self.selectAndQuit) self.quitButton.grid(row=1, column=0) self.quitButton = tk.Button(self.frame, text='Quit', command=self.quitFun) self.quitButton.grid(row=1, column=1)
[docs]def ts(data = None): """ Show the given time-series data. In addition to the (obvious) GUI-interactions, the following options are available: Keyboard interaction: * f ... forward (+ 1/2 frame) * n ... next (+ 1 frame) * b ... back ( -1/2 frame) * p ... previous (-1 frame) * z ... zoom (x-frame = 10% of total length) * a ... all (adjust x- and y-limits) * x ... exit Optimized y-scale: Often one wants to see data symmetrically about the zero-axis. To facilitate this display, adjusting the "Upper Limit" automatically sets the lower limit to the corresponding negative value. Logging: When "Log" is activated, right-mouse clicks are indicated with vertical bars, and the corresponding x-values are stored into the users home-directory, in the file "[varName].log". Since the name of the first value is unknown the first events are stored into "data.log". Load: Pushing the "Load"-button shows you all the plottable variables in your namespace. Plottable variables are: * ndarrays * Pandas DataFrames * Pandas Series Examples: To view a single plottable variable: >>> x = np.random.randn(100,3) >>> view.ts(x) To select a plottable variable from the workspace >>> x = np.random.randn(100,3) >>> t = np.arange(0,10,0.1) >>> y = np.sin(x) >>> view.ts(locals) """ root = tk.Tk() display = Display(root, data) root.mainloop()
if __name__ == '__main__': """ # 2D Viewer ----------------- data = np.random.randn(100,3) t = np.arange(0,2*np.pi,0.1) x = np.sin(t) # Show the data ts(locals()) #ts(data) print('Done') # 3D Viewer ---------------- # Set the parameters omega = np.r_[0, 10, 10] # [deg/s] duration = 2 rate = 100 q0 = [1, 0, 0, 0] ## Calculate the orientation dt = 1./rate num_rep = duration*rate omegas = np.tile(omega, [num_rep, 1]) q = quat.calc_quat(omegas, q0, rate, 'sf') #orientation(q) in_file = r'.\tests\data\data_xsens.txt' from skinematics.sensors.xsens import XSens data = XSens(in_file) out_file = 'demo_patch.mp4' title_text = 'Rotation Demo' orientation(data.quat, out_file=None, title_text='Well done!') # Test pygame-viewer phi = np.arange(360) q = quat.deg2quat(np.column_stack((phi, np.zeros((len(phi), 2))))) viewer = Orientation_Viewer_pygame(quat_in=q) viewer.run() """ # Test OpenGL viewer: in_file = r'.\tests\data\data_xsens.txt' from sensors.xsens import XSens data = XSens(in_file) orientation(data.quat, deltaT=5) #viewer = Orientation_OGL(quat_in=data.quat) #viewer.run(looping=False, rate=100)