Skip to content

Commit e860916

Browse files
committed
Merge branch 'main' of https://github.com/roboflow-ai/roboflow-python into login
2 parents 9d5e611 + 794f864 commit e860916

File tree

7 files changed

+134
-53
lines changed

7 files changed

+134
-53
lines changed

Makefile

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,5 @@ check_code_quality:
1717

1818
publish:
1919
python setup.py sdist bdist_wheel
20-
twine upload -r testpypi dist/* -u ${PYPI_USERNAME} -p ${PYPI_TEST_PASSWORD} --verbose
2120
twine check dist/*
2221
twine upload dist/* -u ${PYPI_USERNAME} -p ${PYPI_PASSWORD} --verbose

roboflow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from roboflow.core.workspace import Workspace
1212
from roboflow.util.general import write_line
1313

14-
__version__ = "0.2.292"
14+
__version__ = "1.0.0"
1515

1616
def check_key(api_key, model, notebook, num_retries=0):
1717
if type(api_key) is not str:

roboflow/core/project.py

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def __image_upload(
268268
hosted_image=False,
269269
split="train",
270270
batch_name=DEFAULT_BATCH_NAME,
271+
tag_names=[],
272+
**kwargs,
271273
):
272274
"""function to upload image to the specific project
273275
:param image_path: path to image you'd like to upload.
@@ -298,6 +300,11 @@ def __image_upload(
298300
batch_name,
299301
]
300302
)
303+
for key, value in kwargs.items():
304+
self.image_upload_url += "&" + str(key) + "=" + str(value)
305+
306+
for tag in tag_names:
307+
self.image_upload_url = self.image_upload_url + f"&tag={tag}"
301308

302309
# Convert to PIL Image
303310
img = cv2.imread(image_path)
@@ -339,34 +346,59 @@ def __image_upload(
339346

340347
return response
341348

342-
def __annotation_upload(self, annotation_path, image_id):
349+
def __annotation_upload(self, annotation_path: str, image_id: str):
343350
"""function to upload annotation to the specific project
344351
:param annotation_path: path to annotation you'd like to upload
345352
:param image_id: image id you'd like to upload that has annotations for it.
346353
"""
347-
# Get annotation string
348-
annotation_string = open(annotation_path, "r").read()
354+
355+
# stop on empty string
356+
if len(annotation_path) == 0:
357+
print("Please provide a non-empty string for annotation_path.")
358+
return {"result": "Please provide a non-empty string for annotation_path."}
359+
360+
# check if annotation file exists
361+
elif os.path.exists(annotation_path):
362+
print("-> found given annotation file")
363+
annotation_string = open(annotation_path, "r").read()
364+
365+
# if not annotation file, check if user wants to upload regular as classification annotation
366+
elif self.type == "classification":
367+
print(f"-> using {annotation_path} as classname for classification project")
368+
annotation_string = annotation_path
369+
370+
# don't attempt upload otherwise
371+
else:
372+
print(
373+
"File not found or uploading to non-classification type project with invalid string"
374+
)
375+
return {
376+
"result": "File not found or uploading to non-classification type project with invalid string"
377+
}
378+
349379
# Set annotation upload url
350380

351381
project_name = self.id.rsplit("/")[1]
352382

353383
self.annotation_upload_url = "".join(
354384
[
355385
API_URL + "/dataset/",
356-
project_name,
386+
self.__project_name,
357387
"/annotate/",
358388
image_id,
359389
"?api_key=",
360390
self.__api_key,
361391
"&name=" + os.path.basename(annotation_path),
362392
]
363393
)
394+
364395
# Get annotation response
365396
annotation_response = requests.post(
366397
self.annotation_upload_url,
367398
data=annotation_string,
368399
headers={"Content-Type": "text/plain"},
369400
)
401+
370402
# Return annotation response
371403
return annotation_response
372404

@@ -382,20 +414,31 @@ def check_valid_image(self, image_path):
382414

383415
def upload(
384416
self,
385-
image_path=None,
386-
annotation_path=None,
387-
hosted_image=False,
388-
image_id=None,
389-
split="train",
390-
num_retry_uploads=0,
391-
batch_name=DEFAULT_BATCH_NAME,
417+
image_path: str = None,
418+
annotation_path: str = None,
419+
hosted_image: bool = False,
420+
image_id: int = None,
421+
split: str = "train",
422+
num_retry_uploads: int = 0,
423+
batch_name: str = DEFAULT_BATCH_NAME,
424+
tag_names: list = [],
425+
**kwargs,
392426
):
393-
"""upload function
394-
:param image_path: path to image you'd like to upload
395-
:param annotation_path: if you're upload annotation, path to it
396-
:param hosted_image: whether the image is hosted
397-
:param image_id: id of the image
398-
:param split: split to upload the image to
427+
428+
"""Upload image function based on the RESTful API
429+
430+
Args:
431+
image_path (str) - path to image you'd like to upload
432+
annotation_path (str) - if you're upload annotation, path to it
433+
hosted_image (bool) - whether the image is hosted
434+
image_id (int) - id of the image
435+
split (str) - to upload the image to
436+
num_retry_uploads (int) - how many times to retry upload on failure
437+
batch_name (str) - name of batch to upload to within project
438+
tag_names (list[str]) - tags to be applied to an image
439+
440+
Returns:
441+
None - returns nothing
399442
"""
400443

401444
is_hosted = image_path.startswith("http://") or image_path.startswith(
@@ -430,6 +473,8 @@ def upload(
430473
split=split,
431474
num_retry_uploads=num_retry_uploads,
432475
batch_name=batch_name,
476+
tag_names=tag_names,
477+
**kwargs,
433478
)
434479
else:
435480
images = os.listdir(image_path)
@@ -444,6 +489,8 @@ def upload(
444489
split=split,
445490
num_retry_uploads=num_retry_uploads,
446491
batch_name=batch_name,
492+
tag_names=tag_names,
493+
**kwargs,
447494
)
448495
print("[ " + path + " ] was uploaded succesfully.")
449496
else:
@@ -459,6 +506,8 @@ def single_upload(
459506
split="train",
460507
num_retry_uploads=0,
461508
batch_name=DEFAULT_BATCH_NAME,
509+
tag_names=[],
510+
**kwargs,
462511
):
463512
success = False
464513
annotation_success = False
@@ -470,6 +519,8 @@ def single_upload(
470519
hosted_image=hosted_image,
471520
split=split,
472521
batch_name=batch_name,
522+
tag_names=tag_names,
523+
**kwargs,
473524
)
474525
# Get JSON response values
475526
try:
@@ -509,6 +560,7 @@ def single_upload(
509560
image_id=image_id,
510561
split=split,
511562
num_retry_uploads=num_retry_uploads - 1,
563+
**kwargs,
512564
)
513565
return
514566
else:
@@ -543,7 +595,12 @@ def single_upload(
543595

544596
# Give user warning that annotation failed to upload
545597
if not annotation_success:
546-
warnings.warn("Annotation, " + annotation_path + ", failed to upload!")
598+
warnings.warn(
599+
"Annotation, "
600+
+ annotation_path
601+
+ "failed to upload!\n Upload correct annotation file to image_id: "
602+
+ image_id
603+
)
547604
else:
548605
annotation_success = True
549606

roboflow/core/version.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@
2929
from roboflow.models.object_detection import ObjectDetectionModel
3030
from roboflow.models.semantic_segmentation import SemanticSegmentationModel
3131
from roboflow.util.general import write_line
32+
from roboflow.util.annotations import amend_data_yaml
3233
from roboflow.util.versions import (
34+
get_wrong_dependencies_versions,
3335
print_warn_for_wrong_dependencies_versions,
34-
warn_for_wrong_dependencies_versions,
3536
)
3637

3738
load_dotenv()
@@ -440,7 +441,7 @@ def deploy(self, model_type: str, model_path: str) -> None:
440441
model_path (str): File path to model weights to be uploaded
441442
"""
442443

443-
supported_models = ["yolov8", "yolov5"]
444+
supported_models = ["yolov8", "yolov5", "yolov7-seg"]
444445

445446
if model_type not in supported_models:
446447
raise (
@@ -463,7 +464,7 @@ def deploy(self, model_type: str, model_path: str) -> None:
463464
[("ultralytics", "<=", "8.0.20")]
464465
)
465466

466-
elif model_type == "yolov5":
467+
elif model_type in ["yolov5", "yolov7-seg"]:
467468
try:
468469
import torch
469470
except ImportError as e:
@@ -510,7 +511,7 @@ def deploy(self, model_type: str, model_path: str) -> None:
510511
"ultralytics_version": ultralytics.__version__,
511512
"model_type": model_type,
512513
}
513-
elif model_type == "yolov5":
514+
elif model_type in ["yolov5", "yolov7-seg"]:
514515
# parse from yaml for yolov5
515516

516517
with open(os.path.join(model_path, "opt.yaml"), "r") as stream:
@@ -538,11 +539,19 @@ def deploy(self, model_type: str, model_path: str) -> None:
538539

539540
with zipfile.ZipFile(model_path + "roboflow_deploy.zip", "w") as zipMe:
540541
for file in lista_files:
541-
zipMe.write(
542-
model_path + file,
543-
arcname=file,
544-
compress_type=zipfile.ZIP_DEFLATED,
545-
)
542+
if os.path.exists(model_path + file):
543+
zipMe.write(
544+
model_path + file,
545+
arcname=file,
546+
compress_type=zipfile.ZIP_DEFLATED,
547+
)
548+
else:
549+
if file in ["model_artifacts.json", "state_dict.pt"]:
550+
raise (
551+
ValueError(
552+
f"File {file} not found. Please make sure to provide a valid model path."
553+
)
554+
)
546555

547556
res = requests.get(
548557
f"{API_URL}/{self.workspace}/{self.project}/{self.version}/uploadModel?api_key={self.__api_key}"
@@ -681,7 +690,7 @@ def __get_format_identifier(self, format):
681690
friendly_formats = {"yolov5": "yolov5pytorch", "yolov7": "yolov7pytorch"}
682691
return friendly_formats.get(format, format)
683692

684-
def __reformat_yaml(self, location, format):
693+
def __reformat_yaml(self, location: str, format: str):
685694
"""
686695
Certain formats seem to require reformatting the downloaded YAML.
687696
It'd be nice if the API did this, but we're doing it in python for now.
@@ -691,28 +700,30 @@ def __reformat_yaml(self, location, format):
691700
692701
:return None:
693702
"""
694-
if format in ["yolov5pytorch", "yolov7pytorch", "yolov8"]:
695-
with open(location + "/data.yaml") as file:
696-
new_yaml = yaml.safe_load(file)
697-
new_yaml["train"] = location + new_yaml["train"].lstrip("..")
698-
new_yaml["val"] = location + new_yaml["val"].lstrip("..")
699-
700-
os.remove(location + "/data.yaml")
701-
702-
with open(location + "/data.yaml", "w") as outfile:
703-
yaml.dump(new_yaml, outfile)
704-
705-
if format == "mt-yolov6":
706-
with open(location + "/data.yaml") as file:
707-
new_yaml = yaml.safe_load(file)
708-
new_yaml["train"] = location + new_yaml["train"].lstrip(".")
709-
new_yaml["val"] = location + new_yaml["val"].lstrip(".")
710-
new_yaml["test"] = location + new_yaml["test"].lstrip(".")
711-
712-
os.remove(location + "/data.yaml")
703+
data_path = os.path.join(location, "data.yaml")
704+
705+
def data_yaml_callback(content: dict) -> dict:
706+
if format == "mt-yolov6":
707+
content["train"] = location + content["train"].lstrip(".")
708+
content["val"] = location + content["val"].lstrip(".")
709+
content["test"] = location + content["test"].lstrip(".")
710+
if format in ["yolov5pytorch", "yolov7pytorch", "yolov8"]:
711+
content["train"] = location + content["train"].lstrip("..")
712+
content["val"] = location + content["val"].lstrip("..")
713+
try:
714+
# get_wrong_dependencies_versions raises exception if ultralytics is not installed at all
715+
if format == "yolov8" and not get_wrong_dependencies_versions(
716+
dependencies_versions=[("ultralytics", ">=", "8.0.30")]
717+
):
718+
content["train"] = "train/images"
719+
content["val"] = "valid/images"
720+
content["test"] = "test/images"
721+
except ModuleNotFoundError:
722+
pass
723+
return content
713724

714-
with open(location + "/data.yaml", "w") as outfile:
715-
yaml.dump(new_yaml, outfile)
725+
if format in ["yolov5pytorch", "mt-yolov6", "yolov7pytorch", "yolov8"]:
726+
amend_data_yaml(path=data_path, callback=data_yaml_callback)
716727

717728
def __str__(self):
718729
"""string representation of version object."""

roboflow/models/classification.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def predict(self, image_path, hosted=False):
5959
# Create buffer
6060
buffered = io.BytesIO()
6161
image.save(buffered, quality=90, format="JPEG")
62+
img_dims = image.size
6263
# Base64 encode image
6364
img_str = base64.b64encode(buffered.getvalue())
6465
img_str = img_str.decode("ascii")
@@ -81,9 +82,9 @@ def predict(self, image_path, hosted=False):
8182

8283
return PredictionGroup.create_prediction_group(
8384
resp.json(),
85+
image_dims=img_dims,
8486
image_path=image_path,
8587
prediction_type=CLASSIFICATION_MODEL,
86-
image_dims=image_dims,
8788
colors=self.colors,
8889
)
8990

roboflow/util/annotations.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import os
2+
from typing import Callable
3+
4+
import yaml
5+
6+
7+
def amend_data_yaml(path: str, callback: Callable[[dict], dict]):
8+
with open(path) as source:
9+
content = yaml.safe_load(source)
10+
content = callback(content)
11+
os.remove(path)
12+
with open(path, "w") as target:
13+
yaml.dump(content, target)

roboflow/util/versions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def get_wrong_dependencies_versions(
88
dependencies_versions: List[Tuple[str, str, str]]
99
) -> List[Tuple[str, str, str, str]]:
1010
"""
11-
Get a list of missmatching dependencies with current version installed.
11+
Get a list of mismatching dependencies with current version installed.
1212
E.g., assuming we pass `get_wrong_dependencies_versions([("torch", "==", "1.2.0")]), we will check if the current version of `torch` is `==1.2.0`. If not, we will return `[("torch", "==", "1.2.0", "<current_installed_version>")]
1313
1414
We support `<=`, `==`, `>=`

0 commit comments

Comments
 (0)