From 82e375f25bfe5793e508760dc8516dda3016b224 Mon Sep 17 00:00:00 2001 From: Martin Zurowietz Date: Fri, 19 Jan 2024 16:23:05 +0100 Subject: [PATCH] Update code to use the new ProcessAnnotatedImage job of biigle/largo This also fixes #148 because the new job generates feature vectors, too. --- .../Api/AnnotationCandidateController.php | 16 ++-- .../Api/TrainingProposalController.php | 17 ++-- src/Jobs/ConvertAnnotationCandidates.php | 21 ++-- .../GenerateAnnotationCandidatePatches.php | 43 +++++++-- src/Jobs/GenerateAnnotationPatches.php | 65 ------------- src/Jobs/GenerateTrainingProposalPatches.php | 43 +++++++-- src/Jobs/ProcessNoveltyDetectedImage.php | 55 +++++++++++ src/Jobs/ProcessObjectDetectedImage.php | 55 +++++++++++ .../Api/AnnotationCandidateControllerTest.php | 9 +- .../Api/TrainingProposalControllerTest.php | 9 +- .../Jobs/ConvertAnnotationCandidatesTest.php | 9 +- ...GenerateAnnotationCandidatePatchesTest.php | 24 +++-- .../GenerateTrainingProposalPatchesTest.php | 24 +++-- .../Jobs/ProcessNoveltyDetectedImageTest.php | 96 +++++++++++++++++++ tests/Jobs/ProcessObjectDetectedImageTest.php | 96 +++++++++++++++++++ 15 files changed, 456 insertions(+), 126 deletions(-) delete mode 100644 src/Jobs/GenerateAnnotationPatches.php create mode 100644 src/Jobs/ProcessNoveltyDetectedImage.php create mode 100644 src/Jobs/ProcessObjectDetectedImage.php create mode 100644 tests/Jobs/ProcessNoveltyDetectedImageTest.php create mode 100644 tests/Jobs/ProcessObjectDetectedImageTest.php diff --git a/src/Http/Controllers/Api/AnnotationCandidateController.php b/src/Http/Controllers/Api/AnnotationCandidateController.php index 4b1d095..2aea02d 100644 --- a/src/Http/Controllers/Api/AnnotationCandidateController.php +++ b/src/Http/Controllers/Api/AnnotationCandidateController.php @@ -3,11 +3,11 @@ namespace Biigle\Modules\Maia\Http\Controllers\Api; use Biigle\Http\Controllers\Api\Controller; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; use Biigle\Modules\Maia\AnnotationCandidateFeatureVector; use Biigle\Modules\Maia\Http\Requests\SubmitAnnotationCandidates; use Biigle\Modules\Maia\Http\Requests\UpdateAnnotationCandidate; use Biigle\Modules\Maia\Jobs\ConvertAnnotationCandidates; +use Biigle\Modules\Maia\Jobs\ProcessObjectDetectedImage; use Biigle\Modules\Maia\MaiaJob; use Illuminate\Http\Response; use Pgvector\Laravel\Distance; @@ -150,17 +150,21 @@ public function submit(SubmitAnnotationCandidates $request) */ public function update(UpdateAnnotationCandidate $request) { + $candidate = $request->candidate; if ($request->filled('points')) { - $request->candidate->points = $request->input('points'); - $disk = config('maia.annotation_candidate_storage_disk'); - GenerateImageAnnotationPatch::dispatch($request->candidate, $disk) + $candidate->points = $request->input('points'); + ProcessObjectDetectedImage::dispatch($candidate->image, + only: [$candidate->id], + maiaJob: $candidate->job, + targetDisk: config('maia.annotation_candidate_storage_disk') + ) ->onQueue(config('largo.generate_annotation_patch_queue')); } if ($request->has('label_id')) { - $request->candidate->label_id = $request->input('label_id'); + $candidate->label_id = $request->input('label_id'); } - $request->candidate->save(); + $candidate->save(); } } diff --git a/src/Http/Controllers/Api/TrainingProposalController.php b/src/Http/Controllers/Api/TrainingProposalController.php index 2b13fc1..2cda06e 100644 --- a/src/Http/Controllers/Api/TrainingProposalController.php +++ b/src/Http/Controllers/Api/TrainingProposalController.php @@ -3,10 +3,10 @@ namespace Biigle\Modules\Maia\Http\Controllers\Api; use Biigle\Http\Controllers\Api\Controller; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; use Biigle\Modules\Maia\Events\MaiaJobContinued; use Biigle\Modules\Maia\Http\Requests\ContinueMaiaJob; use Biigle\Modules\Maia\Http\Requests\UpdateTrainingProposal; +use Biigle\Modules\Maia\Jobs\ProcessNoveltyDetectedImage; use Biigle\Modules\Maia\MaiaJob; use Biigle\Modules\Maia\MaiaJobState as State; use Biigle\Modules\Maia\TrainingProposalFeatureVector; @@ -149,14 +149,19 @@ public function submit(ContinueMaiaJob $request) */ public function update(UpdateTrainingProposal $request) { + $proposal = $request->proposal; if ($request->filled('points')) { - $request->proposal->points = $request->input('points'); - $disk = config('maia.training_proposal_storage_disk'); - GenerateImageAnnotationPatch::dispatch($request->proposal, $disk) + $proposal->points = $request->input('points'); + ProcessNoveltyDetectedImage::dispatch($proposal->image, + only: [$proposal->id], + maiaJob: $proposal->job, + targetDisk: config('maia.training_proposal_storage_disk') + ) ->onQueue(config('largo.generate_annotation_patch_queue')); } - $request->proposal->selected = $request->input('selected', $request->proposal->selected); - $request->proposal->save(); + $proposal->selected = $request->input('selected', $proposal->selected); + + $proposal->save(); } } diff --git a/src/Jobs/ConvertAnnotationCandidates.php b/src/Jobs/ConvertAnnotationCandidates.php index 02f7ea9..a5d36b1 100644 --- a/src/Jobs/ConvertAnnotationCandidates.php +++ b/src/Jobs/ConvertAnnotationCandidates.php @@ -5,12 +5,13 @@ use Biigle\ImageAnnotation; use Biigle\ImageAnnotationLabel; use Biigle\Jobs\Job; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; +use Biigle\Modules\Largo\Jobs\ProcessAnnotatedImage; use Biigle\Modules\Maia\MaiaJob; use Biigle\User; use Carbon\Carbon; use DB; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Collection; class ConvertAnnotationCandidates extends Job { @@ -47,9 +48,9 @@ class ConvertAnnotationCandidates extends Job /** * Newly created annotations. * - * @var array + * @var Collection */ - protected $newAnnotations = []; + protected $newAnnotations; /** * Create a new isntance. @@ -62,6 +63,7 @@ public function __construct(MaiaJob $job, User $user) $this->queue = config('maia.convert_annotations_queue'); $this->job = $job; $this->user = $user; + $this->newAnnotations = collect([]); } /** @@ -79,10 +81,15 @@ public function handle() ->chunkById(10000, [$this, 'processChunk']); }); - foreach ($this->newAnnotations as $annotation) { - GenerateImageAnnotationPatch::dispatch($annotation) - ->onQueue(config('largo.generate_annotation_patch_queue')); - } + $this->newAnnotations + ->groupBy('image_id') + ->each(function ($group) { + $image = $group[0]->image; + $ids = $group->pluck('id')->all(); + ProcessAnnotatedImage::dispatch($image, only: $ids) + ->onQueue(config('largo.generate_annotation_patch_queue')); + }); + } finally { $this->job->convertingCandidates = false; $this->job->save(); diff --git a/src/Jobs/GenerateAnnotationCandidatePatches.php b/src/Jobs/GenerateAnnotationCandidatePatches.php index 74a7273..57904ae 100644 --- a/src/Jobs/GenerateAnnotationCandidatePatches.php +++ b/src/Jobs/GenerateAnnotationCandidatePatches.php @@ -2,23 +2,46 @@ namespace Biigle\Modules\Maia\Jobs; -class GenerateAnnotationCandidatePatches extends GenerateAnnotationPatches +use Biigle\Jobs\Job; +use Biigle\Modules\Maia\MaiaJob; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Queue\SerializesModels; + +class GenerateAnnotationCandidatePatches extends Job implements ShouldQueue { + use SerializesModels; + /** - * Get a query for the annotations that have been created by this job. + * Ignore this job if the MAIA job does not exist any more. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @var bool */ - protected function getCreatedAnnotations() - { - return $this->job->annotationCandidates(); - } + protected $deleteWhenMissingModels = true; /** - * Get the storage disk to store the annotation patches to. + * Create a new isntance. */ - protected function getPatchStorageDisk() + public function __construct(public MaiaJob $maiaJob) + { + // + } + + public function handle(): void { - return config('maia.annotation_candidate_storage_disk'); + $this->maiaJob->volume->images() + ->whereExists(fn ($q) => + $q->select(\DB::raw(1)) + ->from('maia_annotation_candidates') + ->where('maia_annotation_candidates.job_id', $this->maiaJob->id) + ->whereColumn('maia_annotation_candidates.image_id', 'images.id') + ) + ->eachById(fn ($image) => + ProcessObjectDetectedImage::dispatch($image, $this->maiaJob, + // Feature vectors are generated in a separate job on the GPU. + skipFeatureVectors: true, + targetDisk: config('maia.annotation_candidate_storage_disk') + ) + ->onQueue(config('largo.generate_annotation_patch_queue')) + ); } } diff --git a/src/Jobs/GenerateAnnotationPatches.php b/src/Jobs/GenerateAnnotationPatches.php deleted file mode 100644 index b7fcfcd..0000000 --- a/src/Jobs/GenerateAnnotationPatches.php +++ /dev/null @@ -1,65 +0,0 @@ -getPatchStorageDisk(); - $this->getCreatedAnnotations() - ->chunkById(self::JOB_CHUNK_SIZE, function ($chunk) use ($disk) { - foreach ($chunk as $annotation) { - GenerateImageAnnotationPatch::dispatch($annotation, $disk) - ->onQueue(config('largo.generate_annotation_patch_queue')); - } - }); - } - - /** - * Get a query for the annotations that have been created by this job. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - abstract protected function getCreatedAnnotations(); - - /** - * Get the storage disk to store the annotation patches to. - */ - abstract protected function getPatchStorageDisk(); -} diff --git a/src/Jobs/GenerateTrainingProposalPatches.php b/src/Jobs/GenerateTrainingProposalPatches.php index 50e6959..6140ac0 100644 --- a/src/Jobs/GenerateTrainingProposalPatches.php +++ b/src/Jobs/GenerateTrainingProposalPatches.php @@ -2,23 +2,46 @@ namespace Biigle\Modules\Maia\Jobs; -class GenerateTrainingProposalPatches extends GenerateAnnotationPatches +use Biigle\Jobs\Job; +use Biigle\Modules\Maia\MaiaJob; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Queue\SerializesModels; + +class GenerateTrainingProposalPatches extends Job implements ShouldQueue { + use SerializesModels; + /** - * Get a query for the annotations that have been created by this job. + * Ignore this job if the MAIA job does not exist any more. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @var bool */ - protected function getCreatedAnnotations() - { - return $this->job->trainingProposals(); - } + protected $deleteWhenMissingModels = true; /** - * Get the storage disk to store the annotation patches to. + * Create a new isntance. */ - protected function getPatchStorageDisk() + public function __construct(public MaiaJob $maiaJob) + { + // + } + + public function handle(): void { - return config('maia.training_proposal_storage_disk'); + $this->maiaJob->volume->images() + ->whereExists(fn ($q) => + $q->select(\DB::raw(1)) + ->from('maia_training_proposals') + ->where('maia_training_proposals.job_id', $this->maiaJob->id) + ->whereColumn('maia_training_proposals.image_id', 'images.id') + ) + ->eachById(fn ($image) => + ProcessNoveltyDetectedImage::dispatch($image, $this->maiaJob, + // Feature vectors are generated in a separate job on the GPU. + skipFeatureVectors: true, + targetDisk: config('maia.training_proposal_storage_disk') + ) + ->onQueue(config('largo.generate_annotation_patch_queue')) + ); } } diff --git a/src/Jobs/ProcessNoveltyDetectedImage.php b/src/Jobs/ProcessNoveltyDetectedImage.php new file mode 100644 index 0000000..809a603 --- /dev/null +++ b/src/Jobs/ProcessNoveltyDetectedImage.php @@ -0,0 +1,55 @@ +id) + ->where('job_id', $this->maiaJob->id); + } + + /** + * Create the feature vectors based on the Python script output. + */ + protected function updateOrCreateFeatureVectors(Collection $annotations, \Generator $output): void + { + $annotations = $annotations->keyBy('id'); + foreach ($output as $row) { + $annotation = $annotations->get($row[0]); + TrainingProposalFeatureVector::updateOrCreate( + ['id' => $annotation->id], + [ + 'job_id' => $annotation->job_id, + 'vector' => $row[1], + ] + ); + } + } +} diff --git a/src/Jobs/ProcessObjectDetectedImage.php b/src/Jobs/ProcessObjectDetectedImage.php new file mode 100644 index 0000000..947cccd --- /dev/null +++ b/src/Jobs/ProcessObjectDetectedImage.php @@ -0,0 +1,55 @@ +id) + ->where('job_id', $this->maiaJob->id); + } + + /** + * Create the feature vectors based on the Python script output. + */ + protected function updateOrCreateFeatureVectors(Collection $annotations, \Generator $output): void + { + $annotations = $annotations->keyBy('id'); + foreach ($output as $row) { + $annotation = $annotations->get($row[0]); + AnnotationCandidateFeatureVector::updateOrCreate( + ['id' => $annotation->id], + [ + 'job_id' => $annotation->job_id, + 'vector' => $row[1], + ] + ); + } + } +} diff --git a/tests/Http/Controllers/Api/AnnotationCandidateControllerTest.php b/tests/Http/Controllers/Api/AnnotationCandidateControllerTest.php index 87b7263..29e98c4 100644 --- a/tests/Http/Controllers/Api/AnnotationCandidateControllerTest.php +++ b/tests/Http/Controllers/Api/AnnotationCandidateControllerTest.php @@ -3,9 +3,9 @@ namespace Biigle\Tests\Modules\Maia\Http\Controllers\Api; use ApiTestCase; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; use Biigle\Modules\Maia\AnnotationCandidateFeatureVector; use Biigle\Modules\Maia\Jobs\ConvertAnnotationCandidates; +use Biigle\Modules\Maia\Jobs\ProcessObjectDetectedImage; use Biigle\Modules\Maia\MaiaJob; use Biigle\Modules\Maia\MaiaJobState as State; use Biigle\Tests\ImageAnnotationTest; @@ -171,9 +171,12 @@ public function testUpdatePoints() $this->putJson("/api/v1/maia/annotation-candidates/{$a->id}", ['points' => [10, 20, 30]]) ->assertStatus(200); - Queue::assertPushed(GenerateImageAnnotationPatch::class); + Queue::assertPushed(ProcessObjectDetectedImage::class, function ($job) use ($a) { + $this->assertEquals([$a->id], $job->only); + $this->assertFalse($job->skipFeatureVectors); - $this->markTestIncomplete('also push a job to regenerate the feature vector'); + return true; + }); } public function testIndexSimilarity() diff --git a/tests/Http/Controllers/Api/TrainingProposalControllerTest.php b/tests/Http/Controllers/Api/TrainingProposalControllerTest.php index 1fe7adb..fdc4cbe 100644 --- a/tests/Http/Controllers/Api/TrainingProposalControllerTest.php +++ b/tests/Http/Controllers/Api/TrainingProposalControllerTest.php @@ -3,8 +3,8 @@ namespace Biigle\Tests\Modules\Maia\Http\Controllers\Api; use ApiTestCase; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; use Biigle\Modules\Maia\Events\MaiaJobContinued; +use Biigle\Modules\Maia\Jobs\ProcessNoveltyDetectedImage; use Biigle\Modules\Maia\MaiaJob; use Biigle\Modules\Maia\MaiaJobState as State; use Biigle\Modules\Maia\TrainingProposalFeatureVector; @@ -153,9 +153,12 @@ public function testUpdatePoints() $this->putJson("/api/v1/maia/training-proposals/{$a->id}", ['points' => [10, 20, 30]]) ->assertStatus(200); - Queue::assertPushed(GenerateImageAnnotationPatch::class); + Queue::assertPushed(ProcessNoveltyDetectedImage::class, function ($job) use ($a) { + $this->assertEquals([$a->id], $job->only); + $this->assertFalse($job->skipFeatureVectors); - $this->markTestIncomplete('also push a job to regenerate the feature vector'); + return true; + }); } public function testIndexSimilarity() diff --git a/tests/Jobs/ConvertAnnotationCandidatesTest.php b/tests/Jobs/ConvertAnnotationCandidatesTest.php index a5bcba3..8d5b133 100644 --- a/tests/Jobs/ConvertAnnotationCandidatesTest.php +++ b/tests/Jobs/ConvertAnnotationCandidatesTest.php @@ -2,7 +2,7 @@ namespace Biigle\Tests\Modules\Maia\Jobs; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; +use Biigle\Modules\Largo\Jobs\ProcessAnnotatedImage; use Biigle\Modules\Maia\Jobs\ConvertAnnotationCandidates; use Biigle\Tests\ImageAnnotationTest; use Biigle\Tests\LabelTest; @@ -47,6 +47,11 @@ public function testHandle() $this->assertEquals($annotation->id, $c2->fresh()->annotation_id); - Queue::assertPushed(GenerateImageAnnotationPatch::class); + Queue::assertPushed(ProcessAnnotatedImage::class, function ($job) use ($c1, $a) { + $this->assertEquals($c1->image_id, $job->file->id); + $this->assertEquals([$a->id], $job->only); + + return true; + }); } } diff --git a/tests/Jobs/GenerateAnnotationCandidatePatchesTest.php b/tests/Jobs/GenerateAnnotationCandidatePatchesTest.php index 332107e..be8157a 100644 --- a/tests/Jobs/GenerateAnnotationCandidatePatchesTest.php +++ b/tests/Jobs/GenerateAnnotationCandidatePatchesTest.php @@ -2,7 +2,8 @@ namespace Biigle\Tests\Modules\Maia\Jobs; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; +use Biigle\Image; +use Biigle\Modules\Maia\Jobs\ProcessObjectDetectedImage; use Biigle\Modules\Maia\Jobs\GenerateAnnotationCandidatePatches; use Biigle\Modules\Maia\MaiaJob; use Biigle\Modules\Maia\AnnotationCandidate; @@ -13,18 +14,27 @@ class GenerateAnnotationCandidatePatchesTest extends TestCase { public function testHandle() { - $job = MaiaJob::factory()->create(); - $tp = AnnotationCandidate::factory()->create(['job_id' => $job->id]); - $j = new GenerateAnnotationCandidatePatches($job); + $image = Image::factory()->create(); + $job = MaiaJob::factory()->create(['volume_id' => $image->volume_id]); + $tp = AnnotationCandidate::factory()->create([ + 'job_id' => $job->id, + 'image_id' => $image->id, + ]); + $j = new GenerateAnnotationCandidatePatches($tp->job); $j->handle(); - Queue::assertPushed(GenerateImageAnnotationPatch::class); + Queue::assertPushed(ProcessObjectDetectedImage::class, function ($job) { + $this->assertTrue($job->skipFeatureVectors); + + return true; + }); } public function testHandleEmpty() { - $job = MaiaJob::factory()->create(); + $image = Image::factory()->create(); + $job = MaiaJob::factory()->create(['volume_id' => $image->volume_id]); $j = new GenerateAnnotationCandidatePatches($job); $j->handle(); - Queue::assertNotPushed(GenerateImageAnnotationPatch::class); + Queue::assertNotPushed(ProcessObjectDetectedImage::class); } } diff --git a/tests/Jobs/GenerateTrainingProposalPatchesTest.php b/tests/Jobs/GenerateTrainingProposalPatchesTest.php index 88506c0..a52c171 100644 --- a/tests/Jobs/GenerateTrainingProposalPatchesTest.php +++ b/tests/Jobs/GenerateTrainingProposalPatchesTest.php @@ -2,7 +2,8 @@ namespace Biigle\Tests\Modules\Maia\Jobs; -use Biigle\Modules\Largo\Jobs\GenerateImageAnnotationPatch; +use Biigle\Image; +use Biigle\Modules\Maia\Jobs\ProcessNoveltyDetectedImage; use Biigle\Modules\Maia\Jobs\GenerateTrainingProposalPatches; use Biigle\Modules\Maia\MaiaJob; use Biigle\Modules\Maia\TrainingProposal; @@ -13,18 +14,27 @@ class GenerateTrainingProposalPatchesTest extends TestCase { public function testHandle() { - $job = MaiaJob::factory()->create(); - $tp = TrainingProposal::factory()->create(['job_id' => $job->id]); - $j = new GenerateTrainingProposalPatches($job); + $image = Image::factory()->create(); + $job = MaiaJob::factory()->create(['volume_id' => $image->volume_id]); + $tp = TrainingProposal::factory()->create([ + 'job_id' => $job->id, + 'image_id' => $image->id, + ]); + $j = new GenerateTrainingProposalPatches($tp->job); $j->handle(); - Queue::assertPushed(GenerateImageAnnotationPatch::class); + Queue::assertPushed(ProcessNoveltyDetectedImage::class, function ($job) { + $this->assertTrue($job->skipFeatureVectors); + + return true; + }); } public function testHandleEmpty() { - $job = MaiaJob::factory()->create(); + $image = Image::factory()->create(); + $job = MaiaJob::factory()->create(['volume_id' => $image->volume_id]); $j = new GenerateTrainingProposalPatches($job); $j->handle(); - Queue::assertNotPushed(GenerateImageAnnotationPatch::class); + Queue::assertNotPushed(ProcessNoveltyDetectedImage::class); } } diff --git a/tests/Jobs/ProcessNoveltyDetectedImageTest.php b/tests/Jobs/ProcessNoveltyDetectedImageTest.php new file mode 100644 index 0000000..e7ddb1a --- /dev/null +++ b/tests/Jobs/ProcessNoveltyDetectedImageTest.php @@ -0,0 +1,96 @@ +getImageMock(); + $proposal = TrainingProposal::factory()->create([ + 'points' => [200, 200, 10], + ]); + $job = new ProcessNoveltyDetectedImageStub($proposal->image, $proposal->job, + targetDisk: 'test' + ); + $job->mock = $image; + + $image->shouldReceive('crop')->once()->andReturn($image); + $image->shouldReceive('writeToBuffer')->andReturn('abc123'); + + $job->handleFile($proposal->image, 'abc'); + $prefix = fragment_uuid_path($proposal->image->uuid); + $content = $disk->get("{$prefix}/{$proposal->id}.jpg"); + $this->assertEquals('abc123', $content); + } + + public function testGenerateFeatureVector() + { + Storage::fake('test'); + $image = $this->getImageMock(); + $image->shouldReceive('crop')->andReturn($image); + $image->shouldReceive('writeToBuffer')->andReturn('abc123'); + $proposal = TrainingProposal::factory()->create([ + 'points' => [200, 200, 10], + ]); + $job = new ProcessNoveltyDetectedImageStub($proposal->image, $proposal->job, + targetDisk: 'test' + ); + $job->mock = $image; + $job->output = [[$proposal->id, '"'.json_encode(range(0, 383)).'"']]; + $job->handleFile($proposal->image, 'abc'); + + $input = $job->input; + $this->assertCount(1, $input); + $filename = array_keys($input)[0]; + $this->assertArrayHasKey($proposal->id, $input[$filename]); + $box = $input[$filename][$proposal->id]; + $this->assertEquals([190, 190, 210, 210], $box); + + $vectors = TrainingProposalFeatureVector::where('id', $proposal->id)->get(); + $this->assertCount(1, $vectors); + $this->assertEquals($proposal->job_id, $vectors[0]->job_id); + $this->assertEquals(range(0, 383), $vectors[0]->vector->toArray()); + } + + protected function getImageMock($times = 1) + { + $image = Mockery::mock(); + $image->width = 1000; + $image->height = 750; + $image->shouldReceive('resize') + ->times($times) + ->andReturn($image); + + return $image; + } +} + +class ProcessNoveltyDetectedImageStub extends ProcessNoveltyDetectedImage +{ + public $input; + public $outputPath; + public $output = []; + + public function getVipsImage($path) + { + return $this->mock; + } + + protected function python(string $inputPath, string $outputPath) + { + $this->input = json_decode(File::get($inputPath), true); + $this->outputPath = $outputPath; + $csv = implode("\n", array_map(fn ($row) => implode(',', $row), $this->output)); + File::put($outputPath, $csv); + } +} diff --git a/tests/Jobs/ProcessObjectDetectedImageTest.php b/tests/Jobs/ProcessObjectDetectedImageTest.php new file mode 100644 index 0000000..415df56 --- /dev/null +++ b/tests/Jobs/ProcessObjectDetectedImageTest.php @@ -0,0 +1,96 @@ +getImageMock(); + $candidate = AnnotationCandidate::factory()->create([ + 'points' => [200, 200, 10], + ]); + $job = new ProcessObjectDetectedImageStub($candidate->image, $candidate->job, + targetDisk: 'test' + ); + $job->mock = $image; + + $image->shouldReceive('crop')->once()->andReturn($image); + $image->shouldReceive('writeToBuffer')->andReturn('abc123'); + + $job->handleFile($candidate->image, 'abc'); + $prefix = fragment_uuid_path($candidate->image->uuid); + $content = $disk->get("{$prefix}/{$candidate->id}.jpg"); + $this->assertEquals('abc123', $content); + } + + public function testGenerateFeatureVector() + { + Storage::fake('test'); + $image = $this->getImageMock(); + $image->shouldReceive('crop')->andReturn($image); + $image->shouldReceive('writeToBuffer')->andReturn('abc123'); + $candidate = AnnotationCandidate::factory()->create([ + 'points' => [200, 200, 10], + ]); + $job = new ProcessObjectDetectedImageStub($candidate->image, $candidate->job, + targetDisk: 'test' + ); + $job->mock = $image; + $job->output = [[$candidate->id, '"'.json_encode(range(0, 383)).'"']]; + $job->handleFile($candidate->image, 'abc'); + + $input = $job->input; + $this->assertCount(1, $input); + $filename = array_keys($input)[0]; + $this->assertArrayHasKey($candidate->id, $input[$filename]); + $box = $input[$filename][$candidate->id]; + $this->assertEquals([190, 190, 210, 210], $box); + + $vectors = AnnotationCandidateFeatureVector::where('id', $candidate->id)->get(); + $this->assertCount(1, $vectors); + $this->assertEquals($candidate->job_id, $vectors[0]->job_id); + $this->assertEquals(range(0, 383), $vectors[0]->vector->toArray()); + } + + protected function getImageMock($times = 1) + { + $image = Mockery::mock(); + $image->width = 1000; + $image->height = 750; + $image->shouldReceive('resize') + ->times($times) + ->andReturn($image); + + return $image; + } +} + +class ProcessObjectDetectedImageStub extends ProcessObjectDetectedImage +{ + public $input; + public $outputPath; + public $output = []; + + public function getVipsImage($path) + { + return $this->mock; + } + + protected function python(string $inputPath, string $outputPath) + { + $this->input = json_decode(File::get($inputPath), true); + $this->outputPath = $outputPath; + $csv = implode("\n", array_map(fn ($row) => implode(',', $row), $this->output)); + File::put($outputPath, $csv); + } +}