Skip to content

Commit 0b897e7

Browse files
authored
Merge pull request #508 from Labelbox/develop
3.17.0
2 parents afb6f8c + 001fe48 commit 0b897e7

File tree

19 files changed

+340
-128
lines changed

19 files changed

+340
-128
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
# Changelog
2+
3+
# Version 3.17.0 (2022-03-22)
4+
## Added
5+
* Create batches from the SDK (Beta). Learn more about [batches](https://docs.labelbox.com/docs/batches)
6+
* Support for precision and recall metrics on Entity annotations
7+
8+
## Fix
9+
* `client.create_project` type hint added for its return type
10+
11+
## Updated
12+
* Removed batch MVP code
13+
214
# Version 3.16.0 (2022-03-08)
315
## Added
416
* Ability to fetch a model run with `client.get_model_run()`
517
* Ability to fetch labels from a model run with `model_run.export_labels()`
618
- Note: this is only Experimental. To use, client param `enable_experimental` should
719
be set to true
20+
* Ability to delete an attachment
821

922
## Fix
1023
* Logger level is no longer set to INFO

examples/annotation_types/basics.ipynb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@
140140
" - Covered in this notebook\n",
141141
"2. Export labels to an annotation generator\n",
142142
" - `project.label_generator()`\n",
143-
" - `project.video_label_generator()`\n",
144143
"3. Use a converter to load from another format\n",
145144
" - Covered in the converters.ipynb notebook."
146145
]

examples/annotation_types/converters.ipynb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
"* The goal is to create a set of converts that convert to and from labelbox annotation types.\n",
3636
"* This is automatically used when exporting labels from labelbox with:\n",
3737
" 1. `label.label_generator()`\n",
38-
" 2. `label.video_label_generator()`\n",
3938
"* Currently we support:\n",
4039
" 1. NDJson Converter\n",
4140
" - Convert to and from the prediction import format (mea, mal)\n",
@@ -363,7 +362,7 @@
363362
],
364363
"source": [
365364
"project = client.get_project(\"ckqcx1d58068c0y619qv7hzgu\")\n",
366-
"labels = project.video_label_generator()\n",
365+
"labels = project.label_generator()\n",
367366
"\n",
368367
"for label in labels:\n",
369368
" annotation_lookup = label.frame_annotations()\n",

labelbox/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
name = "labelbox"
2-
__version__ = "3.16.0"
2+
__version__ = "3.17.0"
33

4-
from labelbox.schema.project import Project
54
from labelbox.client import Client
5+
from labelbox.schema.project import Project
66
from labelbox.schema.model import Model
77
from labelbox.schema.bulk_import_request import BulkImportRequest
88
from labelbox.schema.annotation_import import MALPredictionImport, MEAPredictionImport, LabelImport
99
from labelbox.schema.dataset import Dataset
1010
from labelbox.schema.data_row import DataRow
1111
from labelbox.schema.label import Label
12+
from labelbox.schema.batch import Batch
1213
from labelbox.schema.review import Review
1314
from labelbox.schema.user import User
1415
from labelbox.schema.organization import Organization

labelbox/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def _get_single(self, db_object_type, uid):
404404
else:
405405
return db_object_type(self, res)
406406

407-
def get_project(self, project_id):
407+
def get_project(self, project_id) -> Project:
408408
""" Gets a single Project with the given ID.
409409
410410
>>> project = client.get_project("<project_id>")
@@ -918,4 +918,4 @@ def get_model_run(self, model_run_id: str) -> ModelRun:
918918
Returns:
919919
A ModelRun object.
920920
"""
921-
return self._get_single(Entity.ModelRun, model_run_id)
921+
return self._get_single(Entity.ModelRun, model_run_id)

labelbox/data/metrics/confusion_matrix/calculation.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import numpy as np
44

5-
from ..iou.calculation import _get_mask_pairs, _get_vector_pairs, miou
5+
from ..iou.calculation import _get_mask_pairs, _get_vector_pairs, _get_ner_pairs, miou
66
from ...annotation_types import (ObjectAnnotation, ClassificationAnnotation,
7-
Mask, Geometry, Checklist, Radio,
7+
Mask, Geometry, Checklist, Radio, TextEntity,
88
ScalarMetricValue, ConfusionMatrixMetricValue)
99
from ..group import (get_feature_pairs, get_identifying_key, has_no_annotations,
1010
has_no_matching_annotations)
@@ -68,6 +68,9 @@ def feature_confusion_matrix(
6868
elif isinstance(predictions[0].value, Geometry):
6969
return vector_confusion_matrix(ground_truths, predictions,
7070
include_subclasses, iou)
71+
elif isinstance(predictions[0].value, TextEntity):
72+
return ner_confusion_matrix(ground_truths, predictions,
73+
include_subclasses, iou)
7174
elif isinstance(predictions[0], ClassificationAnnotation):
7275
return classification_confusion_matrix(ground_truths, predictions)
7376
else:
@@ -288,3 +291,23 @@ def mask_confusion_matrix(ground_truths: List[ObjectAnnotation],
288291
fn_mask = (prediction_np == 0) & (ground_truth_np == 1)
289292
tn_mask = prediction_np == ground_truth_np == 0
290293
return [np.sum(tp_mask), np.sum(fp_mask), np.sum(fn_mask), np.sum(tn_mask)]
294+
295+
296+
def ner_confusion_matrix(ground_truths: List[ObjectAnnotation],
297+
predictions: List[ObjectAnnotation],
298+
include_subclasses: bool,
299+
iou: float) -> Optional[ConfusionMatrixMetricValue]:
300+
"""Computes confusion matrix metric between two lists of TextEntity objects
301+
302+
Args:
303+
ground_truths: List of ground truth mask annotations
304+
predictions: List of prediction mask annotations
305+
Returns:
306+
confusion matrix as a list: [TP,FP,TN,FN]
307+
"""
308+
if has_no_matching_annotations(ground_truths, predictions):
309+
return [0, int(len(predictions) > 0), 0, int(len(ground_truths) > 0)]
310+
elif has_no_annotations(ground_truths, predictions):
311+
return None
312+
pairs = _get_ner_pairs(ground_truths, predictions)
313+
return object_pair_confusion_matrix(pairs, include_subclasses, iou)

labelbox/data/metrics/iou/calculation.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ..group import get_feature_pairs, get_identifying_key, has_no_annotations, has_no_matching_annotations
88
from ...annotation_types import (ObjectAnnotation, ClassificationAnnotation,
99
Mask, Geometry, Point, Line, Checklist, Text,
10-
Radio, ScalarMetricValue)
10+
TextEntity, Radio, ScalarMetricValue)
1111

1212

1313
def miou(ground_truths: List[Union[ObjectAnnotation, ClassificationAnnotation]],
@@ -61,6 +61,8 @@ def feature_miou(ground_truths: List[Union[ObjectAnnotation,
6161
return vector_miou(ground_truths, predictions, include_subclasses)
6262
elif isinstance(predictions[0], ClassificationAnnotation):
6363
return classification_miou(ground_truths, predictions)
64+
elif isinstance(predictions[0].value, TextEntity):
65+
return ner_miou(ground_truths, predictions, include_subclasses)
6466
else:
6567
raise ValueError(
6668
f"Unexpected annotation found. Found {type(predictions[0].value)}")
@@ -269,3 +271,51 @@ def _ensure_valid_poly(poly):
269271
def _mask_iou(mask1: np.ndarray, mask2: np.ndarray) -> ScalarMetricValue:
270272
"""Computes iou between two binary segmentation masks."""
271273
return np.sum(mask1 & mask2) / np.sum(mask1 | mask2)
274+
275+
276+
def _get_ner_pairs(
277+
ground_truths: List[ObjectAnnotation], predictions: List[ObjectAnnotation]
278+
) -> List[Tuple[ObjectAnnotation, ObjectAnnotation, ScalarMetricValue]]:
279+
"""Get iou score for all possible pairs of ground truths and predictions"""
280+
pairs = []
281+
for ground_truth, prediction in product(ground_truths, predictions):
282+
score = _ner_iou(ground_truth.value, prediction.value)
283+
pairs.append((ground_truth, prediction, score))
284+
return pairs
285+
286+
287+
def _ner_iou(ner1: TextEntity, ner2: TextEntity):
288+
"""Computes iou between two text entity annotations"""
289+
intersection_start, intersection_end = max(ner1.start, ner2.start), min(
290+
ner1.end, ner2.end)
291+
union_start, union_end = min(ner1.start,
292+
ner2.start), max(ner1.end, ner2.end)
293+
#edge case of only one character in text
294+
if union_start == union_end:
295+
return 1
296+
#if there is no intersection
297+
if intersection_start > intersection_end:
298+
return 0
299+
return (intersection_end - intersection_start) / (union_end - union_start)
300+
301+
302+
def ner_miou(ground_truths: List[ObjectAnnotation],
303+
predictions: List[ObjectAnnotation],
304+
include_subclasses: bool) -> Optional[ScalarMetricValue]:
305+
"""
306+
Computes iou score for all features with the same feature schema id.
307+
Calculation includes subclassifications.
308+
309+
Args:
310+
ground_truths: List of ground truth ner annotations
311+
predictions: List of prediction ner annotations
312+
Returns:
313+
float representing the iou score for the feature type.
314+
If there are no matches then this returns none
315+
"""
316+
if has_no_matching_annotations(ground_truths, predictions):
317+
return 0.
318+
elif has_no_annotations(ground_truths, predictions):
319+
return None
320+
pairs = _get_ner_pairs(ground_truths, predictions)
321+
return object_pair_miou(pairs, include_subclasses)

labelbox/orm/model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ class Entity(metaclass=EntityMeta):
347347
Invite: Type[labelbox.Invite]
348348
InviteLimit: Type[labelbox.InviteLimit]
349349
ProjectRole: Type[labelbox.ProjectRole]
350+
Batch: Type[labelbox.Batch]
350351

351352
@classmethod
352353
def _attributes_of_type(cls, attr_type):

labelbox/schema/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@
1818
import labelbox.schema.user
1919
import labelbox.schema.webhook
2020
import labelbox.schema.data_row_metadata
21+
import labelbox.schema.batch
2122
import labelbox.schema.iam_integration

labelbox/schema/batch.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from labelbox.orm.db_object import DbObject
2+
from labelbox.orm.model import Field, Relationship
3+
4+
5+
class Batch(DbObject):
6+
""" A Batch is a group of data rows submitted to a project for labeling
7+
8+
Attributes:
9+
name (str)
10+
created_at (datetime)
11+
updated_at (datetime)
12+
deleted (bool)
13+
14+
project (Relationship): `ToOne` relationship to Project
15+
created_by (Relationship): `ToOne` relationship to User
16+
17+
"""
18+
name = Field.String("name")
19+
created_at = Field.DateTime("created_at")
20+
updated_at = Field.DateTime("updated_at")
21+
size = Field.Int("size")
22+
23+
# Relationships
24+
project = Relationship.ToOne("Project")
25+
created_by = Relationship.ToOne("User")

0 commit comments

Comments
 (0)