Simplifying setup of LEAP in Unity

To use the LEAP motion controller in Unity a lot of tedious work is required. First, the RiggedHand component needs to be added to the hand, and RiggedFinger components need to be added to the fingers. Then, the bone structure has to be setup, the type of finger has to be selected and more.

The script below attempts to automate most of these tasks. To use it, simply drop the script below in the Scripts/Editor folder of your Unity project. You can then right click on the hand game object and choose LEAP / Add rigged hand to setup the hand.

using UnityEngine;
using System.Collections;
using UnityEditor;
using Leap;

/**
 * Adds two items to the context menu:
 *
 *  Add rigged hand
 *  ---------------
 *  Adds a RiggedHand component to the selected object and adds RiggedFinger
 *  components to all child objects. It also sets up links between these
 *  objects and tries to guess the finger type and bone structure.
 *
 *  Remove all LEAP objects
 *  -----------------------
 *  Recursively removes all RiggedHand and RiggedFinger components from 
 *  all objects that are descendents of the selected object.
 */
public static class LEAPEditorExtensions
{

    /**
     * Guess the direction of the fingers from the (local) bone positions
     */
    private static Vector3 GuessDirectionFromBones(RiggedFinger finger)
    {
        Transform first = finger.bones[1];
        Transform last = finger.bones[finger.bones.Length - 1];

        return Vector3.Normalize(last.localPosition - first.localPosition);
    }


    /**
     * Guesses the type of the finger based on the name and index (order in which it
     *  appears in the scene graph).
     */
    private static Finger.FingerType GuessFingerType(string name, int index)
    {
        name = name.ToLower();
    
        if(name.Contains("index"))
            return Finger.FingerType.TYPE_INDEX;
        if(name.Contains("thumb"))
            return Finger.FingerType.TYPE_THUMB;
        if(name.Contains("ring"))
            return Finger.FingerType.TYPE_RING;
        if(name.Contains("pinky"))
            return Finger.FingerType.TYPE_PINKY;
        if(name.Contains("middle"))
            return Finger.FingerType.TYPE_MIDDLE;
            
        switch(index) {
            case 0: return Finger.FingerType.TYPE_THUMB;
            case 1: return Finger.FingerType.TYPE_INDEX;
            case 2: return Finger.FingerType.TYPE_MIDDLE;
            case 3: return Finger.FingerType.TYPE_RING;
            case 4: return Finger.FingerType.TYPE_PINKY;
        }
        
        return Finger.FingerType.TYPE_INDEX;
    }


    /**
     * Add a RiggedRinger to the object specified and intializes it.
     */
    static void AddRiggedFingerToObject(GameObject fingerObject, int index = 0)
    {
        // Create RiggedFinger is it doens't already exist
        RiggedFinger finger = fingerObject.GetComponent<RiggedFinger>();

        if(finger == null) {
            finger = fingerObject.AddComponent<RiggedFinger>();
        } else {
            Debug.LogError("Finger already exists");
            return;
        }

                
        // Assign the bones to the finger
        Transform current = fingerObject.transform;
        
        for(int i = 1; i < finger.bones.Length; i++) {        
            finger.bones[i] = current;
            
            if(current.childCount == 0)
                continue;
            
            current = current.GetChild(0);            
        }
        
        
        // Guess type and finger direction
        finger.fingerType = 
            GuessFingerType(fingerObject.name, index);        
        
        finger.modelFingerPointing =
            GuessDirectionFromBones(finger);
    }


    /**
     * Adds a RiggedHand to the specified object and
     *  initializes it.
     */
    static void AddRiggedHandToObject(GameObject handObject)
    {
        RiggedHand hand = handObject.GetComponent<RiggedHand>();    
        
        // Create RiggedHand if it doesn't already exist
        if(hand == null) {
            hand = handObject.AddComponent<RiggedHand>();
        } else {
            Debug.LogError("Hand already exists");
            return;
        }


        // Assign palm, forearm, and arm if not already assigned
        if(hand.palm == null)
            hand.palm = handObject.transform;
        
        if(hand.foreArm == null)
            hand.foreArm = hand.palm.parent;
        
        if(hand.arm == null)
            hand.arm = hand.foreArm.parent;            


        // Add fingers to hand object
        int index = 0;
        foreach(Transform child in handObject.transform) {
            AddRiggedFingerToObject(child.gameObject, index);
            
            if(hand.fingers[index] == null)
                hand.fingers[index] = child.gameObject.GetComponent<RiggedFinger>();
            
            index++;
        }
        
        
        // Find index finger
        foreach(RiggedFinger finger in hand.fingers) {
            if(finger.fingerType == Finger.FingerType.TYPE_INDEX)
                hand.modelFingerPointing = finger.modelFingerPointing;
        }
    }


    [MenuItem("GameObject/LEAP/Add rigged hand", false, 0)]    
    static void AddRiggedHand()    
    {
        AddRiggedHandToObject(Selection.activeGameObject);
    }
    
    
    /**
     * Recursively remove RiggedHand and RiggedFinger objects
     *  from objects in the scene graph. 
     *
     * It would be better to first build a list of all objects and then 
     *  ask the user for confirmation before deleting the objects.
     */    
    private static void RecursiveRemove(GameObject obj)
    {
        RiggedHand hand = obj.GetComponent<RiggedHand>();
        
        if(hand)        
            Object.DestroyImmediate(hand);
        
        RiggedFinger finger = obj.GetComponent<RiggedFinger>();
        
        if(finger)
            Object.DestroyImmediate(finger);
        
        foreach(Transform child in obj.transform)
            RecursiveRemove(child.gameObject);
    }
    
    
    [MenuItem("GameObject/LEAP/Remove all LEAP objects", false, 0)]
    static void RemoveAll()
    {
        GameObject obj = Selection.activeGameObject;
        RecursiveRemove(obj);
    }    
}

Psignifit on Win64

Today I wanted to try Psignifit on my work computer which unfortunately runs Windows 7. As the required 64-bit MEX file is not provided, I’ve compiled it myself. The Psignifit MEX file for AMD64 can be downloaded from my website.

Update:
The 64-bit MEX files seem to be a bit unstable. I’ve noticed that this is because the variable index on line 2092 of psignifit.c can be nan. To fix this problem, the line should be changed from:

if(index < 0.0 || index > (double)(nVals - 1.0)) return NAN;

to:

if(isnan(index) || index < 0.0 || index > (double)(nVals - 1.0)) return NAN;

Note that I did not yet recompile the Windows MEX file.

Color printing in Matlab on Linux

The Linux version of Matlab (2013a at least) defaults to printing in black and white. To print in color, you either have to specify another printer driver as an argument to print (“-dps2c” instead of “-dps2″) or manually select “color” from the “color scale” tab in the print preview dialog. The default for this setting is stored in: $MATLAB/toolbox/local/printopt.m. Look for the line that says dev = '-dps2'; and change it to dev = '-dps2c';.

While you’re at it, you can change default paper size, units and more by editing startup.m. My configuration looks like this:

set(0, 'DefaultFigurePaperType','A4');

set(0, 'DefaultFigurePaperPositionMode', 'Manual');
set(0, 'DefaultFigurePaperUnits', 'Centimeters');

set(0, 'DefaultFigurePaperOrientation', 'Landscape');
set(0, 'DefaultFigurePaperPosition', ...
[1 1 -2 -2] * 1 + ... % Set margin here
[0 0 29.7 21.0]); % Size of A4 in landscape format

Multi-functionals on Linux

[Important: You can skip the first part of this tutorial by generating a PPD for your printer. Thanks Wilbert!]

This document describes how to install the new multifunctions on (Ubuntu) Linux. While this procedure is pretty straight forward, things become a little bit more complicated in case account tracking is enabled. While bits and pieces can be found all over the web, I thought it would be helpful to consolidate everything in one document.

Account tracking

Before you can use account tracking, you need to install a CUPS filter and create a configuration file containing your credentials.

  1. Save the minolta filter to your Downloads directory.
  2. Open a terminal (press ALT-F2, then type “terminal”).
  3. Move the filter to the cups filters directory:
    sudo mv ~/Downloads/minolta /usr/lib/cups/filter/
  4. Make the filter executable:
    sudo chmod 755 /usr/lib/cups/filter/minolta
  5. Make a configuration file containing your credentials. The filter uses the printer name to find the file. You can freely choose your printer name, as long as it matches. I suggest using the name assigned by the University ICT department.
    sudo echo ACCOUNT_NAME=\"\" > /etc/cups/ppd/KM-PR0000.km
    sudo echo ACCOUNT_PASSWORD=\"12345\" >> /etc/cups/ppd/KM-PR0000.km
    sudo echo ACCOUNT_COETYPE=\"0\" >> /etc/cups/ppd/KM-PR0000.km
    Note that you have to replace the bold bits! WordPress keeps changing my quotes, you might need to replace them.

Installing the printer

First, download the PPD for your printer. Note that these have been modified such that it uses the filter we’ve installed above. The original PPD files will NOT work with account tracking.

Then follow the steps below to install your printer:

  1. Open the CUPS administration panel http://localhost:631/
  2. Click the Administration tab
  3. Click Add printer
  4. Select “Windows Printer via SAMBA” at the bottom of the list.
  5. Enter the location (replace the bold parts):
    smb://username:password@ru.nl/payprint03.ru.nl/KM-PR0000
    Special symbols should be percent-encoded (thanks to Micha Hulsbosch).
  6. Enter KM-PR0000 as the printer name.
  7. Click “Browse” and choose the PPD file you downloaded before.
  8. Click Add printer.
  9. Click the “General” tab.
  10. Choose A4 paper and “set default options”.

The printer should now work.

Bonus reading

You can set the name that appears when printing using:
@PJL SET KMUSERNAME=”Zaphod”

You can directly contact the printer using:
socket://km-pr0000.print.ru.nl:9100

You can store your prints in a box on the device using the following commands:
@PJL SET BOXHOLD=STORE
@PJL SET BOXHOLDTYPE=PRIVATE
@PJL SET BOXFILENAME=”Important”
@PJL SET BOXNUM=314159265

Secure printing can be enabled using:
@PJL SET HOLD=ON
@PJL SET HOLDTYPE=PRIVATE
@PJL KMJOBID=”SecurePrintId”
@PJL HOLDKEY2 = “SecPassword”

 

EDF File format

In my current research, I track eye position using the EyeLink system. This system produces EDF files which I currently convert into ASCII files using a manufacturer supplied tool. Then, I parse this ASCII file using a custom built Mex file in order to get the data into Matlab. As I’ve always been particularly interested in figuring out how stuff works, this post documents my attempts to read the EDF files directly.

Every file seems to start with:
* SR_RESEARCH_COMB_FILE\n
* A couple of information strings (each terminated by \n)
* And finally “ENDP:\n” which I guess is for end prelude.

The actual data follows. There seem to be a couple of types of variable length frames:
* 0F 00 21: Seems to contain sample frequency, possibly events or samples settings
* 11 00 21: Seems to contain sample frequency, possibly events or samples settings
* 18 xx 21: Seems to indicate the start of a string message
* 41 C0 21: Unknown
* 81 C0 21: Unknown
* D1 81: A sample with delta-time only
* F1 81: A sample containing full time stamp

I’m guessing that 0x20 indicates the presence of a full time-stamp. For some reason this does not hold for messages?

Messages
* 4 byte: time-stamp
* 1 byte: Unknown
* 2 byte: String length
* n byte: Null terminated string
* 1 byte: Null (there seem to be two)

Samples
* 1 byte: delta or 4 byte: time-stamp
* 2 byte: left x
* 2 byte: left y
* 2 byte: right x
* 2 byte: right y
* 2 byte: pupil ?
* 2 byte: pupil ?
* 2 byte: status (always 04?)