Skip to content

Commit 585a97c

Browse files
Merge pull request #221 from SenteraLLC/feature/clamp-to-image
Feature/clamp to image
2 parents 3452bf7 + 397d58c commit 585a97c

15 files changed

Lines changed: 307 additions & 62 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented here.
55
## [unreleased]
66

77
Nothing yet.
8+
9+
## [0.18.0] - July 21st, 2025
10+
- add `allow_annotations_outside_image` arg to the `ULabel` constructor, which defaults to `true`.
11+
- when set to `false`, new annotations will be limited to points within the image, and attempts to move annotations outside the image will bounce back.
12+
813
## [0.17.0] - May 30th, 2025
914
- Add `anno_scaling_mode` argument to the `ULabel` constructor, which allows users to specify how the size of annotations should be scaled when the zoom level is changed.
1015
- Options are `fixed`, `match-zoom`, and `inverse-zoom`.

api_spec.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,10 @@ Keybind to decrease the brush size. Default is `[`. Requires the active subtask
427427
The number of annotations to render on a single canvas. Default is `100`. Increasing this number may improve performance for jobs with a large number of annotations.
428428
429429
### `click_and_drag_poly_annotations`
430-
If true, the user can click and drag to contiuously place points for polyline and polygon annotations. Default is `true`.
430+
If `true`, the user can click and drag to contiuously place points for polyline and polygon annotations. Default is `true`.
431+
432+
### `allow_annotations_outside_image`
433+
When `false`, new annotations will be limited to points within the image, and attempts to move annotations outside the image will bounce back to inside the image. Default is `true`.
431434
432435
433436
## Display Utility Functions

demo/multi-class.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
"create_bbox_on_initial_crop": "|",
133133
"click_and_drag_poly_annotations": false,
134134
"anno_scaling_mode": "match-zoom",
135+
"allow_annotations_outside_image": false,
135136
});
136137
// Wait for ULabel instance to finish initialization
137138
ulabel.init(function () {

demo/resume-from.html

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,46 @@
188188
"line_size": 3,
189189
"text_payload": "",
190190
"annotation_meta": null
191-
},
191+
},
192+
{
193+
"id": "6ce129ba-9201-4425-ad75-215c2a0a1e0c",
194+
"new": true,
195+
"parent_id": null,
196+
"created_by": "out_of_bounds_test",
197+
"created_at": "2025-07-21T18:17:44.862Z",
198+
"deprecated": false,
199+
"deprecated_by": {
200+
"human": false
201+
},
202+
"spatial_type": "bbox",
203+
"spatial_payload": [
204+
[
205+
1202.358968798912,
206+
959.6967745553865
207+
],
208+
[
209+
1440.9410038651008,
210+
1132.1372553458002
211+
]
212+
],
213+
"classification_payloads": [
214+
{
215+
"class_id": 10,
216+
"confidence": 1
217+
},
218+
{
219+
"class_id": 11,
220+
"confidence": 0
221+
},
222+
{
223+
"class_id": 12,
224+
"confidence": 0
225+
}
226+
],
227+
"line_size": 4,
228+
"text_payload": "",
229+
"annotation_meta": null
230+
}
192231
];
193232

194233
let subtasks = {
@@ -248,6 +287,7 @@
248287
"submit_buttons": on_submit,
249288
"subtasks": subtasks,
250289
"anno_scaling_mode": "inverse-zoom",
290+
"allow_annotations_outside_image": false,
251291
});
252292
// Wait for ULabel instance to finish initialization
253293
ulabel.init(function() {

dist/ulabel.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/ulabel.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ulabel",
33
"description": "An image annotation tool.",
4-
"version": "0.17.0",
4+
"version": "0.18.0",
55
"main": "dist/ulabel.js",
66
"module": "dist/ulabel.js",
77
"scripts": {

src/annotation.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,49 @@ export class ULabelAnnotation {
173173
return true;
174174
}
175175

176+
/**
177+
* Ensure each point in an annotation is within the image.
178+
*
179+
* @param {number} image_width Width of the image.
180+
* @param {number} image_height Height of the image.
181+
* @return {ULabelAnnotation} The annotation with the updated spatial payload.
182+
*/
183+
public clamp_annotation_to_image_bounds(image_width: number, image_height: number): ULabelAnnotation {
184+
if (!this.is_delete_annotation()) {
185+
// Ensure each point in the payload is within the image
186+
// for polygons, we'll need to loop through all points
187+
let active_spatial_payload = this.spatial_payload;
188+
const n_iters = this.spatial_type === "polygon" ? this.spatial_payload.length : 1;
189+
for (let i = 0; i < n_iters; i++) {
190+
if (this.spatial_type === "polygon") {
191+
active_spatial_payload = this.spatial_payload[i];
192+
}
193+
194+
for (let j = 0; j < active_spatial_payload.length; j++) {
195+
active_spatial_payload[j] = GeometricUtils.clamp_point_to_image(
196+
active_spatial_payload[j],
197+
image_width,
198+
image_height,
199+
);
200+
}
201+
}
202+
}
203+
204+
// Return the annotation with the updated spatial payload
205+
return this;
206+
}
207+
208+
/**
209+
* Check if the annotation is a delete annotation, e.g. annotations drawn by the `delete_polygon`
210+
* or `delete_bbox` annotation modes.
211+
*
212+
* @returns {boolean} True if the annotation is a delete annotation, false otherwise.
213+
*/
214+
public is_delete_annotation(): boolean {
215+
// Check if the annotation is a delete annotation
216+
return this.classification_payloads[0]["class_id"] === DELETE_CLASS_ID;
217+
}
218+
176219
public static from_json(json_block: object): ULabelAnnotation {
177220
const ret = new ULabelAnnotation();
178221
Object.assign(ret, json_block);

src/configuration.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ export class Configuration {
179179

180180
public click_and_drag_poly_annotations: boolean = true;
181181

182+
public allow_annotations_outside_image: boolean = true;
183+
182184
constructor(...kwargs: { [key: string]: unknown }[]) {
183185
this.modify_config(...kwargs);
184186
}

0 commit comments

Comments
 (0)