4

この2D ビン パッキング アルゴリズムを JavaScript から PHPに移植し、それを使用してスプライト マップにいくつかの画像を配置しています。

これは、規則的な形状の画像 (すべて正方形など) に対しては機能しますが、より大規模で複雑なデータ セットに対しては、わずかに壊れた結果が生成されます。

サンプル出力

16 は細長いイメージで、118 はその下にぴったりと収まっていることがわかります。次に、57 は少し高くなっていますが、121 と 126 は 118 とインラインで配置され、16 のすぐ下に配置され、57 と重なっています。なぜこれを行っているのかわかりません。

誰が私がどこで間違っているのか知っていますか?

<?php

class Block {
    /** @var int */
    public $width;
    /** @var int */
    public $height;

    public function __construct($width, $height) {
        $this->width = $width;
        $this->height = $height;
    }
}

class Sprite extends Block {
    /** @var int */
    public $x;
    /** @var int */
    public $y;
    /** @var bool */
    public $used ;
    /** @var Sprite  */
    public $down;
    /** @var Sprite  */
    public $right;

    public function __construct($x, $y, $width, $height, $used=false, $down=null, $right=null) {
        $this->x = $x;
        $this->y = $y;
        $this->width = $width;
        $this->height = $height;
        $this->used = $used;
        $this->down = $down;
        $this->right = $right;
    }

    public function __toString() {
        return "$this->x $this->y $this->width $this->height";
    }
}

class Image extends Block {
    /** @var string */
    public $filePath;
    /** @var Sprite */
    public $fit;

    public function __construct($filePath, $width, $height) {
        $this->filePath = $filePath;
        $this->width = $width;
        $this->height = $height;
    }
}

class Packer {
    /** @var Sprite */
    public $root;

    /**
     * @param Image[] $images
     */
    public function fit($images) {
        $len = count($images);
        $w = $len > 0 ? $images[0]->width : 0;
        $h = $len > 0 ? $images[0]->height : 0;
        $this->root = new Sprite(0,0,$w,$h);
        foreach($images as $img) {
            if($node = $this->findNode($this->root, $img->width, $img->height)) {
                $img->fit = $this->splitNode($node, $img->width, $img->height);
            } else {
                $img->fit = $this->growNode($img->width, $img->height);
            }
        }
    }

    /**
     * @param Sprite $node
     * @param int $w
     * @param int $h
     *
     * @return Sprite
     */
    private function findNode($node, $w, $h) {
        if($node->used) {
            return $this->findNode($node->right, $w, $h) ?: $this->findNode($node->down, $w, $h);
        } elseif($w <= $node->width && $h <= $node->height) {
            return $node;
        }
        return null;
    }

    /**
     * @param Sprite $node
     * @param int $w
     * @param int $h
     *
     * @return Sprite
     */
    private function splitNode($node, $w, $h) {
        $node->used = true;
        $node->down = new Sprite($node->x, $node->y + $h, $node->width, $node->height - $h);
        $node->right = new Sprite($node->x + $w, $node->y, $node->width - $w, $node->height);
        return $node;
    }

    private function growNode($w, $h) {
        $canGrowDown = $w <= $this->root->width;
        $canGrowRight = $h <= $this->root->height;

        $shouldGrowDown = $canGrowDown && $this->root->width >= ($this->root->height + $h);
        $shouldGrowRight = $canGrowRight && $this->root->height >= ($this->root->width + $w);

        if($shouldGrowRight) {
            return $this->growRight($w, $h);
        } elseif($shouldGrowDown) {
            return $this->growDown($w, $h);
        } elseif($canGrowRight) {
            return $this->growRight($w, $h);
        } elseif($canGrowDown) {
            return $this->growDown($w, $h);
        }
        throw new Exception("Could not grow");
    }

    /**
     * @param int $w
     * @param int $h
     *
     * @throws Exception
     * @return Sprite
     */
    private function growRight($w, $h) {
        $node = new Sprite($this->root->width, 0, $w, $this->root->height);
        $this->root = new Sprite(0, 0, $this->root->width + $w, $this->root->height, true, $this->root, $node);
        return $this->splitNode($node, $w, $h);
    }

    /**
     * @param int $w
     * @param int $h
     *
     * @throws Exception
     * @return Sprite
     */
    private function growDown($w, $h){
        $node = new Sprite(0, $this->root->height, $this->root->width, $h);
        $this->root = new Sprite(0, 0, $this->root->width, $this->root->height + $h, true, $node, $this->root);
        return $this->splitNode($node, $w, $h);
    }
}

class Program {

    private static function imageCreateFromAny($filename) {
        return imagecreatefromstring(file_get_contents($filename));
    }

    private static function imageCreateTrueColorTransparent($width, $height) {
        $im = imagecreatetruecolor($width, $height);
        imagesavealpha($im, true);
        $transColor = imagecolorallocatealpha($im, 0, 0, 0, 127);
        imagefill($im, 0, 0, $transColor);
        return $im;
    }

    public static function main() {
        /** @var Image[] $images */
        $images = array();
        $di = new DirectoryIterator('test/7');
        foreach($di as $f) {
            /** @var $f DirectoryIterator */
            if(!$f->isFile()) continue;
            $filePath = $f->getPathname();
            list($w, $h) = getimagesize($filePath);
            if(!$w || !$h) {
                echo "could not get width/height for $filePath -- skipping\n";
                continue;
            }
            $images[] = new Image($filePath, $w, $h);
        }
        usort($images, function($a, $b) {
//            return max($a->width, $a->height) < max($b->width, $b->height) ? 1 : -1;
            if($a->width > $a->height) {
                $aMax = $a->width;
                $aMin = $a->height;
            } else {
                $aMin = $a->width;
                $aMax = $a->height;
            }
            if($b->width > $b->height) {
                $bMax = $b->width;
                $bMin = $b->height;
            } else {
                $bMin = $b->width;
                $bMax = $b->height;
            }
            if($aMax > $bMax) return -1;
            if($aMax < $bMax) return 1;
            if($aMin > $bMin) return -1;
            if($aMin < $bMin) return 1;
            return strcmp($a->filePath, $b->filePath);
        });
        $packer = new Packer();
        $packer->fit($images);
        $spritesheet = self::imageCreateTrueColorTransparent($packer->root->width, $packer->root->height);
        $black = imagecolorallocate($spritesheet, 0, 0, 0);
        foreach($images as $i=>$img) {
            $r = mt_rand(0, 255);
            $g = mt_rand(0, 255);
            $b = mt_rand(0, 255);
            imagefilledrectangle($spritesheet, $img->fit->x, $img->fit->y, $img->fit->x+$img->width, $img->fit->y+$img->height, imagecolorallocatealpha($spritesheet, $r, $g, $b, 64));
            imagerectangle($spritesheet, $img->fit->x, $img->fit->y, $img->fit->x+$img->width, $img->fit->y+$img->height, imagecolorallocate($spritesheet, $r, $g, $b));
            imagestring($spritesheet, 5, $img->fit->x + 2, $img->fit->y + 2, $i, $black);
//            imagecopy($spritesheet, self::imageCreateFromAny($img->filePath), $img->fit->x, $img->fit->y, 0, 0, $img->width, $img->height);
        }
        imagepng($spritesheet, 'spritesheet.png');
        echo "done!\n";
    }
}


if(php_sapi_name() === 'cli' && __FILE__ == realpath($argv[0])) {
    Program::main();
}

更新:コードが決してヒットしてはならず、代わりに例外をスローしてはならないいくつかの場所に気付きました。は常に新しくfindNode作成されたノードを見つけるので、既にあるノードを検索しても意味がありません。少しクリーンアップされましたが、まったく同じ動作を示しています。このアルゴリズムは機能しないと考え始めています。

4

2 に答える 2

3

問題は splitNode 関数にあります。

private function splitNode($node, $w, $h) {
        $node->used = true;
        $node->down = new Sprite($node->x, $node->y + $h, $node->width, $node->height - $h);
        $node->right = new Sprite($node->x + $w, $node->y, $node->width - $w, $node->height);
        return $node;
}

特に、新しいノードの高さは、ノードの高さではなく、node->right新しいブロックの高さでなければならないため、この行は間違っています:

$node->right = new Sprite($node->x + $w, $node->y, $node->width - $w, $node->height);

これは修正です:

$node->right = new Sprite($node->x + $w, $node->y, $node->width - $w, $h);

そうしないと、新しいノードが実際のスペースよりも大きくなり、最終的に他のノードとオーバーラップします。

このアルゴリズムと元の JavaScript 実装に関する情報: http://codeincomplete.com/posts/bin-packing/

これは私の php 実装です (アルゴリズムを開始する前に、ブロックで maxside の並べ替えも使用します)。

class Node {
    public $x;
    public $y;
    public $w;
    public $h;
    public $used;
    public $right;
    public $down;

    public function __construct($x, $y, $w, $h, $used=false, $right=null, $down=null) {
        $this->x = $x;
        $this->y = $y;
        $this->w = $w;
        $this->h = $h;
        $this->used = $used;
        $this->right = $right;
        $this->down = $down;        
    }
}


class BinTreePacking {
    public $root;

    public function __construct($w, $h) {
        $this->init($w, $h);
    }

    public function init($w, $h) {        
        $this->root = new Node(0, 0, $w, $h);        
    }

    public function fit($blocks) {

        $blocks = $this->sortMaxside($blocks);

        foreach($blocks as &$block) {

            $block['fit'] = null;

            if($node = $this->findNode($this->root, $block['w'], $block['h'])) {
                $block['fit'] = $this->splitNode($node, $block['w'], $block['h']);
            } 
        }

        return $blocks;        
    }

    public function findNode($node, $w, $h) {
        if($node->used) {
            return $this->findNode($node->right, $w, $h) ?: $this->findNode($node->down, $w, $h);            
        }
        else if($w <= $node->w && $h <= $node->h) {       
            return $node;
        }
        return null;        
    }

    public function splitNode($node, $w, $h) {  
        $node->used = true;      
        $node->down = new Node($node->x, $node->y + $h, $node->w, $node->h - $h);
        $node->right = new Node($node->x + $w, $node->y, $node->w - $w, $h);
        return $node;
    }

    public function sortMaxside($blocks) {
        usort($blocks, function($a, $b) {
            $a_maxside = max($a['w'], $a['h']);
            $b_maxside = max($b['w'], $b['h']);
            return $a_maxside < $b_maxside;
        });
        return $blocks;
    }
}
于 2016-12-21T16:17:34.927 に答える