Index: mythgallery/glsingleview.cpp
===================================================================
--- mythgallery/glsingleview.cpp	(revision 17145)
+++ mythgallery/glsingleview.cpp	(working copy)
@@ -34,14 +34,21 @@
 #include <qdir.h>
 #include <qpainter.h>
 
+#include <qbuffer.h>
+
 // MythTV plugin headers
 #include <mythtv/mythcontext.h>
 #include <mythtv/util.h>
 
 // MythGallery headers
+#include "config.h"
 #include "glsingleview.h"
 #include "galleryutil.h"
 
+#ifdef FDLIB_SUPPORT
+#include <fdlib.h>
+#endif
+
 #define LOC QString("GLView: ")
 #define LOC_ERR QString("GLView, Error: ")
 
@@ -93,7 +100,10 @@
       // Unshared effect state variables
       m_effect_cube_xrot(0.0f),
       m_effect_cube_yrot(0.0f),
-      m_effect_cube_zrot(0.0f)
+      m_effect_cube_zrot(0.0f),
+      m_effect_kenBurns_image_ready(true),
+      m_effect_kenBurns_initialized(false),
+      m_effect_kenBurns_new_image_started(true)
 {
     m_scaleMax = (gContext->GetNumSetting("GalleryScaleMax", 0) > 0);
 
@@ -661,6 +671,7 @@
     m_effect_map.insert("slide (gl)",      "EffectSlide");
     m_effect_map.insert("flutter (gl)",    "EffectFlutter");
     m_effect_map.insert("cube (gl)",       "EffectCube");
+    m_effect_map.insert("Ken Burns (gl)",  "EffectKenBurns");
 }
 
 void GLSingleView::RunEffect(const QString &effect)
@@ -683,6 +694,8 @@
         EffectFlutter();
     else if (effect == "EffectCube")
         EffectCube();
+    else if (effect == "EffectKenBurns")
+        EffectKenBurns();
     else //if (effect == "EffectNone")
         EffectNone();
 }
@@ -1166,6 +1179,190 @@
     m_effect_current_frame++;
 }
 
+void GLSingleView::EffectKenBurns(void)
+{
+
+    float single_image_pct = 0.75;
+    float trans_pct = 1.0 - single_image_pct;
+    float scale_max, x_loc, y_loc;
+    float scale_factor = 0;
+
+    //initialize effect   
+    if (!m_effect_kenBurns_initialized)
+    {
+                
+        m_effect_kenBurns_initialized = !m_effect_kenBurns_initialized;
+        m_effect_kenBurns_item = NULL;
+        // Need to load images in the background to keep effect smooth
+        m_effect_kenBurns_imageLoadThread = new KenBurnsImageLoader(this, m_itemList, m_texSize, m_screenSize);
+        //Since total image time is longer/different than effect time, create image timers
+        m_effect_kenBurns_image_time[m_texCur ? 0 : 1].restart();
+        // Pan image to a random location
+        FindRandXY(m_effect_kenBurns_location_x[0], m_effect_kenBurns_location_y[0]);
+        // Since first two images are preloaded, hardcode  them to zoom in
+        m_effect_kenBurns_projection[0] = 1;
+        m_effect_kenBurns_projection[1] = 1;
+        m_effect_kenBurns_image_timeout = m_effect_transition_timeout + 
+                (m_effect_transition_timeout * trans_pct);
+        m_effect_kenBurns_face_x = 0.0;
+        m_effect_kenBurns_face_y = 0.0;
+    }
+
+    if (m_effect_frame_time.elapsed() >= m_effect_transition_timeout)
+    {
+        // Effect timed out, move new image to old image but don't load new image yet...
+        m_tex1First = !m_tex1First;
+        m_texCur      = (m_texCur) ? 0 : 1;
+        m_effect_current_frame  = 0;
+        m_effect_frame_time.restart(); 
+
+        m_effect_kenBurns_image_ready = false;
+        m_effect_kenBurns_face_x = 0.0;
+        m_effect_kenBurns_face_y = 0.0;
+
+        // Find next image to be loaded
+        int oldpos = m_pos;
+
+        while (true)
+        {
+            m_pos = m_slideshow_sequence->next();
+            m_effect_kenBurns_item = m_itemList.at(m_pos);
+            if (m_effect_kenBurns_item)
+            {
+                // Skip movies
+                if (QFile::exists(m_effect_kenBurns_item->GetPath()) && !GalleryUtil::isMovie(m_effect_kenBurns_item->GetPath()))
+                {
+                    break;
+                }
+            }
+            if (m_pos == oldpos)
+            {
+                // No valid items!!!
+                close();
+            }
+        }
+        m_effect_kenBurns_imageLoadThread->Initialize(m_pos);
+        m_effect_kenBurns_imageLoadThread->start();
+    }
+
+    float t[2], elapsed[2], s[2], effect_pct;
+    elapsed[m_texCur] = m_effect_kenBurns_image_time[m_texCur].elapsed();
+    elapsed[m_texCur ? 0 : 1] = m_effect_kenBurns_image_time[m_texCur ? 0 : 1].elapsed();
+    //progress linearly
+    t[m_texCur] = elapsed[m_texCur] / m_effect_kenBurns_image_timeout;
+    t[m_texCur ? 0 : 1] = elapsed[m_texCur ? 0 : 1] / m_effect_kenBurns_image_timeout;
+    //progress faster initially then slowing down- this is needed to ensure images zoom faster than they pan and
+    //therefore stay completely on the screen
+    s[m_texCur] = sqrt(elapsed[m_texCur]) / sqrt(m_effect_kenBurns_image_timeout);
+    s[m_texCur ? 0 : 1] = sqrt(elapsed[m_texCur ? 0 : 1]) / sqrt(m_effect_kenBurns_image_timeout); 
+    
+    effect_pct = m_effect_frame_time.elapsed() *  m_effect_transition_timeout_inv;
+
+    // Load new image if its ready
+    if (effect_pct > single_image_pct && m_effect_kenBurns_image_ready) 
+    {
+        if (!m_effect_kenBurns_new_image_started)
+        { 			
+            if (m_effect_kenBurns_item) //Do not create textures for first two images, since they are preloaded
+            {
+                m_texItem[!m_tex1First].SetItem(m_effect_kenBurns_item, m_effect_kenBurns_orig_image_size);
+                m_texItem[!m_tex1First].ScaleTo(m_screenSize, m_scaleMax);
+                m_texItem[!m_tex1First].Init(m_effect_kenBurns_image);
+                UpdateLCD(m_effect_kenBurns_item);
+                
+                // If there is no face in this image
+                if ((m_effect_kenBurns_face_x == 0.0) && (m_effect_kenBurns_face_x == 0.0))
+                {
+                    //choose the location and projection (zoom in or out) randomly
+                    FindRandXY(m_effect_kenBurns_location_x[m_texCur], m_effect_kenBurns_location_y[m_texCur]);
+                    m_effect_kenBurns_projection[m_texCur] = 1 + (int)((2.0f * rand() / (RAND_MAX + 1.0f))); 
+                }
+                //else if the face is  close to center
+                else if ((m_effect_kenBurns_face_x < 0.25) && (m_effect_kenBurns_face_x > -0.25) &&
+                        (m_effect_kenBurns_face_y < 0.25) && (m_effect_kenBurns_face_y > -0.25))
+                {                   
+                    //start at random location and zoom out to face in center
+                    FindRandXY(m_effect_kenBurns_location_x[m_texCur], m_effect_kenBurns_location_y[m_texCur]);
+                    m_effect_kenBurns_projection[m_texCur] = 0;
+                }
+                else 
+                {
+                    //start in center and zoom in to face random location
+                    m_effect_kenBurns_location_x[m_texCur] = m_effect_kenBurns_face_x; 
+                    m_effect_kenBurns_location_y[m_texCur] = m_effect_kenBurns_face_y; 
+                    m_effect_kenBurns_projection[m_texCur] = 1;                
+                }
+            } 
+            else  //No item, must be 1 of the first two preloaded items
+            {
+                //start at random location and zoom out to face in center
+                FindRandXY(m_effect_kenBurns_location_x[m_texCur], m_effect_kenBurns_location_y[m_texCur]);
+                m_effect_kenBurns_projection[m_texCur] = 1; 
+            }
+
+            m_effect_kenBurns_image_time[m_texCur].restart();
+            m_effect_kenBurns_new_image_started = true;
+        }
+        if (m_effect_kenBurns_projection[m_texCur] == 1) // Zoom in image
+        {
+            // Start in center and pan out
+            x_loc = m_effect_kenBurns_location_x[m_texCur] * t[m_texCur]; 
+            y_loc = m_effect_kenBurns_location_y[m_texCur] * t[m_texCur]; 		
+            scale_max = FindMaxScale(x_loc,y_loc);
+            scale_factor = 	1.0f + (scale_max * s[m_texCur]); 
+        }
+        else // Zoom out image
+        {
+            // Start at random location and pan to center
+            x_loc = m_effect_kenBurns_location_x[m_texCur] -  m_effect_kenBurns_location_x[m_texCur] * t[m_texCur]; 
+            y_loc = m_effect_kenBurns_location_y[m_texCur] -  m_effect_kenBurns_location_y[m_texCur] * t[m_texCur];
+            scale_max = FindMaxScale(x_loc,y_loc);
+            scale_factor = 	1.0f + scale_max -  (scale_max * t[m_texCur]);
+        } 
+
+        glMatrixMode(GL_MODELVIEW);
+        glLoadIdentity();
+        glTranslatef(x_loc, y_loc, 0.0f);
+
+        m_texItem[m_texCur].MakeQuad((effect_pct-single_image_pct)*4, scale_factor); 
+    }
+   
+    //Load old picture
+    if (m_effect_kenBurns_projection[m_texCur ? 0 : 1] == 1)// Zoom in image
+    {
+        x_loc = m_effect_kenBurns_location_x[m_texCur ? 0 : 1] * t[m_texCur ? 0 : 1]; 
+        y_loc = m_effect_kenBurns_location_y[m_texCur ? 0 : 1] * t[m_texCur ? 0 : 1];
+        scale_max = FindMaxScale(x_loc,y_loc);
+        scale_factor = 	1.0f + (scale_max * s[m_texCur ? 0 : 1]);
+    }
+    else // Zoom out image
+    {
+        x_loc = m_effect_kenBurns_location_x[m_texCur ? 0 : 1] -  
+            m_effect_kenBurns_location_x[m_texCur ? 0 : 1] * t[m_texCur ? 0 : 1]; 
+        y_loc = m_effect_kenBurns_location_y[m_texCur ? 0 : 1] -  
+            m_effect_kenBurns_location_y[m_texCur ? 0 : 1] * t[m_texCur ? 0 : 1];
+        scale_max = FindMaxScale(x_loc,y_loc);
+        scale_factor = 	1.0f + scale_max -  (scale_max * t[m_texCur ? 0 : 1]);
+    } 
+
+    glMatrixMode(GL_MODELVIEW);
+    glLoadIdentity();
+    glTranslatef(x_loc, y_loc, 0.0f);
+
+    if (effect_pct<= single_image_pct) 
+    {
+        m_effect_kenBurns_new_image_started=false;
+        m_texItem[m_texCur ? 0 : 1].MakeQuad(1.0f, scale_factor); //
+    }
+    else // Fade out image
+    {
+        m_texItem[m_texCur ? 0 : 1].MakeQuad(1.0f - ((effect_pct-single_image_pct)*4), scale_factor); 
+
+    }
+    
+    m_effect_current_frame++;
+}
+
 void GLSingleView::SlideTimeout(void)
 {
     bool wasMovie = false, isMovie = false;
@@ -1269,3 +1466,128 @@
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
 }
+
+void GLSingleView::LoadImage(QImage image, QSize origSize, Face *face)
+{
+    m_effect_kenBurns_image = image;
+    m_effect_kenBurns_orig_image_size = origSize;
+    if (face) 
+    {
+        VERBOSE(VB_IMPORTANT, QString("Found face, set location x '%1' y '%2'").arg(face->getX()).arg(face->getY()));
+        m_effect_kenBurns_face_x = face->getX();
+        m_effect_kenBurns_face_y = face->getY();
+    }
+}
+
+float GLSingleView::FindMaxScale(float x_loc, float y_loc)
+{
+    // Zoom big enough to keep the entire image on screen when we pan
+    if (abs(x_loc) > abs(y_loc))
+        return abs(x_loc) * 2;
+    else
+        return abs(y_loc) * 2;
+}
+
+void GLSingleView::FindRandXY(float &x_loc, float &y_loc)
+{
+    x_loc = (0.5 * rand() / (RAND_MAX + 1.0f)) + 0.25;  //Random number between .25 and .75
+    if ((int)(2.0 * rand() / (RAND_MAX + 1.0f)) == 0)
+        x_loc = -1 * x_loc;
+    y_loc = (0.5 * rand() / (RAND_MAX + 1.0f)) + 0.25;  //Random number between .25 and .75
+    if ((int)(2.0 * rand() / (RAND_MAX + 1.0f)) == 0)
+        y_loc = -1 * y_loc;   
+}
+
+KenBurnsImageLoader::KenBurnsImageLoader(GLSingleView *singleView, ThumbList &itemList, QSize texSize, QSize screenSize)
+{
+    m_singleView = singleView;
+    m_itemList = itemList;
+    m_texSize = texSize;
+    m_screenSize = screenSize;
+}
+
+void KenBurnsImageLoader::Initialize(int pos)
+{
+    m_pos = pos;
+}
+
+void KenBurnsImageLoader::run() 
+{
+    ThumbItem *item = m_itemList.at(m_pos);
+    if (!item)
+    {
+        VERBOSE(VB_IMPORTANT, LOC_ERR + "No item at "<<m_pos);
+        return;
+    }
+    QImage image(item->GetPath());
+    if (image.isNull())
+        return;
+    
+#ifdef FDLIB_SUPPORT 
+    
+    int i, n, x[256], y[256], size[256], w, h, threshold;
+    float loc_x, loc_y;
+    unsigned char *bgrdata, *graydata;
+    
+    w = image.width();
+    h = image.height();
+    bgrdata = image.bits(); 
+  
+    graydata = new unsigned char[w*h];
+    for (i=0; i<w*h; i++)
+        graydata[i] = (unsigned char) ((.11*bgrdata[4*i] + .59*bgrdata[4*i+1] + .3*bgrdata[4*i+2]));
+    
+    
+    threshold = 0;
+    fdlib_detectfaces(graydata, w, h, threshold);   
+    delete[] graydata;       
+    n = fdlib_getndetections();
+    if (n==0) 
+    {
+        VERBOSE(VB_IMPORTANT, QString("%1 face found.").arg(n));
+        m_singleView->LoadImage(QGLWidget::convertToGLFormat(image.smoothScale(m_texSize)), image.size());
+    }
+    else if (n==1)
+    {
+        VERBOSE(VB_IMPORTANT, QString("%1 faces found.").arg(n));
+
+        fdlib_getdetection(0, x, y, size);
+        loc_x = (x[0]-(float)w/2)/((float)w/2)*(-1);
+        loc_y = (y[0]-(float)h/2)/((float)h/2)*(-1);
+        VERBOSE(VB_IMPORTANT, QString("x:%1 y:%2 size:%3").arg(x[0]).arg(y[0]).arg(size[0]));
+        m_singleView->LoadImage(QGLWidget::convertToGLFormat(image.smoothScale(m_texSize)), image.size(), new Face(loc_x, loc_y));
+    }
+    else 
+    {
+        VERBOSE(VB_IMPORTANT, QString("%1 faces found.").arg(n));
+        QPtrList<Face> faces;
+        int faceIndex;
+    
+        for (i=0; i<n; i++)
+        {
+            fdlib_getdetection(i, x+i, y+i, size+i);
+            loc_x = (x[i]-(float)w/2)/((float)w/2)*(-1);
+            loc_y = (y[i]-(float)h/2)/((float)h/2)*(-1);
+            VERBOSE(VB_IMPORTANT, QString("x:%1 y:%2 size:%3").arg(x[i]).arg(y[i]).arg(size[i]));
+            faces.append(new Face(loc_x, loc_y));
+        }
+        if (faces.count() > 1)
+            faceIndex = (int) ((faces.count()+1) * rand() / (RAND_MAX + 1.0f)); //
+        else
+            faceIndex = 0;
+            
+        m_singleView->LoadImage(QGLWidget::convertToGLFormat(image.smoothScale(m_texSize)), image.size(), faces.at(faceIndex));
+    }
+    
+#else  //END FDLIB_SUPPORT
+    m_singleView->LoadImage(QGLWidget::convertToGLFormat(image.smoothScale(m_texSize)), image.size());   
+#endif
+    m_singleView->Ready();
+
+}
+
+Face::Face(float x, float y)
+{
+    m_x = x;
+    m_y = y;
+}
Index: mythgallery/glsingleview.h
===================================================================
--- mythgallery/glsingleview.h	(revision 17145)
+++ mythgallery/glsingleview.h	(working copy)
@@ -28,6 +28,7 @@
 #include <qmap.h>
 #include <qsize.h>
 
+
 // MythTV plugin headers
 #include <mythtv/util.h>
 
@@ -41,6 +42,8 @@
 class QTimer;
 
 class GLSingleView;
+class KenBurnsImageLoader;
+class Face;
 
 class GLSDialog : public MythDialog
 {
@@ -66,6 +69,9 @@
     ~GLSingleView();
 
     void CleanUp(void);
+    void Ready(){m_effect_kenBurns_image_ready = true;}
+    void LoadImage(QImage image, QSize origSize, Face *face = NULL);
+    
 
   protected:
     void initializeGL(void);
@@ -103,7 +109,12 @@
     void EffectSlide(void);
     void EffectFlutter(void);
     void EffectCube(void);
-
+    void EffectKenBurns(void);
+  
+  private:
+	float FindMaxScale(float x_loc, float y_loc);
+	void FindRandXY(float &x_loc, float &y_loc);
+    
   private slots:
     void SlideTimeout(void);
 
@@ -134,7 +145,49 @@
     float         m_effect_cube_xrot;
     float         m_effect_cube_yrot;
     float         m_effect_cube_zrot;
+    float         m_effect_kenBurns_location_x[2];
+    float         m_effect_kenBurns_location_y[2];
+    int           m_effect_kenBurns_projection[2];
+    MythTimer 	  m_effect_kenBurns_image_time[2];
+    float         m_effect_kenBurns_image_timeout;
+    KenBurnsImageLoader *m_effect_kenBurns_imageLoadThread;
+    bool          m_effect_kenBurns_image_ready;
+    QImage        m_effect_kenBurns_image;
+    QSize         m_effect_kenBurns_orig_image_size;
+    float         m_effect_kenBurns_face_x;
+    float         m_effect_kenBurns_face_y;
+    ThumbItem     *m_effect_kenBurns_item;
+    bool          m_effect_kenBurns_initialized;
+    bool          m_effect_kenBurns_new_image_started;
+    
 };
 
+class KenBurnsImageLoader : public QThread
+{
+public:
+    KenBurnsImageLoader(GLSingleView *singleView, ThumbList &itemList, QSize m_texSize, QSize m_screenSize);
+    void Initialize(int pos);
+    void run();
+private:
+	GLSingleView *m_singleView;
+    ThumbList     m_itemList;
+    int           m_pos;
+    bool          m_tex1First;
+    QSize         m_screenSize;
+    QSize         m_texSize;
+
+};
+
+class Face
+{
+  public: 
+    Face(float x, float y);
+    float getX() { return m_x; }
+    float getY() { return m_y; }
+  private:
+      float m_x;
+      float m_y;
+};
+
 #endif // USING_OPENGL
 #endif // GLSINGLEVIEW_H
Index: mythgallery/imageview.cpp
===================================================================
--- mythgallery/imageview.cpp	(revision 17145)
+++ mythgallery/imageview.cpp	(working copy)
@@ -136,6 +136,7 @@
 {
     QMap<QString,QString> tmpMap = m_effect_map;
     tmpMap.remove("none");
+    tmpMap.remove("Ken Burns (gl)");
     QStringList t = tmpMap.keys();
     int i = (int) ( (float)(t.count()) * rand() / (RAND_MAX + 1.0f) );
     return tmpMap[t[i]];
Index: mythgallery/gallerysettings.cpp
===================================================================
--- mythgallery/gallerysettings.cpp	(revision 17145)
+++ mythgallery/gallerysettings.cpp	(working copy)
@@ -105,6 +105,7 @@
     gc->addSelection("flutter (gl)");
     gc->addSelection("cube (gl)");
     gc->addSelection("random (gl)");
+    gc->addSelection("Ken Burns (gl)");
     gc->setHelpText(QObject::tr("This is the type of OpenGL transition used "
                     "between pictures in slideshow mode."));
     return gc;
@@ -113,7 +114,7 @@
 static HostSpinBox *SlideshowOpenGLTransitionLength()
 {
     HostSpinBox *gc = new HostSpinBox(
-        "SlideshowOpenGLTransitionLength", 500, 10000, 500);
+        "SlideshowOpenGLTransitionLength", 500, 30000, 500);
     gc->setLabel(QObject::tr("Duration of OpenGL Transition (milliseconds)"));
     gc->setValue(2000);
     return gc;
@@ -159,7 +160,7 @@
 
 static HostSpinBox *SlideshowDelay()
 {
-    HostSpinBox *gc = new HostSpinBox("SlideshowDelay", 1, 600, 1);
+    HostSpinBox *gc = new HostSpinBox("SlideshowDelay", 0, 600, 1);
     gc->setLabel(QObject::tr("Slideshow Delay"));
     gc->setValue(5);
     gc->setHelpText(QObject::tr("This is the number of seconds to display each "
Index: README
===================================================================
--- README	(revision 17145)
+++ README	(working copy)
@@ -37,6 +37,22 @@
 Current, EXIF support only consists of auto-rotating images if the camera
 sets the orientation tag (My Canon S400 does)
 
+You can also enable fdlib support using
+./configure --enable-fdlib
+fdlib - is a facial recognition library that is used as part of the ken
+burns slide show effect. It tries to ensure that slide shows pan/zoom
+to peoples faces rather than their feet or off to the background.
+fdlib can be downloaded from:
+http://www.kyb.mpg.de/bs/people/kienzle/fdlib/fdlib.htm 
+It will require the gcc compatibility packages. Once downloaded
+fdlib.h should be installed in your headers directory (/usr/include)
+libfd.so should be installed in your libs directory (/usr/lib)
+fdlib.dat should be installed in your current working directory when
+mythfrontend is started for me it was (/home/myth). Please note that 
+this is not probably not the same directory where mythfrontend actually 
+resides. If fdlib does not find fdlib.dat, it will fail silently and just
+not find any faces.
+
 2) Next type 'qmake mythgallery.pro' then 'make' in the 
 main distribution directory.
 
