Skip to content

Commit

Permalink
Implement project largo sort by outlier controller
Browse files Browse the repository at this point in the history
  • Loading branch information
mzur committed Jan 11, 2024
1 parent 6960687 commit a5b75b4
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Biigle\Modules\Largo\Http\Controllers\Api\Projects;

use DB;
use Biigle\Http\Controllers\Api\Controller;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\VideoAnnotationLabelFeatureVector;
use Biigle\Project;

class SortAnnotationsByOutliersController extends Controller
{
/**
* Sort annotations with specific label by outliers.
*
* @api {get} projects/:pid/annotations/sort/outliers/:lid Sort annotations with the same label by outliers
* @apiGroup Projects
* @apiName ShowProjectsAnnotationsSortOutliers
* @apiParam {Number} pid The project ID
* @apiParam {Number} lid The Label ID
* @apiPermission projectMember
* @apiDescription Returns a list of image/video annotation IDs with the outliers first.
*
* @param int $pid Project ID
* @param int $lid Label ID
* @return \Illuminate\Http\Response
*/
public function index($pid, $lid)
{
$project = Project::findOrFail($pid);
$this->authorize('access', $project);

// This was too complicated with the query builder. Since there is no rist of SQL
// injection here, we just use raw SQL.
$sql = <<<SQL
SELECT "id" FROM (
(
SELECT CONCAT('i', "annotation_id") AS id, "vector"
FROM "image_annotation_label_feature_vectors"
WHERE "label_id" = :lid AND "volume_id" IN (
SELECT "volume_id" FROM "project_volume" WHERE "project_id" = :pid
)
)
UNION
(
SELECT CONCAT('v', "annotation_id") AS id, "vector"
FROM "video_annotation_label_feature_vectors"
WHERE "label_id" = :lid AND "volume_id" IN (
SELECT "volume_id" FROM "project_volume" WHERE "project_id" = :pid
)
)
) AS "temp"
ORDER BY "temp"."vector" <=> (
SELECT AVG("temp2"."vector") FROM (
SELECT "vector" FROM "image_annotation_label_feature_vectors"
WHERE "label_id" = :lid AND "volume_id" IN (
SELECT "volume_id" FROM "project_volume" WHERE "project_id" = :pid
)
UNION ALL
SELECT "vector" FROM "video_annotation_label_feature_vectors"
WHERE "label_id" = :lid AND "volume_id" IN (
SELECT "volume_id" FROM "project_volume" WHERE "project_id" = :pid
)
) AS temp2
) DESC, id DESC
SQL;

$ids = DB::select($sql, ['pid' => $pid, 'lid' => $lid]);

// Filtering unique IDs is not required here because the UNIQUE in the query
// takes care of that.
return array_map(fn ($v) => $v->id, $ids);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ public function index($vid, $lid)
if ($volume->isVideoVolume()) {
$query = VideoAnnotationLabelFeatureVector::where('volume_id', $vid)
->where('label_id', $lid)
->orderByRaw('vector <=> (SELECT avg(vector) FROM video_annotation_label_feature_vectors WHERE volume_id = ? AND label_id = ?) DESC, annotation_id DESC', [$vid, $lid]);
->orderByRaw('vector <=> (SELECT AVG(vector) FROM video_annotation_label_feature_vectors WHERE volume_id = ? AND label_id = ?) DESC, annotation_id DESC', [$vid, $lid]);
} else {
$query = ImageAnnotationLabelFeatureVector::where('volume_id', $vid)
->where('label_id', $lid)
->orderByRaw('vector <=> (SELECT avg(vector) FROM image_annotation_label_feature_vectors WHERE volume_id = ? AND label_id = ?) DESC, annotation_id DESC', [$vid, $lid]);
->orderByRaw('vector <=> (SELECT AVG(vector) FROM image_annotation_label_feature_vectors WHERE volume_id = ? AND label_id = ?) DESC, annotation_id DESC', [$vid, $lid]);
}

return $query->pluck('annotation_id')
Expand Down
4 changes: 4 additions & 0 deletions src/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
'uses' => 'Labels\VideoAnnotationsController@index',
]);

$router->get('projects/{id}/annotations/sort/outliers/{id2}', [
'uses' => 'Projects\SortAnnotationsByOutliersController@index',
]);

$router->get('projects/{id}/image-annotations/filter/label/{id2}', [
'uses' => 'Projects\FilterImageAnnotationsByLabelController@index',
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace Biigle\Tests\Modules\Largo\Http\Controllers\Api\Projects;

use ApiTestCase;
use Biigle\MediaType;
use Biigle\Volume;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\VideoAnnotationLabelFeatureVector;

class SortAnnotationsByOutliersControllerTest extends ApiTestCase
{
public function testIndex()
{
$id = $this->project()->id;

$v1 = Volume::factory()->create();
$this->project()->addVolumeId($v1->id);

$l1 = ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $v1->id,
]);
$l2 = ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $v1->id,
'label_id' => $l1->label_id,
]);
$l3 = ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $v1->id,
'label_id' => $l1->label_id,
// The other feature vectors are identical so this one will be an outlier.
'vector' => range(1, 384),
]);
$l4 = ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $v1->id,
]);

$v2 = Volume::factory()->create(['media_type_id' => MediaType::videoId()]);
$this->project()->addVolumeId($v2->id);

$l5 = VideoAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $v2->id,
'label_id' => $l1->label_id,
]);
$l6 = VideoAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $v2->id,
'label_id' => $l1->label_id,
'vector' => range(1, 384),
]);

// Annotations from other volume should not appear.
ImageAnnotationLabelFeatureVector::factory()->create();
VideoAnnotationLabelFeatureVector::factory()->create();

$this->doTestApiRoute('GET', "/api/v1/projects/{$id}/annotations/sort/outliers/{$l1->label_id}");

$this->beUser();
$this->get("/api/v1/projects/{$id}/annotations/sort/outliers/{$l1->label_id}")
->assertStatus(403);

$this->beGuest();

$this->get("/api/v1/projects/{$id}/annotations/sort/outliers/{$l1->label_id}")
->assertStatus(200)
->assertExactJson([
'v'.$l6->annotation_id,
'i'.$l3->annotation_id,
'v'.$l5->annotation_id,
'i'.$l2->annotation_id,
'i'.$l1->annotation_id,
]);

$this->get("/api/v1/projects/{$id}/annotations/sort/outliers/{$l4->label_id}")
->assertStatus(200)
->assertExactJson(['i'.$l4->annotation_id]);
}

public function testIndexDuplicate()
{
$id = $this->project()->id;
$vid = $this->volume()->id;
$l1 = ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $vid,
]);
$l2 = ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $vid,
'label_id' => $l1->label_id,
'annotation_id' => $l1->annotation_id,
]);

$this->beEditor();
$this->get("/api/v1/projects/{$id}/annotations/sort/outliers/{$l1->label_id}")
->assertStatus(200)
->assertExactJson(['i'.$l1->annotation_id]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public function testIndexImage()
->assertStatus(200)
->assertExactJson([
$l3->annotation_id,
$l1->annotation_id,
$l2->annotation_id,
$l1->annotation_id,
]);

$this->get("/api/v1/volumes/{$id}/annotations/sort/outliers/{$l4->label_id}")
Expand Down Expand Up @@ -106,8 +106,8 @@ public function testIndexVideo()
->assertStatus(200)
->assertExactJson([
$l3->annotation_id,
$l1->annotation_id,
$l2->annotation_id,
$l1->annotation_id,
]);

$this->get("/api/v1/volumes/{$id}/annotations/sort/outliers/{$l4->label_id}")
Expand Down

0 comments on commit a5b75b4

Please sign in to comment.