Rewrite mosaic algorithm using declarative paradigm #42

Merged
Roflin merged 8 commits from new-mosaic into master 2023-12-03 12:37:41 +01:00
Showing only changes of commit d45b467bb1 - Show all commits

View File

@ -8,11 +8,13 @@
class PhotoMosaic class PhotoMosaic
{ {
const NUM_DAYS_CUTOFF = 7;
private AssetIterator $iterator; private AssetIterator $iterator;
private $layouts; private $layouts;
private $processedImages = 0;
private $queue = []; private $queue = [];
const NUM_DAYS_CUTOFF = 7;
public function __construct(AssetIterator $iterator) public function __construct(AssetIterator $iterator)
{ {
$this->iterator = $iterator; $this->iterator = $iterator;
@ -31,7 +33,8 @@ class PhotoMosaic
'panorama' => [Image::TYPE_PANORAMA], 'panorama' => [Image::TYPE_PANORAMA],
// Big-small juxtapositions // Big-small juxtapositions
'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], 'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE,
Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], 'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE],
// Single row of three // Single row of three
@ -79,87 +82,39 @@ class PhotoMosaic
public function getRow() public function getRow()
{ {
// Fetch the first image... $currentImages = [];
$image = $this->fetchImage(); $selectedLayout = null;
foreach ($this->layouts as $layout => $requiredImageTypes)
{
// Fetch images per the layout requirements
// TODO: pass last image back into fetchImage for date cutoff purposes
$currentImages = array_filter(array_map([$this, 'fetchImage'], $requiredImageTypes));
// No image at all? // Matching requirements?
if (!$image) if (count($currentImages) === count($requiredImageTypes))
{
$selectedLayout = $layout;
break;
}
// Push mismatches back into the queue
array_map([$this, 'pushToQueue'], $currentImages);
$currentImages = [];
}
if ($selectedLayout)
{
// Hurray, we've got something that works
usort($currentImages, [$this, 'orderPhotos']);
$this->processedImages += count($currentImages);
return [$currentImages, $selectedLayout];
}
else
{
// Ensure we have no images left in the iterator before giving up
assert($this->processedImages === $this->iterator->num());
return false; return false;
// Is it a panorama? Then we've got our row!
elseif ($image->isPanorama())
return [[$image], 'panorama'];
// Alright, let's initalise a proper row, then.
$photos = [$image];
$num_portrait = $image->isPortrait() ? 1 : 0;
$num_landscape = $image->isLandscape() ? 1 : 0;
// Get an initial batch of non-panorama images to work with.
for ($i = 1; $i < 3 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
{
$num_portrait += $image->isPortrait() ? 1 : 0;
$num_landscape += $image->isLandscape() ? 1 : 0;
$photos[] = $image;
} }
// Sort photos by priority and date captured.
usort($photos, self::orderPhotos(...));
// Three portraits?
if ($num_portrait === 3)
return [$photos, 'portraits'];
// At least one portrait?
if ($num_portrait >= 1)
{
// Grab two more landscapes, so we can put a total of four tiles on the side.
for ($i = 0; $image && $i < 2 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++)
$photos[] = $image;
// We prefer to have the portrait on the side, so prepare to process that first.
usort($photos, function($a, $b) {
if ($a->isPortrait() && !$b->isPortrait())
return -1;
elseif ($b->isPortrait() && !$a->isPortrait())
return 1;
else
return self::orderPhotos($a, $b);
});
// We might not have a full set of photos, but only bother if we have at least three.
if (count($photos) > 3)
return [$photos, 'portrait'];
}
// One landscape at least, hopefully?
if ($num_landscape >= 1)
{
if (count($photos) === 3)
{
// We prefer to have the landscape on the side, so prepare to process that first.
usort($photos, function($a, $b) {
if ($a->isLandscape() && !$b->isLandscape())
return -1;
elseif ($b->isLandscape() && !$a->isLandscape())
return 1;
else
return self::orderPhotos($a, $b);
});
return [$photos, 'landscape'];
}
elseif (count($photos) === 2)
return [$photos, 'duo'];
else
return [$photos, 'single'];
}
// Last resort: majority vote
if ($num_portrait > $num_landscape)
return [$photos, 'portraits'];
else
return [$photos, 'landscapes'];
} }
private static function matchTypeMask(Image $image, $type_mask) private static function matchTypeMask(Image $image, $type_mask)
@ -171,13 +126,13 @@ class PhotoMosaic
private static function orderPhotos(Image $a, Image $b) private static function orderPhotos(Image $a, Image $b)
{ {
// Show images of highest priority first. // Leave images of different types as-is
$priority_diff = $a->getPriority() - $b->getPriority(); if ($a->isLandscape() !== $b->isLandscape())
if ($priority_diff !== 0) return 0;
return -$priority_diff;
// In other cases, we'll just show the newest first. // Otherwise, show images of highest priority first
return $a->getDateCaptured() <=> $b->getDateCaptured(); $priority_diff = $a->getPriority() - $b->getPriority();
return -$priority_diff;
} }
private function pushToQueue(Image $image) private function pushToQueue(Image $image)