diff --git a/examples/bootstrap.php b/examples/bootstrap.php index 74e6827..b507583 100644 --- a/examples/bootstrap.php +++ b/examples/bootstrap.php @@ -6,5 +6,7 @@ require_once './vendor/autoload.php'; -Transformers::setup()->apply(); +Transformers::setup() +// ->setImageDriver(\Codewithkyrian\Transformers\Utils\ImageDriver::GD) + ->apply(); diff --git a/examples/image-test.php b/examples/image-test.php index 13c3dd3..f02ff9b 100644 --- a/examples/image-test.php +++ b/examples/image-test.php @@ -2,18 +2,55 @@ declare(strict_types=1); -use Codewithkyrian\Transformers\Processors\AutoProcessor; -use Codewithkyrian\Transformers\Utils\Image1; +use Codewithkyrian\Transformers\Transformers; use Codewithkyrian\Transformers\Utils\Image; -use function Codewithkyrian\Transformers\Utils\memoryUsage; +use Codewithkyrian\Transformers\Utils\ImageDriver; +use Codewithkyrian\Transformers\Utils\Tensor; use function Codewithkyrian\Transformers\Utils\timeUsage; require_once './bootstrap.php'; -$processor = AutoProcessor::fromPretrained('Xenova/vit-base-patch16-224'); +function toTensorTest(ImageDriver $imageDriver): Tensor +{ + timeUsage(); -$image = Image::read('images/kyrian-cartoon.jpeg'); + Transformers::setup() + ->setImageDriver($imageDriver) + ->apply(); -$imageInputs = $processor($image); + $image = Image::read('images/butterfly.jpg'); -dd($imageInputs['pixel_values']->shape(), $imageInputs['original_sizes'], $imageInputs['reshaped_input_sizes']); \ No newline at end of file + $image->rgb(); + + $tensor = $image->toTensor(); + + dump("$imageDriver->name (toTensor) : ". timeUsage()); + + return $tensor; +} + +function fromTensorTest(ImageDriver $imageDriver, Tensor $tensor) : Image +{ + Transformers::setup() + ->setImageDriver($imageDriver) + ->apply(); + + $image = Image::fromTensor($tensor); + + dump("$imageDriver->name (fromTensor) : ". timeUsage()); + + return $image; +} + + +// Run the test +dump("------------ toTensor ------------"); +$tensor = toTensorTest(ImageDriver::IMAGICK); +$tensor = toTensorTest(ImageDriver::GD); +$tensor = toTensorTest(ImageDriver::VIPS); + + +dump("------------ fromTensor ------------"); +$image = fromTensorTest(ImageDriver::IMAGICK, $tensor); +$image = fromTensorTest(ImageDriver::GD, $tensor); +$image = fromTensorTest(ImageDriver::VIPS, $tensor); diff --git a/examples/pipelines/object-detection.php b/examples/pipelines/object-detection.php new file mode 100644 index 0000000..60e15df --- /dev/null +++ b/examples/pipelines/object-detection.php @@ -0,0 +1,21 @@ +shape()[0], 64, 64]; + + $pixelMaskData = array_fill(0, array_product($maskSize), 1); + + $pixelMask = new Tensor($pixelMaskData, NDArray::int64, $maskSize); + + return ['pixel_values' => $result['pixel_values'], 'pixel_mask' => $pixelMask]; + } + + + /** + * Post-processes the outputs of the model (for object detection). + * @param ObjectDetectionOutput $outputs The outputs of the model that must be post-processed + * @param float $threshold The threshold to use for the scores. + * @param array|null $targetSizes The sizes of the original images. + * @param bool $isZeroShot Whether zero-shot object detection was performed. + * @return array An array of objects containing the post-processed outputs. + */ + public function postProcessObjectDetection(ObjectDetectionOutput $outputs, float $threshold = 0.5, ?array $targetSizes = null, bool $isZeroShot = false): array + { + return Processor::postProcessObjectDetection($outputs, $threshold, $targetSizes, $isZeroShot); + } +} \ No newline at end of file diff --git a/src/FeatureExtractors/ImageFeatureExtractor.php b/src/FeatureExtractors/ImageFeatureExtractor.php index f45b265..451f4fc 100644 --- a/src/FeatureExtractors/ImageFeatureExtractor.php +++ b/src/FeatureExtractors/ImageFeatureExtractor.php @@ -294,8 +294,8 @@ private function calculateReflectOffset(int $val, int $max): int */ public function rescale(array &$pixelData): void { - foreach ($pixelData as &$pixel) { - $pixel *= $this->rescaleFactor; + for ($i = 0; $i < count($pixelData); ++$i) { + $pixelData[$i] *= $this->rescaleFactor; } } @@ -337,14 +337,15 @@ public function getResizeOutputImageSize(Image $image, int|array|null $size): ar $newWidth = $srcWidth * $shortResizeFactor; $newHeight = $srcHeight * $shortResizeFactor; - // Downscale to ensure the largest dimension is longestEdge + // The new width and height might be greater than `longest_edge`, so + // we downscale to ensure the largest dimension is longestEdge $longResizeFactor = $longestEdge !== null ? min($longestEdge / $newWidth, $longestEdge / $newHeight) : 1; // Round to avoid floating point precision issues - $finalWidth = (int)floor($newWidth * $longResizeFactor); - $finalHeight = (int)floor($newHeight * $longResizeFactor); + $finalWidth = (int)floor(round($srcWidth * $longResizeFactor, 2)); + $finalHeight = (int)floor(round($srcHeight * $longResizeFactor, 2)); if ($this->sizeDivisibility !== null) { [$finalWidth, $finalHeight] = $this->enforceSizeDivisibility([$finalWidth, $finalHeight], $this->sizeDivisibility); @@ -453,11 +454,14 @@ public function preprocess( $reshapedInputSize = [$image->height(), $image->width()]; + // All pixel-level manipulation occurs with data in the hwc format (height, width, channels), // to emulate the behavior of the original Python code (w/ numpy). $pixelData = $image->pixelData(); + $imgShape = [$image->height(), $image->width(), $image->channels]; + if ($this->doRescale) { $this->rescale($pixelData); } diff --git a/src/Models/Auto/AutoModel.php b/src/Models/Auto/AutoModel.php index 445c207..0ab0833 100644 --- a/src/Models/Auto/AutoModel.php +++ b/src/Models/Auto/AutoModel.php @@ -18,6 +18,9 @@ class AutoModel extends PretrainedMixin "clip" => \Codewithkyrian\Transformers\Models\Pretrained\CLIPModel::class, "vit" => \Codewithkyrian\Transformers\Models\Pretrained\ViTModel::class, "deit" => \Codewithkyrian\Transformers\Models\Pretrained\DeiTModel::class, + + 'detr' => \Codewithkyrian\Transformers\Models\Pretrained\DETRModel::class, + 'yolos' => \Codewithkyrian\Transformers\Models\Pretrained\YOLOSModel::class, ]; const ENCODER_DECODER_MODEL_MAPPING = [ diff --git a/src/Models/Auto/AutoModelForObjectDetection.php b/src/Models/Auto/AutoModelForObjectDetection.php new file mode 100644 index 0000000..72520e1 --- /dev/null +++ b/src/Models/Auto/AutoModelForObjectDetection.php @@ -0,0 +1,19 @@ + \Codewithkyrian\Transformers\Models\Pretrained\DetrForObjectDetection::class, + 'yolos' => \Codewithkyrian\Transformers\Models\Pretrained\YolosForObjectDetection::class, + ]; + + const MODEL_CLASS_MAPPINGS = [ + self::MODEL_CLASS_MAPPING, + ]; + +} \ No newline at end of file diff --git a/src/Models/Output/DetrSegmentationOutput.php b/src/Models/Output/DetrSegmentationOutput.php new file mode 100644 index 0000000..604bc05 --- /dev/null +++ b/src/Models/Output/DetrSegmentationOutput.php @@ -0,0 +1,28 @@ + 0.9976370930671692, 'label' => "remote", 'box' => ['xmin' => 29, 'ymin' => 65, 'xmax' => 188, 'ymax' => 122]], + * // ... + * // ['score' => 0.9984092116355896, 'label' => "cat", 'box' => ['xmin' => 332, 'ymin' => 21, 'xmax' => 648, 'ymax' => 366]] + * // ] + * ``` + */ +class ObjectDetectionPipeline extends Pipeline +{ + + public function __invoke(array|string $inputs, ...$args): array + { + $threshold = $options['threshold'] ?? 0.9; + $percentage = $options['percentage'] ?? false; + + + $isBatched = is_array($inputs); + + if ($isBatched && count($inputs) !== 1) { + throw new \Exception("Object detection pipeline currently only supports a batch size of 1."); + } + + $preparedImages = prepareImages($inputs); + + + $imageSizes = $percentage ? null : array_map(fn($x) => [$x->height(), $x->width()], $preparedImages); + + ['pixel_values' => $pixelValues, 'pixel_mask' => $pixelMask] = ($this->processor)($preparedImages); + + /** @var ObjectDetectionOutput $output */ + $output = $this->model->__invoke(['pixel_values' => $pixelValues, 'pixel_mask' => $pixelMask]); + + $processed = $this->processor->featureExtractor->postProcessObjectDetection($output, $threshold, $imageSizes); + + $id2label = $this->model->config['id2label']; + + $result = []; + foreach ($processed as $batch) { + $boxes = $batch['boxes']; + $scores = $batch['scores']; + $classes = $batch['classes']; + + $batchResult = []; + foreach ($boxes as $i => $box) { + $batchResult[] = [ + 'score' => $scores[$i], + 'label' => $id2label[$classes[$i]], + 'box' => getBoundingBox($box, !$percentage), + ]; + } + + $result[] = $batchResult; + } + + return $isBatched ? $result : $result[0]; + } +} \ No newline at end of file diff --git a/src/Pipelines/Task.php b/src/Pipelines/Task.php index 580d8c2..d2acc2d 100644 --- a/src/Pipelines/Task.php +++ b/src/Pipelines/Task.php @@ -8,6 +8,7 @@ use Codewithkyrian\Transformers\Models\Auto\AutoModelForCausalLM; use Codewithkyrian\Transformers\Models\Auto\AutoModelForImageClassification; use Codewithkyrian\Transformers\Models\Auto\AutoModelForMaskedLM; +use Codewithkyrian\Transformers\Models\Auto\AutoModelForObjectDetection; use Codewithkyrian\Transformers\Models\Auto\AutoModelForQuestionAnswering; use Codewithkyrian\Transformers\Models\Auto\AutoModelForSeq2SeqLM; use Codewithkyrian\Transformers\Models\Auto\AutoModelForSequenceClassification; @@ -41,6 +42,8 @@ enum Task: string case ImageClassification = 'image-classification'; case ZeroShotImageClassification = 'zero-shot-image-classification'; + case ObjectDetection = 'object-detection'; + public function pipeline(PretrainedModel $model, ?PretrainedTokenizer $tokenizer, ?Processor $processor): Pipeline { @@ -72,7 +75,9 @@ public function pipeline(PretrainedModel $model, ?PretrainedTokenizer $tokenizer self::ImageClassification => new ImageClassificationPipeline($this, $model, processor: $processor), - self::ZeroShotImageClassification => new ZeroShotImageClassificationPipeline($this, $model, $tokenizer, $processor) + self::ZeroShotImageClassification => new ZeroShotImageClassificationPipeline($this, $model, $tokenizer, $processor), + + self::ObjectDetection => new ObjectDetectionPipeline($this, $model, $tokenizer, $processor), }; } @@ -105,6 +110,8 @@ public function defaultModelName(): string self::ImageClassification => 'Xenova/vit-base-patch16-224', // Original: 'google/vit-base-patch16-224' self::ZeroShotImageClassification => 'Xenova/clip-vit-base-patch32', // Original: 'openai/clip-vit-base-patch32' + + self::ObjectDetection => 'Xenova/detr-resnet-50', // Original: 'facebook/detr-resnet-50', }; } @@ -144,6 +151,8 @@ public function autoModel( self::ImageClassification => AutoModelForImageClassification::fromPretrained($modelNameOrPath, $quantized, $config, $cacheDir, $revision, $modelFilename, $output), self::ZeroShotImageClassification => AutoModel::fromPretrained($modelNameOrPath, $quantized, $config, $cacheDir, $revision, $modelFilename, $output), + + self::ObjectDetection => AutoModelForObjectDetection::fromPretrained($modelNameOrPath, $quantized, $config, $cacheDir, $revision, $modelFilename, $output), }; } @@ -158,7 +167,8 @@ public function autoTokenizer( { return match ($this) { - self::ImageClassification => null, + self::ImageClassification, + self::ObjectDetection => null, self::SentimentAnalysis, @@ -174,7 +184,7 @@ public function autoTokenizer( self::TextGeneration, self::TokenClassification, self::Ner, - self::ImageToText, + self::ImageToText, self::ZeroShotImageClassification => AutoTokenizer::fromPretrained($modelNameOrPath, $quantized, $config, $cacheDir, $revision, null, $output), }; } @@ -191,7 +201,8 @@ public function autoProcessor( self::ImageToText, self::ImageClassification, - self::ZeroShotImageClassification => AutoProcessor::fromPretrained($modelNameOrPath, $config, $cacheDir, $revision, $output), + self::ZeroShotImageClassification, + self::ObjectDetection => AutoProcessor::fromPretrained($modelNameOrPath, $config, $cacheDir, $revision, $output), self::SentimentAnalysis, diff --git a/src/Processors/Processor.php b/src/Processors/Processor.php index 1fc8cdc..c26b04a 100644 --- a/src/Processors/Processor.php +++ b/src/Processors/Processor.php @@ -6,6 +6,10 @@ namespace Codewithkyrian\Transformers\Processors; use Codewithkyrian\Transformers\FeatureExtractors\FeatureExtractor; +use Codewithkyrian\Transformers\Models\Output\ObjectDetectionOutput; +use Codewithkyrian\Transformers\Utils\Math; +use Codewithkyrian\Transformers\Utils\Tensor; +use Rindow\Math\Matrix\MatrixOperator; /** * Represents a Processor that extracts features from an input. @@ -22,4 +26,106 @@ public function __invoke(mixed $input, ...$args) { return $this->featureExtractor->__invoke($input, ...$args); } + + /** + * Post-processes the outputs of the model (for object detection). + * @param ObjectDetectionOutput $outputs The outputs of the model that must be post-processed + * @param float $threshold The threshold to use for the scores. + * @param array|null $targetSizes The sizes of the original images. + * @param bool $isZeroShot Whether zero-shot object detection was performed. + * @return array An array of objects containing the post-processed outputs. + */ + public static function postProcessObjectDetection(ObjectDetectionOutput $outputs, float $threshold = 0.5, ?array $targetSizes = null, bool $isZeroShot = false): array + { + + $outLogits = $outputs->logits; + $outBbox = $outputs->predBoxes; + + [$batchSize, $numBoxes, $numClasses] = $outLogits->shape(); + + + if ($targetSizes !== null && count($targetSizes) !== $batchSize) { + throw new \Exception("Make sure that you pass in as many target sizes as the batch dimension of the logits"); + } + + $toReturn = []; + + for ($i = 0; $i < $batchSize; ++$i) { + $targetSize = $targetSizes !== null ? $targetSizes[$i] : null; + $info = [ + 'boxes' => [], + 'classes' => [], + 'scores' => [] + ]; + $logits = $outLogits[$i]; + $bbox = $outBbox[$i]; + + for ($j = 0; $j < $numBoxes; ++$j) { + $logit = $logits[$j]; + + $indices = []; + $probs = []; + if ($isZeroShot) { + // Get indices of classes with high enough probability + $logitSigmoid = $logit->sigmoid()->buffer(); + foreach ($logitSigmoid as $k => $prob) { + if ($prob > $threshold) { + $indices[] = $k; + } + } + $probs = $logitSigmoid; + } else { + $mo = Tensor::getMo(); + + // Get most probable class + $maxIndex = $mo->argMax($logit); + + if ($maxIndex === $numClasses - 1) { + // This is the background class, skip it + continue; + } + $indices[] = $maxIndex; + + // Compute softmax over classes + $probs = Math::softmax($logit->toArray()); + } + + foreach ($indices as $index) { + $box = $bbox[$j]->toArray(); + + // convert to [x0, y0, x1, y1] format + $box = self::centerToCornersFormat($box); + + if ($targetSize !== null) { + $box = array_map(fn($x, $i) => $x * $targetSize[($i + 1) % 2], $box, array_keys($box)); + } + + $info['boxes'][] = $box; + $info['classes'][] = $index; + $info['scores'][] = $probs[$index]; + } + } + $toReturn[] = $info; + } + return $toReturn; + } + + /** + * Converts bounding boxes from center format to corners format. + * + * @param array $arr The coordinates for the center of the box and its width, height dimensions (center_x, center_y, width, height). + * @return array The coordinates for the top-left and bottom-right corners of the box (top_left_x, top_left_y, bottom_right_x, bottom_right_y). + */ + public static function centerToCornersFormat(array $arr): array + { + [$centerX, $centerY, $width, $height] = $arr; + + return [ + $centerX - $width / 2, + $centerY - $height / 2, + $centerX + $width / 2, + $centerY + $height / 2 + ]; + } + } \ No newline at end of file diff --git a/src/Utils/Helpers.php b/src/Utils/Helpers.php index 98a6269..f5227e4 100644 --- a/src/Utils/Helpers.php +++ b/src/Utils/Helpers.php @@ -96,7 +96,7 @@ function ensureDirectory($filePath): void /** * Prepare images for further tasks. * @param mixed $images Images to prepare. - * @return array Returns processed images. + * @return Image[] Returns processed images. */ function prepareImages(mixed $images): array { @@ -113,3 +113,22 @@ function prepareImages(mixed $images): array return $processedImages; } + +/** + * Helper function to convert list [xmin, xmax, ymin, ymax] into object { "xmin": xmin, ... } + * @param array $box The bounding box as a list. + * @param bool $asInteger Whether to cast to integers. + * @return array The bounding box as an object. + * @private + */ +function getBoundingBox(array $box, bool $asInteger): array +{ + if ($asInteger) { + $box = array_map(fn($x) => (int)$x, $box); + } + + [$xmin, $ymin, $xmax, $ymax] = $box; + + return ['xmin' => $xmin, 'ymin' => $ymin, 'xmax' => $xmax, 'ymax' => $ymax]; +} + diff --git a/src/Utils/Image.php b/src/Utils/Image.php index d070ad5..c0b791f 100644 --- a/src/Utils/Image.php +++ b/src/Utils/Image.php @@ -8,7 +8,6 @@ use Imagine\Image\AbstractImagine; use Imagine\Image\Box; use Imagine\Image\ImageInterface; -use Imagine\Image\Palette\RGB; use Imagine\Image\Point; /** @@ -22,19 +21,68 @@ class Image public function __construct(public ImageInterface $image, public int $channels = 4) { + if ($this->image instanceof \Imagine\Vips\Image) { + $this->channels = $this->image->getVips()->bands; + } } public static function read(string $input, array $options = []): static { - if (filter_var($input, FILTER_VALIDATE_URL)) { - // get from a remote url - } - $image = self::$imagine->open($input, $options); return new self($image); } + public static function fromTensor(Tensor $tensor, string $channelFormat = 'CHW'): static + { + $tensor = $channelFormat === 'CHW' ? $tensor->permute(1, 2, 0) : $tensor; + + [$width, $height, $channels] = $tensor->shape(); + + $image = self::$imagine->create(new Box($width, $height)); + + if ($image instanceof \Imagine\Vips\Image) { + $data = pack('C*', ...$tensor->buffer()->toArray()); + + $vipImage = $image->getVips()::newFromMemory($data, $width, $height, $channels, 'uchar'); + + $image->setVips($vipImage, true); + + return new self($image, $channels); + } + + if($image instanceof \Imagine\Imagick\Image) + { + $map = match ($channels) { + 1 => 'I', + 2 => 'RG', + 3 => 'RGB', + 4 => 'RGBA', + default => throw new \Exception("Unsupported number of channels: $channels"), + }; + + $image->getImagick()->importImagePixels(0, 0, $width, $height, $map, \Imagick::PIXEL_CHAR, $tensor->buffer()->toArray()); + + return new self($image, $channels); + } + + $pixels = $tensor->reshape([$width * $height, $channels])->toArray(); + + for ($y = 0; $y < $height; $y++) { + for ($x = 0; $x < $width; $x++) { + $index = $y * $width + $x; + + $color = $channels === 1 ? $pixels[$index][0] : $pixels[$index]; + + $color = $image->palette()->color([$color[0], $color[1], $color[2]], $color[3] ?? null); + + $image->draw()->dot(new Point($x, $y), $color); + } + } + + return new self($image, $channels); + } + public function height(): int { return $this->image->getSize()->getHeight(); @@ -95,7 +143,7 @@ public function rgb(bool $force = false): static $this->channels = 3; // If it's a Vips image, we can extract the RGB channels - if($this->image instanceof \Imagine\Vips\Image){ + if ($this->image instanceof \Imagine\Vips\Image) { $this->channels = 3; $vipImage = $this->image->getVips()->extract_band(0, ['n' => 3]); @@ -122,7 +170,7 @@ public function rgba(bool $force = false): static $this->channels = 4; // If it's a Vips image, we can handle the RGBA channels - if($this->image instanceof \Imagine\Vips\Image){ + if ($this->image instanceof \Imagine\Vips\Image) { $this->channels = 4; $vipImage = $this->image->getVips(); @@ -253,38 +301,137 @@ public function save(string $path): void */ public function pixelData(): array { + $width = $this->image->getSize()->getWidth(); + $height = $this->image->getSize()->getHeight(); + // If it's a Vips image, we can extract the pixel data directly - if($this->image instanceof \Imagine\Vips\Image){ + if ($this->image instanceof \Imagine\Vips\Image) { return $this->image->getVips()->writeToArray(); } - $width = $this->image->getSize()->getWidth(); - $height = $this->image->getSize()->getHeight(); - - // Initialize an array to store pixel values - $pixels = []; + // If it's an Imagick image, we can export the pixel data directly + if ($this->image instanceof \Imagine\Imagick\Image) { + $map = match ($this->channels) { + 1 => 'I', + 2 => 'RG', + 3 => 'RGB', + 4 => 'RGBA', + default => throw new \Exception("Unsupported number of channels: $this->channels"), + }; + + return $this->image->getImagick()->exportImagePixels(0, 0, $width, $height, $map, \Imagick::PIXEL_CHAR); + } - // Iterate over each pixel in the image + // I didn't find an in-built method to extract pixel data from a GD image, so I'm using this ugly + // brute-force method, suggested by @DewiMorgan on StackOverflow: https://stackoverflow.com/a/30136602/11209184. + // It's faster than other methods I tried, and rivals the speed of the Imagick method so I'll keep it for now. + $alphaLookup = [ + 0x00000000=>"\xff",0x01000000=>"\xfd",0x02000000=>"\xfb",0x03000000=>"\xf9", + 0x04000000=>"\xf7",0x05000000=>"\xf5",0x06000000=>"\xf3",0x07000000=>"\xf1", + 0x08000000=>"\xef",0x09000000=>"\xed",0x0a000000=>"\xeb",0x0b000000=>"\xe9", + 0x0c000000=>"\xe7",0x0d000000=>"\xe5",0x0e000000=>"\xe3",0x0f000000=>"\xe1", + 0x10000000=>"\xdf",0x11000000=>"\xdd",0x12000000=>"\xdb",0x13000000=>"\xd9", + 0x14000000=>"\xd7",0x15000000=>"\xd5",0x16000000=>"\xd3",0x17000000=>"\xd1", + 0x18000000=>"\xcf",0x19000000=>"\xcd",0x1a000000=>"\xcb",0x1b000000=>"\xc9", + 0x1c000000=>"\xc7",0x1d000000=>"\xc5",0x1e000000=>"\xc3",0x1f000000=>"\xc1", + 0x20000000=>"\xbf",0x21000000=>"\xbd",0x22000000=>"\xbb",0x23000000=>"\xb9", + 0x24000000=>"\xb7",0x25000000=>"\xb5",0x26000000=>"\xb3",0x27000000=>"\xb1", + 0x28000000=>"\xaf",0x29000000=>"\xad",0x2a000000=>"\xab",0x2b000000=>"\xa9", + 0x2c000000=>"\xa7",0x2d000000=>"\xa5",0x2e000000=>"\xa3",0x2f000000=>"\xa1", + 0x30000000=>"\x9f",0x31000000=>"\x9d",0x32000000=>"\x9b",0x33000000=>"\x99", + 0x34000000=>"\x97",0x35000000=>"\x95",0x36000000=>"\x93",0x37000000=>"\x91", + 0x38000000=>"\x8f",0x39000000=>"\x8d",0x3a000000=>"\x8b",0x3b000000=>"\x89", + 0x3c000000=>"\x87",0x3d000000=>"\x85",0x3e000000=>"\x83",0x3f000000=>"\x81", + 0x40000000=>"\x7f",0x41000000=>"\x7d",0x42000000=>"\x7b",0x43000000=>"\x79", + 0x44000000=>"\x77",0x45000000=>"\x75",0x46000000=>"\x73",0x47000000=>"\x71", + 0x48000000=>"\x6f",0x49000000=>"\x6d",0x4a000000=>"\x6b",0x4b000000=>"\x69", + 0x4c000000=>"\x67",0x4d000000=>"\x65",0x4e000000=>"\x63",0x4f000000=>"\x61", + 0x50000000=>"\x5f",0x51000000=>"\x5d",0x52000000=>"\x5b",0x53000000=>"\x59", + 0x54000000=>"\x57",0x55000000=>"\x55",0x56000000=>"\x53",0x57000000=>"\x51", + 0x58000000=>"\x4f",0x59000000=>"\x4d",0x5a000000=>"\x4b",0x5b000000=>"\x49", + 0x5c000000=>"\x47",0x5d000000=>"\x45",0x5e000000=>"\x43",0x5f000000=>"\x41", + 0x60000000=>"\x3f",0x61000000=>"\x3d",0x62000000=>"\x3b",0x63000000=>"\x39", + 0x64000000=>"\x37",0x65000000=>"\x35",0x66000000=>"\x33",0x67000000=>"\x31", + 0x68000000=>"\x2f",0x69000000=>"\x2d",0x6a000000=>"\x2b",0x6b000000=>"\x29", + 0x6c000000=>"\x27",0x6d000000=>"\x25",0x6e000000=>"\x23",0x6f000000=>"\x21", + 0x70000000=>"\x1f",0x71000000=>"\x1d",0x72000000=>"\x1b",0x73000000=>"\x19", + 0x74000000=>"\x17",0x75000000=>"\x15",0x76000000=>"\x13",0x77000000=>"\x11", + 0x78000000=>"\x0f",0x79000000=>"\x0d",0x7a000000=>"\x0b",0x7b000000=>"\x09", + 0x7c000000=>"\x07",0x7d000000=>"\x05",0x7e000000=>"\x03",0x7f000000=>"\x00" + ]; // Lookup table for chr(255-(($x >> 23) & 0x7f)). + + $chr = [ + "\x00","\x01","\x02","\x03","\x04","\x05","\x06","\x07","\x08","\x09","\x0A","\x0B","\x0C","\x0D","\x0E","\x0F", + "\x10","\x11","\x12","\x13","\x14","\x15","\x16","\x17","\x18","\x19","\x1A","\x1B","\x1C","\x1D","\x1E","\x1F", + "\x20","\x21","\x22","\x23","\x24","\x25","\x26","\x27","\x28","\x29","\x2A","\x2B","\x2C","\x2D","\x2E","\x2F", + "\x30","\x31","\x32","\x33","\x34","\x35","\x36","\x37","\x38","\x39","\x3A","\x3B","\x3C","\x3D","\x3E","\x3F", + "\x40","\x41","\x42","\x43","\x44","\x45","\x46","\x47","\x48","\x49","\x4A","\x4B","\x4C","\x4D","\x4E","\x4F", + "\x50","\x51","\x52","\x53","\x54","\x55","\x56","\x57","\x58","\x59","\x5A","\x5B","\x5C","\x5D","\x5E","\x5F", + "\x60","\x61","\x62","\x63","\x64","\x65","\x66","\x67","\x68","\x69","\x6A","\x6B","\x6C","\x6D","\x6E","\x6F", + "\x70","\x71","\x72","\x73","\x74","\x75","\x76","\x77","\x78","\x79","\x7A","\x7B","\x7C","\x7D","\x7E","\x7F", + "\x80","\x81","\x82","\x83","\x84","\x85","\x86","\x87","\x88","\x89","\x8A","\x8B","\x8C","\x8D","\x8E","\x8F", + "\x90","\x91","\x92","\x93","\x94","\x95","\x96","\x97","\x98","\x99","\x9A","\x9B","\x9C","\x9D","\x9E","\x9F", + "\xA0","\xA1","\xA2","\xA3","\xA4","\xA5","\xA6","\xA7","\xA8","\xA9","\xAA","\xAB","\xAC","\xAD","\xAE","\xAF", + "\xB0","\xB1","\xB2","\xB3","\xB4","\xB5","\xB6","\xB7","\xB8","\xB9","\xBA","\xBB","\xBC","\xBD","\xBE","\xBF", + "\xC0","\xC1","\xC2","\xC3","\xC4","\xC5","\xC6","\xC7","\xC8","\xC9","\xCA","\xCB","\xCC","\xCD","\xCE","\xCF", + "\xD0","\xD1","\xD2","\xD3","\xD4","\xD5","\xD6","\xD7","\xD8","\xD9","\xDA","\xDB","\xDC","\xDD","\xDE","\xDF", + "\xE0","\xE1","\xE2","\xE3","\xE4","\xE5","\xE6","\xE7","\xE8","\xE9","\xEA","\xEB","\xEC","\xED","\xEE","\xEF", + "\xF0","\xF1","\xF2","\xF3","\xF4","\xF5","\xF6","\xF7","\xF8","\xF9","\xFA","\xFB","\xFC","\xFD","\xFE","\xFF", + ]; // Lookup for chr($x): much faster. + + $imageData = match ($this->channels) { + 1 => str_repeat("\x00", $width * $height), + 2 => str_repeat("\x00\x00", $width * $height), + 3 => str_repeat("\x00\x00\x00", $width * $height), + 4 => str_repeat("\x00\x00\x00\x00", $width * $height), + default => throw new \Exception("Unsupported number of channels: $this->channels"), + }; + + + // Loop over each single pixel. + $j = 0; for ($y = 0; $y < $height; $y++) { for ($x = 0; $x < $width; $x++) { - // Get the color of the pixel - $color = $this->image->getColorAt(new Point($x, $y)); + // Grab the pixel data. + $argb = imagecolorat($this->image->getGdResource(), $x, $y); - // Extract the color components based on the number of channels if ($this->channels >= 1) { - $pixels[] = $color->getRed(); + $imageData[$j++] = $chr[($argb >> 16) & 0xFF]; // R } if ($this->channels >= 2) { - $pixels[] = $color->getGreen(); + $imageData[$j++] = $chr[($argb >> 8) & 0xFF]; // G } if ($this->channels >= 3) { - $pixels[] = $color->getBlue(); + $imageData[$j++] = $chr[$argb & 0xFF]; // B } if ($this->channels >= 4) { - $pixels[] = $color->getAlpha(); + $imageData[$j++] = $alphaLookup[$argb & 0x7f000000]; // A } } } - return $pixels; + + $data = unpack('C*', $imageData); + + return array_values($data); + } + + public function drawRectangle(int $xMin, int $yMin, int $xMax, int $yMax, string $color = 'FFF', $fill = false, float $thickness = 1): void + { + $this->image->draw()->rectangle( + new Point($xMin, $yMin), + new Point($xMax, $yMax), + $this->image->palette()->color($color), + $fill, + $thickness + ); + } + + public function drawText(string $text, string $fontFile, int $fontSize, int $xPos, int $yPos, string $color = 'FFF'): void + { + $font = self::$imagine->font($fontFile, $fontSize, $this->image->palette()->color($color)); + + $position = new Point($xPos, $yPos); + + $this->image->draw()->text($text, $font, $position); } } \ No newline at end of file