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 46 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
46 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
d4f6445
WIP LabelBOT popover
gkourie Feb 20, 2025
f1b13ec
Implement labelBOT popover
gkourie Mar 3, 2025
182489a
Import labelBOT popup correctly
gkourie Mar 3, 2025
559a1dd
Use Typeahead for the LabelBOT popup input search
gkourie Mar 4, 2025
bea432b
Listen to focus event instead of typing in LabelBOT typeahead
gkourie Mar 4, 2025
92b41ab
Fix label re-attach when searching for top 1 label
gkourie Mar 4, 2025
945a2a2
Integrate labelBOT popup into annotator container
gkourie Mar 5, 2025
6de7948
Reset drawing interaction when maximum number of labelBOT's requests …
gkourie Mar 12, 2025
f68996a
Fix labelBOT's max number of requests message
gkourie Mar 12, 2025
8976b0b
Hide LabelBOT popup when deleting top 1 label's annotation from Annot…
gkourie Mar 12, 2025
0afe1b3
Set LabelBOT labels only when labelBOT is on
gkourie Mar 12, 2025
0947824
Set configurable max number of labelBOT's requests
gkourie Mar 12, 2025
5960c34
Fix php coding style
gkourie Mar 12, 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;
}
}
162 changes: 158 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 @@ public function show($id)
* @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,32 @@ public function store(StoreImageAnnotation $request)

$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);

// Convert the feature vector into a Vector object for compatibility with the query.
$featureVector = new Vector($request->input('feature_vector'));

// Perform vector search.
$topNLabels = $this->performVectorSearch($featureVector, $trees, $topNLabels);

// Set labelId to top 1 label.
$labelId = $topNLabels[0];
}

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

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

Expand All @@ -236,6 +258,11 @@ public function store(StoreImageAnnotation $request)

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

// Attach the other two labels if they exist.
for ($i = 1; $i < count($topNLabels); $i++) {
$annotation->labelBOTLabels[] = Label::findOrFail($topNLabels[$i]);
}

return $annotation;
}

Expand Down Expand Up @@ -331,4 +358,131 @@ public function destroy($id)

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 vector search using the Dynamic Index Switching (DIS) technique.
*
* The search process first attempts to retrieve results using an Approximate Nearest Neighbor (ANN) search
* via the HNSW index. If the ANN search returns no results, it falls back to an exact KNN search using the
* B-Tree index for filtering, ensuring that results are always returned.
*
* @param vector $featureVector The input feature vector to search for nearest neighbors.
* @param int[] $trees The label tree IDs to filter the data by.
* @param int[] $topNLabels The array to store the top N labels based on the search results.
*
* @return array The array of top N labels that are the closest to the input feature vector.
*/
protected function performVectorSearch($featureVector, $trees, $topNLabels)
{
// Perform ANN search.
$topNLabels = $this->performAnnSearch($featureVector, $trees);

// Perform KNN search as a fallback if ANN search returns no results.
if (empty($topNLabels)) {
$topNLabels = $this->performKnnSearch($featureVector, $trees);
}

return $topNLabels;
}

/**
* Perform Approximate Nearest Neighbor (ANN) search using the HNSW index with Post-Subquery Filtering (PSF).
*
* The search uses the HNSW index to find the top K nearest neighbors of the input feature vector,
* and then applies filtering based on the label_tree_id values. If no results are found or if the filtering
* removes all results, an empty array is returned.
*
* @param Vector $featureVector The input feature vector to search for nearest neighbors.
* @param int[] $trees The label tree IDs to filter the data by.
*
* @return array The array of label IDs representing the top nearest neighbors.
*/
protected function performAnnSearch($featureVector, $trees)
{
$subquery = ImageAnnotationLabelFeatureVector::select('label_id', 'label_tree_id')
->selectRaw('(vector <=> ?) AS distance', [$featureVector])
->orderBy('distance')
->limit(config('labelbot.K')); // K = 100

return DB::query()->fromSub($subquery, 'subquery')
->whereIn('label_tree_id', $trees)
->groupBy('label_id')
->orderByRaw('MIN(distance)')
->limit(config('labelbot.N')) // N = 3
->pluck('label_id')
->toArray();
}

/**
* Perform exact KNN search using the B-Tree index for filtering.
*
* This search filters the data based on label_tree_id using the B-Tree index,
* and then performs the vector search to find the nearest neighbors of the input feature vector.
* This method is used as a fallback when the ANN search does not return results.
*
* @param Vector $featureVector The input feature vector to search for nearest neighbors.
* @param int[] $trees The label tree IDs to filter the data by.
*
* @return array The array of label IDs representing the top nearest neighbors.
*/
protected function performKnnSearch($featureVector, $trees)
{
$subquery = ImageAnnotationLabelFeatureVector::select('label_id', 'label_tree_id')
->selectRaw('(vector <=> ?) AS distance', [$featureVector])
->whereIn('label_tree_id', $trees) // Apply label tree ID filter in the subquery to use the B-Tree index for faster filtering
->orderBy('distance')
->limit(config('labelbot.K')); // K = 100

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

$topNLabels = DB::query()->fromSub($subquery, 'subquery')
->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;
}
}
10 changes: 7 additions & 3 deletions app/Http/Requests/StoreImageAnnotation.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

use Biigle\Image;
use Biigle\Shape;
use Illuminate\Foundation\Http\FormRequest;

class StoreImageAnnotation extends StoreImageAnnotationLabel
class StoreImageAnnotation extends FormRequest
{
/**
* The image on which the annotation should be created.
Expand Down Expand Up @@ -33,10 +34,13 @@ public function authorize()
*/
public function rules()
{
return array_merge(parent::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',
'shape_id' => 'required|integer|exists:shapes,id',
'points' => 'required|array',
]);
];
}

/**
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
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'];

/**
* The image, this annotation belongs to.
*
Expand Down
27 changes: 27 additions & 0 deletions config/labelbot.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

return [

/*
|--------------------------------------------------------------------------
| K for KNN (K-Nearest Neighbors)
|--------------------------------------------------------------------------
|
| The value of K determines the number of nearest neighbors to consider
| when performing a KNN search. This is used in both Approximate
| Nearest Neighbor (ANN) and Exact KNN searches.
*/
'K' => 100,

/*
|--------------------------------------------------------------------------
| N for Top N Labels
|--------------------------------------------------------------------------
|
| The value of N specifies how many top labels should be returned from
| the search results. After performing a KNN or ANN search, the top N
| labels (based on their distance to the query vector) will be selected
| and returned.
*/
'N' => 3,
];
Loading
Loading