openbot-data 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openbot_data/__init__.py +17 -0
- openbot_data/cli.py +85 -0
- openbot_data/extract.py +214 -0
- openbot_data/video.py +165 -0
- openbot_data-0.0.1.dist-info/METADATA +314 -0
- openbot_data-0.0.1.dist-info/RECORD +9 -0
- openbot_data-0.0.1.dist-info/WHEEL +4 -0
- openbot_data-0.0.1.dist-info/entry_points.txt +2 -0
- openbot_data-0.0.1.dist-info/licenses/LICENSE +21 -0
openbot_data/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBot Data — Inspect robot video data before training.
|
|
3
|
+
|
|
4
|
+
OpenBot Data 0.0.1 provides basic robot video inspection and dataset metadata generation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.0.1"
|
|
8
|
+
|
|
9
|
+
from openbot_data.video import scan_directory, scan_video
|
|
10
|
+
from openbot_data.extract import extract_preview_frames, inspect_dataset
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"scan_directory",
|
|
14
|
+
"scan_video",
|
|
15
|
+
"extract_preview_frames",
|
|
16
|
+
"inspect_dataset",
|
|
17
|
+
]
|
openbot_data/cli.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line interface for OpenBot Data v0.0.1.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from openbot_data.video import scan_directory
|
|
10
|
+
from openbot_data.extract import inspect_dataset
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="openbot-data",
|
|
14
|
+
help="Clean, label, and organize robot data for training and evaluation.",
|
|
15
|
+
add_completion=False
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def scan(
|
|
21
|
+
path: str = typer.Argument(..., help="Path to robot video directory"),
|
|
22
|
+
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output JSON file path")
|
|
23
|
+
):
|
|
24
|
+
"""
|
|
25
|
+
Scan a directory for robot videos and show metadata.
|
|
26
|
+
"""
|
|
27
|
+
import json
|
|
28
|
+
|
|
29
|
+
result = scan_directory(path)
|
|
30
|
+
|
|
31
|
+
if output:
|
|
32
|
+
output_path = Path(output)
|
|
33
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
with open(output_path, "w") as f:
|
|
35
|
+
json.dump(result, f, indent=2)
|
|
36
|
+
typer.echo(f"Scan results saved to {output_path}")
|
|
37
|
+
else:
|
|
38
|
+
typer.echo(json.dumps(result, indent=2))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def inspect(
|
|
43
|
+
video_dir: str = typer.Argument(..., help="Path to video directory"),
|
|
44
|
+
output: str = typer.Option(..., "--out", "-o", help="Output directory"),
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Inspect robot videos: extract preview frames and generate manifest.
|
|
48
|
+
"""
|
|
49
|
+
typer.echo(f"Inspecting robot videos in {video_dir}...")
|
|
50
|
+
|
|
51
|
+
result = inspect_dataset(
|
|
52
|
+
video_dir=video_dir,
|
|
53
|
+
output_dir=output
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if "error" in result:
|
|
57
|
+
typer.echo(f"Error: {result['error']}", err=True)
|
|
58
|
+
raise typer.Exit(1)
|
|
59
|
+
|
|
60
|
+
typer.echo("")
|
|
61
|
+
typer.echo("Inspection complete!")
|
|
62
|
+
typer.echo("")
|
|
63
|
+
typer.echo(f"Output directory: {output}")
|
|
64
|
+
typer.echo(f" Total videos: {result['total_videos']}")
|
|
65
|
+
typer.echo(f" Preview frames: {result['total_previews']}")
|
|
66
|
+
typer.echo("")
|
|
67
|
+
typer.echo("Generated files:")
|
|
68
|
+
typer.echo(f" {result['manifest_path']}")
|
|
69
|
+
typer.echo(f" {result['report_path']}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command()
|
|
73
|
+
def version():
|
|
74
|
+
"""Show version information."""
|
|
75
|
+
from openbot_data import __version__
|
|
76
|
+
typer.echo(f"OpenBot Data v{__version__}")
|
|
77
|
+
typer.echo("https://github.com/openbotai/openbot-data")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def main():
|
|
81
|
+
app()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if __name__ == "__main__":
|
|
85
|
+
main()
|
openbot_data/extract.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Frame extraction module for OpenBot Data v0.0.1.
|
|
3
|
+
Extracts preview frames from videos for inspection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import hashlib
|
|
7
|
+
import cv2
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Any, List
|
|
11
|
+
from dataclasses import dataclass, asdict
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PreviewFrame:
|
|
17
|
+
"""Metadata for a preview frame."""
|
|
18
|
+
frame_id: str
|
|
19
|
+
video_path: str
|
|
20
|
+
frame_number: int
|
|
21
|
+
timestamp: float
|
|
22
|
+
path: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _video_output_id(video_path: Path) -> str:
|
|
26
|
+
digest = hashlib.sha1(str(video_path.resolve()).encode("utf-8")).hexdigest()[:8]
|
|
27
|
+
return f"{video_path.stem}_{digest}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_preview_frames(
|
|
31
|
+
video_path: str,
|
|
32
|
+
output_dir: str,
|
|
33
|
+
max_frames: int = 10
|
|
34
|
+
) -> Dict[str, Any]:
|
|
35
|
+
"""
|
|
36
|
+
Extract a few preview frames from a video.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
video_path: Path to input video
|
|
40
|
+
output_dir: Path to output directory
|
|
41
|
+
max_frames: Maximum number of frames to extract
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Dictionary with extraction results
|
|
45
|
+
"""
|
|
46
|
+
video_path = Path(video_path)
|
|
47
|
+
output_dir = Path(output_dir)
|
|
48
|
+
|
|
49
|
+
previews_dir = output_dir / "previews"
|
|
50
|
+
previews_dir.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
|
|
52
|
+
cap = cv2.VideoCapture(str(video_path))
|
|
53
|
+
|
|
54
|
+
if not cap.isOpened():
|
|
55
|
+
return {"error": f"Cannot open video: {video_path}"}
|
|
56
|
+
|
|
57
|
+
if max_frames <= 0:
|
|
58
|
+
cap.release()
|
|
59
|
+
return {"error": "max_frames must be greater than 0"}
|
|
60
|
+
|
|
61
|
+
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
62
|
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
63
|
+
if fps <= 0:
|
|
64
|
+
fps = 30.0
|
|
65
|
+
|
|
66
|
+
if total_frames <= 0:
|
|
67
|
+
cap.release()
|
|
68
|
+
return {"error": f"Video has no readable frames: {video_path}"}
|
|
69
|
+
|
|
70
|
+
video_name = video_path.stem
|
|
71
|
+
video_output_id = _video_output_id(video_path)
|
|
72
|
+
|
|
73
|
+
sample_count = min(max_frames, total_frames)
|
|
74
|
+
if sample_count == 1:
|
|
75
|
+
frame_indices = [0]
|
|
76
|
+
else:
|
|
77
|
+
frame_indices = [
|
|
78
|
+
int(round(i * (total_frames - 1) / (sample_count - 1)))
|
|
79
|
+
for i in range(sample_count)
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
extracted_frames: List[PreviewFrame] = []
|
|
83
|
+
|
|
84
|
+
for frame_idx in frame_indices:
|
|
85
|
+
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
|
|
86
|
+
ret, frame = cap.read()
|
|
87
|
+
|
|
88
|
+
if not ret:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
timestamp = frame_idx / fps
|
|
92
|
+
frame_id = f"{video_output_id}_preview{len(extracted_frames):02d}.jpg"
|
|
93
|
+
frame_path = previews_dir / frame_id
|
|
94
|
+
|
|
95
|
+
cv2.imwrite(str(frame_path), frame)
|
|
96
|
+
|
|
97
|
+
extracted_frame = PreviewFrame(
|
|
98
|
+
frame_id=frame_id,
|
|
99
|
+
video_path=str(video_path),
|
|
100
|
+
frame_number=frame_idx,
|
|
101
|
+
timestamp=round(timestamp, 2),
|
|
102
|
+
path=str(frame_path)
|
|
103
|
+
)
|
|
104
|
+
extracted_frames.append(extracted_frame)
|
|
105
|
+
|
|
106
|
+
cap.release()
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"video_path": str(video_path),
|
|
110
|
+
"video_name": video_name,
|
|
111
|
+
"total_frames": total_frames,
|
|
112
|
+
"fps": fps,
|
|
113
|
+
"extracted_frames": len(extracted_frames),
|
|
114
|
+
"frames": [asdict(f) for f in extracted_frames]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def inspect_dataset(
|
|
119
|
+
video_dir: str,
|
|
120
|
+
output_dir: str
|
|
121
|
+
) -> Dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Inspect a robot video dataset and generate manifest.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
video_dir: Path to video directory
|
|
127
|
+
output_dir: Path to output directory
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Dictionary with inspection results
|
|
131
|
+
"""
|
|
132
|
+
from openbot_data.video import scan_directory
|
|
133
|
+
|
|
134
|
+
scan_result = scan_directory(video_dir)
|
|
135
|
+
|
|
136
|
+
if "error" in scan_result:
|
|
137
|
+
return scan_result
|
|
138
|
+
|
|
139
|
+
output_dir = Path(output_dir)
|
|
140
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
|
|
142
|
+
metadata_dir = output_dir / "metadata"
|
|
143
|
+
metadata_dir.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
|
|
145
|
+
all_previews = []
|
|
146
|
+
all_videos = []
|
|
147
|
+
|
|
148
|
+
for video_info in tqdm(scan_result["videos"], desc="Extracting previews"):
|
|
149
|
+
if not video_info["is_valid"]:
|
|
150
|
+
all_videos.append({
|
|
151
|
+
**video_info,
|
|
152
|
+
"previews": []
|
|
153
|
+
})
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
video_path = video_info["path"]
|
|
157
|
+
video_name = Path(video_path).stem
|
|
158
|
+
|
|
159
|
+
preview_result = extract_preview_frames(
|
|
160
|
+
video_path,
|
|
161
|
+
str(output_dir),
|
|
162
|
+
max_frames=10
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if "error" not in preview_result:
|
|
166
|
+
all_previews.extend(preview_result["frames"])
|
|
167
|
+
|
|
168
|
+
all_videos.append({
|
|
169
|
+
**video_info,
|
|
170
|
+
"previews": preview_result.get("frames", []) if "error" not in preview_result else []
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
# Generate manifest
|
|
174
|
+
manifest = {
|
|
175
|
+
"version": "0.0.1",
|
|
176
|
+
"source_dir": str(video_dir),
|
|
177
|
+
"output_dir": str(output_dir),
|
|
178
|
+
"total_videos": len(all_videos),
|
|
179
|
+
"valid_videos": sum(1 for v in all_videos if v["is_valid"]),
|
|
180
|
+
"total_previews": len(all_previews),
|
|
181
|
+
"videos": all_videos
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
manifest_path = metadata_dir / "manifest.json"
|
|
185
|
+
with open(manifest_path, "w") as f:
|
|
186
|
+
json.dump(manifest, f, indent=2)
|
|
187
|
+
|
|
188
|
+
# Generate basic report
|
|
189
|
+
report = {
|
|
190
|
+
"version": "0.0.1",
|
|
191
|
+
"source_dir": str(video_dir),
|
|
192
|
+
"output_dir": str(output_dir),
|
|
193
|
+
"total_videos": len(all_videos),
|
|
194
|
+
"valid_videos": sum(1 for v in all_videos if v["is_valid"]),
|
|
195
|
+
"invalid_videos": sum(1 for v in all_videos if not v["is_valid"]),
|
|
196
|
+
"total_duration": round(sum(v["duration"] for v in all_videos if v["is_valid"]), 2),
|
|
197
|
+
"total_size_mb": round(sum(v["size_mb"] for v in all_videos), 2),
|
|
198
|
+
"total_previews": len(all_previews),
|
|
199
|
+
"resolutions": [
|
|
200
|
+
list(resolution)
|
|
201
|
+
for resolution in sorted({(v["width"], v["height"]) for v in all_videos if v["is_valid"]})
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
report_path = metadata_dir / "report.json"
|
|
206
|
+
with open(report_path, "w") as f:
|
|
207
|
+
json.dump(report, f, indent=2)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
"manifest_path": str(manifest_path),
|
|
211
|
+
"report_path": str(report_path),
|
|
212
|
+
"total_videos": len(all_videos),
|
|
213
|
+
"total_previews": len(all_previews)
|
|
214
|
+
}
|
openbot_data/video.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Video processing module for OpenBot Data.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import cv2
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class VideoInfo:
|
|
13
|
+
"""Video metadata."""
|
|
14
|
+
path: str
|
|
15
|
+
filename: str
|
|
16
|
+
width: int
|
|
17
|
+
height: int
|
|
18
|
+
fps: float
|
|
19
|
+
frame_count: int
|
|
20
|
+
duration: float
|
|
21
|
+
size_mb: float
|
|
22
|
+
is_valid: bool
|
|
23
|
+
error: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def scan_video(video_path: str) -> VideoInfo:
|
|
27
|
+
"""
|
|
28
|
+
Scan a video file and extract metadata.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
video_path: Path to video file
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
VideoInfo object with metadata
|
|
35
|
+
"""
|
|
36
|
+
path = Path(video_path)
|
|
37
|
+
|
|
38
|
+
if not path.exists():
|
|
39
|
+
return VideoInfo(
|
|
40
|
+
path=str(path),
|
|
41
|
+
filename=path.name,
|
|
42
|
+
width=0,
|
|
43
|
+
height=0,
|
|
44
|
+
fps=0,
|
|
45
|
+
frame_count=0,
|
|
46
|
+
duration=0,
|
|
47
|
+
size_mb=0,
|
|
48
|
+
is_valid=False,
|
|
49
|
+
error="File not found"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
cap = None
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
size_mb = path.stat().st_size / (1024 * 1024)
|
|
56
|
+
cap = cv2.VideoCapture(str(path))
|
|
57
|
+
|
|
58
|
+
if not cap.isOpened():
|
|
59
|
+
return VideoInfo(
|
|
60
|
+
path=str(path),
|
|
61
|
+
filename=path.name,
|
|
62
|
+
width=0,
|
|
63
|
+
height=0,
|
|
64
|
+
fps=0,
|
|
65
|
+
frame_count=0,
|
|
66
|
+
duration=0,
|
|
67
|
+
size_mb=size_mb,
|
|
68
|
+
is_valid=False,
|
|
69
|
+
error="Cannot open video file"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
73
|
+
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
74
|
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
|
75
|
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
|
76
|
+
|
|
77
|
+
if fps > 0:
|
|
78
|
+
duration = frame_count / fps
|
|
79
|
+
else:
|
|
80
|
+
duration = 0
|
|
81
|
+
|
|
82
|
+
is_valid = width > 0 and height > 0 and frame_count > 0
|
|
83
|
+
|
|
84
|
+
return VideoInfo(
|
|
85
|
+
path=str(path),
|
|
86
|
+
filename=path.name,
|
|
87
|
+
width=width,
|
|
88
|
+
height=height,
|
|
89
|
+
fps=fps,
|
|
90
|
+
frame_count=frame_count,
|
|
91
|
+
duration=duration,
|
|
92
|
+
size_mb=size_mb,
|
|
93
|
+
is_valid=is_valid,
|
|
94
|
+
error=None if is_valid else "Video has no readable frames"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return VideoInfo(
|
|
99
|
+
path=str(path),
|
|
100
|
+
filename=path.name,
|
|
101
|
+
width=0,
|
|
102
|
+
height=0,
|
|
103
|
+
fps=0,
|
|
104
|
+
frame_count=0,
|
|
105
|
+
duration=0,
|
|
106
|
+
size_mb=path.stat().st_size / (1024 * 1024) if path.exists() else 0,
|
|
107
|
+
is_valid=False,
|
|
108
|
+
error=str(e)
|
|
109
|
+
)
|
|
110
|
+
finally:
|
|
111
|
+
if cap is not None:
|
|
112
|
+
cap.release()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def scan_directory(directory: str) -> Dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Scan a directory for video files.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
directory: Path to directory
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Dictionary with scan results
|
|
124
|
+
"""
|
|
125
|
+
path = Path(directory)
|
|
126
|
+
|
|
127
|
+
if not path.exists() or not path.is_dir():
|
|
128
|
+
return {
|
|
129
|
+
"error": f"Directory not found: {directory}",
|
|
130
|
+
"videos": []
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
video_extensions = {".mp4", ".mov", ".avi", ".mkv", ".webm"}
|
|
134
|
+
video_files = []
|
|
135
|
+
|
|
136
|
+
for f in sorted(path.rglob("*")):
|
|
137
|
+
if f.is_file() and f.suffix.lower() in video_extensions:
|
|
138
|
+
info = scan_video(str(f))
|
|
139
|
+
video_files.append({
|
|
140
|
+
"path": str(f),
|
|
141
|
+
"filename": f.name,
|
|
142
|
+
"width": info.width,
|
|
143
|
+
"height": info.height,
|
|
144
|
+
"fps": info.fps,
|
|
145
|
+
"frame_count": info.frame_count,
|
|
146
|
+
"duration": round(info.duration, 2),
|
|
147
|
+
"size_mb": round(info.size_mb, 2),
|
|
148
|
+
"is_valid": info.is_valid,
|
|
149
|
+
"error": info.error
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
total_videos = len(video_files)
|
|
153
|
+
valid_videos = sum(1 for v in video_files if v["is_valid"])
|
|
154
|
+
total_size_mb = sum(v["size_mb"] for v in video_files)
|
|
155
|
+
total_duration = sum(v["duration"] for v in video_files if v["is_valid"])
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
"directory": str(path),
|
|
159
|
+
"total_videos": total_videos,
|
|
160
|
+
"valid_videos": valid_videos,
|
|
161
|
+
"invalid_videos": total_videos - valid_videos,
|
|
162
|
+
"total_size_mb": round(total_size_mb, 2),
|
|
163
|
+
"total_duration": round(total_duration, 2),
|
|
164
|
+
"videos": video_files
|
|
165
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openbot-data
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Clean, label, and organize robot data for training and evaluation.
|
|
5
|
+
Project-URL: Homepage, https://openbot.ai/data
|
|
6
|
+
Project-URL: Repository, https://github.com/openbotai/openbot-data
|
|
7
|
+
Project-URL: Issues, https://github.com/openbotai/openbot-data/issues
|
|
8
|
+
Author-email: OpenBot <hello@openbot.ai>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: annotation,computer-vision,data-cleaning,dataset,egocentric-video,embodied-ai,failure-analysis,robot-data,robotics,wrist-camera
|
|
12
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Image Processing
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: numpy>=1.24.0
|
|
24
|
+
Requires-Dist: opencv-python>=4.8.0
|
|
25
|
+
Requires-Dist: tqdm>=4.65.0
|
|
26
|
+
Requires-Dist: typer>=0.9.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build>=1.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# OpenBot Data
|
|
34
|
+
|
|
35
|
+
**Clean, label, and organize robot data for training and evaluation.**
|
|
36
|
+
|
|
37
|
+
OpenBot Data is a lightweight toolkit for cleaning, labeling, organizing, and preparing robot data for model training, evaluation, failure analysis, and deployment debugging.
|
|
38
|
+
|
|
39
|
+
> Robots do not just need more data.
|
|
40
|
+
> They need cleaner data, better labels, and harder failure cases.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Current Status
|
|
45
|
+
|
|
46
|
+
**OpenBot Data is in early development.**
|
|
47
|
+
|
|
48
|
+
The current release (0.0.1) provides basic robot video inspection and dataset metadata generation:
|
|
49
|
+
|
|
50
|
+
- Scan robot video directories
|
|
51
|
+
- Extract video metadata (duration, fps, resolution, frame count)
|
|
52
|
+
- Extract preview frames for quick inspection
|
|
53
|
+
- Generate dataset manifest
|
|
54
|
+
- Generate basic quality reports
|
|
55
|
+
|
|
56
|
+
Planned features include dataset triage, quality-based filtering, annotation export, and failure case mining.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Why OpenBot Data?
|
|
61
|
+
|
|
62
|
+
Robot data is messy.
|
|
63
|
+
|
|
64
|
+
Raw robot recordings often contain:
|
|
65
|
+
|
|
66
|
+
- Motion blur
|
|
67
|
+
- Camera shake
|
|
68
|
+
- Dark or overexposed frames
|
|
69
|
+
- Hand, gripper, or object occlusion
|
|
70
|
+
- Target objects moving out of view
|
|
71
|
+
- Repeated or invalid clips
|
|
72
|
+
- Failed actions mixed with successful ones
|
|
73
|
+
- Unclear task phases
|
|
74
|
+
- Noisy or incomplete annotations
|
|
75
|
+
- Long-tail failure cases
|
|
76
|
+
- Data that should not enter the training set
|
|
77
|
+
|
|
78
|
+
OpenBot Data helps robotics teams convert raw robot data into structured datasets that can be used for training, evaluation, benchmarking, and deployment iteration.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## First focus: ego-centric robot data
|
|
83
|
+
|
|
84
|
+
The first public version of OpenBot Data starts with ego-centric robot videos because they are one of the most common and messy data sources in embodied AI.
|
|
85
|
+
|
|
86
|
+
This includes:
|
|
87
|
+
|
|
88
|
+
- First-person robot videos
|
|
89
|
+
- Wrist-camera videos
|
|
90
|
+
- Close-range manipulation videos
|
|
91
|
+
- Robot vision clips
|
|
92
|
+
- Failure clips from real-world deployment
|
|
93
|
+
- Hard cases from evaluation runs
|
|
94
|
+
|
|
95
|
+
OpenBot Data helps turn these raw videos into clean, annotation-ready datasets and reusable failure cases.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## What it does
|
|
100
|
+
|
|
101
|
+
OpenBot Data focuses on four core workflows:
|
|
102
|
+
|
|
103
|
+
### 1. Clean robot data
|
|
104
|
+
|
|
105
|
+
Detect and organize low-quality frames and clips, including:
|
|
106
|
+
|
|
107
|
+
- Blurry frames
|
|
108
|
+
- Dark scenes
|
|
109
|
+
- Duplicate frames
|
|
110
|
+
- Camera shake
|
|
111
|
+
- Invalid clips
|
|
112
|
+
- Target-out-of-view cases
|
|
113
|
+
- Heavy occlusion
|
|
114
|
+
- Low-quality training samples
|
|
115
|
+
|
|
116
|
+
### 2. Prepare data for annotation
|
|
117
|
+
|
|
118
|
+
Convert raw robot data into annotation-ready datasets.
|
|
119
|
+
|
|
120
|
+
Planned annotation types include:
|
|
121
|
+
|
|
122
|
+
- Object labels
|
|
123
|
+
- Bounding boxes
|
|
124
|
+
- Task phases
|
|
125
|
+
- Success / failure labels
|
|
126
|
+
- Failure reasons
|
|
127
|
+
- Scene conditions
|
|
128
|
+
- Hand or gripper occlusion
|
|
129
|
+
- Object visibility
|
|
130
|
+
- Trainable / not-trainable flags
|
|
131
|
+
|
|
132
|
+
### 3. Mine failure cases
|
|
133
|
+
|
|
134
|
+
Find hard cases that matter for real-world robot deployment:
|
|
135
|
+
|
|
136
|
+
- Motion blur failures
|
|
137
|
+
- Hand or gripper occlusion failures
|
|
138
|
+
- Target lost from view
|
|
139
|
+
- Dark-scene failures
|
|
140
|
+
- Reflective object failures
|
|
141
|
+
- Small-object failures
|
|
142
|
+
- Failed grasp or manipulation moments
|
|
143
|
+
- Long-tail deployment failures
|
|
144
|
+
|
|
145
|
+
### 4. Export structured datasets
|
|
146
|
+
|
|
147
|
+
Export cleaned data for training, evaluation, and benchmarking.
|
|
148
|
+
|
|
149
|
+
Planned formats:
|
|
150
|
+
|
|
151
|
+
- OpenBot JSON
|
|
152
|
+
- COCO
|
|
153
|
+
- YOLO
|
|
154
|
+
- WebDataset
|
|
155
|
+
- Parquet
|
|
156
|
+
- LeRobot-compatible datasets
|
|
157
|
+
- RLDS-compatible datasets
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Installation
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
pip install openbot-data
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Quickstart (v0.0.1)
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Scan robot videos and inspect metadata
|
|
173
|
+
openbot-data scan ./robot_videos
|
|
174
|
+
|
|
175
|
+
# Full inspection: extract frames and generate reports
|
|
176
|
+
openbot-data inspect ./robot_videos --out ./openbot_dataset
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Example output:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
openbot_dataset/
|
|
183
|
+
previews/ # Preview frames from videos
|
|
184
|
+
metadata/
|
|
185
|
+
manifest.json # Dataset manifest with video metadata
|
|
186
|
+
report.json # Quality metrics summary
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Usage
|
|
192
|
+
|
|
193
|
+
### CLI
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
openbot-data scan ./videos # Scan and show video metadata
|
|
197
|
+
openbot-data inspect ./videos --out ./dataset # Full inspection pipeline
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Python API
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from openbot_data import scan_directory, inspect_dataset
|
|
204
|
+
|
|
205
|
+
# Scan videos
|
|
206
|
+
result = scan_directory("./robot_videos")
|
|
207
|
+
print(f"Found {result['valid_videos']} videos")
|
|
208
|
+
|
|
209
|
+
# Inspect and extract preview frames, manifest, and report
|
|
210
|
+
result = inspect_dataset(
|
|
211
|
+
video_dir="./robot_videos",
|
|
212
|
+
output_dir="./dataset",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
print(result["manifest_path"])
|
|
216
|
+
print(result["report_path"])
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Development checks
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
pip install -e ".[dev]"
|
|
223
|
+
pytest
|
|
224
|
+
python -m build
|
|
225
|
+
python -m twine check dist/*
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Example use cases
|
|
231
|
+
|
|
232
|
+
OpenBot Data can be used for:
|
|
233
|
+
|
|
234
|
+
* Robot vision dataset cleaning
|
|
235
|
+
* Ego-centric video preparation
|
|
236
|
+
* Wrist-camera data processing
|
|
237
|
+
* Embodied AI data preparation
|
|
238
|
+
* Failure case mining
|
|
239
|
+
* Deployment evaluation data preparation
|
|
240
|
+
* Synthetic data generation input curation
|
|
241
|
+
* Robot benchmark dataset construction
|
|
242
|
+
* Training / validation / test split management
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Roadmap
|
|
247
|
+
|
|
248
|
+
### v0.0.1 — Installable Preview (Current)
|
|
249
|
+
* [x] Robot video scanner
|
|
250
|
+
* [x] Video metadata extraction (duration, fps, resolution, frame count)
|
|
251
|
+
* [x] Dataset manifest generation
|
|
252
|
+
* [x] Basic quality reports (JSON)
|
|
253
|
+
* [x] Preview frame extraction
|
|
254
|
+
|
|
255
|
+
### v0.0.2 — Dataset Triage
|
|
256
|
+
* [ ] Blur score detection (Laplacian variance)
|
|
257
|
+
* [ ] Brightness score detection
|
|
258
|
+
* [ ] Black frame detection
|
|
259
|
+
* [ ] Basic duplicate-like frame detection
|
|
260
|
+
* [ ] keep / review / drop grouping
|
|
261
|
+
* [ ] HTML triage reports
|
|
262
|
+
|
|
263
|
+
### v0.0.3 — Annotation Batch
|
|
264
|
+
* [ ] Quality-based sampling
|
|
265
|
+
* [ ] Annotation task generation
|
|
266
|
+
* [ ] Trainable/not-trainable flags
|
|
267
|
+
|
|
268
|
+
### v0.0.4 — Export
|
|
269
|
+
* [ ] OpenBot JSON schema
|
|
270
|
+
* [ ] COCO export
|
|
271
|
+
* [ ] YOLO export
|
|
272
|
+
|
|
273
|
+
### v0.1.0 — Robot Data Cleaning Pipeline
|
|
274
|
+
* [ ] Stable cleaning, triage, sampling, and export workflow
|
|
275
|
+
* [ ] Failure case tagging
|
|
276
|
+
* [ ] Multi-view robot data support
|
|
277
|
+
* [ ] LeRobot dataset compatibility
|
|
278
|
+
* [ ] RLDS compatibility
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## OpenBot ecosystem
|
|
283
|
+
|
|
284
|
+
OpenBot Data is part of the OpenBot robotics intelligence infrastructure.
|
|
285
|
+
|
|
286
|
+
Planned modules:
|
|
287
|
+
|
|
288
|
+
* **OpenBot Data** — clean, label, and organize robot data
|
|
289
|
+
* **OpenBot Bench** — evaluate robot models before deployment
|
|
290
|
+
* **OpenBot Synth** — generate hard cases from failures
|
|
291
|
+
* **OpenBot API** — connect data, evaluation, and deployment workflows
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## License
|
|
296
|
+
|
|
297
|
+
MIT License.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Citation
|
|
302
|
+
|
|
303
|
+
No formal citation is required for the 0.0.1 preview. If you use OpenBot Data in research or production, link to the project repository.
|
|
304
|
+
|
|
305
|
+
If you use OpenBot Data in research or production, please cite the project repository.
|
|
306
|
+
|
|
307
|
+
```bibtex
|
|
308
|
+
@software{openbot_data,
|
|
309
|
+
title = {OpenBot Data},
|
|
310
|
+
author = {OpenBot},
|
|
311
|
+
year = {2026},
|
|
312
|
+
url = {https://github.com/openbotai/openbot-data}
|
|
313
|
+
}
|
|
314
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
openbot_data/__init__.py,sha256=wR3fW4SyEhhFh9oCFhiI9kHTFmekHqfTVvnN1afV3qw,421
|
|
2
|
+
openbot_data/cli.py,sha256=wpJbdZ6a6AYMsByyCzZVQY3hEXNiUgTLoRwGMq_VesE,2226
|
|
3
|
+
openbot_data/extract.py,sha256=Q_K5Y5jVsXwacMiMPe0zgPhtSWb9-MN1nltru2P3clk,5916
|
|
4
|
+
openbot_data/video.py,sha256=NDOpH4Kxhk3sl3dho3ojbOl3m3SwSbvVbmkvBGgtcCA,4297
|
|
5
|
+
openbot_data-0.0.1.dist-info/METADATA,sha256=El3lQxNzuY45YDixT71qk4SZa8CK1GLTjK0F3LqbzBE,7886
|
|
6
|
+
openbot_data-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
openbot_data-0.0.1.dist-info/entry_points.txt,sha256=nPRI02-6zWiKTnFiEZ1_pxQ3-uOVYhPKAmPNasa3hpM,55
|
|
8
|
+
openbot_data-0.0.1.dist-info/licenses/LICENSE,sha256=GTnnKzIdwIPfS5ywshDGkHJkjklUutUZlgv4nU2YOjA,1067
|
|
9
|
+
openbot_data-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenBot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACCOUNTING CONTRACT OR TORT OR OTHERWISE, ARISING
|
|
20
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
21
|
+
IN THE SOFTWARE.
|