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
20 changes: 20 additions & 0 deletions CLI-COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,26 @@ Uploading to existing project my-workspace/my-chess
[UPLOADED] /home/jonny/tmp/chess/112_jpg.rf.1a6e7b87410fa3f787f10e82bd02b54e.jpg (7tWtAn573cKrefeg5pIO) / annotations = OK
```

## Example: upload a single image

Upload a single image to a project, optionally with annotations, tags, and metadata:

```bash
roboflow upload image.jpg -p my-project -s train
```

Upload with custom metadata (JSON string):

```bash
roboflow upload image.jpg -p my-project -M '{"camera_id":"cam001","location":"warehouse-3"}'
```

Upload with annotation and tags:

```bash
roboflow upload image.jpg -p my-project -a annotation.xml -t "outdoor,daytime" -s valid
```

## Example: list workspaces
List the workspaces you have access to

Expand Down
26 changes: 26 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,32 @@ Or from the CLI:
roboflow search-export "class:person" -f coco -d my-project -l ./my-export
```

### Upload with Metadata

Attach custom key-value metadata to images during upload:

```python
project = workspace.project("my-project")

# Upload a local image with metadata
project.upload(
image_path="./image.jpg",
metadata={"camera_id": "cam001", "location": "warehouse-3"},
)

# Upload a hosted image with metadata
project.upload(
image_path="https://example.com/image.jpg",
metadata={"camera_id": "cam002", "shift": "night"},
)
```

Or from the CLI:

```bash
roboflow upload image.jpg -p my-project -M '{"camera_id":"cam001","location":"warehouse-3"}'
```

## Library Structure

The Roboflow Python library is structured using the same Workspace, Project, and Version ontology that you will see in the Roboflow application.
Expand Down
2 changes: 1 addition & 1 deletion roboflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from roboflow.models import CLIPModel, GazeModel # noqa: F401
from roboflow.util.general import write_line

__version__ = "1.2.14"
__version__ = "1.2.15"


def check_key(api_key, model, notebook, num_retries=0):
Expand Down
29 changes: 20 additions & 9 deletions roboflow/adapters/rfapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def upload_image(
tag_names: Optional[List[str]] = None,
sequence_number: Optional[int] = None,
sequence_size: Optional[int] = None,
metadata: Optional[Dict] = None,
**kwargs,
):
"""
Expand All @@ -218,6 +219,8 @@ def upload_image(
image_path (str): path to image you'd like to upload
hosted_image (bool): whether the image is hosted on Roboflow
split (str): the dataset split the image to
metadata (dict, optional): custom key-value metadata to attach to the image.
Example: {"camera_id": "cam001", "location": "warehouse"}
"""

coalesced_batch_name = batch_name or DEFAULT_BATCH_NAME
Expand All @@ -232,13 +235,14 @@ def upload_image(
upload_url = _local_upload_url(
api_key, project_url, coalesced_batch_name, tag_names, sequence_number, sequence_size, kwargs
)
m = MultipartEncoder(
fields={
"name": image_name,
"split": split,
"file": ("imageToUpload", imgjpeg, "image/jpeg"),
}
)
fields = {
"name": image_name,
"split": split,
"file": ("imageToUpload", imgjpeg, "image/jpeg"),
}
if metadata is not None:
fields["metadata"] = json.dumps(metadata)
m = MultipartEncoder(fields=fields)

try:
response = requests.post(upload_url, data=m, headers={"Content-Type": m.content_type}, timeout=(300, 300))
Expand All @@ -247,7 +251,12 @@ def upload_image(

else:
# Hosted image upload url
upload_url = _hosted_upload_url(api_key, project_url, image_path, split, coalesced_batch_name, tag_names)
hosted_kwargs = dict(kwargs)
if metadata is not None:
hosted_kwargs["metadata"] = json.dumps(metadata)
upload_url = _hosted_upload_url(
api_key, project_url, image_path, split, coalesced_batch_name, tag_names, hosted_kwargs
)

try:
# Get response
Expand Down Expand Up @@ -363,7 +372,8 @@ def _upload_url(api_key, project_url, **kwargs):
return url


def _hosted_upload_url(api_key, project_url, image_path, split, batch_name, tag_names):
def _hosted_upload_url(api_key, project_url, image_path, split, batch_name, tag_names, kwargs=None):
extra = kwargs or {}
return _upload_url(
api_key,
project_url,
Expand All @@ -372,6 +382,7 @@ def _hosted_upload_url(api_key, project_url, image_path, split, batch_name, tag_
image=image_path,
batch=batch_name,
tag=tag_names,
**extra,
)


Expand Down
11 changes: 11 additions & 0 deletions roboflow/core/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ def upload(
batch_name: Optional[str] = None,
tag_names: Optional[List[str]] = None,
is_prediction: bool = False,
metadata: Optional[Dict] = None,
**kwargs,
):
"""
Expand All @@ -405,6 +406,8 @@ def upload(
batch_name (str): name of batch to upload to within project
tag_names (list[str]): tags to be applied to an image
is_prediction (bool): whether the annotation data is a prediction rather than ground truth
metadata (dict, optional): custom key-value metadata to attach to the image.
Example: {"camera_id": "cam001", "location": "warehouse"}

Example:
>>> import roboflow
Expand All @@ -420,6 +423,8 @@ def upload(
tag_names = []

is_hosted = image_path.startswith("http://") or image_path.startswith("https://")
if is_hosted:
hosted_image = True

is_file = os.path.isfile(image_path) or is_hosted
is_dir = os.path.isdir(image_path)
Expand Down Expand Up @@ -450,6 +455,7 @@ def upload(
batch_name=batch_name,
tag_names=tag_names,
is_prediction=is_prediction,
metadata=metadata,
**kwargs,
)

Expand All @@ -468,6 +474,7 @@ def upload(
batch_name=batch_name,
tag_names=tag_names,
is_prediction=is_prediction,
metadata=metadata,
**kwargs,
)
print("[ " + path + " ] was uploaded succesfully.")
Expand All @@ -485,6 +492,7 @@ def upload_image(
tag_names: Optional[List[str]] = None,
sequence_number=None,
sequence_size=None,
metadata: Optional[Dict] = None,
**kwargs,
):
project_url = self.id.rsplit("/")[1]
Expand All @@ -508,6 +516,7 @@ def upload_image(
tag_names=tag_names,
sequence_number=sequence_number,
sequence_size=sequence_size,
metadata=metadata,
**kwargs,
)
upload_retry_attempts = retry.retries
Expand Down Expand Up @@ -571,6 +580,7 @@ def single_upload(
annotation_overwrite=False,
sequence_number=None,
sequence_size=None,
metadata: Optional[Dict] = None,
**kwargs,
):
if tag_names is None:
Expand All @@ -597,6 +607,7 @@ def single_upload(
tag_names,
sequence_number,
sequence_size,
metadata=metadata,
**kwargs,
)
image_id = uploaded_image["id"] # type: ignore[index]
Expand Down
8 changes: 8 additions & 0 deletions roboflow/roboflowpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ def upload_image(args):
rf = roboflow.Roboflow()
workspace = rf.workspace(args.workspace)
project = workspace.project(args.project)
metadata = json.loads(args.metadata) if args.metadata else None
project.single_upload(
image_path=args.imagefile,
annotation_path=args.annotation,
Expand All @@ -81,6 +82,7 @@ def upload_image(args):
batch_name=args.batch,
tag_names=args.tag_names.split(",") if args.tag_names else [],
is_prediction=args.is_prediction,
metadata=metadata,
)


Expand Down Expand Up @@ -333,6 +335,12 @@ def _add_upload_parser(subparsers):
help="Whether this upload is a prediction (optional)",
action="store_true",
)
upload_parser.add_argument(
"-M",
"--metadata",
dest="metadata",
help='JSON string of metadata to attach to the image (e.g. \'{"camera_id":"cam001"}\')',
)
upload_parser.set_defaults(func=upload_image)


Expand Down
4 changes: 2 additions & 2 deletions tests/manual/uselocal
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#!/bin/env bash
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cp $SCRIPT_DIR/data/.config-staging $SCRIPT_DIR/data/.config
export API_URL=https://localhost.roboflow.one
export APP_URL=https://localhost.roboflow.one
export API_URL=https://localapi.roboflow.one
export APP_URL=https://localapp.roboflow.one
export DEDICATED_DEPLOYMENT_URL=https://staging.roboflow.cloud
export ROBOFLOW_CONFIG_DIR=$SCRIPT_DIR/data/.config
# need to set it in /etc/hosts to the IP of host.docker.internal!
51 changes: 51 additions & 0 deletions tests/test_rfapi.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import unittest
import urllib
Expand Down Expand Up @@ -121,6 +122,56 @@ def test_upload_image_hosted(self):
result = upload_image(self.API_KEY, self.PROJECT_URL, self.IMAGE_PATH_HOSTED, **upload_image_payload)
self.assertTrue(result["success"], msg=f"Failed in scenario: {scenario['desc']}")

@responses.activate
@patch("roboflow.util.image_utils.file2jpeg")
def test_upload_image_local_with_metadata(self, mock_file2jpeg):
mock_file2jpeg.return_value = b"image_data"

metadata = {"camera_id": "cam001", "location": "warehouse"}
expected_url = (
f"{API_URL}/dataset/{self.PROJECT_URL}/upload?"
f"api_key={self.API_KEY}&batch={urllib.parse.quote_plus(DEFAULT_BATCH_NAME)}"
f"&tag=lonely-tag"
)
responses.add(responses.POST, expected_url, json={"success": True}, status=200)

result = upload_image(
self.API_KEY,
self.PROJECT_URL,
self.IMAGE_PATH_LOCAL,
tag_names=self.TAG_NAMES_LOCAL,
metadata=metadata,
)
self.assertTrue(result["success"])

# Verify metadata was sent as a multipart field
request_body = responses.calls[0].request.body
self.assertIn(b'"camera_id"', request_body)
self.assertIn(b'"warehouse"', request_body)

@responses.activate
def test_upload_image_hosted_with_metadata(self):
metadata = {"camera_id": "cam001", "location": "warehouse"}
metadata_encoded = urllib.parse.quote_plus(json.dumps(metadata))
expected_url = (
f"{API_URL}/dataset/{self.PROJECT_URL}/upload?"
f"api_key={self.API_KEY}&name={self.IMAGE_NAME_HOSTED}"
f"&split=train&image={urllib.parse.quote_plus(self.IMAGE_PATH_HOSTED)}"
f"&batch={urllib.parse.quote_plus(DEFAULT_BATCH_NAME)}"
f"&tag=tag1&tag=tag2&metadata={metadata_encoded}"
)
responses.add(responses.POST, expected_url, json={"success": True}, status=200)

result = upload_image(
self.API_KEY,
self.PROJECT_URL,
self.IMAGE_PATH_HOSTED,
hosted_image=True,
tag_names=self.TAG_NAMES_HOSTED,
metadata=metadata,
)
self.assertTrue(result["success"])

def _reset_responses(self):
responses.reset()

Expand Down