001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.hardware.kinect;
031
032import java.util.ArrayList;
033import java.util.List;
034
035import org.bridj.Pointer;
036import org.bridj.ValuedEnum;
037import org.openimaj.hardware.kinect.freenect.freenect_raw_tilt_state;
038import org.openimaj.hardware.kinect.freenect.freenect_registration;
039import org.openimaj.hardware.kinect.freenect.libfreenectLibrary;
040import org.openimaj.hardware.kinect.freenect.libfreenectLibrary.freenect_context;
041import org.openimaj.hardware.kinect.freenect.libfreenectLibrary.freenect_device;
042import org.openimaj.hardware.kinect.freenect.libfreenectLibrary.freenect_led_options;
043import org.openimaj.hardware.kinect.freenect.libfreenectLibrary.freenect_tilt_status_code;
044import org.openimaj.image.FImage;
045import org.openimaj.video.VideoDisplay;
046
047/**
048 * The event thread for Freenect
049 * 
050 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
051 */
052class EventThread extends Thread {
053        private volatile boolean alive = true;
054
055        public EventThread() {
056                setDaemon(true);
057                setName("FreenectEventThread");
058        }
059
060        public void kill() {
061                this.alive = false;
062        }
063
064        @Override
065        public void run() {
066                while (alive) {
067                        libfreenectLibrary.freenect_process_events(KinectController.CONTEXT);
068                }
069        }
070};
071
072/**
073 * The OpenIMAJ Kinect Interface.
074 * 
075 * @author Jonathon Hare (jsh2@ecs.soton.ac.uk)
076 */
077public class KinectController {
078        private static final int DEPTH_X_RES = 640;
079        private static final int DEPTH_Y_RES = 480;
080        protected volatile static Pointer<freenect_context> CONTEXT;
081        protected volatile static EventThread EVENT_THREAD;
082        protected volatile static List<KinectController> ACTIVE_CONTROLLERS = new ArrayList<KinectController>();
083
084        protected Pointer<freenect_device> device;
085
086        /**
087         * The RGB or IR video stream
088         */
089        public KinectStream<?> videoStream;
090
091        /**
092         * The depth stream
093         */
094        public KinectDepthStream depthStream;
095
096        /**
097         * Construct with given device
098         * 
099         * @param deviceId
100         *            the device id
101         * @throws KinectException
102         */
103        public KinectController(int deviceId) throws KinectException {
104                this(deviceId, false, false);
105        }
106
107        /**
108         * Construct with the first device in the given mode.
109         * 
110         * @param irmode
111         *            if true then the camera is set to IR mode; otherwise its in
112         *            RGB mode
113         * @throws KinectException
114         */
115        public KinectController(boolean irmode) throws KinectException {
116                this(0, irmode, false);
117        }
118
119        /**
120         * Default constructor. Uses the first device in RGB mode.
121         * 
122         * @throws KinectException
123         */
124        public KinectController() throws KinectException {
125                this(0, false, false);
126        }
127
128        /**
129         * Construct with the given device and mode.
130         * 
131         * @param deviceId
132         *            the device identifier. 0 for the first one.
133         * @param irmode
134         *            whether to use infra-red mode or rgb mode.
135         * @param registeredDepthMode
136         *            whether to register the depth image. If true, depth
137         *            measurements are in millimeters.
138         * @throws KinectException
139         */
140        public KinectController(int deviceId, boolean irmode, boolean registeredDepthMode) throws KinectException {
141                // init the context and start thread if necessary
142                init();
143
144                final int cd = connectedDevices();
145                if (deviceId >= cd || deviceId < 0) {
146                        throw new IllegalArgumentException("Invalid device id");
147                }
148
149                ACTIVE_CONTROLLERS.add(this);
150
151                // init device
152                final Pointer<Pointer<freenect_device>> devicePtr = Pointer.pointerToPointer(null);
153                libfreenectLibrary.freenect_open_device(CONTEXT, devicePtr, deviceId);
154                device = devicePtr.get();
155
156                // setup listeners
157                if (irmode)
158                        videoStream = new KinectIRVideoStream(this);
159                else
160                        videoStream = new KinectRGBVideoStream(this);
161                depthStream = new KinectDepthStream(this, registeredDepthMode);
162        }
163
164        @Override
165        public void finalize() {
166                close();
167        }
168
169        /**
170         * Init the freenect library. This only has to be done once.
171         * 
172         * @throws KinectException
173         */
174        private static synchronized void init() throws KinectException {
175                if (KinectController.CONTEXT == null) {
176                        final Pointer<Pointer<freenect_context>> ctxPointer = Pointer.pointerToPointer(null);
177                        libfreenectLibrary.freenect_init(ctxPointer, Pointer.NULL);
178
179                        if (ctxPointer == null)
180                                throw new KinectException("Unable to initialise libfreenect.");
181
182                        CONTEXT = ctxPointer.get();
183
184                        if (CONTEXT == null)
185                                throw new KinectException("Unable to initialise libfreenect.");
186
187                        if (libfreenectLibrary.freenect_num_devices(CONTEXT) == 0) {
188                                libfreenectLibrary.freenect_shutdown(CONTEXT);
189                                throw new KinectException("Unable to initialise libfreenect; No devices found.");
190                        }
191
192                        EVENT_THREAD = new EventThread();
193                        EVENT_THREAD.start();
194
195                        // turn off the devices on shutdown
196                        Runtime.getRuntime().addShutdownHook(new Thread() {
197                                @Override
198                                public synchronized void run() {
199                                        shutdownFreenect();
200                                }
201                        });
202                }
203        }
204
205        /**
206         * Completely shutdown the context. This turns off all cameras. The context
207         * will be restarted upon creation of a new KinectController.
208         */
209        public synchronized static void shutdownFreenect() {
210                while (ACTIVE_CONTROLLERS.size() > 0) {
211                        ACTIVE_CONTROLLERS.get(0).close();
212                }
213
214                if (EVENT_THREAD != null) {
215                        EVENT_THREAD.kill();
216                }
217
218                if (CONTEXT != null)
219                        libfreenectLibrary.freenect_shutdown(CONTEXT);
220
221                CONTEXT = null;
222                EVENT_THREAD = null;
223        }
224
225        /**
226         * Switch the main camera between InfraRed and RGB modes.
227         * 
228         * @param irmode
229         *            if true, then switches to IR mode, otherwise switches to RGB
230         *            mode.
231         */
232        public void setIRMode(boolean irmode) {
233                if (device == null)
234                        return;
235
236                if (irmode) {
237                        if (!(videoStream instanceof KinectIRVideoStream)) {
238                                videoStream.stop();
239                                videoStream = new KinectIRVideoStream(this);
240                        }
241                } else {
242                        if (!(videoStream instanceof KinectRGBVideoStream)) {
243                                videoStream.stop();
244                                videoStream = new KinectRGBVideoStream(this);
245                        }
246                }
247        }
248
249        /**
250         * Set whether depth should be registered
251         * 
252         * @param rdepth
253         *            if true, then switches to depth registered mode, otherwise
254         *            depth is not registered
255         */
256        public void setRegisteredDepth(boolean rdepth) {
257                if (device == null)
258                        return;
259
260                if (depthStream.registered != rdepth) {
261                        depthStream.stop();
262                        depthStream = new KinectDepthStream(this, rdepth);
263                }
264        }
265
266        /**
267         * Get the number of connected devices.
268         * 
269         * @return the number of devices connected.
270         * @throws KinectException
271         */
272        public static synchronized int connectedDevices() throws KinectException {
273                init();
274                return libfreenectLibrary.freenect_num_devices(CONTEXT);
275        }
276
277        /**
278         * Close the device. Closing an already closed device has no effect.
279         */
280        public synchronized void close() {
281                if (device == null)
282                        return;
283
284                videoStream.stop();
285                depthStream.stop();
286                libfreenectLibrary.freenect_close_device(device);
287                device = null;
288
289                ACTIVE_CONTROLLERS.remove(this);
290        }
291
292        /**
293         * Get the current angle in degrees
294         * 
295         * @return the angle or 0 if the device is closed
296         */
297        public synchronized double getTilt() {
298                if (device == null)
299                        return 0;
300
301                libfreenectLibrary.freenect_update_tilt_state(device);
302                final Pointer<freenect_raw_tilt_state> state = libfreenectLibrary.freenect_get_tilt_state(device);
303                return libfreenectLibrary.freenect_get_tilt_degs(state);
304        }
305
306        /**
307         * Set the tilt to the given angle in degrees
308         * 
309         * @param angle
310         *            the angle
311         */
312        public synchronized void setTilt(double angle) {
313                if (device == null)
314                        return;
315
316                if (angle < -30)
317                        angle = -30;
318                if (angle > 30)
319                        angle = 30;
320                libfreenectLibrary.freenect_set_tilt_degs(device, angle);
321        }
322
323        /**
324         * Determine the current tilt status of the device
325         * 
326         * @return the tilt status or null if the device is closed
327         */
328        public synchronized KinectTiltStatus getTiltStatus() {
329                if (device == null)
330                        return null;
331
332                libfreenectLibrary.freenect_update_tilt_state(device);
333                final Pointer<freenect_raw_tilt_state> state = libfreenectLibrary.freenect_get_tilt_state(device);
334                final ValuedEnum<freenect_tilt_status_code> v = libfreenectLibrary.freenect_get_tilt_status(state);
335
336                for (final freenect_tilt_status_code c : freenect_tilt_status_code.values())
337                        if (c.value == v.value())
338                                return KinectTiltStatus.valueOf(c.name());
339
340                return null;
341        }
342
343        /**
344         * Set the device status LEDs
345         * 
346         * @param option
347         *            the LED status to set
348         */
349        public synchronized void setLED(KinectLEDMode option) {
350                if (device == null)
351                        return;
352
353                final ValuedEnum<freenect_led_options> o = freenect_led_options.valueOf(option.name());
354                libfreenectLibrary.freenect_set_led(device, o);
355        }
356
357        /**
358         * Sets the LEDs to blink red and yellow for five seconds before resetting
359         * to green.
360         */
361        public synchronized void identify() {
362                if (device == null)
363                        return;
364
365                setLED(KinectLEDMode.LED_BLINK_RED_YELLOW);
366                try {
367                        Thread.sleep(5000);
368                } catch (final InterruptedException e) {
369                }
370                setLED(KinectLEDMode.LED_GREEN);
371        }
372
373        /**
374         * Get the current acceleration values from the device
375         * 
376         * @return the acceleration or null if the device is closed
377         */
378        public synchronized KinectAcceleration getAcceleration() {
379                if (device == null)
380                        return null;
381
382                final Pointer<Double> px = Pointer.pointerToDouble(0);
383                final Pointer<Double> py = Pointer.pointerToDouble(0);
384                final Pointer<Double> pz = Pointer.pointerToDouble(0);
385
386                libfreenectLibrary.freenect_update_tilt_state(device);
387                final Pointer<freenect_raw_tilt_state> state = libfreenectLibrary.freenect_get_tilt_state(device);
388                libfreenectLibrary.freenect_get_mks_accel(state, px, py, pz);
389
390                return new KinectAcceleration(px.getDouble(), py.getDouble(), pz.getDouble());
391        }
392
393        /**
394         * Compute the world coordinates from the pixel location and registered
395         * depth.
396         * 
397         * @param x
398         *            pixel x-ordinate
399         * @param y
400         *            pixel y-ordinate
401         * @param depth
402         *            the depth
403         * @return the (x,y,z) coordinate in world space
404         */
405        public double[] cameraToWorld(int x, int y, int depth) {
406                final Pointer<Double> wx = Pointer.allocateDouble();
407                final Pointer<Double> wy = Pointer.allocateDouble();
408                libfreenectLibrary.freenect_camera_to_world(device, x, y, depth, wx, wy);
409
410                return new double[] { wx.get(), wy.get(), depth };
411        }
412
413        /**
414         * Compute the scaling factor for computing world coordinates.
415         * 
416         * @see #cameraToWorld(int, int, int, double)
417         * 
418         * @return the scaling factor
419         */
420        public double computeScalingFactor() {
421                // Struct-by-value isn't currently working in bridj. When it is
422                // we can do:
423                // freenect_registration regInfo =
424                // libfreenectLibrary.freenect_copy_registration(device);
425                // double ref_pix_size =
426                // regInfo.zero_plane_info().reference_pixel_size();
427                // double ref_distance = regInfo.zero_plane_info().reference_distance();
428                // return 2 * ref_pix_size / ref_distance;
429
430                // for now we can work around by calculating the factor from a projected
431                // point
432                final int x = (DEPTH_X_RES / 2) + 1;
433                final double[] xyz = cameraToWorld(x, 0, 1000);
434                return xyz[0] / 1000.0;
435        }
436
437        /**
438         * Compute the world coordinates from the pixel location and registered
439         * depth. This method requires you pre-compute the constant scaling factor
440         * using {@link #computeScalingFactor()}, but it should be faster than using
441         * {@link #cameraToWorld(int, int, int)}.
442         * 
443         * @param x
444         *            pixel x-ordinate
445         * @param y
446         *            pixel y-ordinate
447         * @param depth
448         *            the depth
449         * @param factor
450         *            the scaling factor
451         * @return the (x,y,z) coordinate in world space
452         */
453        public float[] cameraToWorld(int x, int y, int depth, double factor) {
454                final float[] pt = new float[3];
455                pt[0] = (float) ((x - DEPTH_X_RES / 2) * factor * depth);
456                pt[1] = (float) ((y - DEPTH_Y_RES / 2) * factor * depth);
457                pt[2] = depth;
458                return pt;
459        }
460
461        /**
462         * Compute the world coordinates from the pixel location and registered
463         * depth. This method requires you pre-compute the constant scaling factor
464         * using {@link #computeScalingFactor()}, but it should be faster than using
465         * {@link #cameraToWorld(int, int, int)}.
466         * 
467         * This method is the same as {@link #cameraToWorld(int, int, int, double)},
468         * but reuses the point array for efficiency.
469         * 
470         * @param x
471         *            pixel x-ordinate
472         * @param y
473         *            pixel y-ordinate
474         * @param depth
475         *            the depth
476         * @param factor
477         * @param pt
478         *            the point to write to
479         * @return the (x,y,z) coordinate in world space
480         */
481        public float[] cameraToWorld(int x, int y, int depth, double factor, float[] pt) {
482                pt[0] = (float) ((x - DEPTH_X_RES / 2) * factor * depth);
483                pt[1] = (float) ((y - DEPTH_Y_RES / 2) * factor * depth);
484                pt[2] = depth;
485                return pt;
486        }
487
488        /**
489         * Compute the world coordinates from the pixel locations in the given
490         * registered depth image. This method basically gives you a fully
491         * registered point cloud.
492         * 
493         * @param regDepth
494         *            the registered depth image
495         * @param startx
496         *            the starting x-ordinate in the image
497         * @param stopx
498         *            the stopping x-ordinate in the image
499         * @param stepx
500         *            the number of pixels in the x direction to skip between
501         *            samples
502         * @param starty
503         *            the starting y-ordinate in the image
504         * @param stopy
505         *            the stopping y-ordinate in the image
506         * @param stepy
507         *            the number of pixels in the y direction to skip between
508         *            samples
509         * @return an array of 3d vectors
510         */
511        public float[][] cameraToWorld(FImage regDepth, int startx, int stopx, int stepx, int starty, int stopy, int stepy) {
512                final freenect_registration regInfo = libfreenectLibrary.freenect_copy_registration(device);
513                final double ref_pix_size = regInfo.zero_plane_info().reference_pixel_size();
514                final double ref_distance = regInfo.zero_plane_info().reference_distance();
515
516                final int nx = (stopx - startx) / stepx;
517                final int ny = (stopy - starty) / stepy;
518                final float[][] points = new float[nx * ny][3];
519                final float[][] depths = regDepth.pixels;
520                final double factor = 2 * ref_pix_size / ref_distance;
521
522                for (int i = 0, y = starty; y < stopy; y += stepy) {
523                        for (int x = startx; x < stopx; x += stepx) {
524                                final float depth = depths[y][x];
525
526                                points[i][0] = (float) ((x - DEPTH_X_RES / 2) * factor * depth);
527                                points[i][1] = (float) ((y - DEPTH_Y_RES / 2) * factor * depth);
528                                points[i][2] = depth;
529                        }
530                }
531
532                return points;
533        }
534
535        /**
536         * Test
537         * 
538         * @param args
539         * @throws KinectException
540         */
541        public static void main(String[] args) throws KinectException {
542                VideoDisplay.createVideoDisplay(new KinectController(0).videoStream);
543        }
544}