Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix support for tiled images and feature vectors #133

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Jobs/GenerateFeatureVectors.php
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,8 @@ protected function generateFileInput(VolumeFile $file, Collection $annotations):

if (!$zeroSize) {
// Convert width and height to "right" and "bottom" coordinates.
$box[2] = $box[0] + $box[2];
$box[3] = $box[1] + $box[3];
$box[2] += $box[0];
$box[3] += $box[1];

$boxes[$a->id] = $box;
}
Expand Down
69 changes: 61 additions & 8 deletions src/Jobs/ProcessAnnotatedImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use VipsImage;
use Jcupitt\Vips\Image as VipsImage;

class ProcessAnnotatedImage extends ProcessAnnotatedFile
{
Expand All @@ -22,7 +22,12 @@ class ProcessAnnotatedImage extends ProcessAnnotatedFile
public function handleFile(VolumeFile $file, $path)
{
if (!$this->skipPatches) {
$image = $this->getVipsImage($path);
$options = [];
// Optimize for extracting only a single patch.
if (count($this->only) === 1) {
$options['access'] = 'sequential';
}
$image = $this->getVipsImage($path, $options);
} else {
$image = null;
}
Expand Down Expand Up @@ -71,17 +76,65 @@ protected function updateOrCreateFeatureVectors(Collection $annotations, \Genera
}
}

/**
* {@inheritdoc}
*/
protected function generateFeatureVectors(Collection $annotations, array|string $filePath): void
{
// Tiled images cannot be processed directly. Instead, a crop has to be
// generated for each annotation.
if (!$this->file->tiled) {
parent::generateFeatureVectors($annotations, $filePath);
return;
}

$boxes = $this->generateFileInput($this->file, $annotations);

if (empty($boxes)) {
return;
}

$inputPath = tempnam(sys_get_temp_dir(), 'largo_feature_vector_input');
$outputPath = tempnam(sys_get_temp_dir(), 'largo_feature_vector_output');
$tmpFiles = [$inputPath, $outputPath];

try {
$input = [];
$options = [];
// Optimize for extracting only a single patch.
if ($annotations->count() === 1) {
$options['access'] = 'sequential';
}
$image = $this->getVipsImage($filePath, $options);

foreach ($boxes as $id => $box) {
// Convert right and bottom back to width and height.
$box[2] -= $box[0];
$box[3] -= $box[1];

$path = tempnam(sys_get_temp_dir(), 'largo_feature_vector_patch');
$tmpFiles[] = $path;
$image->crop(...$box)->pngsave($path);

$input[$path] = [$id => [0, 0, $box[2], $box[3]]];
}

File::put($inputPath, json_encode($input));
$this->python($inputPath, $outputPath);
$output = $this->readOutputCsv($outputPath);
$this->updateOrCreateFeatureVectors($annotations, $output);
} finally {
File::delete($tmpFiles);
}
}

/**
* Get the vips image instance.
*
* @param string $path
*
* @return \Jcupitt\Vips\Image
*/
protected function getVipsImage($path)
protected function getVipsImage(string $path, array $options = [])
{
// Must not use sequential access because multiple patches could be extracted.
return VipsImage::newFromFile($path);
return VipsImage::newFromFile($path, $options);
}

/**
Expand Down
45 changes: 43 additions & 2 deletions tests/Jobs/ProcessAnnotatedImageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Biigle\Tests\Modules\Largo\Jobs;

use Biigle\FileCache\Exceptions\FileLockedException;
use Biigle\Image;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\Jobs\ProcessAnnotatedImage;
use Biigle\Shape;
Expand All @@ -12,7 +13,7 @@
use Exception;
use File;
use FileCache;
use Jcupitt\Vips\Image;
use Jcupitt\Vips\Image as VipsImage;
use Log;
use Mockery;
use Storage;
Expand Down Expand Up @@ -642,6 +643,46 @@ public function testHandleOnlyAnnotations()
$this->assertEquals(1, ImageAnnotationLabelFeatureVector::count());
}

public function testHandleFeatureVectorTiledImage()
{
$vipsImage = $this->getImageMock(0);
$vipsImage->shouldReceive('crop')
->once()
->with(19888, 19888, 224, 224)
->andReturn($vipsImage);
$vipsImage->shouldReceive('pngsave')->once()->andReturn($vipsImage);

$disk = Storage::fake('test');
$image = Image::factory()->create([
'attrs' => ['width' => 40000, 'height' => 40000],
'tiled' => true,
]);
$annotation = ImageAnnotationTest::create([
'points' => [20000, 20000],
'shape_id' => Shape::pointId(),
'image_id' => $image->id,
]);
ImageAnnotationLabelTest::create(['annotation_id' => $annotation->id]);
$job = new ProcessAnnotatedImageStub($image,
skipPatches: true,
skipSvgs: true
);
$job->output = [[$annotation->id, '"'.json_encode(range(1, 384)).'"']];
$job->mock = $vipsImage;

$job->handle();
$prefix = fragment_uuid_path($annotation->image->uuid);
$this->assertEquals(1, ImageAnnotationLabelFeatureVector::count());

$input = $job->input;
$this->assertCount(1, $input);
$filename = array_keys($input)[0];
$this->assertArrayHasKey($annotation->id, $input[$filename]);
$box = $input[$filename][$annotation->id];
// These are the coordinates of the cropped image.
$this->assertEquals([0, 0, 224, 224], $box);
}

protected function getImageMock($times = 1)
{
$image = Mockery::mock();
Expand All @@ -661,7 +702,7 @@ class ProcessAnnotatedImageStub extends ProcessAnnotatedImage
public $outputPath;
public $output = [];

public function getVipsImage($path)
public function getVipsImage(string $path, array $options = [])
{
return $this->mock;
}
Expand Down