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
22 changes: 20 additions & 2 deletions roboflow/adapters/rfapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,17 +780,30 @@ def save_annotation(
is_prediction: bool = False,
annotation_labelmap=None,
overwrite: bool = False,
add_to_dataset: Optional[bool] = None,
):
"""
Upload an annotation to a specific project.

Args:
annotation_path (str): path to annotation you'd like to upload
image_id (str): image id you'd like to upload that has annotations for it.
add_to_dataset (bool | None): whether the annotated image joins the project's Dataset,
and so the next dataset version. `None` (the default) leaves the choice to the API,
which adds the image. Pass `False` to store the annotation without changing Dataset
membership — for example while the image is still waiting to be labeled or reviewed
in a batch. Prediction uploads never add the image to the Dataset.
"""

upload_url = _save_annotation_url(
api_key, project_url, annotation_name, image_id, job_name, is_prediction, overwrite
api_key,
project_url,
annotation_name,
image_id,
job_name,
is_prediction,
overwrite,
add_to_dataset,
)

try:
Expand Down Expand Up @@ -829,14 +842,19 @@ def save_annotation(
return responsejson


def _save_annotation_url(api_key, project_url, name, image_id, job_name, is_prediction, overwrite=False):
def _save_annotation_url(
api_key, project_url, name, image_id, job_name, is_prediction, overwrite=False, add_to_dataset=None
):
url = f"{API_URL}/dataset/{project_url}/annotate/{image_id}?api_key={api_key}&name={name}"
if job_name:
url += f"&jobName={job_name}"
if is_prediction:
url += "&prediction=true"
if overwrite:
url += "&overwrite=true"
# Omitted means "whatever the API does by default", which today is to add the image.
if add_to_dataset is not None:
url += f"&addToDataset={'true' if add_to_dataset else 'false'}"
return url


Expand Down
14 changes: 14 additions & 0 deletions roboflow/core/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,18 @@ def save_annotation(
is_prediction: bool = False,
annotation_overwrite=False,
num_retry_uploads=0,
add_to_dataset: Optional[bool] = None,
):
"""
Upload an annotation for an image that is already in the project.

Args:
add_to_dataset (bool | None): whether the annotated image joins the project's
Dataset, and so the next dataset version. `None` (the default) leaves the
choice to the API, which adds the image. Pass `False` to store the annotation
without changing Dataset membership — for example while the image is still
waiting to be labeled or reviewed in a batch.
"""
project_url = self.id.rsplit("/")[1]
annotation_name, annotation_str = self._annotation_params(annotation_path)
t0 = time.time()
Expand All @@ -571,6 +582,7 @@ def save_annotation(
is_prediction=is_prediction,
annotation_labelmap=annotation_labelmap,
overwrite=annotation_overwrite,
add_to_dataset=add_to_dataset,
)
upload_retry_attempts = retry.retries
except AnnotationSaveError as e:
Expand All @@ -597,6 +609,7 @@ def single_upload(
sequence_number=None,
sequence_size=None,
metadata: Optional[Dict] = None,
add_to_dataset: Optional[bool] = None,
**kwargs,
):
if tag_names is None:
Expand Down Expand Up @@ -637,6 +650,7 @@ def single_upload(
is_prediction,
annotation_overwrite,
num_retry_uploads=num_retry_uploads,
add_to_dataset=add_to_dataset,
)

return {
Expand Down
43 changes: 43 additions & 0 deletions tests/test_rfapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from roboflow.adapters.rfapi import (
RoboflowError,
_save_annotation_url,
create_training_v2,
delete_version_training,
get_train_recipe,
Expand Down Expand Up @@ -434,3 +435,45 @@ def test_restore_trash_item_restores_a_training(self):

if __name__ == "__main__":
unittest.main()


class TestSaveAnnotationUrl(unittest.TestCase):
API_KEY = "test_api_key"
PROJECT_URL = "test_project"
IMAGE_ID = "test_image_id"
ANNOTATION_NAME = "annotation.json"

def _url(self, **kwargs):
return _save_annotation_url(
self.API_KEY,
self.PROJECT_URL,
self.ANNOTATION_NAME,
self.IMAGE_ID,
kwargs.pop("job_name", None),
kwargs.pop("is_prediction", False),
**kwargs,
)

def test_add_to_dataset_is_omitted_by_default(self):
# Omitting the parameter leaves the choice to the API, which adds the image to the
# Dataset. Sending it unasked would silently change what existing callers upload.
self.assertNotIn("addToDataset", self._url())

def test_add_to_dataset_false_opts_out(self):
self.assertIn("addToDataset=false", self._url(add_to_dataset=False))

def test_add_to_dataset_true_is_explicit(self):
self.assertIn("addToDataset=true", self._url(add_to_dataset=True))

def test_add_to_dataset_composes_with_the_other_parameters(self):
url = self._url(
job_name="my-job",
is_prediction=True,
overwrite=True,
add_to_dataset=False,
)

self.assertIn("jobName=my-job", url)
self.assertIn("prediction=true", url)
self.assertIn("overwrite=true", url)
self.assertIn("addToDataset=false", url)
Loading