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

LabelBOT #1012

Draft
wants to merge 33 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6f0dd0d
Perform vector search to get label_id if not provided in request
gkourie Dec 10, 2024
d956947
Add test case for vector search without hnsw index
gkourie Dec 10, 2024
e2c26c2
Add vector search helper functions
gkourie Dec 11, 2024
d838cb2
Use Dynamic Index Switching in vector search
gkourie Dec 11, 2024
f441d15
Move vector search functions to controller
gkourie Dec 18, 2024
a6410b3
Define new attribute for Annotation model to attach multiple labels t…
gkourie Dec 19, 2024
ca8c43a
Add new request format for storing annotation without label_id
gkourie Dec 19, 2024
6df585f
Remove feature vector rule
gkourie Dec 19, 2024
8ffba23
Attach full label models to the labelBOTlabels
gkourie Dec 20, 2024
4945ba0
Avoid adding the subquery to the query as a string
gkourie Dec 20, 2024
ae7d882
Refactor Code and expand documentation
gkourie Dec 20, 2024
296d0f9
Implement annotation-store rules directly in StoreImageAnnotation
gkourie Dec 20, 2024
602b253
Expand labelbot config comments
gkourie Dec 20, 2024
89bc3c8
Fix lint
gkourie Dec 20, 2024
bb3c221
Use WhereIn to get the labelBOTlabels
gkourie Jan 7, 2025
11989cd
Append labelBOTlabels attribute only to the response of the store() c…
gkourie Jan 7, 2025
87ed509
Implement the HNSW drop and rollback logic
gkourie Jan 8, 2025
915438e
Fix lint
gkourie Jan 8, 2025
802764d
Fix store feature vector test case
gkourie Jan 8, 2025
8fafb6e
Set HNSW search paramter to K
gkourie Jan 9, 2025
99ad2f3
Delete unnecessary index existence check
gkourie Jan 9, 2025
bdc02fd
Make index name unconfigurable
gkourie Jan 10, 2025
c8130c5
Add LabelBOT button
gkourie Jan 29, 2025
1f1a513
Fix wrong scss class import
gkourie Jan 30, 2025
128855c
Fix activation/deactivation logic of LabelBOT
gkourie Jan 30, 2025
3a18573
Make drawing possible when LabelBOT is on
gkourie Jan 31, 2025
f5120cb
Fix handle LabelBOT method name
gkourie Jan 31, 2025
2093bc9
Use another canvas to save image for LabelBOT
gkourie Feb 2, 2025
486375f
WIP LabelBOT logic
gkourie Feb 3, 2025
9a2d2de
Fix error loading image which was caused by an empty line in labelbot…
gkourie Feb 5, 2025
24e0d60
Merge branch 'master' into labelbot
gkourie Feb 5, 2025
8716cf7
Add onnx dependency
gkourie Feb 5, 2025
fd7695d
Fix lint-js
gkourie Feb 5, 2025
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
15 changes: 15 additions & 0 deletions app/Annotation.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ abstract class Annotation extends Model implements AnnotationContract
'points' => 'array',
];

/**
* The additional labels suggested by the LabelBOT.
*/
public $labelBOTLabels = [];

/**
* Scope a query to only include annotations that are visible for a certain user.
*
Expand Down Expand Up @@ -209,4 +214,14 @@ public function getFile(): VolumeFile
{
return $this->file;
}

/**
* Get the LabelBOT suggested labels.
*
* @return array<int>
*/
public function getLabelBOTLabelsAttribute(): array
{
return $this->labelBOTLabels;
}
}
138 changes: 134 additions & 4 deletions app/Http/Controllers/Api/ImageAnnotationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@
use Biigle\ImageAnnotation;
use Biigle\ImageAnnotationLabel;
use Biigle\Label;
use Biigle\LabelTree;
use Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector;
use Biigle\Project;
use Biigle\Role;
use Biigle\Shape;
use DB;
use Exception;
use Generator;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Pgvector\Laravel\Vector;
use Symfony\Component\HttpFoundation\StreamedJsonResponse;

class ImageAnnotationController extends Controller
Expand Down Expand Up @@ -156,7 +161,8 @@
* @apiParam {Number} id The image ID.
*
* @apiParam (Required arguments) {Number} shape_id ID of the shape of the new annotation.
* @apiParam (Required arguments) {Number} label_id ID of the initial category label of the new annotation.
* @apiParam (Required arguments) {Number} label_id ID of the initial category label of the new annotation. Required if 'feature_vector' is not provided.
* @apiParam (Required arguments) {Number[]} feature_vector A feature vector array of size 384 for label prediction. Required if 'label_id' is not provided.
* @apiParam (Required arguments) {Number} confidence Confidence of the initial annotation label of the new annotation. Must be a value between 0 and 1.
* @apiParam (Required arguments) {Number[]} points Array of the initial points of the annotation. Must contain at least one point. The points array is interpreted as alternating x and y coordinates like this `[x1, y1, x2, y2...]`. The interpretation of the points of the different shapes is as follows:
* **Point:** The first point is the center of the annotation point.
Expand Down Expand Up @@ -212,16 +218,31 @@

$annotation = new ImageAnnotation;
$annotation->shape_id = $request->input('shape_id');
$annotation->image()->associate($request->image);

$image = $request->image;
$annotation->image()->associate($image);
try {
$annotation->validatePoints($points);
} catch (Exception $e) {
throw ValidationException::withMessages(['points' => [$e->getMessage()]]);
}

$annotation->points = $points;
$label = Label::findOrFail($request->input('label_id'));
$labelId = $request->input('label_id');
$topNLabels = [];
if (is_null($labelId) && $request->has('feature_vector')) {
// Get label tree id(s).
$trees = $this->getLabelTreeIds($request->user(), $image->volume_id);
// Perform ANN search.
$topNLabels = $this->performAnnSearch($request->input('feature_vector'), $trees);
// Perform KNN search as a fallback if ANN search returns no results.
if (empty($topNLabels)) {
$topNLabels = $this->performKnnSearch($request->input('feature_vector'), $trees);
}
// Set labelId to top 1 label.
$labelId = $topNLabels[0];
}

$label = Label::findOrFail($labelId);

$this->authorize('attach-label', [$annotation, $label]);

Expand All @@ -236,6 +257,11 @@

$annotation->load('labels.label', 'labels.user');

// Attach the other two labels if they exist.
for ($i = 1; $i < count($topNLabels); $i++) {
$annotation->labelBOTLabels[] = $topNLabels[$i];
mzur marked this conversation as resolved.
Show resolved Hide resolved
}

return $annotation;
}

Expand Down Expand Up @@ -331,4 +357,108 @@

return response('Deleted.', 200);
}

/**
* Get all label trees that are used by all projects which are visible to the user.
*
* @param mixed $user
* @param int $volumeId
*
* @return array
*/
protected function getLabelTreeIds($user, $volumeId)
{
if ($user->can('sudo')) {
// Global admins have no restrictions.
$projectIds = DB::table('project_volume')
->where('volume_id', $volumeId)
->pluck('project_id');
} else {
// Array of all project IDs that the user and the image have in common
// and where the user is editor, expert or admin.
$projectIds = Project::inCommon($user, $volumeId, [
Role::editorId(),
Role::expertId(),
Role::adminId(),
])->pluck('id');
}
$trees = LabelTree::select('id', 'name', 'version_id')
->with('labels', 'version')
->whereIn('id', function ($query) use ($projectIds) {
$query->select('label_tree_id')
->from('label_tree_project')
->whereIn('project_id', $projectIds);
})
->pluck('id')
->toArray();

return $trees;
}

/**
* Perform ANN search (HNSW + Post-Subquery-Filtering).
mzur marked this conversation as resolved.
Show resolved Hide resolved
*
* @param vector $featureVector
* @param int[] $trees
*
* @return array
*/
protected function performAnnSearch($featureVector, $trees)
{
$featureVector = new Vector($featureVector);
mzur marked this conversation as resolved.
Show resolved Hide resolved

$subquery = ImageAnnotationLabelFeatureVector::select('label_id', 'label_tree_id')

Check failure on line 410 in app/Http/Controllers/Api/ImageAnnotationController.php

View workflow job for this annotation

GitHub Actions / lint-php

Call to static method select() on an unknown class Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector.
->selectRaw('(vector <=> ?) AS distance', [$featureVector])
->orderBy('distance')
// K = 100
->limit(config('labelbot.K'));

return DB::table(DB::raw("({$subquery->toSql()}) as subquery"))
->setBindings([$featureVector])
mzur marked this conversation as resolved.
Show resolved Hide resolved
->whereIn('label_tree_id', $trees)
->select('label_id')
mzur marked this conversation as resolved.
Show resolved Hide resolved
->groupBy('label_id')
->orderByRaw('MIN(distance)')
->limit(config('labelbot.N')) // N = 3
->pluck('label_id')
->toArray();
mzur marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Perform KNN search (B-Tree + Post-Filtering).
*
* @param Vector $featureVector
* @param int[] $trees
*
* @return array
*/
protected function performKnnSearch($featureVector, $trees)
mzur marked this conversation as resolved.
Show resolved Hide resolved
{
$featureVector = new Vector($featureVector);

$subquery = ImageAnnotationLabelFeatureVector::select('label_id', 'label_tree_id')

Check failure on line 439 in app/Http/Controllers/Api/ImageAnnotationController.php

View workflow job for this annotation

GitHub Actions / lint-php

Call to static method select() on an unknown class Biigle\Modules\Largo\ImageAnnotationLabelFeatureVector.
->selectRaw('(vector <=> ?) AS distance', [$featureVector])
// filter by label tree id in subquery
// to use B-Tree index for filtering and speeding up the vector search
->whereIn('label_tree_id', $trees)
->orderBy('distance')
->limit(config('labelbot.K')); // K = 100

// TODO: Drop HNSW index temporary
// DB::beginTransaction();

$topNLabels = DB::table(DB::raw("({$subquery->toSql()}) as subquery"))
->setBindings(array_merge([$featureVector], $trees))
->select('label_id')
->groupBy('label_id')
->orderByRaw('MIN(distance)')
->limit(config('labelbot.N')) // N = 3
->pluck('label_id')
->toArray();

// TODO: Rollback the HNSW index drop
// DB::rollback();

return $topNLabels;
}
}
2 changes: 1 addition & 1 deletion app/Http/Requests/StoreImageAnnotation.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use Biigle\Image;
use Biigle\Shape;

class StoreImageAnnotation extends StoreImageAnnotationLabel
class StoreImageAnnotation extends StoreImageAnnotationLabelFeatureVector
mzur marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* The image on which the annotation should be created.
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Requests/StoreImageAnnotationLabel.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function authorize()
public function rules()
{
return [
'label_id' => 'required|integer|exists:labels,id',
'label_id' => 'required|integer|exists:labels,id',
'confidence' => 'required|numeric|between:0,1',
];
}
Expand Down
51 changes: 51 additions & 0 deletions app/Http/Requests/StoreImageAnnotationLabelFeatureVector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Biigle\Http\Requests;

use Biigle\ImageAnnotation;
use Biigle\Label;
use Illuminate\Foundation\Http\FormRequest;

class StoreImageAnnotationLabelFeatureVector extends FormRequest
{
/**
* The annotation to which the label should be attached.
*
* @var ImageAnnotation
*/
public $annotation;

/**
* The label that should be attached.
*
* @var Label
*/
public $label;

/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
$this->annotation = ImageAnnotation::findOrFail($this->route('id'));
$this->label = Label::findOrFail($this->input('label_id'));
mzur marked this conversation as resolved.
Show resolved Hide resolved

return $this->user()->can('attach-label', [$this->annotation, $this->label]);
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'label_id' => 'required_without:feature_vector|integer|exists:labels,id',
'feature_vector' => 'required_without:label_id|array|size:384',
'confidence' => 'required|numeric|between:0,1',
];
}
}
7 changes: 7 additions & 0 deletions app/ImageAnnotation.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ class ImageAnnotation extends Annotation
'points' => 'array',
];

/**
* The attributes that should be included in the JSON response.
*
* @var array<int, string>
*/
protected $appends = ['labelBOTLabels'];

mzur marked this conversation as resolved.
Show resolved Hide resolved
/**
* The image, this annotation belongs to.
*
Expand Down
14 changes: 14 additions & 0 deletions config/labelbot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

return [

/*
| K for KNN
*/
'K' => 100,

/*
| N for top N labels
*/
'N' => 3,
mzur marked this conversation as resolved.
Show resolved Hide resolved
];
Loading
Loading