Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented here.
## [unreleased]

Nothing yet.

## [0.18.0] - July 21st, 2025
- add `allow_annotations_outside_image` arg to the `ULabel` constructor, which defaults to `true`.
- 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.

## [0.17.0] - May 30th, 2025
- 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.
- Options are `fixed`, `match-zoom`, and `inverse-zoom`.
Expand Down
5 changes: 4 additions & 1 deletion api_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,10 @@ Keybind to decrease the brush size. Default is `[`. Requires the active subtask
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.

### `click_and_drag_poly_annotations`
If true, the user can click and drag to contiuously place points for polyline and polygon annotations. Default is `true`.
If `true`, the user can click and drag to contiuously place points for polyline and polygon annotations. Default is `true`.

### `allow_annotations_outside_image`
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`.


## Display Utility Functions
Expand Down
1 change: 1 addition & 0 deletions demo/multi-class.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"create_bbox_on_initial_crop": "|",
"click_and_drag_poly_annotations": false,
"anno_scaling_mode": "match-zoom",
"allow_annotations_outside_image": false,
});
// Wait for ULabel instance to finish initialization
ulabel.init(function () {
Expand Down
42 changes: 41 additions & 1 deletion demo/resume-from.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,46 @@
"line_size": 3,
"text_payload": "",
"annotation_meta": null
},
},
{
"id": "6ce129ba-9201-4425-ad75-215c2a0a1e0c",
"new": true,
"parent_id": null,
"created_by": "out_of_bounds_test",
"created_at": "2025-07-21T18:17:44.862Z",
"deprecated": false,
"deprecated_by": {
"human": false
},
"spatial_type": "bbox",
"spatial_payload": [
[
1202.358968798912,
959.6967745553865
],
[
1440.9410038651008,
1132.1372553458002
]
],
"classification_payloads": [
{
"class_id": 10,
"confidence": 1
},
{
"class_id": 11,
"confidence": 0
},
{
"class_id": 12,
"confidence": 0
}
],
"line_size": 4,
"text_payload": "",
"annotation_meta": null
}
];

let subtasks = {
Expand Down Expand Up @@ -248,6 +287,7 @@
"submit_buttons": on_submit,
"subtasks": subtasks,
"anno_scaling_mode": "inverse-zoom",
"allow_annotations_outside_image": false,
});
// Wait for ULabel instance to finish initialization
ulabel.init(function() {
Expand Down
2 changes: 1 addition & 1 deletion dist/ulabel.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/ulabel.min.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ulabel",
"description": "An image annotation tool.",
"version": "0.17.0",
"version": "0.18.0",
"main": "dist/ulabel.js",
"module": "dist/ulabel.js",
"scripts": {
Expand Down
43 changes: 43 additions & 0 deletions src/annotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,49 @@ export class ULabelAnnotation {
return true;
}

/**
* Ensure each point in an annotation is within the image.
*
* @param {number} image_width Width of the image.
* @param {number} image_height Height of the image.
* @return {ULabelAnnotation} The annotation with the updated spatial payload.
*/
public clamp_annotation_to_image_bounds(image_width: number, image_height: number): ULabelAnnotation {
if (!this.is_delete_annotation()) {
// Ensure each point in the payload is within the image
// for polygons, we'll need to loop through all points
let active_spatial_payload = this.spatial_payload;
const n_iters = this.spatial_type === "polygon" ? this.spatial_payload.length : 1;
for (let i = 0; i < n_iters; i++) {
if (this.spatial_type === "polygon") {
active_spatial_payload = this.spatial_payload[i];
}

for (let j = 0; j < active_spatial_payload.length; j++) {
active_spatial_payload[j] = GeometricUtils.clamp_point_to_image(
active_spatial_payload[j],
image_width,
image_height,
);
}
}
}

// Return the annotation with the updated spatial payload
return this;
}

/**
* Check if the annotation is a delete annotation, e.g. annotations drawn by the `delete_polygon`
* or `delete_bbox` annotation modes.
*
* @returns {boolean} True if the annotation is a delete annotation, false otherwise.
*/
public is_delete_annotation(): boolean {
// Check if the annotation is a delete annotation
return this.classification_payloads[0]["class_id"] === DELETE_CLASS_ID;
}

public static from_json(json_block: object): ULabelAnnotation {
const ret = new ULabelAnnotation();
Object.assign(ret, json_block);
Expand Down
2 changes: 2 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export class Configuration {

public click_and_drag_poly_annotations: boolean = true;

public allow_annotations_outside_image: boolean = true;

constructor(...kwargs: { [key: string]: unknown }[]) {
this.modify_config(...kwargs);
}
Expand Down
25 changes: 25 additions & 0 deletions src/geometric_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,31 @@ export class GeometricUtils {
];
}

/**
* Clamp a point to the image boundaries.
* Ensures the point lies within [0, width) and [0, height).
* @param pt [x, y] point
* @param width image width
* @param height image height
* @returns [x, y] clamped point
*/
public static clamp_point_to_image(pt: Point2D, width: number, height: number): Point2D {
const x = Math.max(0, Math.min(pt[0], width));
const y = Math.max(0, Math.min(pt[1], height));
return [x, y];
}

/**
* Check if a point is within the image bounds.
* @param pt [x, y] point
* @param width image width
* @param height image height
* @returns true if the point is within the image bounds, false otherwise
*/
public static point_is_within_image_bounds(pt: Point2D, width: number, height: number): boolean {
return (pt[0] >= 0 && pt[0] < width && pt[1] >= 0 && pt[1] < height);
}

// Check if two points are equal
public static points_are_equal(pt1: Point2D, pt2: Point2D): boolean {
return (pt1[0] === pt2[0]) && (pt1[1] === pt2[1]);
Expand Down
Loading