From: Michael J. Rubinsky Date: Fri, 31 Jul 2009 03:47:10 +0000 (-0400) Subject: First attempt at cleaning up Ansel_Faces code X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=54ca0b6c6d0d56d2beed1839774f743f9e425256;p=horde.git First attempt at cleaning up Ansel_Faces code --- diff --git a/ansel/faces/claim.php b/ansel/faces/claim.php index 06c820417..97e305b01 100644 --- a/ansel/faces/claim.php +++ b/ansel/faces/claim.php @@ -66,7 +66,7 @@ if ($form->validate()) { } } - header('Location: ' . $faces->getLink($face)); + header('Location: ' . Ansel_Faces::getLink($face)); exit; } diff --git a/ansel/faces/gallery.php b/ansel/faces/gallery.php index 7694cd7c9..90aae33c5 100644 --- a/ansel/faces/gallery.php +++ b/ansel/faces/gallery.php @@ -14,9 +14,6 @@ * @author Duck */ require_once dirname(__FILE__) . '/../lib/base.php'; -require_once ANSEL_BASE . '/lib/Faces.php'; -require_once 'Horde/Serialize.php'; -require_once 'Horde/UI/Pager.php'; $gallery_id = (int)Horde_Util::getFormData('gallery'); if (empty($gallery_id)) { @@ -42,7 +39,8 @@ $images = $gallery->getImages($page * $perpage, $perpage); $reloadimage = $registry->getImageDir('horde') . '/reload.png'; $customimage = $registry->getImageDir('horde') . '/layout.png'; $customurl = Horde_Util::addParameter(Horde::applicationUrl('faces/custom.php'), 'page', $page); -$autogenerate = Ansel_Faces::autogenerate(); +$face = Ansel_Faces::factory(); +$autogenerate = $face->canAutogenerate(); $vars = Horde_Variables::getDefaultVariables(); $pager = new Horde_UI_Pager( diff --git a/ansel/faces/img.php b/ansel/faces/img.php index 23387ab62..72767e25f 100644 --- a/ansel/faces/img.php +++ b/ansel/faces/img.php @@ -35,8 +35,8 @@ if ($conf['vfs']['src'] == 'sendfile') { // We definitely have an image for the face. $filename = $ansel_vfs->readFile( - $faces->getVFSPath($face['image_id']) . 'faces', - $face_id . $faces->getExtension()); + Ansel_Faces::getVFSPath($face['image_id']) . 'faces', + $face_id . Ansel_Faces::getExtension()); if (is_a($filename, 'PEAR_ERROR')) { Horde::logMessage($filename, __FILE__, __LINE__, PEAR_LOG_ERR); exit; diff --git a/ansel/faces/report.php b/ansel/faces/report.php index 666b2f485..36d491484 100644 --- a/ansel/faces/report.php +++ b/ansel/faces/report.php @@ -69,7 +69,7 @@ if ($form->validate()) { } - header('Location: ' . $faces->getLink($face)); + header('Location: ' . Ansel_Faces::getLink($face)); exit; } diff --git a/ansel/faces/search/image_define.php b/ansel/faces/search/image_define.php index c3ae5343d..c083f8143 100644 --- a/ansel/faces/search/image_define.php +++ b/ansel/faces/search/image_define.php @@ -15,7 +15,7 @@ require_once 'tabs.php'; /* check if image exists */ $tmp = Horde::getTempDir(); -$path = $tmp . '/search_face_' . Horde_Auth::getAuth() . $faces->getExtension(); +$path = $tmp . '/search_face_' . Horde_Auth::getAuth() . Ansel_Faces::getExtension(); if (file_exists($path) !== true) { $notification->push(_("You must upload the search photo first")); diff --git a/ansel/faces/search/image_save.php b/ansel/faces/search/image_save.php index cdc052844..9c608e283 100644 --- a/ansel/faces/search/image_save.php +++ b/ansel/faces/search/image_save.php @@ -16,7 +16,7 @@ require_once 'Horde/Image.php'; /* Check if image exists. */ $tmp = Horde::getTempDir(); -$path = $tmp . '/search_face_' . Horde_Auth::getAuth() . $faces->getExtension(); +$path = $tmp . '/search_face_' . Horde_Auth::getAuth() . Ansel_Faces::getExtension(); if (!file_exists($path)) { $notification->push(_("You must upload the search photo first")); @@ -59,7 +59,7 @@ if ($img->_width >= 50) { } /* Save image. */ -$path = $tmp . '/search_face_thumb_' . Horde_Auth::getAuth() . $faces->getExtension(); +$path = $tmp . '/search_face_thumb_' . Horde_Auth::getAuth() . Ansel_Faces::getExtension(); if (!file_put_contents($path, $img->raw())) { $notification->push(_("Cannot store search photo")); header('Location: ' . Horde::applicationUrl('faces/search/image.php')); diff --git a/ansel/lib/Block/recent_faces.php b/ansel/lib/Block/recent_faces.php index a7f3019ee..521fb7b29 100644 --- a/ansel/lib/Block/recent_faces.php +++ b/ansel/lib/Block/recent_faces.php @@ -38,7 +38,7 @@ class Horde_Block_ansel_recent_faces extends Horde_Block { { require_once dirname(__FILE__) . '/../base.php'; require_once ANSEL_BASE . '/lib/Faces.php'; - $faces = Ansel_Faces::singleton(); + $faces = Ansel_Faces::factory(); $results = $faces->allFaces(0, $this->_params['limit']); if (is_a($results, 'PEAR_Error')) { @@ -48,7 +48,7 @@ class Horde_Block_ansel_recent_faces extends Horde_Block { $html = ''; foreach ($results as $face_id => $face) { $facename = htmlspecialchars($face['face_name'], ENT_COMPAT, Horde_Nls::getCharset()); - $html .= '' + $html .= '' . '' . $facenane  . ''; } diff --git a/ansel/lib/Faces.php b/ansel/lib/Faces.php index 212a3b1ff..0b92654a7 100755 --- a/ansel/lib/Faces.php +++ b/ansel/lib/Faces.php @@ -2,29 +2,19 @@ /** * Face recognition class * + * Copyright 2007-2009 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. * @author Duck * @package Ansel */ -class Ansel_Faces { - - /** - * Attempts to return a reference to a concrete Ansel_Faces instance. - */ - function &singleton() - { - static $face; - - if (!isset($face)) { - $face = Ansel_Faces::factory(); - } - - return $face; - } - +class Ansel_Faces +{ /** * Create instance */ - function factory($driver = null, $params = array()) + static function factory($driver = null, $params = array()) { if ($driver === null) { $driver = $GLOBALS['conf']['faces']['driver']; @@ -34,862 +24,33 @@ class Ansel_Faces { $params = $GLOBALS['conf']['faces']; } - $class_name = 'Ansel_Faces'; - - // Load system helpers if possible - if (Ansel_Faces::autogenerate($driver)) { - require_once ANSEL_BASE . '/lib/Faces/' . basename($driver) . '.php'; - $class_name .= '_' . $driver; - if (!class_exists($class_name)) { - $err = PEAR::raiseError(_("Face driver does not exist.")); - Horde::logMessage($err, __FILE__, __LINE__, PEAR_LOG_ERR); - return $err; - } - } - + $class_name = 'Ansel_Faces_' . $driver; $parser = new $class_name($params); return $parser; } /** - * Tell if the driver can auto generate faces - * - * @param string $driver Driver name - */ - function autogenerate($driver = null) - { - if ($driver === null) { - $driver = $GLOBALS['conf']['faces']['driver']; - } - - return $driver == 'opencv' || - ($driver == 'facedetect' && - version_compare(PHP_VERSION, '5.0.0', '>')); - } - - /** - * Get faces - * - * @param string $file Picture filename - * @abstract - */ - function _getFaces($file) - { - return array(); - } - - /** - * Get all the coordinates for faces in an image. - * - * @param mixed $image The Ansel_Image or a path to the image to check. - * - * @return mixed Array of face data || PEAR_Error - */ - function getFaces(&$image) - { - if (is_a($image, 'Ansel_Image')) { - // First check if screen view exists - if (is_a($result = $image->load('screen'), 'PEAR_Error')) { - return $result; - } - - // Make sure we have an on-disk copy of the file. - $file = $GLOBALS['ansel_vfs']->readFile($image->getVFSPath('screen'), - $image->getVFSName('screen')); - } elseif (empty($file) || !is_string($image)) { - return array(); - } - - // Get faces from driver - $faces = $this->_getFaces($file); - if (is_a($faces, 'PEAR_Error')) { - return $faces; - } - if (empty($faces)) { - return array(); - } - - // Remove faces containg faces - // for example when 2 are together we can have 3 faces - foreach ($faces as $face) { - $id = $this->_isInFace($face, $faces); - if ($id !== false) { - unset($faces[$id]); - } - } - - return $faces; - } - - /** - * Get existing faces data from storage for the given image. - * - * Used if we need to build the face image at some point after it is - * detected. - * - * @param integer $image_id The image_id of the Ansel_Image these faces are - * for. - * @param boolean $full Get full face data or just face_id and - * face_name. - * - * @return mixed Array of faces data || PEAR_Error - */ - function getImageFacesData($image_id, $full = false) - { - $sql = 'SELECT face_id, face_name '; - if ($full) { - $sql .= ', gallery_id, face_x1, face_y1, face_x2, face_y2'; - } - $sql .= ' FROM ansel_faces WHERE image_id = ' . (int)$image_id - . ' ORDER BY face_id DESC'; - - Horde::logMessage('SQL Query by Ansel_Faces::getImageFacesData: ' . $sql, - __FILE__, __LINE__, PEAR_LOG_DEBUG); - $result = $GLOBALS['ansel_db']->query($sql); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } elseif ($result->numRows() == 0) { - return array(); - } - - $faces = array(); - while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { - if ($full) { - $faces[$face['face_id']] = array( - 'face_name' => $face['face_name'], - 'face_id' => $face['face_id'], - 'gallery_id' => $face['gallery_id'], - 'face_x1' => $face['face_x1'], - 'face_y1' => $face['face_y1'], - 'face_x2' => $face['face_x2'], - 'face_y2' => $face['face_y2'], - 'image_id' => $image_id); - } else { - $faces[$face['face_id']] = $face['face_name']; - } - } - - return $faces; - } - - /** - * Get existing faces data for an entire gallery. - * - * @param integer $gallery gallery_id to get data for.\ - * - * @return mixed array of faces data || PEAR_Error - */ - function getGalleryFaces($gallery) - { - $sql = 'SELECT face_id, image_id, gallery_id, face_name FROM ansel_faces ' - . ' WHERE gallery_id = ' . (int)$gallery . ' ORDER BY face_id DESC'; - - $result = $GLOBALS['ansel_db']->query($sql); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } elseif ($result->numRows() == 0) { - return array(); - } - - $faces = array(); - while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { - $faces[$face['face_id']] = array('face_name' => $face['face_name'], - 'face_id' => $face['face_id'], - 'gallery_id' => $face['gallery_id'], - 'image_id' => $face['image_id']); - } - - return $faces; - } - - /** - * Fetchs all faces from all galleries the current user has READ access to? - * - * @param array $info Array of select criteria - * @param integer $from Offset - * @param integer $count Limit - * - * @return mixed An array of faces data || PEAR_Error - */ - function _fetchFaces($info, $from = 0, $count = 0) - { - // add gallery permission - // FIXME: This is a REALLY ugly hack, permissions checking like this - // should be encapsulated by the shares driver and not parsed from - // an internally generated query string fragment. Will need to split - // this out into two seperate operations somehow. - $share = substr($GLOBALS['ansel_storage']->shares->_getShareCriteria( - Horde_Auth::getAuth(), PERMS_READ), 5); - - $sql = 'SELECT f.face_id, f.gallery_id, f.image_id, f.face_name FROM ansel_faces f, ' - . str_replace('WHERE', 'WHERE (', $share) - . ' ) AND f.gallery_id = s.share_id' - . (isset($info['filter']) ? ' AND ' . $info['filter'] : '') - . ' ORDER BY ' . (isset($info['order']) ? $info['order'] : ' f.face_id DESC'); - - $GLOBALS['ansel_db']->setLimit($count, $from); - $result = $GLOBALS['ansel_db']->query($sql); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } elseif ($result->numRows() == 0) { - return array(); - } - - $faces = array(); - while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { - $faces[$face['face_id']] = array('face_name' => $face['face_name'], - 'face_id' => $face['face_id'], - 'gallery_id' => $face['gallery_id'], - 'image_id' => $face['image_id']); - } - - return $faces; - } - - /** - * Count faces - * - * @param array $info Array of select criteria - */ - function _countFaces($info) - { - // add gallery permission - // FIXME: Ditto on the REALLY ugly hack comment from above! - $share = substr($GLOBALS['ansel_storage']->shares->_getShareCriteria( - Horde_Auth::getAuth(), PERMS_READ), 5); - - $sql = 'SELECT COUNT(*) FROM ansel_faces f, ' - . str_replace('WHERE', 'WHERE (', $share) - . ' ) AND f.gallery_id = s.share_id' - . (isset($info['filter']) ? ' AND ' . $info['filter'] : ''); - - return $GLOBALS['ansel_db']->queryOne($sql); - } - - /** - * Get all faces - * - * Note: I removed the 'random' parameter since it won't work across - * different RDBMS and it's incredibly resource intensive as it - * causes the RDBMS to generate a rand() number for each row and THEN - * sort the table by those numbers. - * @param integer $from Offset - * @param integer $count Limit - */ - function allFaces($from = 0, $count = 0) - { - $info = array('order' => 'f.face_id DESC'); - return $this->_fetchFaces($info, $from, $count); - } - - /** - * Get named faces - * - * @param integer $from Offset - * @param integer $count Limit - */ - function namedFaces($from = 0, $count = 0) - { - $info = array('filter' => 'f.face_name IS NOT NULL AND f.face_name <> \'\''); - return $this->_fetchFaces($info, $from, $count); - } - - /** - * Get faces owned by user - * - * @param string $owner User - * @param integer $from Offset - * @param integer $count Limit - */ - function ownerFaces($owner, $from = 0, $count = 0) - { - $info = array( - 'filter' => 's.share_owner = ' . $GLOBALS['ansel_db']->quote($owner), - 'order' => 'f.face_id DESC'); - - if ($owner != Horde_Auth::getAuth()) { - $info['filter'] .= ' AND s.gallery_passwd IS NULL'; - } - - return $this->_fetchFaces($info, $from, $count); - } - - /** - * Seach faces for a name - * - * @param string $name Search string - * @param integer $from Offset - * @param integer $count Limit - */ - function searchFaces($name, $from = 0, $count = 0) - { - $info = array('filter' => 'f.face_name LIKE ' . $GLOBALS['ansel_db']->quote("%$name%")); - return $this->_fetchFaces($info, $from, $count); - } - - /** - * Get faces owned by owner - * - * @param string $owner User - */ - function countOwnerFaces($owner) - { - $info = array('filter' => 's.share_owner = ' . $GLOBALS['ansel_db']->quote($owner)); - if ($owner != Horde_Auth::getAuth()) { - $info['filter'] .= ' AND s.gallery_passwd IS NULL'; - } - - return $this->_countFaces($info); - } - - /** - * Count all faces - */ - function countAllFaces() - { - return $this->_countFaces(array()); - } - - /** - * Get named faces - */ - function countNamedFaces() - { - $sql = 'SELECT COUNT(*) FROM ansel_faces WHERE face_name IS NOT NULL AND face_name <> \'\''; - return $GLOBALS['ansel_db']->queryOne($sql); - } - - /** - * Seach faces for a name - * - * @param string $name Search string - */ - function countSearchFaces($name) - { - $info = array('filter' => 'f.face_name LIKE ' . $GLOBALS['ansel_db']->quote("%$name%")); - return $this->_countFaces($info); - } - - - /** - * Checks to see that a given face image exists in the VFS. - * - * If $create is true, the image is created if it does not - * exist. Otherwise false is returned if the image does not exist. True is - * returned both if the image already existed OR if it did not exist, but - * was successfully created. - * - * @param integer $image_id The image_id the face belongs to. - * @param integer $face_id The face_id we are checking for. - * @param boolean $create Automatically create the image if it is not - * found. - * - * @return boolean True if image exists at end of function call, false - * otherwise. - */ - function viewExists($image_id, $face_id, $create = true) - { - $vfspath = $this->getVFSPath($image_id) . 'faces'; - $vfsname = $face_id . $this->getExtension(); - if (!$GLOBALS['ansel_vfs']->exists($vfspath, $vfsname)) { - if (!$create) { - return false; - } - $data = $this->getFaceById($face_id, true); - if (is_a($data, 'PEAR_Error')) { - return $data; - } - $image = &$GLOBALS['ansel_storage']->getImage($image_id); - if (is_a($image, 'PEAR_Error')) { - return $image; - } - - // Actually create the image. - $result = $this->createView( - $face_id, - $image, - $data['face_x1'], - $data['face_y1'], - $data['face_x2'], - $data['face_y2']); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - $this->saveSignature($image_id, $face_id); - } - return true; - } - - /** - * Get a Horde_Image object representing the requested face. - * - * @param integer $face_id The requested face_id - * - * @return mixed The requeste Horde_Image object || PEAR_Error - */ - function getFaceImageObject($face_id) - { - $face = $this->getFaceById($face_id, true); - if (is_a($face, 'PEAR_Error')) { - Horde::logMessage($face, __FILE__, __LINE__, PEAR_LOG_ERR); - return $face; - } - - // Load the image for this face - if (!$this->viewExists($face['image_id'], $face_id, true)) { - $err = PEAR::raiseError(sprintf("Unable to create or locate face_id %u", $face_id)); - Horde::logMessage($err, __FILE__, __LINE__, PEAR_LOG_ERR); - return $err; - } - $vfspath = $this->getVFSPath($face['image_id']) . 'faces'; - $vfsname = $face_id . $this->getExtension(); - $img = Ansel::getImageObject(); - $data = $GLOBALS['ansel_vfs']->read($vfspath, $vfsname); - if (is_a($data, 'PEAR_Error')) { - Horde::logMessage($data, __FILE__, __LINE__, PEAR_LOG_ERR); - return $data; - } - $img->loadString($face_id, $data); - return $img; - } - - /** - * Get a URL for a face image suitable for using as the src attribute in an - * image tag. - * - * @param integer $image_id Image ID to get url for - * @param integer $face_id Face ID to get url for - * @param boolean $full Should we generate a full URL? - * - * @return string The URL for the face image suitable for use as the src - * attribute in an tag. - */ - function getFaceUrl($image_id, $face_id, $full = false) - { - global $conf; - - // If we won't be using img.php to generate it, make sure the image - // is generated before returning a url to access it. - if ($conf['vfs']['src'] != 'php') { - $this->viewExists($image_id, $face_id, true); - } - - // If not viewing directly out of the VFS, hand off to img.php - if ($conf['vfs']['src'] != 'direct') { - return Horde::applicationUrl( - Horde_Util::addParameter('faces/img.php', 'face', $face_id), $full); - } else { - $path = substr(str_pad($image_id, 2, 0, STR_PAD_LEFT), -2) . '/faces'; - return $GLOBALS['conf']['vfs']['path'] . htmlspecialchars($path . '/' . $face_id . $this->getExtension()); - } - } - - /** - * Get image path - * - * @param integer $image Image ID to get - * @static - */ - function getVFSPath($image) - { - return '.horde/ansel/' . substr(str_pad($image, 2, 0, STR_PAD_LEFT), -2) . '/'; - } - - /** - * Get filename extension - * - * @static - */ - function getExtension() - { - if ($GLOBALS['conf']['image']['type'] == 'jpeg') { - return '.jpg'; - } else { - return '.png'; - } - } - - /** - * Associates a given rectangle with the given image and creates the face - * image. Used for setting a face range explicitly. - * - * @param integer $face_id Face id to save - * @param integer $image Image face belongs to - * @param integer $x1 The top left corner of the cropped image. - * @param integer $y1 The top right corner of the cropped image. - * @param integer $x2 The bottom left corner of the cropped image. - * @param integer $y2 The bottom right corner of the cropped image. - * @param string $name Face name - * - * @return array Faces found - */ - function saveCustomFace($face_id, $image, $x1, $y1, $x2, $y2, $name = '') - { - $image = &$GLOBALS['ansel_storage']->getImage($image); - if (is_a($image, 'PEAR_Error')) { - return $image; - } - $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery); - if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) { - return PEAR::raiseError(_("Access denied editing the photo.")); - } - - if (empty($face_id)) { - $new = true; - $face_id = $GLOBALS['ansel_db']->nextId('ansel_faces'); - if (is_a($face_id, 'PEAR_Error')) { - return $face_id; - } - } - - // The user edits the screen image not the full image - $image->load('screen'); - - // Process the image - $result = $this->createView($face_id, - $image, - $x1, - $y1, - $x2, - $y2); - - // Clean up as images are static and all gallery images data will remain in memory - $image->reset(); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - // Store face id db - if (empty($new)) { - $sql = 'UPDATE ansel_faces SET face_name = ?, face_x1 = ?, face_y1 = ?, face_x2 = ?, face_y2 = ?' - . ' WHERE face_id = ?'; - $params = array($name, - $x1, - $y1, - $x2, - $y2, - $face_id); - } else { - - $sql = 'INSERT INTO ansel_faces (face_id, image_id, gallery_id, face_name, ' - . ' face_x1, face_y1, face_x2, face_y2)' - . ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; - $params = array($face_id, - $image->id, - $image->gallery, - $name, - $x1, - $y1, - $x2, - $y2); - } - - $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); - if (is_a($q, 'PEAR_Error')) { - Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR); - return $q; - } - $result = $q->execute($params); - $q->free(); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } - - // Update gallery and image counts - $GLOBALS['ansel_db']->exec('UPDATE ansel_images SET image_faces = image_faces + 1 WHERE image_id = ' . $image->id); - $GLOBALS['ansel_db']->exec('UPDATE ansel_shares SET attribute_faces = attribute_faces + 1 WHERE gallery_id = ' . $image->gallery); - - // Save signature - $this->saveSignature($image->id, $face_id); - - return $face_id; - } - - - /** - * Look for and save faces in a picture, and optionally create the face - * image. - * - * @param mixed $image Image Object/ID to check - * @param boolen $create Create images or store data? - * - * @return array Faces found - */ - function getFromPicture(&$image, $create = false) - { - // get image if ID is passed - if (!is_a($image, 'Ansel_Image')) { - $image = &$GLOBALS['ansel_storage']->getImage($image); - if (is_a($image, 'PEAR_Error')) { - return $image; - } - $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery); - if (is_a($gallery, 'PEAR_Error')) { - return $gallery; - } - if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) { - return PEAR::raiseError(_("Access denied editing the photo.")); - } - } - - // Get the rectangles for any faces in this image. - $faces = $this->getFaces($image); - if (is_a($faces, 'PEAR_Error')) { - return $faces; - } elseif (empty($faces)) { - return array(); - } - - // Clean up any existing faces we may have had in this image. - $result = $this->delete($image); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - // Process faces - $fids = array(); - foreach ($faces as $i => $rect) { - // Create Face id - $face_id = $GLOBALS['ansel_db']->nextId('ansel_faces'); - if (is_a($face_id, 'PEAR_Error')) { - Horde::logMessage($face_id, __FILE__, __LINE__, PEAR_LOG_ERR); - return $face_id; - } - - // Store face id db - $sql = 'INSERT INTO ansel_faces (face_id, image_id, gallery_id, face_x1, ' - . ' face_y1, face_x2, face_y2)' - . ' VALUES (?, ?, ?, ?, ?, ?, ?)'; - - $params = $this->_getParamsArray($face_id, $image, $rect); - - $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); - if (is_a($q, 'PEAR_Error')) { - Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR); - return $q; - } - $result = $q->execute($params); - $q->free(); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } - if ($create) { - // Process image - $result = $this->_createView($face_id, $image, $rect); - - // Clear any loaded views to save on memory usage. - // TODO: Not sure if this is really necessary or not. - $image->reset(); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - $this->saveSignature($image->id, $face_id); - } - $fids[$face_id] = ''; - - } - - // Update gallery and image counts - $GLOBALS['ansel_db']->exec('UPDATE ansel_images SET image_faces = ' . count($fids) . ' WHERE image_id = ' . $image->id); - $GLOBALS['ansel_db']->exec('UPDATE ansel_shares SET attribute_faces = attribute_faces + ' . count($fids) . ' WHERE gallery_id = ' . $image->gallery); - - // Expire gallery cache - if ($GLOBALS['conf']['ansel_cache']['usecache']) { - $GLOBALS['cache']->expire('Ansel_Gallery' . $gallery->id); - } - - return $fids; - } - - /** - * Create a face image from the given data. - * - * @param integer $face_id Face id to generate - * @param integer $image Image face belongs to - * @param integer $x1 The top left corner of the cropped image. - * @param integer $y1 The top right corner of the cropped image. - * @param integer $x2 The bottom left corner of the cropped image. - * @param integer $y2 The bottom right corner of the cropped image. - * - * @return mixed the face id or PEAR_Error on failure. - */ - function createView($face_id, &$image, $x1, $y1, $x2, $y2) - { - // Make sure screen view is created and loaded - $image->load('screen'); - - // Crop to the face - $result = $image->_image->crop($x1, $y1, $x2, $y2); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - // Resize and save - $ext = $this->getExtension(); - $path = $this->getVFSPath($image->id); - $image->_image->resize(50, 50, false); - $result = $GLOBALS['ansel_vfs']->writeData($path . 'faces', $face_id . $ext, - $image->_image->raw(), true); - if (is_a($result, 'PEAR_Error')) { - return $result; - } - - return $face_id; - } - - /** - * Get get face signature from an existing face image. - * - * @param integer $image_id Image ID face belongs to - * @param integer $face_id Face ID to check - * - * @return mixed True || PEAR_Error - */ - function saveSignature($image_id, $face_id) - { - // can we get it? - if (empty($GLOBALS['conf']['faces']['search']) || - Horde_Util::loadExtension('libpuzzle') === false) { - - return ''; - } - - // Ensure we have an on-disk file to read the signature from. - $path = $GLOBALS['ansel_vfs']->readFile($this->getVFSPath($image_id) . '/faces', - $face_id . $this->getExtension()); - - $signature = puzzle_fill_cvec_from_file($path); - if (empty($signature)) { - return ''; - } - // save compressed signature - $sql = 'UPDATE ansel_faces SET face_signature = ? WHERE face_id = ?'; - $params = array(puzzle_compress_cvec($signature), $face_id); - $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); - if (is_a($q, 'PEAR_Error')) { - Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR); - return $q; - } - $result = $q->execute($params); - $q->free(); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } - - // create index - $word_len = $GLOBALS['conf']['faces']['search']; - $str_len = strlen($signature); - $times = $str_len / $word_len; - $data = array(); - for ($i = 0; $i < $times; $i++) { - $data[] = array($face_id, - $i, - substr($signature, $i * $word_len, $word_len)); - } - - $GLOBALS['ansel_db']->exec('DELETE FROM ansel_faces_index WHERE face_id = ' . $face_id); - $q = &$GLOBALS['ansel_db']->prepare('INSERT INTO ansel_faces_index (face_id, index_position, index_part) VALUES (?, ?, ?)'); - if (is_a($q, 'PEAR_Error')) { - Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR); - return $q; - } - - $GLOBALS['ansel_db']->loadModule('Extended'); - $GLOBALS['ansel_db']->executeMultiple($q, $data); - $q->free(); - - return true; - } - - /** - * Get an image signature from an arbitrary file. Currently used when - * searching for faces that appear in a user-supplied image. - * - * @param integer $filename Image filename to check - * - * @return binary vector signature - */ - function getSignatureFromFile($filename) - { - if ($GLOBALS['conf']['faces']['search'] == 0 || - Horde_Util::loadExtension('libpuzzle') === false) { - - return ''; - } - - return puzzle_fill_cvec_from_file($filename); - } - - /** - * Get faces for all images in a gallery - * - * @param integer $gallery_id The share_id/gallery_id of the gallery to - * check. - * @param boolen $create Create faces and signatures or just store coordniates? - * @param boolen $force Force recreation even if image has faces - * - * @return array Faces found - */ - function getFromGallery($gallery_id, $create = false, $force = false) - { - $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id); - if (is_a($gallery, 'PEAR_Error')) { - return $gallery; - } elseif (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) { - return PEAR::raiseError(sprintf(_("Access denied editing gallery \"%s\"."), $gallery->get('name'))); - } - - $images = $gallery->getImages(); - if (is_a($images, 'PEAR_Error')) { - return $images; - } - - $faces = array(); - foreach ($images as $image) { - if ($image->facesCount && $force == false) { - continue; - } - $result = $this->getFromPicture($image, $create); - if (is_a($result, 'PEAR_Error')) { - return $result; - } elseif (!empty($result)) { - $faces[$image->id] = $result; - } - unset($image); - } - - return $faces; - } - - /** * Delete faces from VFS and DB storage. * * @param Ansel_Image $image Image object to delete faces for * @param integer $face Face id * @static */ - function delete(&$image, $face = null) + static public function delete(&$image, $face = null) { if ($image->facesCount == 0) { return true; } - $path = Ansel_Faces::getVFSPath($image->id) . '/faces'; - $ext = Ansel_Faces::getExtension(); + $path = self::getVFSPath($image->id) . '/faces'; + $ext = self::getExtension(); if ($face === null) { $sql = 'SELECT face_id FROM ansel_faces WHERE image_id = ' . $image->id; $face = $GLOBALS['ansel_db']->queryCol($sql); - if (is_a($face, 'PEAR_Error')) { - Horde::logMessage($face, __FILE__, __LINE__, PEAR_LOG_ERR); - return $face; + if ($face instanceof PEAR_Error) { + throw new Horde_Exception($face); } foreach ($face as $id) { @@ -910,23 +71,26 @@ class Ansel_Faces { } /** - * Set face name + * Get image path * - * @param integer $face Face id - * @param string $name Face name + * @param integer $image Image ID to get */ - function setName($face, $name) + static public function getVFSPath($image) { - $sql = 'UPDATE ansel_faces SET face_name = ? WHERE face_id = ?'; - $params = array($name, $face); + return '.horde/ansel/' . substr(str_pad($image, 2, 0, STR_PAD_LEFT), -2) . '/'; + } - $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); - if (is_a($q, 'PEAR_Error')) { - Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR); - return $q; + /** + * Get filename extension + * + */ + static public function getExtension() + { + if ($GLOBALS['conf']['image']['type'] == 'jpeg') { + return '.jpg'; + } else { + return '.png'; } - - return $q->execute($params); } /** @@ -937,7 +101,7 @@ class Ansel_Faces { * @static * @return string The url for the image this face belongs to. */ - function getLink($face) + static public function getLink($face) { return Ansel::getUrlFor('view', array('view' => 'Image', @@ -946,150 +110,11 @@ class Ansel_Faces { } /** - * Get face data - * - * @param integer $face_id Face id - * @param boolean $full Retreive full face data? - */ - function getFaceById($face_id, $full = false) - { - $sql = 'SELECT image_id, gallery_id, face_name'; - if ($full) { - $sql .= ', face_x1, face_y1, face_x2, face_y2, face_signature'; - } - $sql .= ' FROM ansel_faces WHERE face_id = ?'; - $q = $GLOBALS['ansel_db']->prepare($sql); - if (is_a($q, 'PEAR_Error')) { - Horde::logMessage($q, __FILE__, __LINE__, PEAR_LOG_ERR); - return $q; - } - - $result = $q->execute((int)$face_id); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } elseif ($result->numRows() == 0) { - return PEAR::raiseError(_("Face does not exist")); - } - - $face = $result->fetchRow(MDB2_FETCHMODE_ASSOC); - if (is_a($face, 'PEAR_Error')) { - Horde::logMessage($face, __FILE__, __LINE__, PEAR_LOG_ERR); - return $face; - } - - // Always return the face_id - $face['face_id'] = $face_id; - - if ($full && $GLOBALS['conf']['faces']['search'] && - function_exists('puzzle_uncompress_cvec')) { - $face['face_signature'] = puzzle_uncompress_cvec($face['face_signature']); - } - - if (empty($face['face_name'])) { - $face['galleries'][$face['gallery_id']][] = $face['image_id']; - return $face; - } - - $sql = 'SELECT gallery_id, image_id FROM ansel_faces WHERE face_name = ' . $GLOBALS['ansel_db']->quote($face['face_name']); - $result = $GLOBALS['ansel_db']->query($sql); - - if (is_a($result, 'PEAR_Error')) { - return $result; - } elseif ($result->numRows() == 0) { - return PEAR::RaiseError(_("Face does not exist")); - } - - while ($gallery = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { - $face['galleries'][$gallery['gallery_id']][] = $gallery['image_id']; - } - - return $face; - } - - /** - * Get possible matches from sql index - * - * @param binary $signature Image signature - * @param integer $from Offset - * @param integer $count Limit - * - * @return binary vector signature - */ - function getSignatureMatches($signature, $face_id = 0, $from = 0, $count = 0) - { - $word_len = $GLOBALS['conf']['faces']['search']; - $str_len = strlen($signature); - $times = $str_len / $word_len; - - $indexes = array(); - for ($i = 0; $i < $times; $i++) { - $indexes[] = '(index_position = ' - . $GLOBALS['ansel_db']->quote($i, 'integer') - . ' AND index_part = ' - . $GLOBALS['ansel_db']->quote( - substr($signature, $i * $word_len, $word_len)) - . ')'; - } - - $sql = 'SELECT COUNT(*) as face_matches, i.face_id, f.face_name, ' - . 'f.image_id, f.gallery_id, f.face_signature ' - . 'FROM ansel_faces_index i, ansel_faces f ' - . 'WHERE f.face_id = i.face_id'; - if ($face_id) { - $sql .= ' AND i.face_id <> ' - . $GLOBALS['ansel_db']->quote($face_id, 'integer'); - } - if ($indexes) { - $sql .= ' AND (' . implode(' OR ', $indexes) . ')'; - } - $sql .= ' GROUP BY i.face_id HAVING face_matches > 0 ' - . 'ORDER BY face_matches DESC'; - $GLOBALS['ansel_db']->setLimit($count, $from); - - $result = $GLOBALS['ansel_db']->query($sql); - if (is_a($result, 'PEAR_Error')) { - Horde::logMessage($result, __FILE__, __LINE__, PEAR_LOG_ERR); - return $result; - } elseif ($result->numRows() == 0) { - return array(); - } - - $faces = array(); - while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { - $faces[$face['face_id']] = array( - 'face_name' => $face['face_name'], - 'face_id' => $face['face_id'], - 'gallery_id' => $face['gallery_id'], - 'image_id' => $face['image_id'], - 'similarity' => puzzle_vector_normalized_distance( - $signature, - puzzle_uncompress_cvec($face['face_signature']))); - } - uasort($faces, array($this, '_getSignatureMatches')); - - return $faces; - } - - /** - * Compare faces by similarity. - * - * @param array $a - * @param array $b - */ - function _getSignatureMatches($a, $b) - { - return $a['similarity'] > $b['similarity']; - } - - /** - * Output HTML for this face's tile - * @static + * Output HTML for a face's tile */ - function getFaceTile($face) + static public function getFaceTile($face) { - $faces = Ansel_Faces::singleton(); - + $faces = Ansel_Faces::factory(); if (!is_array($face)) { $face = $faces->getFaceById($face, true); } @@ -1132,5 +157,4 @@ class Ansel_Faces { return $html; } - } diff --git a/ansel/lib/Faces/Base.php b/ansel/lib/Faces/Base.php new file mode 100644 index 000000000..93303d566 --- /dev/null +++ b/ansel/lib/Faces/Base.php @@ -0,0 +1,886 @@ + + * @package Ansel + */ +class Ansel_Faces_Base +{ + public function canAutogenerate() { + return false; + } + + /** + * Get faces + * + * @param string $file Picture filename + */ + protected function _getFaces($file) + { + return array(); + } + + /** + * Get all the coordinates for faces in an image. + * + * @param mixed $image The Ansel_Image or a path to the image to check. + * + * @return mixed Array of face data + */ + public function getFaces(&$image) + { + if ($image instanceof Ansel_Image) { + // First check if screen view exists + $image->load('screen'); + + // Make sure we have an on-disk copy of the file. + $file = $GLOBALS['ansel_vfs']->readFile($image->getVFSPath('screen'), + $image->getVFSName('screen')); + } elseif (empty($file) || !is_string($image)) { + return array(); + } + + // Get faces from driver + $faces = $this->_getFaces($file); + if (empty($faces)) { + return array(); + } + + // Remove faces containg faces + // for example when 2 are together we can have 3 faces + foreach ($faces as $face) { + $id = $this->_isInFace($face, $faces); + if ($id !== false) { + unset($faces[$id]); + } + } + + return $faces; + } + + /** + * Get existing faces data from storage for the given image. + * + * Used if we need to build the face image at some point after it is + * detected. + * + * @param integer $image_id The image_id of the Ansel_Image these faces are + * for. + * @param boolean $full Get full face data or just face_id and + * face_name. + * + * @return Array of faces data + * @throws Horde_Exception + */ + public function getImageFacesData($image_id, $full = false) + { + $sql = 'SELECT face_id, face_name '; + if ($full) { + $sql .= ', gallery_id, face_x1, face_y1, face_x2, face_y2'; + } + $sql .= ' FROM ansel_faces WHERE image_id = ' . (int)$image_id + . ' ORDER BY face_id DESC'; + + Horde::logMessage('SQL Query by Ansel_Faces::getImageFacesData: ' . $sql, + __FILE__, __LINE__, PEAR_LOG_DEBUG); + $result = $GLOBALS['ansel_db']->query($sql); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } elseif ($result->numRows() == 0) { + return array(); + } + + $faces = array(); + while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { + if ($full) { + $faces[$face['face_id']] = array( + 'face_name' => $face['face_name'], + 'face_id' => $face['face_id'], + 'gallery_id' => $face['gallery_id'], + 'face_x1' => $face['face_x1'], + 'face_y1' => $face['face_y1'], + 'face_x2' => $face['face_x2'], + 'face_y2' => $face['face_y2'], + 'image_id' => $image_id); + } else { + $faces[$face['face_id']] = $face['face_name']; + } + } + + return $faces; + } + + /** + * Get existing faces data for an entire gallery. + * + * @param integer $gallery gallery_id to get data for. + * + * @return mixed array of faces data. + * @throws Horde_Exception + */ + public function getGalleryFaces($gallery) + { + $sql = 'SELECT face_id, image_id, gallery_id, face_name FROM ansel_faces ' + . ' WHERE gallery_id = ' . (int)$gallery . ' ORDER BY face_id DESC'; + + $result = $GLOBALS['ansel_db']->query($sql); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } elseif ($result->numRows() == 0) { + return array(); + } + + $faces = array(); + while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { + $faces[$face['face_id']] = array('face_name' => $face['face_name'], + 'face_id' => $face['face_id'], + 'gallery_id' => $face['gallery_id'], + 'image_id' => $face['image_id']); + } + + return $faces; + } + + /** + * Fetchs all faces from all galleries the current user has READ access to? + * + * @param array $info Array of select criteria + * @param integer $from Offset + * @param integer $count Limit + * + * @return mixed An array of faces data + */ + protected function _fetchFaces($info, $from = 0, $count = 0) + { + // add gallery permission + // FIXME: This is a REALLY ugly hack, permissions checking like this + // should be encapsulated by the shares driver and not parsed from + // an internally generated query string fragment. Will need to split + // this out into two seperate operations somehow. + $share = substr($GLOBALS['ansel_storage']->shares->_getShareCriteria( + Horde_Auth::getAuth(), PERMS_READ), 5); + + $sql = 'SELECT f.face_id, f.gallery_id, f.image_id, f.face_name FROM ansel_faces f, ' + . str_replace('WHERE', 'WHERE (', $share) + . ' ) AND f.gallery_id = s.share_id' + . (isset($info['filter']) ? ' AND ' . $info['filter'] : '') + . ' ORDER BY ' . (isset($info['order']) ? $info['order'] : ' f.face_id DESC'); + + $GLOBALS['ansel_db']->setLimit($count, $from); + $result = $GLOBALS['ansel_db']->query($sql); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } elseif ($result->numRows() == 0) { + return array(); + } + + $faces = array(); + while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { + $faces[$face['face_id']] = array('face_name' => $face['face_name'], + 'face_id' => $face['face_id'], + 'gallery_id' => $face['gallery_id'], + 'image_id' => $face['image_id']); + } + + return $faces; + } + + /** + * Count faces + * + * @param array $info Array of select criteria + */ + protected function _countFaces($info) + { + // add gallery permission + // FIXME: Ditto on the REALLY ugly hack comment from above! + $share = substr($GLOBALS['ansel_storage']->shares->_getShareCriteria( + Horde_Auth::getAuth(), PERMS_READ), 5); + + $sql = 'SELECT COUNT(*) FROM ansel_faces f, ' + . str_replace('WHERE', 'WHERE (', $share) + . ' ) AND f.gallery_id = s.share_id' + . (isset($info['filter']) ? ' AND ' . $info['filter'] : ''); + + return $GLOBALS['ansel_db']->queryOne($sql); + } + + /** + * Get all faces + * + * @param integer $from Offset + * @param integer $count Limit + */ + public function allFaces($from = 0, $count = 0) + { + $info = array('order' => 'f.face_id DESC'); + return $this->_fetchFaces($info, $from, $count); + } + + /** + * Get named faces + * + * @param integer $from Offset + * @param integer $count Limit + */ + public function namedFaces($from = 0, $count = 0) + { + $info = array('filter' => 'f.face_name IS NOT NULL AND f.face_name <> \'\''); + return $this->_fetchFaces($info, $from, $count); + } + + /** + * Get faces owned by user + * + * @param string $owner User + * @param integer $from Offset + * @param integer $count Limit + */ + public function ownerFaces($owner, $from = 0, $count = 0) + { + $info = array( + 'filter' => 's.share_owner = ' . $GLOBALS['ansel_db']->quote($owner), + 'order' => 'f.face_id DESC'); + + if ($owner != Horde_Auth::getAuth()) { + $info['filter'] .= ' AND s.gallery_passwd IS NULL'; + } + + return $this->_fetchFaces($info, $from, $count); + } + + /** + * Seach faces for a name + * + * @param string $name Search string + * @param integer $from Offset + * @param integer $count Limit + */ + public function searchFaces($name, $from = 0, $count = 0) + { + $info = array('filter' => 'f.face_name LIKE ' . $GLOBALS['ansel_db']->quote("%$name%")); + return $this->_fetchFaces($info, $from, $count); + } + + /** + * Get faces owned by owner + * + * @param string $owner User + */ + public function countOwnerFaces($owner) + { + $info = array('filter' => 's.share_owner = ' . $GLOBALS['ansel_db']->quote($owner)); + if ($owner != Horde_Auth::getAuth()) { + $info['filter'] .= ' AND s.gallery_passwd IS NULL'; + } + + return $this->_countFaces($info); + } + + /** + * Count all faces + */ + public function countAllFaces() + { + return $this->_countFaces(array()); + } + + /** + * Get named faces + */ + public function countNamedFaces() + { + $sql = 'SELECT COUNT(*) FROM ansel_faces WHERE face_name IS NOT NULL AND face_name <> \'\''; + return $GLOBALS['ansel_db']->queryOne($sql); + } + + /** + * Seach faces for a name + * + * @param string $name Search string + */ + public function countSearchFaces($name) + { + $info = array('filter' => 'f.face_name LIKE ' . $GLOBALS['ansel_db']->quote("%$name%")); + return $this->_countFaces($info); + } + + + /** + * Checks to see that a given face image exists in the VFS. + * + * If $create is true, the image is created if it does not + * exist. Otherwise false is returned if the image does not exist. True is + * returned both if the image already existed OR if it did not exist, but + * was successfully created. + * + * @param integer $image_id The image_id the face belongs to. + * @param integer $face_id The face_id we are checking for. + * @param boolean $create Automatically create the image if it is not + * found. + * + * @return boolean True if image exists at end of function call, false + * otherwise. + */ + public function viewExists($image_id, $face_id, $create = true) + { + $vfspath = Ansel_Faces::getVFSPath($image_id) . 'faces'; + $vfsname = $face_id . Ansel_Faces::getExtension(); + if (!$GLOBALS['ansel_vfs']->exists($vfspath, $vfsname)) { + if (!$create) { + return false; + } + $data = $this->getFaceById($face_id, true); + + $image = &$GLOBALS['ansel_storage']->getImage($image_id); + + // Actually create the image. + $result = $this->createView( + $face_id, + $image, + $data['face_x1'], + $data['face_y1'], + $data['face_x2'], + $data['face_y2']); + + $this->saveSignature($image_id, $face_id); + } + + return true; + } + + /** + * Get a Horde_Image object representing the requested face. + * + * @param integer $face_id The requested face_id + * + * @return Horde_Image The requested Horde_Image object + * @throws Horde_Exception + */ + public function getFaceImageObject($face_id) + { + $face = $this->getFaceById($face_id, true); + + // Load the image for this face + if (!$this->viewExists($face['image_id'], $face_id, true)) { + throw new Horde_Exception(sprintf("Unable to create or locate face_id %u", $face_id)); + } + $vfspath = Ansel_Faces::getVFSPath($face['image_id']) . 'faces'; + $vfsname = $face_id . Ansel_Faces::getExtension(); + $img = Ansel::getImageObject(); + $data = $GLOBALS['ansel_vfs']->read($vfspath, $vfsname); + if ($data instanceof PEAR_Error) { + throw new Horde_Exception($data); + } + $img->loadString($face_id, $data); + + return $img; + } + + /** + * Get a URL for a face image suitable for using as the src attribute in an + * image tag. + * + * @param integer $image_id Image ID to get url for + * @param integer $face_id Face ID to get url for + * @param boolean $full Should we generate a full URL? + * + * @return string The URL for the face image suitable for use as the src + * attribute in an tag. + */ + public function getFaceUrl($image_id, $face_id, $full = false) + { + global $conf; + + // If we won't be using img.php to generate it, make sure the image + // is generated before returning a url to access it. + if ($conf['vfs']['src'] != 'php') { + $this->viewExists($image_id, $face_id, true); + } + + // If not viewing directly out of the VFS, hand off to img.php + if ($conf['vfs']['src'] != 'direct') { + return Horde::applicationUrl( + Horde_Util::addParameter('faces/img.php', 'face', $face_id), $full); + } else { + $path = substr(str_pad($image_id, 2, 0, STR_PAD_LEFT), -2) . '/faces'; + return $GLOBALS['conf']['vfs']['path'] . htmlspecialchars($path . '/' . $face_id . Ansel_Faces::getExtension()); + } + } + + /** + * Associates a given rectangle with the given image and creates the face + * image. Used for setting a face range explicitly. + * + * @param integer $face_id Face id to save + * @param integer $image Image face belongs to + * @param integer $x1 The top left corner of the cropped image. + * @param integer $y1 The top right corner of the cropped image. + * @param integer $x2 The bottom left corner of the cropped image. + * @param integer $y2 The bottom right corner of the cropped image. + * @param string $name Face name + * + * @return array Faces found + * @throws Horde_Exception + */ + public function saveCustomFace($face_id, $image, $x1, $y1, $x2, $y2, $name = '') + { + $image = &$GLOBALS['ansel_storage']->getImage($image); + $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery); + if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) { + //TODO: Do we throw exceptions for access denied? + throw new Horde_Exception('Access denied editing the photo.'); + } + + if (empty($face_id)) { + $new = true; + $face_id = $GLOBALS['ansel_db']->nextId('ansel_faces'); + if ($face_id instanceof PEAR_Error) { + throw new Horde_Exception($face_id); + } + } + + // The user edits the screen image not the full image + $image->load('screen'); + + // Process the image + $result = $this->createView($face_id, + $image, + $x1, + $y1, + $x2, + $y2); + + // Clean up as images are static and all gallery images data will remain in memory + $image->reset(); + + + // Store face id db + if (empty($new)) { + $sql = 'UPDATE ansel_faces SET face_name = ?, face_x1 = ?, face_y1 = ?, face_x2 = ?, face_y2 = ?' + . ' WHERE face_id = ?'; + $params = array($name, + $x1, + $y1, + $x2, + $y2, + $face_id); + } else { + + $sql = 'INSERT INTO ansel_faces (face_id, image_id, gallery_id, face_name, ' + . ' face_x1, face_y1, face_x2, face_y2)' + . ' VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; + $params = array($face_id, + $image->id, + $image->gallery, + $name, + $x1, + $y1, + $x2, + $y2); + } + + $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); + if ($q instanceof PEAR_Error) { + throw new Horde_Exception($q); + } + $result = $q->execute($params); + $q->free(); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + // Update gallery and image counts + $GLOBALS['ansel_db']->exec('UPDATE ansel_images SET image_faces = image_faces + 1 WHERE image_id = ' . $image->id); + $GLOBALS['ansel_db']->exec('UPDATE ansel_shares SET attribute_faces = attribute_faces + 1 WHERE gallery_id = ' . $image->gallery); + + // Save signature + $this->saveSignature($image->id, $face_id); + + return $face_id; + } + + + /** + * Look for and save faces in a picture, and optionally create the face + * image. + * + * @param mixed $image Image Object/ID to check + * @param boolen $create Create images or store data? + * + * @return array Faces found + */ + public function getFromPicture(&$image, $create = false) + { + // get image if ID is passed + if (!is_a($image, 'Ansel_Image')) { + $image = &$GLOBALS['ansel_storage']->getImage($image); + $gallery = $GLOBALS['ansel_storage']->getGallery($image->gallery); + if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) { + throw new Horde_Exception('Access denied editing the photo.'); + } + } + + // Get the rectangles for any faces in this image. + $faces = $this->getFaces($image); + if (empty($faces)) { + return array(); + } + + // Clean up any existing faces we may have had in this image. + $result = $this->delete($image); + + // Process faces + $fids = array(); + foreach ($faces as $i => $rect) { + // Create Face id + $face_id = $GLOBALS['ansel_db']->nextId('ansel_faces'); + if ($face_id instanceof PEAR_Error) { + throw new Horde_Exception($face_id); + } + + // Store face id db + $sql = 'INSERT INTO ansel_faces (face_id, image_id, gallery_id, face_x1, ' + . ' face_y1, face_x2, face_y2)' + . ' VALUES (?, ?, ?, ?, ?, ?, ?)'; + + $params = $this->_getParamsArray($face_id, $image, $rect); + + $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); + if ($q instanceof PEAR_Error) { + throw new Horde_Exception($q); + } + $result = $q->execute($params); + $q->free(); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + if ($create) { + // Process image + $result = $this->_createView($face_id, $image, $rect); + // Clear any loaded views to save on memory usage. + $image->reset(); + $this->saveSignature($image->id, $face_id); + } + $fids[$face_id] = ''; + } + + // Update gallery and image counts + $GLOBALS['ansel_db']->exec('UPDATE ansel_images SET image_faces = ' . count($fids) . ' WHERE image_id = ' . $image->id); + $GLOBALS['ansel_db']->exec('UPDATE ansel_shares SET attribute_faces = attribute_faces + ' . count($fids) . ' WHERE gallery_id = ' . $image->gallery); + + // Expire gallery cache + if ($GLOBALS['conf']['ansel_cache']['usecache']) { + $GLOBALS['cache']->expire('Ansel_Gallery' . $gallery->id); + } + + return $fids; + } + + /** + * Create a face image from the given data. + * + * @param integer $face_id Face id to generate + * @param integer $image Image face belongs to + * @param integer $x1 The top left corner of the cropped image. + * @param integer $y1 The top right corner of the cropped image. + * @param integer $x2 The bottom left corner of the cropped image. + * @param integer $y2 The bottom right corner of the cropped image. + * + * @return integer the face id + */ + public function createView($face_id, &$image, $x1, $y1, $x2, $y2) + { + // Make sure screen view is created and loaded + $image->load('screen'); + + // Crop to the face + try { + $result = $image->_image->crop($x1, $y1, $x2, $y2); + } catch (Horde_Image_Exception $e) { + throw new Horde_Exception($e->getMessage()); + } + // Resize and save + $ext = Ansel_Faces::getExtension(); + $path = Ansel_Faces::getVFSPath($image->id); + $image->_image->resize(50, 50, false); + $result = $GLOBALS['ansel_vfs']->writeData($path . 'faces', $face_id . $ext, + $image->_image->raw(), true); + if (is_a($result, 'PEAR_Error')) { + throw new Horde_Exception($result); + } + + return $face_id; + } + + /** + * Get face signature from an existing face image. + * + * @param integer $image_id Image ID face belongs to + * @param integer $face_id Face ID to check + * + * @return boolean + */ + function saveSignature($image_id, $face_id) + { + // can we get it? + if (empty($GLOBALS['conf']['faces']['search']) || + Horde_Util::loadExtension('libpuzzle') === false) { + + return ''; + } + + // Ensure we have an on-disk file to read the signature from. + $path = $GLOBALS['ansel_vfs']->readFile(Ansel_Faces::getVFSPath($image_id) . '/faces', + $face_id . Ansel_Faces::getExtension()); + + $signature = puzzle_fill_cvec_from_file($path); + if (empty($signature)) { + return false; + } + // save compressed signature + $sql = 'UPDATE ansel_faces SET face_signature = ? WHERE face_id = ?'; + $params = array(puzzle_compress_cvec($signature), $face_id); + $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); + if ($q instanceof PEAR_Error) { + throw new Horde_Exception($q); + } + $result = $q->execute($params); + $q->free(); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } + + // create index + $word_len = $GLOBALS['conf']['faces']['search']; + $str_len = strlen($signature); + $times = $str_len / $word_len; + $data = array(); + for ($i = 0; $i < $times; $i++) { + $data[] = array($face_id, + $i, + substr($signature, $i * $word_len, $word_len)); + } + + $GLOBALS['ansel_db']->exec('DELETE FROM ansel_faces_index WHERE face_id = ' . $face_id); + $q = &$GLOBALS['ansel_db']->prepare('INSERT INTO ansel_faces_index (face_id, index_position, index_part) VALUES (?, ?, ?)'); + if ($q instanceof PEAR_Error) { + throw new Horde_Exception($q); + } + + $GLOBALS['ansel_db']->loadModule('Extended'); + $GLOBALS['ansel_db']->executeMultiple($q, $data); + $q->free(); + + return true; + } + + /** + * Get an image signature from an arbitrary file. Currently used when + * searching for faces that appear in a user-supplied image. + * + * @param integer $filename Image filename to check + * + * @return binary vector signature + */ + public function getSignatureFromFile($filename) + { + if ($GLOBALS['conf']['faces']['search'] == 0 || + Horde_Util::loadExtension('libpuzzle') === false) { + + return ''; + } + + return puzzle_fill_cvec_from_file($filename); + } + + /** + * Get faces for all images in a gallery + * + * @param integer $gallery_id The share_id/gallery_id of the gallery to + * check. + * @param boolen $create Create faces and signatures or just store coordniates? + * @param boolen $force Force recreation even if image has faces + * + * @return array Faces found + */ + public function getFromGallery($gallery_id, $create = false, $force = false) + { + $gallery = $GLOBALS['ansel_storage']->getGallery($gallery_id); + if (!$gallery->hasPermission(Horde_Auth::getAuth(), PERMS_EDIT)) { + throw new Horde_Exception(sprintf("Access denied editing gallery \"%s\".", $gallery->get('name'))); + } + + $images = $gallery->getImages(); + $faces = array(); + foreach ($images as $image) { + if ($image->facesCount && $force == false) { + continue; + } + $result = $this->getFromPicture($image, $create); + if (!empty($result)) { + $faces[$image->id] = $result; + } + unset($image); + } + + return $faces; + } + + /** + * Set face name + * + * @param integer $face Face id + * @param string $name Face name + */ + public function setName($face, $name) + { + $sql = 'UPDATE ansel_faces SET face_name = ? WHERE face_id = ?'; + $params = array($name, $face); + + $q = $GLOBALS['ansel_db']->prepare($sql, null, MDB2_PREPARE_MANIP); + if ($q instanceof PEAR_Error) { + throw new Horde_Exception($q); + } + + return $q->execute($params); + } + + /** + * Get face data + * + * @param integer $face_id Face id + * @param boolean $full Retreive full face data? + */ + public function getFaceById($face_id, $full = false) + { + $sql = 'SELECT image_id, gallery_id, face_name'; + if ($full) { + $sql .= ', face_x1, face_y1, face_x2, face_y2, face_signature'; + } + $sql .= ' FROM ansel_faces WHERE face_id = ?'; + $q = $GLOBALS['ansel_db']->prepare($sql); + if ($q instanceof PEAR_Error) { + throw new Horde_Exception($q); + } + + $result = $q->execute((int)$face_id); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } elseif ($result->numRows() == 0) { + throw new Horde_Exception('Face does not exist'); + } + + $face = $result->fetchRow(MDB2_FETCHMODE_ASSOC); + if (is_a($face, 'PEAR_Error')) { + throw new Horde_Exception($face); + } + + // Always return the face_id + $face['face_id'] = $face_id; + + if ($full && $GLOBALS['conf']['faces']['search'] && + function_exists('puzzle_uncompress_cvec')) { + $face['face_signature'] = puzzle_uncompress_cvec($face['face_signature']); + } + + if (empty($face['face_name'])) { + $face['galleries'][$face['gallery_id']][] = $face['image_id']; + return $face; + } + + $sql = 'SELECT gallery_id, image_id FROM ansel_faces WHERE face_name = ' . $GLOBALS['ansel_db']->quote($face['face_name']); + $result = $GLOBALS['ansel_db']->query($sql); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } elseif ($result->numRows() == 0) { + throw new Horde_Exception('Face does not exist'); + } + + while ($gallery = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { + $face['galleries'][$gallery['gallery_id']][] = $gallery['image_id']; + } + + return $face; + } + + /** + * Get possible matches from sql index + * + * @param binary $signature Image signature + * @param integer $from Offset + * @param integer $count Limit + * + * @return binary vector signature + */ + public function getSignatureMatches($signature, $face_id = 0, $from = 0, $count = 0) + { + $word_len = $GLOBALS['conf']['faces']['search']; + $str_len = strlen($signature); + $times = $str_len / $word_len; + + $indexes = array(); + for ($i = 0; $i < $times; $i++) { + $indexes[] = '(index_position = ' + . $GLOBALS['ansel_db']->quote($i, 'integer') + . ' AND index_part = ' + . $GLOBALS['ansel_db']->quote( + substr($signature, $i * $word_len, $word_len)) + . ')'; + } + + $sql = 'SELECT COUNT(*) as face_matches, i.face_id, f.face_name, ' + . 'f.image_id, f.gallery_id, f.face_signature ' + . 'FROM ansel_faces_index i, ansel_faces f ' + . 'WHERE f.face_id = i.face_id'; + if ($face_id) { + $sql .= ' AND i.face_id <> ' + . $GLOBALS['ansel_db']->quote($face_id, 'integer'); + } + if ($indexes) { + $sql .= ' AND (' . implode(' OR ', $indexes) . ')'; + } + $sql .= ' GROUP BY i.face_id HAVING face_matches > 0 ' + . 'ORDER BY face_matches DESC'; + $GLOBALS['ansel_db']->setLimit($count, $from); + + $result = $GLOBALS['ansel_db']->query($sql); + if ($result instanceof PEAR_Error) { + throw new Horde_Exception($result); + } elseif ($result->numRows() == 0) { + return array(); + } + + $faces = array(); + while ($face = $result->fetchRow(MDB2_FETCHMODE_ASSOC)) { + $faces[$face['face_id']] = array( + 'face_name' => $face['face_name'], + 'face_id' => $face['face_id'], + 'gallery_id' => $face['gallery_id'], + 'image_id' => $face['image_id'], + 'similarity' => puzzle_vector_normalized_distance( + $signature, + puzzle_uncompress_cvec($face['face_signature']))); + } + uasort($faces, array($this, '_getSignatureMatches')); + + return $faces; + } + + /** + * Compare faces by similarity. + * + * @param array $a + * @param array $b + */ + protected function _getSignatureMatches($a, $b) + { + return $a['similarity'] > $b['similarity']; + } + +} diff --git a/ansel/lib/Faces/facedetect.php b/ansel/lib/Faces/facedetect.php index 80b661662..720ddffe4 100644 --- a/ansel/lib/Faces/facedetect.php +++ b/ansel/lib/Faces/facedetect.php @@ -5,7 +5,7 @@ * @author Duck * @package Ansel */ -class Ansel_Faces_facedetect extends Ansel_Faces +class Ansel_Faces_facedetect extends Ansel_Faces_Base { /** * Where the face defintions are stored @@ -20,6 +20,11 @@ class Ansel_Faces_facedetect extends Ansel_Faces $this->_defs = $params['defs']; } + public function canAutogenerate() + { + return true; + } + /** * Get faces * diff --git a/ansel/lib/Faces/opencv.php b/ansel/lib/Faces/opencv.php index 0b201162d..f3669057d 100644 --- a/ansel/lib/Faces/opencv.php +++ b/ansel/lib/Faces/opencv.php @@ -5,7 +5,7 @@ * @author Duck * @package Ansel */ -class Ansel_Faces_opencv extends Ansel_Faces +class Ansel_Faces_opencv extends Ansel_Faces_Base { /** * Where the face defintions are stored @@ -20,6 +20,11 @@ class Ansel_Faces_opencv extends Ansel_Faces $this->_defs = $params['defs']; } + public function canAutogenerate() + { + return true; + } + /** * Get faces * diff --git a/ansel/lib/Widget/ImageFaces.php b/ansel/lib/Widget/ImageFaces.php index f8a11c1b9..337a4d032 100644 --- a/ansel/lib/Widget/ImageFaces.php +++ b/ansel/lib/Widget/ImageFaces.php @@ -27,7 +27,7 @@ class Ansel_Widget_ImageFaces extends Ansel_Widget_Base */ public function __construct($params) { - parent::Ansel_Widget($params); + parent::__construct($params); $this->_title = _("People in this photo"); } diff --git a/ansel/lib/Widget/OwnerFaces.php b/ansel/lib/Widget/OwnerFaces.php index d473c7702..c33a71123 100644 --- a/ansel/lib/Widget/OwnerFaces.php +++ b/ansel/lib/Widget/OwnerFaces.php @@ -60,7 +60,7 @@ class Ansel_Widget_OwnerFaces extends Ansel_Widget_Base . ';width:100%;max-height:300px;overflow:auto;" id="faces_widget_content" >'; foreach ($results as $face_id => $face) { $facename = htmlspecialchars($face['face_name']); - $html .= '' + $html .= '' . '' . $facename . ''; } diff --git a/ansel/templates/faces/index.inc b/ansel/templates/faces/index.inc index b7d25a6cb..0455d37a6 100755 --- a/ansel/templates/faces/index.inc +++ b/ansel/templates/faces/index.inc @@ -14,7 +14,7 @@ if (empty($results)) { echo _("No faces found"); } else { foreach ($results as $face_id => $face) { - echo '' + echo '' . '' . htmlspecialchars($face['face_name']) . ''; } diff --git a/ansel/templates/tile/face.inc b/ansel/templates/tile/face.inc index f8d9d7c73..cdcaf2f20 100755 --- a/ansel/templates/tile/face.inc +++ b/ansel/templates/tile/face.inc @@ -1,6 +1,6 @@
getLink($face); +$face_url = Ansel_Faces::getLink($face); $facename = htmlspecialchars($face['face_name']); echo '' . '