Skip to content

Commit 24ca570

Browse files
authored
Merge pull request #383 from roboflow/tony/upload-floderstruct-classification
upload classification project with the CLI
2 parents ae5de70 + 4ed210e commit 24ca570

18 files changed

+256
-7
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,4 @@ tests/manual/data
154154
README.roboflow.txt
155155
*.zip
156156
.DS_Store
157+
.claude

CLAUDE.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Development Commands
6+
7+
### Running Tests
8+
```bash
9+
python -m unittest
10+
```
11+
12+
### Linting and Code Quality
13+
```bash
14+
# Format code with ruff
15+
make style
16+
17+
# Check code quality (includes ruff and mypy)
18+
make check_code_quality
19+
20+
# Individual commands
21+
ruff format roboflow
22+
ruff check roboflow --fix
23+
mypy roboflow
24+
```
25+
26+
### Building Documentation
27+
```bash
28+
# Install documentation dependencies
29+
python -m pip install mkdocs mkdocs-material mkdocstrings mkdocstrings[python]
30+
31+
# Serve documentation locally
32+
mkdocs serve
33+
```
34+
35+
### Installing Development Environment
36+
```bash
37+
# Create virtual environment
38+
python3 -m venv env
39+
source env/bin/activate
40+
41+
# Install in editable mode with dev dependencies
42+
pip install -e ".[dev]"
43+
44+
# Install pre-commit hooks
45+
pip install pre-commit
46+
pre-commit install
47+
```
48+
49+
## Architecture Overview
50+
51+
The Roboflow Python SDK follows a hierarchical object model that mirrors the Roboflow platform structure:
52+
53+
### Core Components
54+
55+
1. **Roboflow** (`roboflow/__init__.py`) - Entry point and authentication
56+
- Handles API key management and workspace initialization
57+
- Provides `login()` for CLI authentication
58+
- Creates workspace connections
59+
60+
2. **Workspace** (`roboflow/core/workspace.py`) - Manages Roboflow workspaces
61+
- Lists and accesses projects
62+
- Handles dataset uploads and model deployments
63+
- Manages workspace-level operations
64+
65+
3. **Project** (`roboflow/core/project.py`) - Represents a computer vision project
66+
- Manages project metadata and versions
67+
- Handles image/annotation uploads
68+
- Supports different project types (object-detection, classification, etc.)
69+
70+
4. **Version** (`roboflow/core/version.py`) - Dataset version management
71+
- Downloads datasets in various formats
72+
- Deploys models
73+
- Provides access to trained models for inference
74+
75+
5. **Model Classes** (`roboflow/models/`) - Type-specific inference models
76+
- `ObjectDetectionModel` - Bounding box predictions
77+
- `ClassificationModel` - Image classification
78+
- `InstanceSegmentationModel` - Pixel-level segmentation
79+
- `SemanticSegmentationModel` - Class-based segmentation
80+
- `KeypointDetectionModel` - Keypoint predictions
81+
82+
### API Adapters
83+
84+
- **rfapi** (`roboflow/adapters/rfapi.py`) - Low-level API communication
85+
- **deploymentapi** (`roboflow/adapters/deploymentapi.py`) - Model deployment operations
86+
87+
### CLI Interface
88+
89+
The `roboflow` command line tool (`roboflow/roboflowpy.py`) provides:
90+
- Authentication: `roboflow login`
91+
- Dataset operations: `roboflow download`, `roboflow upload`, `roboflow import`
92+
- Inference: `roboflow infer`
93+
- Project/workspace management: `roboflow project`, `roboflow workspace`
94+
95+
### Key Design Patterns
96+
97+
1. **Hierarchical Access**: Always access objects through their parent (Workspace → Project → Version → Model)
98+
2. **API Key Flow**: API key is passed down through the object hierarchy
99+
3. **Format Flexibility**: Supports multiple dataset formats (YOLO, COCO, Pascal VOC, etc.)
100+
4. **Batch Operations**: Upload and download operations support concurrent processing
101+
102+
## Project Configuration
103+
104+
- **Python Version**: 3.8+
105+
- **Main Dependencies**: See `requirements.txt`
106+
- **Entry Point**: `roboflow=roboflow.roboflowpy:main`
107+
- **Code Style**: Enforced by ruff with Google docstring convention
108+
- **Type Checking**: mypy configured for Python 3.8
109+
110+
## Important Notes
111+
112+
- API keys are stored in `~/.config/roboflow/config.json` (Unix) or `~/roboflow/config.json` (Windows)
113+
- The SDK supports both hosted inference (Roboflow platform) and local inference (via Roboflow Inference)
114+
- Pre-commit hooks automatically run formatting and linting checks
115+
- Test files intentionally excluded from linting: `tests/manual/debugme.py`

roboflow/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from roboflow.models import CLIPModel, GazeModel # noqa: F401
1616
from roboflow.util.general import write_line
1717

18-
__version__ = "1.1.64"
18+
__version__ = "1.1.65"
1919

2020

2121
def check_key(api_key, model, notebook, num_retries=0):

roboflow/core/workspace.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,10 +301,11 @@ def upload_dataset(
301301
""" # noqa: E501 // docs
302302
if dataset_format != "NOT_USED":
303303
print("Warning: parameter 'dataset_format' is deprecated and will be removed in a future release")
304-
parsed_dataset = folderparser.parsefolder(dataset_path)
305304
project, created = self._get_or_create_project(
306305
project_id=project_name, license=project_license, type=project_type
307306
)
307+
is_classification = project.type == "classification"
308+
parsed_dataset = folderparser.parsefolder(dataset_path, is_classification=is_classification)
308309
if created:
309310
print(f"Created project {project.id}")
310311
else:
@@ -361,15 +362,19 @@ def _save_annotation(image_id, imagedesc):
361362

362363
annotationdesc = imagedesc.get("annotationfile")
363364
if isinstance(annotationdesc, dict):
364-
if annotationdesc.get("rawText"):
365+
if annotationdesc.get("type") == "classification_folder":
366+
annotation_path = annotationdesc.get("classification_label")
367+
elif annotationdesc.get("rawText"):
365368
annotation_path = annotationdesc
366-
else:
369+
elif annotationdesc.get("file"):
367370
annotation_path = f"{location}{annotationdesc['file']}"
368-
labelmap = annotationdesc.get("labelmap")
371+
labelmap = annotationdesc.get("labelmap")
369372

370373
if isinstance(labelmap, str):
371374
labelmap = load_labelmap(labelmap)
372-
else:
375+
376+
# If annotation_path is still None at this point, then no annotation will be saved.
377+
if annotation_path is None:
373378
return None, None
374379

375380
annotation, upload_time, _retry_attempts = project.save_annotation(

roboflow/util/folderparser.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def _patch_sep(filename):
2121
return filename.replace("\\", "/")
2222

2323

24-
def parsefolder(folder):
24+
def parsefolder(folder, is_classification=False):
2525
folder = _patch_sep(folder).strip().rstrip("/")
2626
if not os.path.exists(folder):
2727
raise Exception(f"folder does not exist. {folder}")
@@ -36,6 +36,8 @@ def parsefolder(folder):
3636
if not _map_annotations_to_images_1to1(images, annotations):
3737
annotations = _loadAnnotations(folder, annotations)
3838
_map_annotations_to_images_1tomany(images, annotations)
39+
if is_classification:
40+
_infer_classification_labels_from_folders(images)
3941
return {
4042
"location": folder,
4143
"images": images,
@@ -299,3 +301,16 @@ def _list_map(my_list, key):
299301
for i in my_list:
300302
d.setdefault(i[key], []).append(i)
301303
return d
304+
305+
306+
def _infer_classification_labels_from_folders(images):
307+
for image in images:
308+
if image.get("annotationfile"):
309+
continue
310+
dirname = image.get("dirname", "").strip("/")
311+
if not dirname or dirname == ".":
312+
# Skip images in root directory or invalid paths
313+
continue
314+
class_name = os.path.basename(dirname)
315+
if class_name and class_name != ".":
316+
image["annotationfile"] = {"classification_label": class_name, "type": "classification_folder"}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Synthetic Corrosion Dataset > 2022-08-16 10:23am
2+
https://universe.roboflow.com/classification/synthetic-corrosion-dataset
3+
4+
Provided by Roboflow
5+
License: CC BY 4.0

0 commit comments

Comments
 (0)