Skip to content
16 changes: 10 additions & 6 deletions app/Http/Controllers/Api/ImageAnnotationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,15 +236,19 @@ public function store(StoreImageAnnotation $request, LabelBotService $labelBotSe
}
}

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

DB::transaction(function () use ($annotation, $request, $label) {
$annotation->save();
$annotationLabel = new ImageAnnotationLabel;
$annotationLabel->label_id = $label->id;
$annotationLabel->user_id = $request->user()->id;
$annotationLabel->confidence = $request->input('confidence');
$annotation->labels()->save($annotationLabel);
if ($label) {
$annotationLabel = new ImageAnnotationLabel;
$annotationLabel->label_id = $label->id;
$annotationLabel->user_id = $request->user()->id;
$annotationLabel->confidence = $request->input('confidence');
$annotation->labels()->save($annotationLabel);
}
});

$annotation->load('labels.label', 'labels.user');
Expand Down
16 changes: 10 additions & 6 deletions app/Http/Controllers/Api/VideoAnnotationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,15 +241,19 @@ public function store(StoreVideoAnnotation $request, LabelBotService $labelBotSe
}
}

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

$annotation = DB::transaction(function () use ($annotation, $request, $label) {
$annotation->save();
VideoAnnotationLabel::create([
'label_id' => $label->id,
'user_id' => $request->user()->id,
'annotation_id' => $annotation->id,
]);
if ($label) {
VideoAnnotationLabel::create([
'label_id' => $label->id,
'user_id' => $request->user()->id,
'annotation_id' => $annotation->id,
]);
}
Comment on lines +250 to +256

return $annotation;
});
Expand Down
3 changes: 1 addition & 2 deletions app/Services/LabelBot/LabelBotService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
use Illuminate\Http\Request;
use InvalidArgumentException;
use Pgvector\Laravel\Vector;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;

class LabelBotService
Expand Down Expand Up @@ -53,7 +52,7 @@ public function getLabelsForAnnotation(
}

if (empty($topNLabels)) {
throw new NotFoundHttpException("LabelBOT could not find similar annotations.");
return [];
}
$labelModels = Label::whereIn('id', $topNLabels)->get()->keyBy('id');

Expand Down
12 changes: 11 additions & 1 deletion resources/assets/js/annotations/annotatorContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,19 @@ export default {
.then(() => {
if (lastLabel) {
this.handleDetachAnnotationLabel(annotation, lastLabel);
} else {
// In case LabelBOT returned no results and the user selected a label by themselves, there is no label to detach.
// In this case, we need to refresh the annotation to show the attached label.
this.refreshSingleAnnotation(annotation);
}
})
.catch(handleErrorResponse);
.catch((error) => {
if (annotation.labels.length === 0) {
this.handleDeleteAnnotation(annotation);
}

handleErrorResponse(error);
});
}
},
handleForceSwapLabel(annotation, label) {
Expand Down
66 changes: 51 additions & 15 deletions resources/assets/js/annotations/components/labelbotPopup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
<i class="fas fa-grip-lines"></i>
</div>
<ul class="labelbot-labels">
<li
<li v-if="noLabels" class="labelbot-popup__message">
<p>
LabelBOT could not find similar annotations. Enter a label manually below, or close this popup to delete the annotation.
</p>
</li>
<li v-else
v-for="(label, index) in labels"
class="labelbot-label"
:class="{'labelbot-label--progress': index === 0 && hasProgressBar}"
Expand All @@ -35,15 +40,15 @@
</li>
<li>
<typeahead
:items="allLabels"
class="typeahead--block"
more-info="tree.versionedName"
placeholder="Find label"
ref="popupTypeahead"
title="Choose a different label"
@focus="handleTypeaheadFocus"
@select="selectTypeaheadLabel"
></typeahead>
:items="allLabels"
class="typeahead--block"
more-info="tree.versionedName"
placeholder="Find label"
ref="popupTypeahead"
title="Choose a different label"
@focus="handleTypeaheadFocus"
@select="selectTypeaheadLabel"
></typeahead>
</li>
</ul>
</div>
Expand Down Expand Up @@ -112,6 +117,7 @@ export default {
dragging: false,
dragStartMousePosition: [0, 0],
dragStartOverlayOffset: [0, 0],
deletePendingLabelbotAnnotation: true, // needed when LabelBOT returns no suggestions
};
},
computed: {
Expand Down Expand Up @@ -147,7 +153,13 @@ export default {
return this.popupKey === this.focusedPopupKey;
},
labels() {
return [this.annotation.labels[0].label].concat(this.annotation.labelBOTLabels);
const suggestedLabels = this.annotation.labelBOTLabels || [];
const top1 = this.annotation.labels?.[0]?.label;

return top1 ? [top1, ...suggestedLabels] : suggestedLabels;
},
noLabels() {
return this.labels.length === 0;
},
classObject() {
return {
Expand Down Expand Up @@ -184,15 +196,20 @@ export default {
methods: {
updateAndClose(label) {
// Top 1 label is already attached/selected
if (this.selectedLabel.id !== label.id) {
this.$emit('update', {label: label, annotation: this.annotation});
if (this.noLabels || this.selectedLabel.id !== label.id) {
this.$emit('update', {label: label, annotation: this.annotation})
this.deletePendingLabelbotAnnotation = false;
}

this.emitClose();
},
confirmAndClose() {
this.emitClose();
Events.emit('labelbot.chose_label_1');
if (this.noLabels) {
this.deleteLabelAnnotation();
} else {
Events.emit('labelbot.chose_label_1');
}
},
emitClose() {
this.$emit('close', this.annotation);
Expand Down Expand Up @@ -344,7 +361,14 @@ export default {
const line = new LineString([popupPosition, popupPosition]);
this.lineFeature = markRaw(new Feature(line));
this.lineFeature.set('unselectable', true);
this.lineFeature.set('color', this.labels[0].color);

if (this.noLabels) {
//this.annotation.labelbotPending = true;
annotationFeature.setStyle(Styles.editing);
} else {
this.lineFeature.set('color', this.labels[0].color);
}

this.lineFeature.setStyle(Styles.editing);

this.lineFeature._updateCoordinates = () => {
Expand Down Expand Up @@ -415,12 +439,24 @@ export default {
this.createOverlay(this.$parent);

this.$refs.popupTypeahead?.$refs.input?.addEventListener("keydown", this.handleTypeaheadKey);

// Autofocus the typeahead input if no labels were suggested by LabelBOT
if (this.noLabels) {
this.$nextTick(() => {
this.$refs.popupTypeahead?.$refs.input?.focus();
this.typeaheadFocused = true;
});
}
},
beforeUnmount() {
this.$parent.map.removeOverlay(this.overlay);
this.$parent.labelbotSource.removeFeature(this.lineFeature);
this.listenerKeys.forEach(unByKey);

if (this.noLabels && this.deletePendingLabelbotAnnotation) {
this.deleteLabelAnnotation();
}

Keyboard.off('Escape', this.handleEsc, 'labelbot');
Keyboard.off('Backspace', this.deleteLabelAnnotation, 'labelbot');
Keyboard.off('Tab', this.enterTypeahead, 'labelbot');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
background-color: $body-bg;
}

.labelbot-popup__message {
list-style-type: none;
max-width: 300px;
margin: 0;
padding: $padding-small-vertical;
background-color: $body-bg;
}

.labelbot-label__color {
position: absolute;
top: 14px;
Expand Down
15 changes: 14 additions & 1 deletion tests/php/Http/Controllers/Api/ImageAnnotationControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,20 @@ public function testStoreWithFeatureVectorWithoutHNSW()
$label = LabelTest::create();
// Label must be attached to a label tree
$this->project()->labelTrees()->attach($label->label_tree_id);
// Save it in DB

// Run with empty database
$response = $this->json('POST', "/api/v1/images/{$this->image->id}/annotations", [
'shape_id' => Shape::pointId(),
'feature_vector' => range(1, 384),
'confidence' => 0.5,
'points' => [10, 11],
]);
$response->assertSuccessful();

// We expect an empty array
$response->assertJsonFragment([]);

// Save label in DB
ImageAnnotationLabelFeatureVector::factory()->create([
'volume_id' => $this->volume()->id,
'label_id' => $label->id,
Expand Down