<commit_before>from __future__ import division, with_statement

import pkg_resources
import data_format # strokelitude

import motmot.utils.config
import motmot.FastImage.FastImage as FastImage
import motmot.fview.traited_plugin as traited_plugin

# Weird bugs arise in class identity testing if this is not absolute
#   import...  ... but Python won't let me import it as absolute. Ahh,
#   well, workaround the bug by setting selectable on InstanceEditor
#   directly.
import plugin as strokelitude_plugin_module

import wx
import warnings, time, threading

import numpy as np
import cairo
import Queue
import os, warnings, pickle

import tables # pytables

import enthought.traits.api as traits
from enthought.traits.ui.api import View, Item, Group, Handler, HGroup, \
     VGroup, RangeEditor, InstanceEditor, ButtonEditor

from enthought.enable.api import Component, Container
from enthought.enable.wx_backend.api import Window
from enthought.chaco.api import DataView, ArrayDataSource, ScatterPlot, \
     LinePlot, LinearMapper, ArrayPlotData, Plot, gray
from enthought.enable.component_editor import ComponentEditor
import enthought.chaco.default_colormaps as default_colormaps
from enthought.chaco.data_range_1d import DataRange1D
from enthought.chaco.api import create_line_plot, add_default_axes, \
     add_default_grids
from enthought.chaco.tools.api import PanTool, ZoomTool
from enthought.chaco.tools.image_inspector_tool import ImageInspectorTool, \
     ImageInspectorOverlay
import motmot.fview_ext_trig.live_timestamp_modeler as modeler_module

DataReadyEvent = wx.NewEventType()
BGReadyEvent = wx.NewEventType()

D2R = np.pi/180.0
R2D = 180.0/np.pi

def load_plugins():
    # modified from motmot.fview.plugin_manager
    PluginClasses = []
    pkg_env = pkg_resources.Environment()
    for name in pkg_env:
        egg = pkg_env[name][0]
        modules = []

        for name in egg.get_entry_map('strokelitude.plugins'):
            egg.activate()
            entry_point = egg.get_entry_info('strokelitude.plugins', name)
            try:
                PluginClass = entry_point.load()
            except Exception,x:
                if int(os.environ.get('FVIEW_RAISE_ERRORS','0')):
                    raise
                else:
                    import warnings
                    warnings.warn('could not load plugin (set env var '
                                  'FVIEW_RAISE_ERRORS to raise error) %s: %s'%(
                        str(entry_point),str(x)))
                    continue
            PluginClasses.append( PluginClass )
            modules.append(entry_point.module_name)
    # make instances of plugins
    plugins = [PluginClass() for PluginClass in PluginClasses]
    return plugins

class BufferAllocator:
    def __call__(self, w, h):
        return FastImage.FastImage8u(FastImage.Size(w,h))

mult_sign={True:{'left':1,
                 'right':-1},
           False:{'left':-1,
                  'right':1}}

class MaskData(traits.HasTraits):
    # lengths, in pixels

    # The upper bounds of these ranges should all be dynamically set
    # based on image size
    x = traits.Range(0.0, 656.0, 401.0, mode='slider', set_enter=True)
    y = traits.Range(0.0, 491.0, 273.5, mode='slider', set_enter=True)
    wingsplit = traits.Range(0.0, 180.0, 70.3, mode='slider', set_enter=True)

    view_from_below = traits.Bool(True)

    # The bounds of these should be dynamically set
    r1 = traits.Range(0.0, 656.0, 22.0, mode='slider', set_enter=True)
    r2 = traits.Range(0.0, 656.0, 22.0, mode='slider', set_enter=True)

    # angles, in degrees
    alpha = traits.Range(-90.0, 90.0, 87.0, mode='slider', set_enter=True)
    beta = traits.Range(-90.0, 90.0, 14.0, mode='slider', set_enter=True)
    gamma = traits.Range(0.0, 360.0, 206.0, mode='slider', set_enter=True)

    # number of angular bins
    nbins = traits.Int(30)

    # these are just necesary for establishing limits in the view:
    maxx = traits.Float(699.9)
    maxy = traits.Float(699.9)
    maxdim = traits.Property(depends_on=['maxx','maxy'])

    rotation = traits.Property(depends_on=['gamma'])
    translation = traits.Property(depends_on=['x','y'])

    quads_left = traits.Property(depends_on=[
        'wingsplit', 'r1', 'r2', 'alpha', 'beta', 'nbins',
        'rotation','translation','view_from_below',
        ])

    mean_quad_angles_deg = traits.Property(depends_on=[
        'alpha', 'beta', 'nbins'])

    quads_right = traits.Property(depends_on=[
        'wingsplit', 'r1', 'r2', 'alpha', 'beta', 'nbins',
        'rotation','translation','view_from_below',
        ])

    extra_linesegs = traits.Property(depends_on=[
        'wingsplit', 'r1', 'r2', 'alpha', 'beta', 'nbins',
        'rotation','translation','view_from_below',
        ])

    all_linesegs = traits.Property(depends_on=[
        'quads_left', 'quads_right', 'extra_linesegs',
        ] )

    @traits.cached_property
    def _get_all_linesegs(self):
        # concatenate lists
        return (self.quads_left + self.quads_right + self.extra_linesegs)

    @traits.cached_property
    def _get_maxdim(self):
        return np.sqrt(self.maxx**2+self.maxy**2)

    def _get_wingsplit_translation(self,side):
        sign = mult_sign[self.view_from_below][side]
        return np.array([[0.0],[sign*self.wingsplit]])

    @traits.cached_property
    def _get_rotation(self):
        gamma = self.gamma*D2R
        return np.array([[ np.cos( gamma ), -np.sin(gamma)],
                         [ np.sin( gamma ), np.cos(gamma)]])

    @traits.cached_property
    def _get_translation(self):
        return np.array( [[self.x],
                          [self.y]],
                         dtype=np.float64 )

    traits_view = View( Group( ( Item('x',
                                      style='custom',
                                      ),
                                 Item('y',
                                      style='custom',
                                      ),
                                 Item('view_from_below'),
                                 Item('wingsplit',
                                      style='custom',
                                      ),
                                 Item('r1',style='custom',
                                      ),
                                 Item('r2',style='custom',
                                      ),
                                 Item('alpha',style='custom'),
                                 Item('beta',style='custom'),
                                 Item('gamma',style='custom'),
                                 ),
                               ),
                        title = 'Mask Parameters',
                        resizable = True,
                        )

    @traits.cached_property
    def _get_extra_linesegs(self):
        return self.get_extra_linesegs()

    @traits.cached_property
    def _get_quads_left(self):
        return self.get_quads('left')

    @traits.cached_property
    def _get_quads_right(self):
        return self.get_quads('right')

    @traits.cached_property
    def _get_mean_quad_angles_deg(self):
        x = np.linspace(self.alpha,self.beta,self.nbins+1)
        av = (x[:-1]+x[1:])*0.5 # local means
        return av

    def get_extra_linesegs(self):
        """return linesegments that contextualize parameters"""
        linesegs = []
        if 1:
            # longitudinal axis (along fly's x coord)
            verts = np.array([[-100,100],
                              [0,     0]],dtype=np.float)
            verts = np.dot(self.rotation, verts) + self.translation
            linesegs.append( verts.T.ravel() )
        if 1:
            # transverse axis (along fly's y coord)
            verts = np.array([[0,0],
                              [-10,10]],dtype=np.float)
            verts = np.dot(self.rotation, verts) + self.translation
            linesegs.append( verts.T.ravel() )
        if 1:
            # half-circle centers
            n = 10
            theta = np.linspace(0,2*np.pi,10)
            verts = np.array([ 5*np.cos(theta),
                               5*np.sin(theta) ])
            vleft = verts+self._get_wingsplit_translation('left')
            vright = verts+self._get_wingsplit_translation('right')

            vleft = np.dot(self.rotation, vleft) + self.translation
            vright = np.dot(self.rotation, vright) + self.translation

            linesegs.append( vleft.T.ravel() )
            linesegs.append( vright.T.ravel() )

        if 1:
            # left wing
            verts = np.array([[0.0, 0.0, 20.0],
                              [20.0,0.0, 0.0]])
            vleft = verts+self._get_wingsplit_translation('left')

            vleft = np.dot(self.rotation, vleft) + self.translation

            linesegs.append( vleft.T.ravel() )

        return linesegs

    def get_quads(self,side):
        """return linesegments outlining the pattern on a given side.

        This can be used to draw the pattern (e.g. wtih OpenGL).

        Return a list of linesegments [seg1, seg2, ..., segn]

        Each line segment is in the form (x0,y0, x1,y1, ..., xn,yn)

        """
        # This work is insignificant compared to the function call
        # overhead to draw the linesegs in the dumb way done by
        # wxglvideo (caching the values didn't reduce CPU time on
        # "Mesa DRI Intel(R) 946GZ 4.1.3002 x86/MMX/SSE2, OpenGL 1.4
        # Mesa 7.0.3-rc2") - ADS 20081015

        alpha = self.alpha*D2R
        beta = self.beta*D2R

        sign = mult_sign[self.view_from_below][side]

        # ordered from front to back, zero straight out from fly, positive towards head
        all_theta_user_coords = np.linspace(alpha,beta,self.nbins+1)

        # convert to drawing coord system
        all_theta = 90*D2R - all_theta_user_coords # make
        all_theta *= sign

        linesegs = []
        for i in range(self.nbins):
            theta = all_theta[i:(i+2)]
            # inner radius
            inner = np.array([self.r1*np.cos(theta),
                              self.r1*np.sin(theta)])
            # outer radius
            outer = np.array([self.r2*np.cos(theta[::-1]),
                              self.r2*np.sin(theta[::-1])])

            wing_verts =  np.hstack(( inner, outer, inner[:,np.newaxis,0] ))
            wing_verts += self._get_wingsplit_translation(side)

            wing_verts = np.dot(self.rotation, wing_verts) + self.translation
            linesegs.append( wing_verts.T.ravel() )

        return linesegs

    def index2angle(self,side,idx):
        """convert index to angle (in radians)"""
        alpha = self.alpha*D2R
        beta = self.beta*D2R

        frac = idx/self.nbins
        diff = beta-alpha
        return alpha+frac*diff

    def get_span_lineseg(self,side,theta_user):
        """draw line on side at angle theta_user (in radians)"""
        linesegs = []
        sign = mult_sign[self.view_from_below][side]
        theta = 90*D2R - theta_user
        theta *= sign

        verts = np.array( [[ 0, 1000.0*np.cos(theta)],
                           [ 0, 1000.0*np.sin(theta)]] )
        verts = verts + self._get_wingsplit_translation(side)
        verts = np.dot(self.rotation, verts) + self.translation
        linesegs.append( verts.T.ravel() )
        return linesegs

def quad2fastimage_offset(quad,width,height,debug_count=0):
    """convert a quad to an image vector"""
    surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                 width, height)
    ctx = cairo.Context(surface)
    ctx.set_source_rgb(0,0,0)

    ctx.set_operator(cairo.OPERATOR_SOURCE)
    ctx.paint()

    x0=quad[0]
    y0=quad[1]
    ctx.move_to(x0,y0)
    xmin = int(np.floor(x0))
    xmax = int(np.ceil(x0+1))
    ymin = int(np.floor(y0))
    ymax = int(np.ceil(y0+1))

    for (x,y) in zip(quad[2::2],
                     quad[3::2]):
        ctx.line_to(x,y)
        xmin = min(int(np.floor(x)),xmin)
        xmax = max(int(np.ceil(x+1)),xmax)
        ymin = min(int(np.floor(y)),ymin)
        ymax = max(int(np.ceil(y+1)),ymax)
    ctx.close_path()
    ctx.set_source_rgb(1,1,1)
    ctx.fill()
    if 0:
        fname = 'poly_left_%05d.png'%debug_count
        print 'saving',fname
        surface.write_to_png(fname)
    buf = surface.get_data()

    # Now, convert to numpy
    arr = np.frombuffer(buf, np.uint8)
    arr.shape = (height, width, 4)
    if 0:
        import scipy.misc.pilutil
        fname = 'mask_%05d.png'%debug_count
        print 'saving',fname
        im = scipy.misc.pilutil.toimage(arr)
        im.save(fname)
    arr = arr[:,:,0] # only red channel

    # Now, crop to only region of interest
    roi = arr[ymin:ymax,xmin:xmax]
    fi_roi = FastImage.asfastimage(roi)
    offset = xmin,ymin
    return fi_roi, xmin, ymin

def compute_sparse_mult(list_of_fi_offsets, fi_im):
    result = []
    for imA,left,bottom in list_of_fi_offsets:
        imB = fi_im.roi( left, bottom, imA.size )
        result.append( imA.dot(imB, imA.size) )
    result = np.array(result)
    return result

StrokelitudeDataDescription = data_format.StrokelitudeDataDescription
StrokelitudeDataCol_dtype = tables.Description(
    StrokelitudeDataDescription().columns)._v_nestedDescr

AnalogInputWordstreamDescription = data_format.AnalogInputWordstreamDescription
AnalogInputWordstream_dtype =  tables.Description(
    AnalogInputWordstreamDescription().columns)._v_nestedDescr

TimeDataDescription = data_format.TimeDataDescription
TimeData_dtype =  tables.Description(
    TimeDataDescription().columns)._v_nestedDescr

class LiveDataPlot(traits.HasTraits):
    left_angle = traits.Float
    right_angle = traits.Float
    left_plot = traits.Instance(Plot)
    right_plot = traits.Instance(Plot)
    traits_view = View(Group(Item('left_angle',style='readonly'),
                             Item('right_angle',style='readonly'),
                             Item('left_plot',
                                  editor=ComponentEditor(),
                                  height=300,
                                  width=500,
                                  show_label=False,
                                  ),
                             Item('right_plot',
                                  editor=ComponentEditor(),
                                  height=300,
                                  width=500,
                                  show_label=False,
                                  ),
                             ),
                       title='live data plot',
                       resizable = True,
                       )

class PopUpPlot(traits.HasTraits):
    plot = traits.Instance(Plot)
    traits_view = View(Group(
                             Item('plot',
                                  editor=ComponentEditor(),
                                  height=300,
                                  width=500,
                                  show_label=False,
                                  ),
                             ),
                       title='plot',
                       resizable = True,
                       )

class AmplitudeFinder(traits.HasTraits):
    strokelitude_instance = traits.Any # really, instance of StrokelitudeClass

    def post_init(self, strokelitude_instance):
        self.strokelitude_instance = strokelitude_instance # my parent

    def camera_starting_notification(self,cam_id,
                                     pixel_format=None,
                                     max_width=None,
                                     max_height=None):
        # this method exists primarily to be overriden
        pass

    def offline_startup_func(self,arg):
        # this method exists primarily to be overriden
        pass

    def process_frame(self,buf,buf_offset,timestamp,framenumber):
        """do the work"""
        left_angle_degrees = np.nan
        right_angle_degrees = np.nan

        return left_angle_degrees, right_angle_degrees

class BackgroundSubtractionDotProductFinder(AmplitudeFinder):
    mask_dirty = traits.Bool(False) # let __init__ set True

    recompute_mask = traits.Event
    take_bg = traits.Event
    clear_bg = traits.Event

    background_viewer = traits.Instance(PopUpPlot)
    live_data_viewer = traits.Instance(LiveDataPlot)
    live_data_plot = traits.Instance(LiveDataPlot)

    processing_enabled = traits.Bool(False)
    light_on_dark = traits.Bool(True)
    threshold_fraction = traits.Float(0.5)

    traits_view = View( Group( Item('recompute_mask',
                                    editor=ButtonEditor(),show_label=False),
                               Item('processing_enabled'),
                               Item('take_bg',
                                    editor=ButtonEditor(),show_label=False),
                               Item('clear_bg',
                                    editor=ButtonEditor(),show_label=False),
                               Item('background_viewer',show_label=False),
                               Item('live_data_viewer',show_label=False),
                               Item('live_data_plot',show_label=False),
                               Item(name='threshold_fraction',
                                    ),
                               Item(name='light_on_dark',
                                    ),
                               ))
    def __init__(self,*args,**kwargs):
        super(BackgroundSubtractionDotProductFinder,self).__init__(*args,**kwargs)
        self.bg_cmd_queue = Queue.Queue()
        self.new_bg_image_lock = threading.Lock()
        self.new_bg_image = None
        self.recomputing_lock = threading.Lock()

        self.background_viewer = PopUpPlot()
        self.live_data_plot = LiveDataPlot()
        self.vals_queue = Queue.Queue()

    def _take_bg_fired(self):
        self.mask_dirty = True
        self.bg_cmd_queue.put('take')

    def _clear_bg_fired(self):
        self.mask_dirty = True
        self.bg_cmd_queue.put('clear')

    def OnNewBGReady(self,event):
        with self.new_bg_image_lock:
            new_bg_image = self.new_bg_image
            self.new_bg_image = None
        if self.strokelitude_instance.save_to_disk:
            print 'should save BG image to disk'
        self.bg_pd.set_data("imagedata", new_bg_image)
        self.bg_plot.request_redraw()
        # we have not yet done the sparse multiplication yet

    def OnDataReady(self, event):
        lrvals = None
        while 1:
            # We only care about most recent results. Discard older data.
            try:
                lrvals = self.vals_queue.get_nowait()
            except Queue.Empty:
                break
        if lrvals is None:
            return

        left_vals, right_vals,left_angle_degrees,right_angle_degrees = lrvals
        self.live_pd['left'].set_data('live',left_vals)
        self.live_pd['right'].set_data('live',right_vals)
        self.live_data_plot.left_angle = left_angle_degrees
        self.live_data_plot.right_angle = right_angle_degrees

    def post_init(self, *args,**kwargs):
        super(BackgroundSubtractionDotProductFinder,self).post_init(*args,**kwargs)

        self.strokelitude_instance.frame.Connect( -1, -1, DataReadyEvent, self.OnDataReady )
        self.strokelitude_instance.frame.Connect( -1, -1, BGReadyEvent, self.OnNewBGReady )
        self.mask_dirty = True

        if 1:
            self.live_pd = {}
            for side in ['left','right']:

                x=self.strokelitude_instance.maskdata.mean_quad_angles_deg
                y=np.zeros_like(x)

                pd=ArrayPlotData(index=x)
                pd.set_data('live',y)
                pd.set_data('bg',np.zeros_like(x))
                self.live_pd[side]=pd
                plot = Plot(pd, title=side,
                            padding=50,
                            border_visible=True,
                            overlay_border=True)
                plot.legend.visible = True
                plot.plot(("index", 'live'), name=side, color="red")
                plot.plot(("index", "bg"), name="background", color="blue")

                if side=='left':
                    self.left_plot=plot
                    self.live_data_plot.left_plot=plot
                elif side=='right':
                    self.right_plot=plot
                    self.live_data_plot.right_plot=plot
            del plot,x,y,pd # don't let these escape and pollute namespace

        if 1:
            # a temporary initial background image
            image = np.zeros((640,480), dtype=np.uint8)

            # Create a plot data obect and give it this data
            self.bg_pd = ArrayPlotData()
            self.bg_pd.set_data("imagedata", image)

            # Create the plot
            self.bg_plot = Plot(self.bg_pd, default_origin="top left")
            self.bg_plot.x_axis.orientation = "top"
            colormap = default_colormaps.gray(DataRange1D(low=0,high=255))
            self.bg_plot.img_plot("imagedata",colormap=colormap)[0]

            self.bg_plot.padding = 30
            self.bg_plot.bgcolor = "white"

            # Attach some tools to the plot
            self.bg_plot.tools.append(PanTool(self.bg_plot,
                                              constrain_key="shift"))
            self.bg_plot.overlays.append(ZoomTool(component=self.bg_plot,
                                          tool_mode="box", always_on=False))

            self.background_viewer.plot = self.bg_plot

        self.strokelitude_instance.maskdata.on_trait_change( self.on_mask_change )

    def camera_starting_notification(self,cam_id,
                                     pixel_format=None,
                                     max_width=None,
                                     max_height=None):

        self.width = max_width
        self.height = max_height

        with self.recomputing_lock:
            self.bg_image = np.zeros( (self.height,self.width), dtype=np.uint8 )

    def offline_startup_func(self,arg):
        # automatically recompute mask and enable processing in offline mode
        self.recompute_mask = True # fire event
        assert self.mask_dirty==False
        self.processing_enabled = True

    def process_frame(self,buf,buf_offset,timestamp,framenumber):
        """do the work"""
        left_angle_degrees = np.nan
        right_angle_degrees = np.nan

        have_lock = self.recomputing_lock.acquire(False) # don't block
        if not have_lock:
            return left_angle_degrees, right_angle_degrees

        try:
            command = None
            while 1:
                try:
                    # all we care about is last command
                    command = self.bg_cmd_queue.get_nowait()
                except Queue.Empty, err:
                    break

            bg_image_changed = False
            this_image = np.asarray(buf)
            if command == 'clear':
                bg_image_changed = True
                self.bg_image = np.zeros_like( this_image )
            elif command == 'take':
                bg_image_changed = True
                self.bg_image = np.array( this_image, copy=True )
            if bg_image_changed:
                with self.new_bg_image_lock:
                    self.new_bg_image = np.array( self.bg_image, copy=True )

                event = wx.CommandEvent(BGReadyEvent)
                event.SetEventObject(self.strokelitude_instance.frame)

                # trigger call to self.OnNewBGReady
                wx.PostEvent(self.strokelitude_instance.frame, event)

            # XXX naughty to cross thread boundary to get enabled_box value, too
            if (self.processing_enabled and not self.mask_dirty):

                h,w = this_image.shape
                if not (self.width==w and self.height==h):
                    raise NotImplementedError('need to support ROI')

                left_mat = self.left_mat
                right_mat = self.right_mat

                bg_left_vec = self.bg_left_vec
                bg_right_vec = self.bg_right_vec

                this_image_fi = FastImage.asfastimage(this_image)
                left_vals  = compute_sparse_mult(left_mat,this_image_fi)
                right_vals = compute_sparse_mult(right_mat,this_image_fi)

                left_vals = left_vals - bg_left_vec
                right_vals = right_vals - bg_right_vec

                results = []
                for side in ('left','right'):
                    if side=='left':
                        vals = left_vals
                    else:
                        vals = right_vals

                    if not self.light_on_dark:
                        vals = -vals

                    min_val = vals.min()
                    mid_val = (vals.max() - min_val)*self.threshold_fraction + min_val
                    if min_val==mid_val:
                        # no variation in luminance
                        interp_idx = -1
                    else:
                        first_idx=None
                        for i in range(len(vals)-1):
                            if (vals[i] < mid_val) and (vals[i+1] >= mid_val):
                                first_idx = i
                                second_idx = i+1
                                break
                        if first_idx is None:
                            interp_idx = -1
                        else:
                            # slope (indices are unity apart)
                            # y = mx+b
                            m = vals[second_idx] - vals[first_idx]
                            b = vals[first_idx] - m*first_idx
                            interp_idx = (mid_val-b)/m

                    #latency_sec = time.time()-timestamp
                    #print 'msec % 5.1f'%(latency_sec*1000.0,)

                    if interp_idx != -1:
                        angle_radians = self.strokelitude_instance.maskdata.index2angle(side,
                                                                                        interp_idx)
                        results.append( angle_radians*R2D ) # keep results in degrees

                    else:
                        results.append( np.nan )

                left_angle_degrees, right_angle_degrees = results

                ## for queue in self.plugin_data_queues:
                ##     queue.put( (cam_id,timestamp,framenumber,results) )

                # send values from each quad to be drawn
                self.vals_queue.put((left_vals, right_vals,
                                     left_angle_degrees,right_angle_degrees,
                                     ))
                event = wx.CommandEvent(DataReadyEvent)
                event.SetEventObject(self.strokelitude_instance.frame)

                # trigger call to self.OnDataReady
                wx.PostEvent(self.strokelitude_instance.frame, event)

        finally:
            self.recomputing_lock.release()
        return left_angle_degrees, right_angle_degrees

    def _recompute_mask_fired(self):
        with self.recomputing_lock:
            count = 0

            left_quads = self.strokelitude_instance.maskdata.quads_left
            right_quads = self.strokelitude_instance.maskdata.quads_right

            self.left_mat = []
            for quad in left_quads:
                fi_roi, left, bottom = quad2fastimage_offset(
                    quad,
                    self.width,self.height,
                    debug_count=count)
                self.left_mat.append( (fi_roi, left, bottom) )
                count+=1

            self.right_mat = []
            for quad in right_quads:
                fi_roi, left, bottom = quad2fastimage_offset(
                    quad,
                    self.width,self.height,
                    debug_count=count)
                self.right_mat.append( (fi_roi, left, bottom) )
                count+=1

            bg = FastImage.asfastimage(self.bg_image)
            self.bg_left_vec = compute_sparse_mult(self.left_mat,bg)
            self.bg_right_vec= compute_sparse_mult(self.right_mat,bg)

            self.live_pd['left'].set_data('bg',self.bg_left_vec)
            self.live_pd['right'].set_data('bg',self.bg_right_vec)<commit_msg>fview_strokelitude: move mask calculation to MaskData<commit_after>from __future__ import division, with_statement

import pkg_resources
import data_format # strokelitude

import motmot.utils.config
import motmot.FastImage.FastImage as FastImage
import motmot.fview.traited_plugin as traited_plugin

# Weird bugs arise in class identity testing if this is not absolute
#   import...  ... but Python won't let me import it as absolute. Ahh,
#   well, workaround the bug by setting selectable on InstanceEditor
#   directly.
import plugin as strokelitude_plugin_module

import wx
import warnings, time, threading

import numpy as np
import cairo
import Queue
import os, warnings, pickle

import tables # pytables

import enthought.traits.api as traits
from enthought.traits.ui.api import View, Item, Group, Handler, HGroup, \
     VGroup, RangeEditor, InstanceEditor, ButtonEditor

from enthought.enable.api import Component, Container
from enthought.enable.wx_backend.api import Window
from enthought.chaco.api import DataView, ArrayDataSource, ScatterPlot, \
     LinePlot, LinearMapper, ArrayPlotData, Plot, gray
from enthought.enable.component_editor import ComponentEditor
import enthought.chaco.default_colormaps as default_colormaps
from enthought.chaco.data_range_1d import DataRange1D
from enthought.chaco.api import create_line_plot, add_default_axes, \
     add_default_grids
from enthought.chaco.tools.api import PanTool, ZoomTool
from enthought.chaco.tools.image_inspector_tool import ImageInspectorTool, \
     ImageInspectorOverlay
import motmot.fview_ext_trig.live_timestamp_modeler as modeler_module

DataReadyEvent = wx.NewEventType()
BGReadyEvent = wx.NewEventType()

D2R = np.pi/180.0
R2D = 180.0/np.pi

def load_plugins():
    # modified from motmot.fview.plugin_manager
    PluginClasses = []
    pkg_env = pkg_resources.Environment()
    for name in pkg_env:
        egg = pkg_env[name][0]
        modules = []

        for name in egg.get_entry_map('strokelitude.plugins'):
            egg.activate()
            entry_point = egg.get_entry_info('strokelitude.plugins', name)
            try:
                PluginClass = entry_point.load()
            except Exception,x:
                if int(os.environ.get('FVIEW_RAISE_ERRORS','0')):
                    raise
                else:
                    import warnings
                    warnings.warn('could not load plugin (set env var '
                                  'FVIEW_RAISE_ERRORS to raise error) %s: %s'%(
                        str(entry_point),str(x)))
                    continue
            PluginClasses.append( PluginClass )
            modules.append(entry_point.module_name)
    # make instances of plugins
    plugins = [PluginClass() for PluginClass in PluginClasses]
    return plugins

class BufferAllocator:
    def __call__(self, w, h):
        return FastImage.FastImage8u(FastImage.Size(w,h))

mult_sign={True:{'left':1,
                 'right':-1},
           False:{'left':-1,
                  'right':1}}

class MaskData(traits.HasTraits):
    # lengths, in pixels

    # The upper bounds of these ranges should all be dynamically set
    # based on image size
    x = traits.Range(0.0, 656.0, 401.0, mode='slider', set_enter=True)
    y = traits.Range(0.0, 491.0, 273.5, mode='slider', set_enter=True)
    wingsplit = traits.Range(0.0, 180.0, 70.3, mode='slider', set_enter=True)

    view_from_below = traits.Bool(True)

    # The bounds of these should be dynamically set
    r1 = traits.Range(0.0, 656.0, 22.0, mode='slider', set_enter=True)
    r2 = traits.Range(0.0, 656.0, 22.0, mode='slider', set_enter=True)

    # angles, in degrees
    alpha = traits.Range(-90.0, 90.0, 87.0, mode='slider', set_enter=True)
    beta = traits.Range(-90.0, 90.0, 14.0, mode='slider', set_enter=True)
    gamma = traits.Range(0.0, 360.0, 206.0, mode='slider', set_enter=True)

    # number of angular bins
    nbins = traits.Int(30)

    # these are just necesary for establishing limits in the view:
    maxx = traits.Float(699.9)
    maxy = traits.Float(699.9)
    maxdim = traits.Property(depends_on=['maxx','maxy'])

    rotation = traits.Property(depends_on=['gamma'])
    translation = traits.Property(depends_on=['x','y'])

    quads_left = traits.Property(depends_on=[
        'wingsplit', 'r1', 'r2', 'alpha', 'beta', 'nbins',
        'rotation','translation','view_from_below',
        ])

    left_mat = traits.Property(depends_on=['quads_left'])

    mean_quad_angles_deg = traits.Property(depends_on=[
        'alpha', 'beta', 'nbins'])

    quads_right = traits.Property(depends_on=[
        'wingsplit', 'r1', 'r2', 'alpha', 'beta', 'nbins',
        'rotation','translation','view_from_below',
        ])
    right_mat = traits.Property(depends_on=['quads_right'])

    extra_linesegs = traits.Property(depends_on=[
        'wingsplit', 'r1', 'r2', 'alpha', 'beta', 'nbins',
        'rotation','translation','view_from_below',
        ])

    all_linesegs = traits.Property(depends_on=[
        'quads_left', 'quads_right', 'extra_linesegs',
        ] )

    @traits.cached_property
    def _get_all_linesegs(self):
        # concatenate lists
        return (self.quads_left + self.quads_right + self.extra_linesegs)

    @traits.cached_property
    def _get_maxdim(self):
        return np.sqrt(self.maxx**2+self.maxy**2)

    def _get_wingsplit_translation(self,side):
        sign = mult_sign[self.view_from_below][side]
        return np.array([[0.0],[sign*self.wingsplit]])

    @traits.cached_property
    def _get_rotation(self):
        gamma = self.gamma*D2R
        return np.array([[ np.cos( gamma ), -np.sin(gamma)],
                         [ np.sin( gamma ), np.cos(gamma)]])

    @traits.cached_property
    def _get_translation(self):
        return np.array( [[self.x],
                          [self.y]],
                         dtype=np.float64 )

    traits_view = View( Group( ( Item('x',
                                      style='custom',
                                      ),
                                 Item('y',
                                      style='custom',
                                      ),
                                 Item('view_from_below'),
                                 Item('wingsplit',
                                      style='custom',
                                      ),
                                 Item('r1',style='custom',
                                      ),
                                 Item('r2',style='custom',
                                      ),
                                 Item('alpha',style='custom'),
                                 Item('beta',style='custom'),
                                 Item('gamma',style='custom'),
                                 ),
                               ),
                        title = 'Mask Parameters',
                        resizable = True,
                        )

    @traits.cached_property
    def _get_extra_linesegs(self):
        return self.get_extra_linesegs()

    @traits.cached_property
    def _get_quads_left(self):
        return self.get_quads('left')

    @traits.cached_property
    def _get_quads_right(self):
        return self.get_quads('right')

    @traits.cached_property
    def _get_left_mat(self):
        result = []
        for quad in self.quads_left:
            fi_roi, left, bottom = quad2fastimage_offset(quad,
                                                         self.maxx,self.maxy)
            result.append( (fi_roi, left, bottom) )
        return result

    @traits.cached_property
    def _get_right_mat(self):
        result = []
        for quad in self.quads_right:
            fi_roi, left, bottom = quad2fastimage_offset(quad,
                                                         self.maxx,self.maxy)
            result.append( (fi_roi, left, bottom) )
        return result

    @traits.cached_property
    def _get_mean_quad_angles_deg(self):
        x = np.linspace(self.alpha,self.beta,self.nbins+1)
        av = (x[:-1]+x[1:])*0.5 # local means
        return av

    def get_extra_linesegs(self):
        """return linesegments that contextualize parameters"""
        linesegs = []
        if 1:
            # longitudinal axis (along fly's x coord)
            verts = np.array([[-100,100],
                              [0,     0]],dtype=np.float)
            verts = np.dot(self.rotation, verts) + self.translation
            linesegs.append( verts.T.ravel() )
        if 1:
            # transverse axis (along fly's y coord)
            verts = np.array([[0,0],
                              [-10,10]],dtype=np.float)
            verts = np.dot(self.rotation, verts) + self.translation
            linesegs.append( verts.T.ravel() )
        if 1:
            # half-circle centers
            n = 10
            theta = np.linspace(0,2*np.pi,10)
            verts = np.array([ 5*np.cos(theta),
                               5*np.sin(theta) ])
            vleft = verts+self._get_wingsplit_translation('left')
            vright = verts+self._get_wingsplit_translation('right')

            vleft = np.dot(self.rotation, vleft) + self.translation
            vright = np.dot(self.rotation, vright) + self.translation

            linesegs.append( vleft.T.ravel() )
            linesegs.append( vright.T.ravel() )

        if 1:
            # left wing
            verts = np.array([[0.0, 0.0, 20.0],
                              [20.0,0.0, 0.0]])
            vleft = verts+self._get_wingsplit_translation('left')

            vleft = np.dot(self.rotation, vleft) + self.translation

            linesegs.append( vleft.T.ravel() )

        return linesegs

    def get_quads(self,side):
        """return linesegments outlining the pattern on a given side.

        This can be used to draw the pattern (e.g. wtih OpenGL).

        Return a list of linesegments [seg1, seg2, ..., segn]

        Each line segment is in the form (x0,y0, x1,y1, ..., xn,yn)

        """
        # This work is insignificant compared to the function call
        # overhead to draw the linesegs in the dumb way done by
        # wxglvideo (caching the values didn't reduce CPU time on
        # "Mesa DRI Intel(R) 946GZ 4.1.3002 x86/MMX/SSE2, OpenGL 1.4
        # Mesa 7.0.3-rc2") - ADS 20081015

        alpha = self.alpha*D2R
        beta = self.beta*D2R

        sign = mult_sign[self.view_from_below][side]

        # ordered from front to back, zero straight out from fly, positive towards head
        all_theta_user_coords = np.linspace(alpha,beta,self.nbins+1)

        # convert to drawing coord system
        all_theta = 90*D2R - all_theta_user_coords # make
        all_theta *= sign

        linesegs = []
        for i in range(self.nbins):
            theta = all_theta[i:(i+2)]
            # inner radius
            inner = np.array([self.r1*np.cos(theta),
                              self.r1*np.sin(theta)])
            # outer radius
            outer = np.array([self.r2*np.cos(theta[::-1]),
                              self.r2*np.sin(theta[::-1])])

            wing_verts =  np.hstack(( inner, outer, inner[:,np.newaxis,0] ))
            wing_verts += self._get_wingsplit_translation(side)

            wing_verts = np.dot(self.rotation, wing_verts) + self.translation
            linesegs.append( wing_verts.T.ravel() )

        return linesegs

    def index2angle(self,side,idx):
        """convert index to angle (in radians)"""
        alpha = self.alpha*D2R
        beta = self.beta*D2R

        frac = idx/self.nbins
        diff = beta-alpha
        return alpha+frac*diff

    def get_span_lineseg(self,side,theta_user):
        """draw line on side at angle theta_user (in radians)"""
        linesegs = []
        sign = mult_sign[self.view_from_below][side]
        theta = 90*D2R - theta_user
        theta *= sign

        verts = np.array( [[ 0, 1000.0*np.cos(theta)],
                           [ 0, 1000.0*np.sin(theta)]] )
        verts = verts + self._get_wingsplit_translation(side)
        verts = np.dot(self.rotation, verts) + self.translation
        linesegs.append( verts.T.ravel() )
        return linesegs

def quad2fastimage_offset(quad,width,height,debug_count=0):
    """convert a quad to an image vector"""
    surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                 width, height)
    ctx = cairo.Context(surface)
    ctx.set_source_rgb(0,0,0)

    ctx.set_operator(cairo.OPERATOR_SOURCE)
    ctx.paint()

    x0=quad[0]
    y0=quad[1]
    ctx.move_to(x0,y0)
    xmin = int(np.floor(x0))
    xmax = int(np.ceil(x0+1))
    ymin = int(np.floor(y0))
    ymax = int(np.ceil(y0+1))

    for (x,y) in zip(quad[2::2],
                     quad[3::2]):
        ctx.line_to(x,y)
        xmin = min(int(np.floor(x)),xmin)
        xmax = max(int(np.ceil(x+1)),xmax)
        ymin = min(int(np.floor(y)),ymin)
        ymax = max(int(np.ceil(y+1)),ymax)
    ctx.close_path()
    ctx.set_source_rgb(1,1,1)
    ctx.fill()
    if 0:
        fname = 'poly_left_%05d.png'%debug_count
        print 'saving',fname
        surface.write_to_png(fname)
    buf = surface.get_data()

    # Now, convert to numpy
    arr = np.frombuffer(buf, np.uint8)
    arr.shape = (height, width, 4)
    if 0:
        import scipy.misc.pilutil
        fname = 'mask_%05d.png'%debug_count
        print 'saving',fname
        im = scipy.misc.pilutil.toimage(arr)
        im.save(fname)
    arr = arr[:,:,0] # only red channel

    # Now, crop to only region of interest
    roi = arr[ymin:ymax,xmin:xmax]
    fi_roi = FastImage.asfastimage(roi)
    offset = xmin,ymin
    return fi_roi, xmin, ymin

def compute_sparse_mult(list_of_fi_offsets, fi_im):
    result = []
    for imA,left,bottom in list_of_fi_offsets:
        imB = fi_im.roi( left, bottom, imA.size )
        result.append( imA.dot(imB, imA.size) )
    result = np.array(result)
    return result

StrokelitudeDataDescription = data_format.StrokelitudeDataDescription
StrokelitudeDataCol_dtype = tables.Description(
    StrokelitudeDataDescription().columns)._v_nestedDescr

AnalogInputWordstreamDescription = data_format.AnalogInputWordstreamDescription
AnalogInputWordstream_dtype =  tables.Description(
    AnalogInputWordstreamDescription().columns)._v_nestedDescr

TimeDataDescription = data_format.TimeDataDescription
TimeData_dtype =  tables.Description(
    TimeDataDescription().columns)._v_nestedDescr

class LiveDataPlot(traits.HasTraits):
    left_angle = traits.Float
    right_angle = traits.Float
    left_plot = traits.Instance(Plot)
    right_plot = traits.Instance(Plot)
    traits_view = View(Group(Item('left_angle',style='readonly'),
                             Item('right_angle',style='readonly'),
                             Item('left_plot',
                                  editor=ComponentEditor(),
                                  height=300,
                                  width=500,
                                  show_label=False,
                                  ),
                             Item('right_plot',
                                  editor=ComponentEditor(),
                                  height=300,
                                  width=500,
                                  show_label=False,
                                  ),
                             ),
                       title='live data plot',
                       resizable = True,
                       )

class PopUpPlot(traits.HasTraits):
    plot = traits.Instance(Plot)
    traits_view = View(Group(
                             Item('plot',
                                  editor=ComponentEditor(),
                                  height=300,
                                  width=500,
                                  show_label=False,
                                  ),
                             ),
                       title='plot',
                       resizable = True,
                       )

class AmplitudeFinder(traits.HasTraits):
    strokelitude_instance = traits.Any # really, instance of StrokelitudeClass

    def post_init(self, strokelitude_instance):
        self.strokelitude_instance = strokelitude_instance # my parent

    def camera_starting_notification(self,cam_id,
                                     pixel_format=None,
                                     max_width=None,
                                     max_height=None):
        # this method exists primarily to be overriden
        pass

    def offline_startup_func(self,arg):
        # this method exists primarily to be overriden
        pass

    def process_frame(self,buf,buf_offset,timestamp,framenumber):
        """do the work"""
        left_angle_degrees = np.nan
        right_angle_degrees = np.nan

        return left_angle_degrees, right_angle_degrees

class BackgroundSubtractionDotProductFinder(AmplitudeFinder):
    mask_dirty = traits.Bool(False) # let __init__ set True

    recompute_mask = traits.Event
    take_bg = traits.Event
    clear_bg = traits.Event

    background_viewer = traits.Instance(PopUpPlot)
    live_data_viewer = traits.Instance(LiveDataPlot)
    live_data_plot = traits.Instance(LiveDataPlot)

    processing_enabled = traits.Bool(False)
    light_on_dark = traits.Bool(True)
    threshold_fraction = traits.Float(0.5)

    traits_view = View( Group( Item('recompute_mask',
                                    editor=ButtonEditor(),show_label=False),
                               Item('processing_enabled'),
                               Item('take_bg',
                                    editor=ButtonEditor(),show_label=False),
                               Item('clear_bg',
                                    editor=ButtonEditor(),show_label=False),
                               Item('background_viewer',show_label=False),
                               Item('live_data_viewer',show_label=False),
                               Item('live_data_plot',show_label=False),
                               Item(name='threshold_fraction',
                                    ),
                               Item(name='light_on_dark',
                                    ),
                               ))
    def __init__(self,*args,**kwargs):
        super(BackgroundSubtractionDotProductFinder,self).__init__(*args,**kwargs)
        self.bg_cmd_queue = Queue.Queue()
        self.new_bg_image_lock = threading.Lock()
        self.new_bg_image = None
        self.recomputing_lock = threading.Lock()

        self.background_viewer = PopUpPlot()
        self.live_data_plot = LiveDataPlot()
        self.vals_queue = Queue.Queue()

    def _take_bg_fired(self):
        self.mask_dirty = True
        self.bg_cmd_queue.put('take')

    def _clear_bg_fired(self):
        self.mask_dirty = True
        self.bg_cmd_queue.put('clear')

    def OnNewBGReady(self,event):
        with self.new_bg_image_lock:
            new_bg_image = self.new_bg_image
            self.new_bg_image = None
        if self.strokelitude_instance.save_to_disk:
            print 'should save BG image to disk'
        self.bg_pd.set_data("imagedata", new_bg_image)
        self.bg_plot.request_redraw()
        # we have not yet done the sparse multiplication yet

    def OnDataReady(self, event):
        lrvals = None
        while 1:
            # We only care about most recent results. Discard older data.
            try:
                lrvals = self.vals_queue.get_nowait()
            except Queue.Empty:
                break
        if lrvals is None:
            return

        left_vals, right_vals,left_angle_degrees,right_angle_degrees = lrvals
        self.live_pd['left'].set_data('live',left_vals)
        self.live_pd['right'].set_data('live',right_vals)
        self.live_data_plot.left_angle = left_angle_degrees
        self.live_data_plot.right_angle = right_angle_degrees

    def post_init(self, *args,**kwargs):
        super(BackgroundSubtractionDotProductFinder,self).post_init(*args,**kwargs)

        self.strokelitude_instance.frame.Connect( -1, -1, DataReadyEvent, self.OnDataReady )
        self.strokelitude_instance.frame.Connect( -1, -1, BGReadyEvent, self.OnNewBGReady )
        self.mask_dirty = True

        if 1:
            self.live_pd = {}
            for side in ['left','right']:

                x=self.strokelitude_instance.maskdata.mean_quad_angles_deg
                y=np.zeros_like(x)

                pd=ArrayPlotData(index=x)
                pd.set_data('live',y)
                pd.set_data('bg',np.zeros_like(x))
                self.live_pd[side]=pd
                plot = Plot(pd, title=side,
                            padding=50,
                            border_visible=True,
                            overlay_border=True)
                plot.legend.visible = True
                plot.plot(("index", 'live'), name=side, color="red")
                plot.plot(("index", "bg"), name="background", color="blue")

                if side=='left':
                    self.left_plot=plot
                    self.live_data_plot.left_plot=plot
                elif side=='right':
                    self.right_plot=plot
                    self.live_data_plot.right_plot=plot
            del plot,x,y,pd # don't let these escape and pollute namespace

        if 1:
            # a temporary initial background image
            image = np.zeros((640,480), dtype=np.uint8)

            # Create a plot data obect and give it this data
            self.bg_pd = ArrayPlotData()
            self.bg_pd.set_data("imagedata", image)

            # Create the plot
            self.bg_plot = Plot(self.bg_pd, default_origin="top left")
            self.bg_plot.x_axis.orientation = "top"
            colormap = default_colormaps.gray(DataRange1D(low=0,high=255))
            self.bg_plot.img_plot("imagedata",colormap=colormap)[0]

            self.bg_plot.padding = 30
            self.bg_plot.bgcolor = "white"

            # Attach some tools to the plot
            self.bg_plot.tools.append(PanTool(self.bg_plot,
                                              constrain_key="shift"))
            self.bg_plot.overlays.append(ZoomTool(component=self.bg_plot,
                                          tool_mode="box", always_on=False))

            self.background_viewer.plot = self.bg_plot

        self.strokelitude_instance.maskdata.on_trait_change( self.on_mask_change )

    def camera_starting_notification(self,cam_id,
                                     pixel_format=None,
                                     max_width=None,
                                     max_height=None):

        self.width = max_width
        self.height = max_height

        with self.recomputing_lock:
            self.bg_image = np.zeros( (self.height,self.width), dtype=np.uint8 )

    def offline_startup_func(self,arg):
        # automatically recompute mask and enable processing in offline mode
        self.recompute_mask = True # fire event
        assert self.mask_dirty==False
        self.processing_enabled = True

    def process_frame(self,buf,buf_offset,timestamp,framenumber):
        """do the work"""
        left_angle_degrees = np.nan
        right_angle_degrees = np.nan

        have_lock = self.recomputing_lock.acquire(False) # don't block
        if not have_lock:
            return left_angle_degrees, right_angle_degrees

        try:
            command = None
            while 1:
                try:
                    # all we care about is last command
                    command = self.bg_cmd_queue.get_nowait()
                except Queue.Empty, err:
                    break

            bg_image_changed = False
            this_image = np.asarray(buf)
            if command == 'clear':
                bg_image_changed = True
                self.bg_image = np.zeros_like( this_image )
            elif command == 'take':
                bg_image_changed = True
                self.bg_image = np.array( this_image, copy=True )
            if bg_image_changed:
                with self.new_bg_image_lock:
                    self.new_bg_image = np.array( self.bg_image, copy=True )

                event = wx.CommandEvent(BGReadyEvent)
                event.SetEventObject(self.strokelitude_instance.frame)

                # trigger call to self.OnNewBGReady
                wx.PostEvent(self.strokelitude_instance.frame, event)

            # XXX naughty to cross thread boundary to get enabled_box value, too
            if (self.processing_enabled and not self.mask_dirty):

                h,w = this_image.shape
                if not (self.width==w and self.height==h):
                    raise NotImplementedError('need to support ROI')

                left_mat = self.left_mat
                right_mat = self.right_mat

                bg_left_vec = self.bg_left_vec
                bg_right_vec = self.bg_right_vec

                this_image_fi = FastImage.asfastimage(this_image)
                left_vals  = compute_sparse_mult(left_mat,this_image_fi)
                right_vals = compute_sparse_mult(right_mat,this_image_fi)

                left_vals = left_vals - bg_left_vec
                right_vals = right_vals - bg_right_vec

                results = []
                for side in ('left','right'):
                    if side=='left':
                        vals = left_vals
                    else:
                        vals = right_vals

                    if not self.light_on_dark:
                        vals = -vals

                    min_val = vals.min()
                    mid_val = (vals.max() - min_val)*self.threshold_fraction + min_val
                    if min_val==mid_val:
                        # no variation in luminance
                        interp_idx = -1
                    else:
                        first_idx=None
                        for i in range(len(vals)-1):
                            if (vals[i] < mid_val) and (vals[i+1] >= mid_val):
                                first_idx = i
                                second_idx = i+1
                                break
                        if first_idx is None:
                            interp_idx = -1
                        else:
                            # slope (indices are unity apart)
                            # y = mx+b
                            m = vals[second_idx] - vals[first_idx]
                            b = vals[first_idx] - m*first_idx
                            interp_idx = (mid_val-b)/m

                    #latency_sec = time.time()-timestamp
                    #print 'msec % 5.1f'%(latency_sec*1000.0,)

                    if interp_idx != -1:
                        angle_radians = self.strokelitude_instance.maskdata.index2angle(side,
                                                                                        interp_idx)
                        results.append( angle_radians*R2D ) # keep results in degrees

                    else:
                        results.append( np.nan )

                left_angle_degrees, right_angle_degrees = results

                ## for queue in self.plugin_data_queues:
                ##     queue.put( (cam_id,timestamp,framenumber,results) )

                # send values from each quad to be drawn
                self.vals_queue.put((left_vals, right_vals,
                                     left_angle_degrees,right_angle_degrees,
                                     ))
                event = wx.CommandEvent(DataReadyEvent)
                event.SetEventObject(self.strokelitude_instance.frame)

                # trigger call to self.OnDataReady
                wx.PostEvent(self.strokelitude_instance.frame, event)

        finally:
            self.recomputing_lock.release()
        return left_angle_degrees, right_angle_degrees

    def _recompute_mask_fired(self):
        with self.recomputing_lock:
            left_quads = self.strokelitude_instance.maskdata.quads_left
            right_quads = self.strokelitude_instance.maskdata.quads_right

            self.left_mat = self.strokelitude_instance.maskdata.left_mat
            self.right_mat = self.strokelitude_instance.maskdata.right_mat

            bg = FastImage.asfastimage(self.bg_image)
            self.bg_left_vec = compute_sparse_mult(self.left_mat,bg)
            self.bg_right_vec= compute_sparse_mult(self.right_mat,bg)

            self.live_pd['left'].set_data('bg',self.bg_left_vec)
            self.live_pd['right'].set_data('bg',self.bg_right_vec)
{'fraction_non_alphanumeric': 0.09637834036568214, 'fraction_numerical': 0.011656118143459916, 'mean_word_length': 1.7153427534848196, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 116, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '216229', 'n_tokens_mistral': 17201, 'n_tokens_neox': 16116, 'n_words': 3138}