Skip to content

Commit 50b5f15

Browse files
Optimize Image -> Tensor and vice versa conversion speeds
1 parent 7d00a27 commit 50b5f15

2 files changed

Lines changed: 54 additions & 27 deletions

File tree

src/Utils/Image.php

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -260,10 +260,11 @@ public static function fromTensor(Tensor $tensor, string $channelFormat = 'CHW')
260260

261261
$image = self::$imagine->create(new Box($width, $height));
262262

263-
if ($image instanceof \Imagine\Vips\Image) {
264-
$data = pack('C*', ...$tensor->buffer()->toArray());
263+
// Make sure the tensor is the right data type
264+
$tensor = $tensor->to(Tensor::uint8);
265265

266-
$vipImage = $image->getVips()::newFromMemory($data, $width, $height, $channels, 'uchar');
266+
if ($image instanceof \Imagine\Vips\Image) {
267+
$vipImage = $image->getVips()::newFromMemory($tensor->buffer()->dump(), $width, $height, $channels, 'uchar');
267268

268269
$image->setVips($vipImage, true);
269270

@@ -279,10 +280,7 @@ public static function fromTensor(Tensor $tensor, string $channelFormat = 'CHW')
279280
default => throw new Exception("Unsupported number of channels: $channels"),
280281
};
281282

282-
$bufferArray = [];
283-
for ($i = 0; $i < $tensor->size(); $i++) {
284-
$bufferArray[] = $tensor->buffer()[$i];
285-
}
283+
$bufferArray = $tensor->toBufferArray();
286284

287285
$image->getImagick()->importImagePixels(0, 0, $width, $height, $map, Imagick::PIXEL_CHAR, $bufferArray);
288286

@@ -311,9 +309,10 @@ public function toTensor(string $channelFormat = 'CHW'): Tensor
311309
$width = $this->image->getSize()->getWidth();
312310
$height = $this->image->getSize()->getHeight();
313311

314-
$pixels = $this->pixelData();
312+
$pixelData = $this->getPixelData();
315313

316-
$tensor = new Tensor($pixels, Tensor::float32, [$height, $width, $this->channels]);
314+
$tensor = Tensor::fromString($pixelData, Tensor::uint8, [$height, $width, $this->channels])
315+
->to(Tensor::float32);
317316

318317
if ($channelFormat === 'HWC') {
319318
// Do nothing
@@ -326,20 +325,17 @@ public function toTensor(string $channelFormat = 'CHW'): Tensor
326325
return $tensor;
327326
}
328327

329-
/**
330-
* @return array
331-
*/
332-
public function pixelData(): array
328+
public function getPixelData(): string
333329
{
334-
$width = $this->image->getSize()->getWidth();
335-
$height = $this->image->getSize()->getHeight();
330+
$width = $this->width();
331+
$height = $this->height();
336332

337-
// If it's a Vips image, we can extract the pixel data directly
333+
/** For Vips images, we can export the pixel data directly */
338334
if ($this->image instanceof \Imagine\Vips\Image) {
339-
return $this->image->getVips()->writeToArray();
335+
return $this->image->getVips()->writeToMemory();
340336
}
341337

342-
// If it's an Imagick image, we can export the pixel data directly
338+
/** For Imagick images, we can export the pixel data directly */
343339
if ($this->image instanceof \Imagine\Imagick\Image) {
344340
$map = match ($this->channels) {
345341
1 => 'I',
@@ -349,12 +345,17 @@ public function pixelData(): array
349345
default => throw new Exception("Unsupported number of channels: $this->channels"),
350346
};
351347

352-
return $this->image->getImagick()->exportImagePixels(0, 0, $width, $height, $map, Imagick::PIXEL_CHAR);
348+
$pixels = $this->image->getImagick()->exportImagePixels(0, 0, $width, $height, $map, Imagick::PIXEL_CHAR);
349+
350+
return pack('C*', ...$pixels);
353351
}
354352

355-
// I didn't find an in-built method to extract pixel data from a GD image, so I'm using this ugly
356-
// brute-force method, suggested by @DewiMorgan on StackOverflow: https://stackoverflow.com/a/30136602/11209184.
357-
// It's faster than other methods I tried, and rivals the speed of the Imagick method so I'll keep it for now.
353+
/** For GD images, we need to extract the pixel data manually
354+
*
355+
* I didn't find an in-built method to extract pixel data from a GD image, so I'm using this ugly
356+
* brute-force method, suggested by @DewiMorgan on StackOverflow: https://stackoverflow.com/a/30136602/11209184.
357+
* It's tons faster than other methods I tried, and rivals the speed of the Imagick method, so I'll keep it for now.
358+
*/
358359
$alphaLookup = [
359360
0x00000000 => "\xff", 0x01000000 => "\xfd", 0x02000000 => "\xfb", 0x03000000 => "\xf9",
360361
0x04000000 => "\xf7", 0x05000000 => "\xf5", 0x06000000 => "\xf3", 0x07000000 => "\xf1",
@@ -440,9 +441,7 @@ public function pixelData(): array
440441
}
441442
}
442443

443-
$data = unpack('C*', $imageData);
444-
445-
return array_values($data);
444+
return $imageData;
446445
}
447446

448447
public function save(string $path): void

src/Utils/Tensor.php

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ class Tensor implements NDArray, Countable, Serializable, IteratorAggregate
4444
protected int $dtype;
4545
protected Buffer $buffer;
4646

47+
protected static $pack = [
48+
NDArray::bool => 'C',
49+
NDArray::int8 => 'c',
50+
NDArray::int16 => 's',
51+
NDArray::int32 => 'l',
52+
NDArray::int64 => 'q',
53+
NDArray::uint8 => 'C',
54+
NDArray::uint16 => 'S',
55+
NDArray::uint32 => 'L',
56+
NDArray::uint64 => 'Q',
57+
//NDArray::float8 => 'N/A',
58+
//NDArray::float16 => 'N/A',
59+
NDArray::float32 => 'g',
60+
NDArray::float64 => 'e',
61+
NDArray::complex64 => 'g',
62+
NDArray::complex128 => 'e',
63+
];
64+
4765
protected bool $portableSerializeMode = false;
4866

4967
public function __construct(
@@ -365,6 +383,14 @@ public static function fromArray(array|NDArray $array, ?int $dtype = null, $shap
365383
return new static($array, $dtype, $shape);
366384
}
367385

386+
387+
public static function fromString(string $string, int $dtype, array $shape): static
388+
{
389+
$buffer = Tensor::newBuffer(array_product($shape), $dtype);
390+
$buffer->load($string);
391+
return new static($buffer, $dtype, $shape, 0);
392+
}
393+
368394
/**
369395
* Convert the tensor into an array.
370396
*/
@@ -382,9 +408,11 @@ public function toArray()
382408
/**
383409
* Convert the tensor into a flat array of the buffer contents.
384410
*/
385-
public function toBufferArray()
411+
public function toBufferArray(): array
386412
{
387-
throw new Exception('toBufferArray is not implemented yet');
413+
$fmt = self::$pack[$this->dtype].'*';
414+
415+
return array_values(unpack($fmt, $this->buffer->dump()));
388416
}
389417

390418
public static function fill(array $shape, float|int $value, ?int $dtype = null): static

0 commit comments

Comments
 (0)