Skip to content

Commit

Permalink
Merge pull request #173 from biigle/issue-125
Browse files Browse the repository at this point in the history
Sort by similarity
  • Loading branch information
mzur authored Jun 27, 2024
2 parents 8de3a5b + ca1a343 commit 8e0f304
Show file tree
Hide file tree
Showing 21 changed files with 833 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

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
Expand All @@ -30,7 +28,7 @@ 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
// This was too complicated with the query builder. Since there is no risk of SQL
// injection here, we just use raw SQL.
$sql = <<<SQL
SELECT "id" FROM (
Expand Down Expand Up @@ -67,7 +65,7 @@ public function index($pid, $lid)

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

// Filtering unique IDs is not required here because the UNIQUE in the query
// Filtering unique IDs is not required here because the UNION in the query
// takes care of that.
return array_map(fn ($v) => $v->id, $ids);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

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

use Biigle\Http\Controllers\Api\Controller;
use Biigle\Modules\Largo\Http\Requests\IndexProjectAnnotationsSimilarity;
use DB;

class SortAnnotationsBySimilarityController extends Controller
{
/**
* Sort annotations with specific label by similarity.
*
* @api {get} projects/:id/annotations/sort/similarity Sort annotations with the same label by similarity
* @apiGroup Projects
* @apiName ShowProjectsAnnotationsSortSimilarity
* @apiParam {Number} id The project ID
* @apiParam (Required arguments) {Number} label_id The Label ID
* @apiParam (Required arguments) {Number} image_annotation_id The reference image annotation to sort by similarity. This is not required if `video_annotation_id` is provided.
* @apiParam (Required arguments) {Number} video_annotation_id The reference video annotation to sort by similarity. This is not required if `image_annotation_id` is provided.
* @apiPermission projectMember
* @apiDescription Returns a list of image/video annotation IDs with the most similar first (without the reference annotation ID). Image annotation IDs are prefixed with `i` (e.g. `i123`) and video annotation IDs are prefixed with `v` (e.g. `v456`).
*
* @param IndexProjectAnnotationsSimilarity $request
*/
public function index(IndexProjectAnnotationsSimilarity $request)
{
$r = $request->reference;

// This was too complicated with the query builder. Since there is no risk 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 "annotation_id" != :iid 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 "annotation_id" != :vid AND "volume_id" IN (
SELECT "volume_id" FROM "project_volume" WHERE "project_id" = :pid
)
)
) AS "temp"
ORDER BY "temp"."vector" <=> :vector, id DESC
SQL;

$ids = DB::select($sql, [
'pid' => $request->project->id,
'lid' => $r->label_id,
'vector' => $r->vector,
// We need only one at a time but since the ID 0 never exists, we just take
// it as the other ID.
'iid' => $request->input('image_annotation_id', 0),
'vid' => $request->input('video_annotation_id', 0),
]);

// Filtering unique IDs is not required here because the UNION 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
@@ -0,0 +1,49 @@
<?php

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

use Biigle\Http\Controllers\Api\Controller;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\VideoAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\Http\Requests\IndexVolumeAnnotationsSimilarity;

class SortAnnotationsBySimilarityController extends Controller
{
/**
* Sort annotations with specific label by similarity.
*
* @api {get} volumes/:id/annotations/sort/similarity Sort annotations with the same label by similarity
* @apiGroup Volumes
* @apiName ShowVolumesAnnotationsSortSimilarity
* @apiParam {Number} id The volume ID
* @apiParam (Required arguments) {Number} label_id The Label ID
* @apiParam (Required arguments) {Number} annotation_id The reference annotation to sort by similarity
* @apiPermission projectMember
* @apiDescription Returns a list of image/video annotation IDs with the most similar first (without the reference annotation ID).
*
* @param IndexVolumeAnnotationsSimilarity $request
*/
public function index(IndexVolumeAnnotationsSimilarity $request)
{
$r = $request->reference;

if ($request->volume->isVideoVolume()) {
$query = VideoAnnotationLabelFeatureVector::where('volume_id', $r->volume_id)
->where('label_id', $r->label_id)
->where('id', '!=', $r->id)
->orderByRaw('vector <=> ?, annotation_id DESC', [$r->vector]);
} else {
$query = ImageAnnotationLabelFeatureVector::where('volume_id', $r->volume_id)
->where('label_id', $r->label_id)
->where('id', '!=', $r->id)
->orderByRaw('vector <=> ?, annotation_id DESC', [$r->vector]);
}

return $query->pluck('annotation_id')
// Use distinct/unique *after* fetchig from the DB because otherwise the
// vector would need to be selected, too (order by expressions must appear in
// the select list). But we don't want to get the huge vectors.
->unique()
->values();
}
}
87 changes: 87 additions & 0 deletions src/Http/Requests/IndexProjectAnnotationsSimilarity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace Biigle\Modules\Largo\Http\Requests;

use Biigle\Annotation;
use Biigle\ImageAnnotation;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\VideoAnnotationLabelFeatureVector;
use Biigle\VideoAnnotation;
use Biigle\Project;
use Illuminate\Foundation\Http\FormRequest;

class IndexProjectAnnotationsSimilarity extends FormRequest
{
/**
* The project of which to index the annotations.
*/
public Project $project;

/**
* The reference annotation for sorting.
*/
public $reference;

/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$this->project = Project::findOrFail($this->route('id'));

return $this->user()->can('access', $this->project);
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'label_id' => 'required|bail|integer',
'image_annotation_id' => 'required_without:video_annotation_id|bail|integer',
'video_annotation_id' => 'required_without:image_annotation_id|bail|integer',
];
}

/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($validator->errors()->isNotEmpty()) {
return;
}

$ids = $this->project->volumes()->pluck('id');
if ($this->input('image_annotation_id')) {
$this->reference = ImageAnnotationLabelFeatureVector::whereIn('volume_id', $ids)
->where('label_id', $this->input('label_id'))
->where('annotation_id', $this->input('image_annotation_id'))
->first();

if (is_null($this->reference)) {
$validator->errors()->add('image_annotation_id', 'The annotation does not exist in the project.');
}
} else {
$this->reference = VideoAnnotationLabelFeatureVector::whereIn('volume_id', $ids)
->where('label_id', $this->input('label_id'))
->where('annotation_id', $this->input('video_annotation_id'))
->first();

if (is_null($this->reference)) {
$validator->errors()->add('video_annotation_id', 'The annotation does not exist in the project.');
}
}

});
}
}
76 changes: 76 additions & 0 deletions src/Http/Requests/IndexVolumeAnnotationsSimilarity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Biigle\Modules\Largo\Http\Requests;

use Biigle\Annotation;
use Biigle\ImageAnnotation;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Modules\Largo\VideoAnnotationLabelFeatureVector;
use Biigle\VideoAnnotation;
use Biigle\Volume;
use Illuminate\Foundation\Http\FormRequest;

class IndexVolumeAnnotationsSimilarity extends FormRequest
{
/**
* The volume of which to index the annotations.
*/
public Volume $volume;

/**
* The reference annotation for sorting.
*/
public $reference;

/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$this->volume = Volume::findOrFail($this->route('id'));

return $this->user()->can('access', $this->volume);
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'label_id' => 'required|bail|integer',
'annotation_id' => 'required|bail|integer',
];
}

/**
* Configure the validator instance.
*
* @param \Illuminate\Validation\Validator $validator
* @return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($this->volume->isImageVolume()) {
$this->reference = ImageAnnotationLabelFeatureVector::where('volume_id', $this->volume->id)
->where('label_id', $this->input('label_id'))
->where('annotation_id', $this->input('annotation_id'))
->first();
} else {
$this->reference = VideoAnnotationLabelFeatureVector::where('volume_id', $this->volume->id)
->where('label_id', $this->input('label_id'))
->where('annotation_id', $this->input('annotation_id'))
->first();
}

if (is_null($this->reference)) {
$validator->errors()->add('annotation_id', 'The annotation does not exist in the volume.');
}
});
}
}
8 changes: 8 additions & 0 deletions src/Http/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@
'uses' => 'Projects\SortAnnotationsByOutliersController@index',
]);

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

$router->get('projects/{id}/image-annotations/filter/label/{id2}', [
'uses' => 'Projects\FilterImageAnnotationsByLabelController@index',
]);
Expand All @@ -53,6 +57,10 @@
'uses' => 'Volumes\SortAnnotationsByOutliersController@index',
]);

$router->get('volumes/{id}/annotations/sort/similarity', [
'uses' => 'Volumes\SortAnnotationsBySimilarityController@index',
]);

$router->get('volumes/{id}/image-annotations/examples/{id2}', [
'uses' => 'Volumes\ImageAnnotationExamplesController@index',
]);
Expand Down
2 changes: 1 addition & 1 deletion src/public/assets/scripts/main.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/public/assets/styles/main.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/public/mix-manifest.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"/assets/scripts/main.js": "/assets/scripts/main.js?id=9d428530385ad8ea4cbd82e4600d8cd0",
"/assets/styles/main.css": "/assets/styles/main.css?id=f5673570cda3cacb644da42a2840c218"
"/assets/scripts/main.js": "/assets/scripts/main.js?id=608af5558117398ca4c3d3598a905abb",
"/assets/styles/main.css": "/assets/styles/main.css?id=5295955359904ac00dbeabaf613c5792"
}
4 changes: 4 additions & 0 deletions src/resources/assets/js/api/projects.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,8 @@ export default Vue.resource('api/v1/projects{/id}/largo', {}, {
method: 'GET',
url: 'api/v1/projects{/id}/annotations/sort/outliers{/label_id}',
},
sortAnnotationsBySimilarity: {
method: 'GET',
url: 'api/v1/projects{/id}/annotations/sort/similarity',
},
});
4 changes: 4 additions & 0 deletions src/resources/assets/js/api/volumes.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,8 @@ export default Vue.resource('api/v1/volumes{/id}/largo', {}, {
method: 'GET',
url: 'api/v1/volumes{/id}/annotations/sort/outliers{/label_id}',
},
sortAnnotationsBySimilarity: {
method: 'GET',
url: 'api/v1/volumes{/id}/annotations/sort/similarity',
},
});
Loading

0 comments on commit 8e0f304

Please sign in to comment.