Pages

Monday, June 30, 2014

Automated ECG Device Input using an Arduino Micro

Medical devices are intended for the diagnosis, prevention, and treatment of diseases and other medical conditions. Therefore, these devices are limited in purpose, scope, and functionality. These limitations, in my experience, translate to numerous pitfalls for consumers of medical devices. For example, a particular ECG machine may output continuous ECG measurements in digital and print format, while another ECG machine outputs in print format only.

I find this irritating. While it is true that major features of the device are consistent (e.g., ECG machines measure ECG signals), general feature sets that expand device usability and functionality are limited. Device manufacturers do offer options to skirt around limitations, but premium functionality comes at a premium price.

I talk about my experience with a GE MAC 1600, or resting ECG device, in this post. I note that this device does exactly what it is suppose to do, and I do not intend on unfairly representing the device. Simply, I have a feature requirement that is not met by the device, and therefore I am forced to build in my own feature with auxiliary hardware.

My background story in short: the GE MAC 1600 can only store 100 ECG signals, each of length 10 seconds. I want to collect for periods longer than 10 seconds. This is impossible using functions provided by the device alone. Therefore, I must address this problem using an alternative method that is not supported by the device.

A crawl through the GE MAC's documentation was my first step toward solving the problem. I wanted to know if there was an option or configuration that might enable the storage of ECG signals longer than 10 seconds. I quickly found that there were no such options. I then began a different search through the documentation: does the device support external inputs such as USB keyboard? I found that it did!

I attached a USB keyboard and then began iterating through button presses to learn the device button mappings. Figure 1 shows that the keyboard mapping to the ECG button is not as intuitive as, "F5 for ECG." I found that upon a few iterations that the F10 key mapped to the ECG button. Great.

Figure 1: GE MAC 1600 Keyboard. The red button is the ECG Button.

Two approaches became clear to me. The first approach was for me to press the ECG button every 10 seconds to capture a continuous window of time greater than 10 seconds. This is not optimal as it introduces human error (i.e., missed button presses and time inaccuracies). The second approach mitigates this error by relying on hardware; specifically, an Arduino Micro.

Arduino is a prototype platform that has gained popularity because of its open-source approach to development, accessibility, and simple programming language based on Wiring. Arduino micro is a microcontroller based on the ATmega32u4, which uses built-in USB communication for serial comm and USB HID device emulation --- keyboards!

I prototyped my ECG button presser via an Arduino Micro, breadboard, two LEDs, jumper wires, and two 3-way rocker switches (i.e., components scrounged from neighboring labs). I developed a breadboard layout for the replication of my design using Fritzing. See Figure 2 for the design and Figure 3 for the design implementation. Side note: I'm still new to electronics and just noticed that I did not put a resistor between the digital pin and the rocker switch. The circuit still functions as intended.

Figure 2: Breadboard Design of ECG Button Presser.

Figure 3: Design Implementation.

I then wrote a sketch, or Arduino program, that uses serial output over the USB connected device class (CDC) driver to verify the correctness of my switch/LED logic. I left these statements in the sketch and you can find them in the code below. The sketch uses the digitalRead() and digitalWrite() functions to read digital pin values for the switches, either HIGH or LOW, or set digital pin values for the LEDs, also HIGH or LOW. And, the sketch uses the mouse and keyboard libraries to send keystrokes to an attached computer.
/* GE MAC 1600 Keyboard Control
 * 
 * Controls the GE MAC 1600 keyboard via USB. 
 *
 * Ref: http://arduino.cc/en/Reference/MouseKeyboard
 */

// Define our setup constants. Adjust for your board.
const int ecgRest_switch =          8; // Resting Switch.
const int ecgRest_LEDpin =          4; // Yellow.
const int ecgWalk_switch =         12; // Walking Switch.
const int ecgWalk_LEDpin =          7; // Red.
const unsigned long t_sample =  10000; // Sample period.
const unsigned long tf_rest  = 900000; // 15 min in ms.
const unsigned long tf_walk1 = 600000; // 10 min.
const unsigned long tf_walk2 = 300000; // 5 min.
boolean start               =    true;
boolean resting             =   false;
boolean walking             =   false;
boolean walking_sit         =   false;
boolean attention           =   false;

void setup() {
  // Online forums recommended adding a delay for USB enumeration.
  delay(500);

  // Virtual serial driver reserved for USB CDC communication.
  // USB speed 12 Mbits/sec.
  //Serial.begin(9600);

  // Init keyboard emulation to connected ECG machine.
  Keyboard.begin();

  // Set our switches as input.
  pinMode(ecgRest_switch, INPUT);
  pinMode(ecgWalk_switch, INPUT);

  // Set our LEDs as output.
  pinMode(ecgRest_LEDpin, OUTPUT);
  pinMode(ecgWalk_LEDpin, OUTPUT);
}

void setLEDs(boolean ecgrest, int ecgrest_val, boolean ecgwalk, int ecgwalk_val) {
  if(ecgrest) {
    if(ecgrest_val < 1) {
      digitalWrite(ecgRest_LEDpin, LOW);
    } 
    else {
      digitalWrite(ecgRest_LEDpin, HIGH);
    }
  }
  if(ecgwalk) {
    if(ecgwalk_val < 1) {
      digitalWrite(ecgWalk_LEDpin, LOW);
    } 
    else {
      digitalWrite(ecgWalk_LEDpin, HIGH);
    }
  }
}

void loop() {  
  // On start check that both switches are LOW.
  if(start == true) {
    delay(1000);
    //Serial.println("Starting...");        
    setLEDs(true, 1, true, 1);
  }

  while(start) {
    // Only when both switchers are LOW can we start.
    if(digitalRead(ecgRest_switch) == LOW &&
      digitalRead(ecgWalk_switch) == LOW) {
      start = false;
      setLEDs(true, 0, true, 0);
    }
  }

  // When the user puts the rest switch at high we
  // will loop until global constant tf is met.  
  if(digitalRead(ecgRest_switch) == HIGH) {
    resting = true;
    unsigned long time_initial = millis();
    setLEDs(true, 1, false, 0);
    //Serial.println("Starting ECG Resting test...");

    // Loop until the maximum time with periodic
    // keyboard input --- F10 for ECG button.
    while(resting) {
      // Emulate keyboard press and release for ECG start.
      Keyboard.write(KEY_F10);

      //Serial.println(millis()-time_initial);       
      delay(t_sample);     
      // Additionally, when the SD Card fills it will start
      // to spit out error messages. Lets ignore those with
      // ESCAPE key press.
      Keyboard.write(KEY_ESC);

      // If current time is greater than or equal 
      // to our maximum time, set false and break.
      if((millis()-time_initial) >= tf_rest) {
        resting = false;
      }
    }

    // Set both LEDs to HIGH until the user sets them
    // both LOW again. We do this by setting start=true.
    start = true;
  }

  // When the user puts the walk switch at high we
  // will loop until global constant tf_walk is met.
  if(digitalRead(ecgWalk_switch) == HIGH) {
    walking = true;
    walking_sit = true;
    unsigned long time_initial = millis();
    setLEDs(false, 0, true, 1);
    //Serial.println("Starting ECG Walking test...");

    // Loop until the maximum time with periodic
    // keyboard input --- F10 for ECG button.
    while(walking) {
      // Emulate keyboard press and release.
      Keyboard.write(KEY_F10);      
 
      //Serial.println(millis()-time_initial);       
      delay(t_sample);
      Keyboard.write(KEY_ESC);     
      
      // Same as above with one exception. Once the
      // time tf_walk1 has been met, we will start 
      // a new loop for tf_walk2 and pause for the
      // to indicate start. 
      if((millis()-time_initial) >= tf_walk1) {
        setLEDs(true, 1, false, 1);
        boolean wait_user = true;
        walking = false;

        while(wait_user) {
          // Tricky with only two LEDs. We ask the user
          // to set the switch LOW to continue.
          if(digitalRead(ecgWalk_switch) == LOW) {
            wait_user = false;
          }
        }
      }
    }

    // After the user sets walk to LOW we will get the
    // current time and start capturing ECG data for
    // walking_sit.
    //Serial.println("Starting ECG sitting test...");
    setLEDs(false, 0, true, 1);

    // Reset this or it'll just run for one iteration. 
    // Day ruined! 
    time_initial = millis();
    while(walking_sit) {
      // Emulate keyboard press and release.
      Keyboard.write(KEY_F10);   
   
      //Serial.println(millis()-time_initial);       
      delay(t_sample);
      Keyboard.write(KEY_ESC);   

      if((millis()-time_initial) >= tf_walk2) {
        setLEDs(true, 0, true, 0);
        walking_sit = false;
      }
    }

    // Set both LEDs to HIGH until the user sets them
    // both LOW again. We do this by setting start=true.
    start = false;
  }
}
Once I finished coding up and testing my sketch, I thought I was ready. I uploaded my sketch to the Arduino micro, and then I connected it to various devices (e.g., Macbook, Linux VM, RPi). The circuit behaved just as expected. I then connected my Arduino Micro to the GE MAC 1600, and...

A constantly lit RX (receive) light. What can it possibly be receiving? On inspection of other device connections the RX light briefly flashed on boot and then went dark. I tried pushing a switch which  resulted in a constantly lit TX (transmit) light. Something was clearly wrong.

My first attempt to alleviate this seemingly random problem was to debug my code. I still had serial statements in the sketch and I thought this might be the problem. I commented those out and tried again. Same problem. I quickly became confused and frustrated. How is it that the only device this doesn't work for is the device I intended to use it on? How am I suppose to learn what serial input it was receiving if I didn't have a serial monitor or external storage? Was it a USB HID problem? If it were, I could spoof a Dell Keyboard configuration by mirroring the device descriptor...

Searching the internet and trail-and-error for a few days brought me to one conclusion: the GE MAC must recognize the Arduino Micro CDC driver first, flooding it with input (e.g., external barcode scanner). I discovered one solution to alleviate this problem: burn a new USB keyboard-only bootloader onto the Micro. I could burn this new bootloader with another Arduino, or with dedicated programmers (I did not have one in the lab), using the in-circuit serial programming (ICSP) header. I ended up not acting on this solution because I stumbled on a simpler solution first. I did, however, go as far as setting up an Arduino Mega 2560 as an ISP in Figure 4. I have provided a Fritzing breadboard diagram to help those wanting to do something similar.

Figure 4: Arduino Mega 2560 ISP for Arduino Micro


Figure 5: Breadboard Diagram

I relent that I tried many solutions before finding the path-of-least-resistance. For example, I tried the Arduino device firmware update (DFU) software. If you're interested, you can program Atmega8u2 chips directly using the DFU USB protocol, but it wasn't clear to me if this was the case for Arduino Micros and their Atmega32u4. If you have a Mac, you can give this a try by installing the appropraite binaries via MacPorts: sudo port install dfu-programmer dfu-util.

The solution that works is, thankfully, much simpler. I noticed, using lsusb and dmesg, that the Arduino Micro was describing itself as USB CDC and USB HID. I thought about how an Arduino sketch would support both CDC and HID. There must be a library. I pulled up the USB and HID code directly: /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/cores/arduino/. I found the following:

USBDesc.h has one simple and powerful macro. Can you spot it?
/* Copyright (c) 2011, Peter Barrett  
**  
** Permission to use, copy, modify, and/or distribute this software for  
** any purpose with or without fee is hereby granted, provided that the  
** above copyright notice and this permission notice appear in all copies.  
** 
** THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL  
** WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED  
** WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR  
** BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES  
** OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,  
** WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,  
** ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS  
** SOFTWARE.  
*/

#define CDC_ENABLED
#define HID_ENABLED


#ifdef CDC_ENABLED
#define CDC_INTERFACE_COUNT 2
#define CDC_ENPOINT_COUNT 3
#else
#define CDC_INTERFACE_COUNT 0
#define CDC_ENPOINT_COUNT 0
#endif

#ifdef HID_ENABLED
#define HID_INTERFACE_COUNT 1
#define HID_ENPOINT_COUNT 1
#else
#define HID_INTERFACE_COUNT 0
#define HID_ENPOINT_COUNT 0
#endif

All you need to do is set #define CDC_ENABLED to #define CDC_DISABLED (or any text for that matter). Finally. I was able to use my Arduino Micro to automate ECG button presses on the GE MAC 1600! There are some complications with this method. For example, the Arduino IDE uses CDC to upload sketches. You cannot upload sketches with CDC disabled.

I discovered a way what seems like a bricked arduino (I assume you that it is not). First, note that we not burn a new bootloader. When the stock Atmega32u4 bootloader boots up, CDC communication is available until it hands off to the sketch. This allows you to upload a new sketch by giving power to the micro (or resetting it via the onboard button), and immediately uploading a new sketch. You may have to attempt this more than once if you receive an error in the IDE. In fact, I have received errors and still had method this work. Also note that if you want CDC turned back, you have to enable it in the USBDesc header file: #define CDC_ENABLED.

All of this work for one feature. Perhaps this effort speaks volumes about the current state of medical devices or perhaps it only says that, "you get what you pay for"? Either way, as medical devices become more integrated one can only hope that feature sets include more functionality for future applications. Of course we would hope for more security as well... that's a topic for another day.

Thursday, June 19, 2014

Eclipse Recommended Heap Size for Macbooks 2009+

If you have a Macbook from 2009 and up, you should have at least 4 GB of RAM (e.g., PC3-8500 DDR3 1066 MHz). And, if you are using Eclipse, you should definitely increase the initial and maximum Java heap size. Why you ask? Let us consider this example: You're doing some awesome JavaScript development (yeah, there is a plugin for that). You want to load in some precomputed arrays via server-side include so you develop with this file in the project directory. Eclipse loads the 32MB file into the Heap and crashes. True story.

The problem is that the minimum Java heap for Eclipse, by default, is 40MB and the maximum allowable size was 512MB. You can imagine that this will be depleted quickly. To Eclipses benefit, it doesn't actually crash. It just fails to load the source file (or *file), stops loading, complains to the user by throwing an OutOfMemory error, and tries again. If you're using OS X 10.6+, the simple fix is to modify -Xms and -Xmx in the eclipse.ini file. This is found here /opt/eclipse/Eclipse.app/Contents/MacOS/eclipse.ini or wherever you placed Eclipse (e.g., common placement is /Applications or ~/). 

Before closing out this short post, lets try something new. I wonder if hyperlinks to the filesystem will work. This is for OS X users that are interested ONLY: file:///Users/Shared/. Ok, it doesn't. I am allowed to publish the link but the browser will not resolve it. Interesting!


Wednesday, June 18, 2014

rTalk: Multi-Person Motion Tracking via RF Body Reflections

Meta note (or is it meta-note):

I have considered how best to provide short posts to get people interested in academic research that I find interesting. My past approach was to blast out variable length summaries that I titled reviews but did not really meet the qualification of a review. I wasn't assigning a score. Instead, I want to title these posts: "rTalk: <Title of Paper>." The idea of these posts is to simply talk about the paper. A summary with some intuition if you will.

I thank you, the reader, for your persistence with respect to this blog's growing pains. It's a dynamic system of maturity and growth, for both the blog and myself.


Multi-Person Motion Tracking via RF Body Reflections
Fadel Adib, Zachary Kabelac, and Dina Katabi
download paper

The authors present WiZ, a multi-person, centimeter scale motion tracking system. This system can spatially localize humans, two at most with the current implementation, and track hand gestures using radio reflections in the presence of obstructions such as walls (i.e., static objects). The authors use the time-of-flight (TOF), or time of a directed RF signal to leave the transceiver and return to a receiver via reflection, to track motion. Collected TOFs are composed into a heatmap visualization that presents reflections color-coded from red to blue. Red colors, or hot, indicate areas where reflections occurred --- we found a person! Blue colors, or not-so-hot, indicates areas of little reflection --- its just a wall. Specifically, we would imagine a human body on a two dimensional plane as being red and the emptiness around her as blue. This technique, however, is non-optimal, or simply really difficult, when tracking more than one person. The intuition as to why is because received signal reflections may overpower others (e.g., a person standing further away) and there is no way to distinguish individual reflected signal points (e.g., which signal was reflected from person A).

 The authors introduce two new algorithms to overcome this limitation. They are successive silhouette cancelation (SSC) and multi-shift frequency modulation carrier (FMCW) waves. Multi-shift FMCW delays transmit signals for some time equal to the TOF limit, or maximum time a TOF could take. This allows us to distinguish received signal reflections from one another and thus use multiple transmitters and receivers. Successive silhouette cancelation addresses what is informally referenced as the near/far problem. This problem is described in the example above as two people which we would like to track, however one person is closer to the receiver and thus overwhelms the received signal reflection from the person that is further away. The authors propose SSC as a series of algorithmic steps to first detect the strongest reflector (i.e., the person that is closer), remap the reflectors locations to the set of TOFs, cancel out the ramped TOFs, and finally re-compose the heatmap visualization. This process can be iterative, thus tracking motion of individuals exceedingly further away.

If you're interested in seeing the experimental setup and a different summarization, please check out the following references: http://www.extremetech.com/extreme/184347-mit-perfects-cheap-accurate-through-wall-movement-and-heartbeat-detection-with-wifi and http://newsoffice.mit.edu/2014/could-wireless-replace-wearables. Before closing, I want you to think about how new technologies, such as motion tracking here, might impact privacy. Are the implications similar to Kyllo v. United States? How could privacy advocates protect themselves? If deployed, what sort of companies might use this technology (e.g., wifi tracking via smartphone, or iBeacon). What positives results are there for motion tracking. Fantastic work overall. I, personally, cannot wait to see what this group does next!

Wednesday, June 11, 2014

A Tale of CMAKE + Matlab + OpenCV + OS X 10.9

I entered my blogging user interface, clicked drafts, and found an assortment of entires that roughly describe my usual development exploits and frustrations. One such draft detailed my first interaction with the Matlab mex script, it's listed as a compilation function but if you cat the file its a shell script that provides interoperability by checking system variables and the like, to compile and link C source files into a binary file that is callable in Matlab. Very cool!

Well, very cool if you're on Linux or Windows even. Apple's subtle and not-so-subtle changes per the developer ecosystem can be infuriating. It ain't BSD-like anymore folks. While I applaud some neat and not heavily known features of the latest OS X, such as:
  1. Attempting to use git
  2. git not found 
  3. Would you like to install developer tools (not all of XCode)
I still find my workflow affected by clang specifics (e.g., compiler permissiveness, flags), apple specifics (e.g., sandbox, default configurations), etc. One such problem surfaced when I was attempting to recompile some source that was graciously provided to aid my own research. The code had a dependency on an older version of OpenCV. My options, modify the source to accept the latest version of OpenCV, which had deprecated functions and refactored functionality into new headers, or attempt to compile the old version and fix whatever clang might spit out at me. I tried both options. I ended with option two.

I first address my issue with clang on OS X 10.9 and mex. On my installed Matlab version, R2014a, the default mexopts.sh, found in /Applications/MATLAB_R2014a.app/bin/, specified the development target as 10.6. This is problematic because, for example, the latest version of clang uses a different standard C++ library, libc++ vice libstdc++ (example problem), that is not supported in 10.6. I needed to change this.

I first ran /Applications/MATLAB_R2014a.app/bin/mex --setup. This created a local copy of mexopts to ~/.matlab/R2014a/ and will be interpreted on subsequent calls to mex unless -f is passed (pointing to a different mexopts script). This allowed me to work on a local copy of mexopts.sh without fear of mucking up the default file. I then modified the local copy to look as follows: 
        maci64)
#----------------------------------------------------------------------------
            # StorageVersion: 1.0
            # CkeyName: Clang
            # CkeyManufacturer: Apple
            # CkeyLanguage: C
            # CkeyVersion:
            # CkeyLinkerName:
            # CkeyLinkerVersion:
            CC='xcrun  -sdk macosx10.8  clang'
## workaround clang defect temporarily use line below           SDKROOT='/Developer/SDKs/MacOSX10.6.sdk'
# compute SDK root on the fly
# target 10.8 by Mike R. 
            MW_SDKROOT_TMP="find `xcode-select -print-path` -name MacOSX10.8.sdk"
			MW_SDKROOT=`$MW_SDKROOT_TMP`
            MACOSX_DEPLOYMENT_TARGET='10.8'
            ARCHS='x86_64'
            #ARCHS='x86_64 -stdlib=libstdc++'
            CFLAGS="-fno-common -arch $ARCHS -isysroot $MW_SDKROOT -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
            CFLAGS="$CFLAGS  -fexceptions"
            CLIBS="$MLIBS"
            COPTIMFLAGS='-O2 -DNDEBUG'
            CDEBUGFLAGS='-g'
#
            CLIBS="$CLIBS -lstdc++"
            # C++keyName: Clang++
            # C++keyManufacturer: Apple
            # C++keyLanguage: C++
            # C++keyVersion:
            # C++keyLinkerName:
            # C++keyLinkerVersion:
            CXX='xcrun  -sdk macosx10.8  clang++'
            CXXFLAGS="-fno-common -fexceptions -arch $ARCHS -isysroot $MW_SDKROOT -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
            CXXLIBS="$MLIBS -lstdc++"
            CXXOPTIMFLAGS='-O2 -DNDEBUG'
            CXXDEBUGFLAGS='-g'
#
            # FortrankeyName: GNU Fortran
            # FortrankeyManufacturer: GNU
            # FortrankeyLanguage: Fortran
            # FortrankeyVersion: 
            # FortrankeyLinkerName: 
            # FortrankeyLinkerVersion:
            FC='gfortran'
            FFLAGS='-fexceptions -m64 -fbackslash'
            FC_LIBDIR=`$FC -print-file-name=libgfortran.dylib 2>&1 | sed -n '1s/\/*libgfortran\.dylib//p'`
            FC_LIBDIR2=`$FC -print-file-name=libgfortranbegin.a 2>&1 | sed -n '1s/\/*libgfortranbegin\.a//p'`
            FLIBS="$MLIBS -L$FC_LIBDIR -lgfortran -L$FC_LIBDIR2 -lgfortranbegin"
            FOPTIMFLAGS='-O'
            FDEBUGFLAGS='-g'
#
            LD="$CC"
            LDEXTENSION='.mexmaci64'
            LDFLAGS="-arch $ARCHS -Wl,-syslibroot,$MW_SDKROOT -mmacosx-version-min=$MACOSX_DEPLOYMENT_TARGET"
            LDFLAGS="$LDFLAGS -bundle -Wl,-exported_symbols_list,$TMW_ROOT/extern/lib/$Arch/$MAPFILE"
            LDOPTIMFLAGS='-O'
            LDDEBUGFLAGS='-g'
#
            POSTLINK_CMDS=':'
#----------------------------------------------------------------------------
            ;;
The basis for this change are explained here, and officially here. Once this was done, I had to patch one or two errors in opencv2.4.2 that bubbled up because of clang (patches which I will share when I find them). I then used cmake to build opencv without error:
cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX:PATH=/opt/opencv -DCMAKE_OSX_ARCHITECTURES=x86_64 -DBUILD_opencv_matlab=NO -DCMAKE_CXX_FLAGS="-stdlib=libstdc++" ..
On a quick aside, I am still green with respect to cmake. I usually dig through the cmake configuration files to find build specific variables. I then stumbled on this command: cmake -i. If you do this in the source directory it will print all variables to stdout. I'm sure there might be a better way, but this made my day.

Finally, the following use of mex then went without a hitch:
/Applications/MATLAB_R2014a.app/bin/mex -v -O -largeArrayDims -I/opt/opencv/include -L/opt/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_objdetect -lopencv_highgui mysource.cpp

Take a look at the following compilation command and screenshot for a laugh. It was a late night stumper for me. Why was I getting duplicate symbols? If I dropped one linked library I would get a linker error but the duplication went away. Was there some sort of circular linking going on? Could this post be the answer?
/Applications/MATLAB_R2014a.app/bin/mex -v -c -O -largeArrayDims -I/opt/opencv/include facedetection.cpp -L/opt/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_objdetect -lopencv_highgui facedetection.cpp
Silly, silly programmer. Do you see the error? That's all for this post. Please come back later! I plan on writing up my experience with ArrayBuffers in different browsers and doing video encoding on a Raspberry Pi.