pixlint 1.0__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.
- pixlint/__init__.py +132 -0
- pixlint/analysis/__init__.py +0 -0
- pixlint/analysis/active_learning.py +175 -0
- pixlint/analysis/autolabel.py +136 -0
- pixlint/analysis/captioning.py +207 -0
- pixlint/analysis/diff.py +79 -0
- pixlint/analysis/distribution.py +198 -0
- pixlint/analysis/duplicates.py +257 -0
- pixlint/analysis/embeddings.py +288 -0
- pixlint/analysis/health.py +144 -0
- pixlint/analysis/integrity.py +85 -0
- pixlint/analysis/label_errors.py +135 -0
- pixlint/analysis/outliers.py +171 -0
- pixlint/analysis/quality.py +242 -0
- pixlint/analysis/query.py +185 -0
- pixlint/analysis/readiness.py +164 -0
- pixlint/analysis/slices.py +157 -0
- pixlint/analysis/statistics.py +89 -0
- pixlint/analysis/video.py +169 -0
- pixlint/augmentation/__init__.py +0 -0
- pixlint/augmentation/auto_augment.py +108 -0
- pixlint/augmentation/pipeline.py +194 -0
- pixlint/augmentation/transforms.py +196 -0
- pixlint/core/__init__.py +0 -0
- pixlint/core/cache.py +67 -0
- pixlint/core/cloud.py +161 -0
- pixlint/core/curation.py +321 -0
- pixlint/core/index.py +177 -0
- pixlint/core/loader.py +339 -0
- pixlint/core/merge.py +163 -0
- pixlint/core/metadata.py +37 -0
- pixlint/core/pipeline.py +361 -0
- pixlint/dashboard.py +293 -0
- pixlint/export/__init__.py +0 -0
- pixlint/export/extra_formats.py +322 -0
- pixlint/export/hdf5.py +133 -0
- pixlint/export/huggingface.py +105 -0
- pixlint/export/pytorch.py +197 -0
- pixlint/export/tensorflow.py +190 -0
- pixlint/export/ultralytics.py +119 -0
- pixlint/server.py +1883 -0
- pixlint/splitting/__init__.py +0 -0
- pixlint/splitting/cross_validation.py +81 -0
- pixlint/splitting/leakage.py +66 -0
- pixlint/splitting/splitter.py +157 -0
- pixlint/transformation/__init__.py +0 -0
- pixlint/transformation/format_converter.py +354 -0
- pixlint/transformation/normalize.py +111 -0
- pixlint/transformation/resize.py +126 -0
- pixlint/utils/__init__.py +0 -0
- pixlint/utils/hashing.py +74 -0
- pixlint/utils/image_io.py +53 -0
- pixlint/utils/parallel.py +24 -0
- pixlint/utils/progress.py +74 -0
- pixlint/utils/schemas.py +446 -0
- pixlint/utils/security.py +871 -0
- pixlint/visualization/__init__.py +0 -0
- pixlint/visualization/charts.py +284 -0
- pixlint/visualization/embeddings_viz.py +166 -0
- pixlint/visualization/previews.py +164 -0
- pixlint-1.0.dist-info/METADATA +199 -0
- pixlint-1.0.dist-info/RECORD +66 -0
- pixlint-1.0.dist-info/WHEEL +5 -0
- pixlint-1.0.dist-info/entry_points.txt +2 -0
- pixlint-1.0.dist-info/licenses/LICENSE +116 -0
- pixlint-1.0.dist-info/top_level.txt +1 -0
pixlint/__init__.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""PixLint - Computer Vision Dataset Management and Analysis."""
|
|
2
|
+
|
|
3
|
+
from pixlint.core.loader import CVDataset, load_dataset, detect_format
|
|
4
|
+
from pixlint.analysis.duplicates import find_duplicates
|
|
5
|
+
from pixlint.analysis.quality import analyze_quality
|
|
6
|
+
from pixlint.analysis.integrity import check_integrity
|
|
7
|
+
from pixlint.analysis.distribution import analyze_distribution
|
|
8
|
+
from pixlint.analysis.statistics import compute_statistics, sample_dataset
|
|
9
|
+
from pixlint.analysis.embeddings import compute_embeddings, semantic_search, cluster_dataset
|
|
10
|
+
from pixlint.analysis.outliers import detect_outliers
|
|
11
|
+
from pixlint.analysis.health import dataset_health_score
|
|
12
|
+
from pixlint.augmentation.pipeline import augment_dataset, preview_augmentation
|
|
13
|
+
from pixlint.transformation.format_converter import convert_format
|
|
14
|
+
from pixlint.transformation.resize import resize_dataset
|
|
15
|
+
from pixlint.transformation.normalize import normalize_dataset, compute_channel_stats
|
|
16
|
+
from pixlint.splitting.splitter import split_dataset
|
|
17
|
+
from pixlint.splitting.cross_validation import generate_kfold_splits
|
|
18
|
+
from pixlint.splitting.leakage import detect_leakage
|
|
19
|
+
from pixlint.export.pytorch import export_pytorch
|
|
20
|
+
from pixlint.export.tensorflow import export_tensorflow
|
|
21
|
+
from pixlint.export.ultralytics import export_ultralytics
|
|
22
|
+
from pixlint.export.hdf5 import export_hdf5
|
|
23
|
+
from pixlint.export.extra_formats import (
|
|
24
|
+
export_webdataset,
|
|
25
|
+
export_fiftyone,
|
|
26
|
+
export_cvat_xml,
|
|
27
|
+
export_labelme_json,
|
|
28
|
+
)
|
|
29
|
+
from pixlint.visualization.previews import preview_images, preview_single_image
|
|
30
|
+
from pixlint.visualization.charts import (
|
|
31
|
+
plot_distribution,
|
|
32
|
+
plot_quality_scores,
|
|
33
|
+
plot_spatial_heatmap,
|
|
34
|
+
plot_duplicate_groups,
|
|
35
|
+
)
|
|
36
|
+
from pixlint.visualization.embeddings_viz import plot_embeddings
|
|
37
|
+
from pixlint.analysis.active_learning import uncertainty_sampling, diversity_sampling, query_strategy
|
|
38
|
+
from pixlint.analysis.video import extract_frames, video_to_dataset, video_batch_to_datasets, temporal_split
|
|
39
|
+
from pixlint.analysis.captioning import generate_captions, auto_tag_dataset, enrich_metadata
|
|
40
|
+
from pixlint.analysis.diff import dataset_diff
|
|
41
|
+
from pixlint.core.metadata import list_datasets, get_dataset_info, register_dataset
|
|
42
|
+
from pixlint.core.merge import merge_datasets
|
|
43
|
+
from pixlint.core.pipeline import (
|
|
44
|
+
execute_pipeline,
|
|
45
|
+
register_pipeline,
|
|
46
|
+
list_pipelines,
|
|
47
|
+
get_pipeline,
|
|
48
|
+
save_pipeline_to_json,
|
|
49
|
+
load_pipeline_from_json,
|
|
50
|
+
get_template,
|
|
51
|
+
list_templates,
|
|
52
|
+
)
|
|
53
|
+
from pixlint.utils.schemas import PipelineDefinition
|
|
54
|
+
|
|
55
|
+
__version__ = "1.0"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
# Core
|
|
59
|
+
"CVDataset",
|
|
60
|
+
"load_dataset",
|
|
61
|
+
"detect_format",
|
|
62
|
+
"list_datasets",
|
|
63
|
+
"get_dataset_info",
|
|
64
|
+
"register_dataset",
|
|
65
|
+
"merge_datasets",
|
|
66
|
+
# Analysis
|
|
67
|
+
"find_duplicates",
|
|
68
|
+
"analyze_quality",
|
|
69
|
+
"check_integrity",
|
|
70
|
+
"analyze_distribution",
|
|
71
|
+
"compute_statistics",
|
|
72
|
+
"sample_dataset",
|
|
73
|
+
"compute_embeddings",
|
|
74
|
+
"semantic_search",
|
|
75
|
+
"cluster_dataset",
|
|
76
|
+
"detect_outliers",
|
|
77
|
+
"dataset_health_score",
|
|
78
|
+
# Augmentation
|
|
79
|
+
"augment_dataset",
|
|
80
|
+
"preview_augmentation",
|
|
81
|
+
# Transformation
|
|
82
|
+
"convert_format",
|
|
83
|
+
"resize_dataset",
|
|
84
|
+
"normalize_dataset",
|
|
85
|
+
"compute_channel_stats",
|
|
86
|
+
# Splitting
|
|
87
|
+
"split_dataset",
|
|
88
|
+
"generate_kfold_splits",
|
|
89
|
+
"detect_leakage",
|
|
90
|
+
# Export
|
|
91
|
+
"export_pytorch",
|
|
92
|
+
"export_tensorflow",
|
|
93
|
+
"export_ultralytics",
|
|
94
|
+
"export_hdf5",
|
|
95
|
+
"export_webdataset",
|
|
96
|
+
"export_fiftyone",
|
|
97
|
+
"export_cvat_xml",
|
|
98
|
+
"export_labelme_json",
|
|
99
|
+
# Visualization
|
|
100
|
+
"preview_images",
|
|
101
|
+
"preview_single_image",
|
|
102
|
+
"plot_distribution",
|
|
103
|
+
"plot_quality_scores",
|
|
104
|
+
"plot_spatial_heatmap",
|
|
105
|
+
"plot_duplicate_groups",
|
|
106
|
+
"plot_embeddings",
|
|
107
|
+
# Active Learning
|
|
108
|
+
"uncertainty_sampling",
|
|
109
|
+
"diversity_sampling",
|
|
110
|
+
"query_strategy",
|
|
111
|
+
# Video
|
|
112
|
+
"extract_frames",
|
|
113
|
+
"video_to_dataset",
|
|
114
|
+
"video_batch_to_datasets",
|
|
115
|
+
"temporal_split",
|
|
116
|
+
# Captioning
|
|
117
|
+
"generate_captions",
|
|
118
|
+
"auto_tag_dataset",
|
|
119
|
+
"enrich_metadata",
|
|
120
|
+
# Diff
|
|
121
|
+
"dataset_diff",
|
|
122
|
+
# Pipeline
|
|
123
|
+
"execute_pipeline",
|
|
124
|
+
"register_pipeline",
|
|
125
|
+
"list_pipelines",
|
|
126
|
+
"get_pipeline",
|
|
127
|
+
"save_pipeline_to_json",
|
|
128
|
+
"load_pipeline_from_json",
|
|
129
|
+
"get_template",
|
|
130
|
+
"list_templates",
|
|
131
|
+
"PipelineDefinition",
|
|
132
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from pixlint.analysis.embeddings import compute_embeddings
|
|
8
|
+
from pixlint.core.loader import CVDataset
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_SKLEARN_AVAILABLE = False
|
|
12
|
+
try:
|
|
13
|
+
from sklearn.ensemble import GradientBoostingClassifier
|
|
14
|
+
from sklearn.metrics import pairwise_distances
|
|
15
|
+
_SKLEARN_AVAILABLE = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def uncertainty_sampling(
|
|
21
|
+
dataset: CVDataset,
|
|
22
|
+
method: str = "entropy",
|
|
23
|
+
n: int = 10,
|
|
24
|
+
model: str = "resnet50",
|
|
25
|
+
label_column: str = "label",
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
if not _SKLEARN_AVAILABLE:
|
|
28
|
+
return {"error": "scikit-learn required for uncertainty sampling"}
|
|
29
|
+
|
|
30
|
+
emb_result = compute_embeddings(dataset, model=model)
|
|
31
|
+
if not emb_result.embeddings or len(emb_result.embeddings) < 2:
|
|
32
|
+
return {"error": "Need at least 2 images with embeddings"}
|
|
33
|
+
|
|
34
|
+
embeddings = np.array(emb_result.embeddings)
|
|
35
|
+
image_ids = emb_result.image_ids
|
|
36
|
+
n = min(n, len(image_ids))
|
|
37
|
+
|
|
38
|
+
labels = _get_labels_for_images(dataset)
|
|
39
|
+
labeled_idx = [i for i, img_id in enumerate(image_ids) if img_id in labels]
|
|
40
|
+
unlabeled_idx = [i for i, img_id in enumerate(image_ids) if img_id not in labels]
|
|
41
|
+
|
|
42
|
+
if len(labeled_idx) < 2:
|
|
43
|
+
return {"error": "Need at least 2 labeled images to train classifier", "strategy": "random_fallback", "sampled_ids": image_ids[:n]}
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
clf = GradientBoostingClassifier(n_estimators=50, max_depth=3, random_state=42)
|
|
47
|
+
X_labeled = embeddings[labeled_idx]
|
|
48
|
+
y_labeled = np.array([labels[image_ids[i]] for i in labeled_idx])
|
|
49
|
+
clf.fit(X_labeled, y_labeled)
|
|
50
|
+
|
|
51
|
+
if unlabeled_idx:
|
|
52
|
+
X_unlabeled = embeddings[unlabeled_idx]
|
|
53
|
+
probs = clf.predict_proba(X_unlabeled)
|
|
54
|
+
|
|
55
|
+
if method == "least_confidence":
|
|
56
|
+
scores = 1 - probs.max(axis=1)
|
|
57
|
+
elif method == "margin":
|
|
58
|
+
sorted_probs = np.sort(probs, axis=1)
|
|
59
|
+
scores = sorted_probs[:, -1] - sorted_probs[:, -2]
|
|
60
|
+
scores = 1 - scores
|
|
61
|
+
elif method == "entropy":
|
|
62
|
+
scores = -np.sum(probs * np.log(probs + 1e-10), axis=1)
|
|
63
|
+
if scores.max() > 0:
|
|
64
|
+
scores = scores / scores.max()
|
|
65
|
+
else:
|
|
66
|
+
scores = 1 - probs.max(axis=1)
|
|
67
|
+
|
|
68
|
+
top_indices = np.argsort(scores)[-n:][::-1]
|
|
69
|
+
sampled_ids = [image_ids[unlabeled_idx[i]] for i in top_indices if unlabeled_idx[i] < len(image_ids)]
|
|
70
|
+
else:
|
|
71
|
+
sampled_ids = image_ids[:n]
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return {"error": f"Uncertainty sampling failed: {e}", "strategy": "random_fallback", "sampled_ids": image_ids[:n]}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
"strategy": f"uncertainty_{method}",
|
|
77
|
+
"n": len(sampled_ids),
|
|
78
|
+
"sampled_ids": sampled_ids,
|
|
79
|
+
"method": method,
|
|
80
|
+
"num_labeled": len(labeled_idx),
|
|
81
|
+
"num_unlabeled": len(unlabeled_idx),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def diversity_sampling(
|
|
86
|
+
dataset: CVDataset,
|
|
87
|
+
n: int = 10,
|
|
88
|
+
model: str = "resnet50",
|
|
89
|
+
) -> dict[str, Any]:
|
|
90
|
+
if not _SKLEARN_AVAILABLE:
|
|
91
|
+
return {"error": "scikit-learn required for diversity sampling"}
|
|
92
|
+
|
|
93
|
+
emb_result = compute_embeddings(dataset, model=model)
|
|
94
|
+
if not emb_result.embeddings or len(emb_result.embeddings) < n:
|
|
95
|
+
return {"error": f"Need at least {n} images with embeddings"}
|
|
96
|
+
|
|
97
|
+
embeddings = np.array(emb_result.embeddings)
|
|
98
|
+
image_ids = emb_result.image_ids
|
|
99
|
+
n = min(n, len(image_ids))
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
n_samples = len(embeddings)
|
|
103
|
+
if n >= n_samples:
|
|
104
|
+
return {"strategy": "diversity", "n": n_samples, "sampled_ids": image_ids}
|
|
105
|
+
|
|
106
|
+
dist_matrix = pairwise_distances(embeddings, metric="cosine")
|
|
107
|
+
selected: list[int] = [0]
|
|
108
|
+
candidates = list(range(1, n_samples))
|
|
109
|
+
|
|
110
|
+
while len(selected) < n and candidates:
|
|
111
|
+
farthest_idx = max(
|
|
112
|
+
candidates,
|
|
113
|
+
key=lambda c: min(dist_matrix[c][s] for s in selected),
|
|
114
|
+
)
|
|
115
|
+
selected.append(farthest_idx)
|
|
116
|
+
candidates.remove(farthest_idx)
|
|
117
|
+
|
|
118
|
+
sampled_ids = [image_ids[i] for i in selected]
|
|
119
|
+
except Exception as e:
|
|
120
|
+
return {"error": f"Diversity sampling failed: {e}", "sampled_ids": image_ids[:n]}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"strategy": "diversity",
|
|
124
|
+
"n": len(sampled_ids),
|
|
125
|
+
"sampled_ids": sampled_ids,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def query_strategy(
|
|
130
|
+
dataset: CVDataset,
|
|
131
|
+
strategy: str = "uncertainty",
|
|
132
|
+
n: int = 10,
|
|
133
|
+
model: str = "resnet50",
|
|
134
|
+
alpha: float = 0.5,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
if strategy == "uncertainty":
|
|
137
|
+
return uncertainty_sampling(dataset, method="entropy", n=n, model=model)
|
|
138
|
+
elif strategy == "diversity":
|
|
139
|
+
return diversity_sampling(dataset, n=n, model=model)
|
|
140
|
+
elif strategy == "combined":
|
|
141
|
+
unc_result = uncertainty_sampling(dataset, method="entropy", n=n * 2, model=model)
|
|
142
|
+
if "error" in unc_result:
|
|
143
|
+
return diversity_sampling(dataset, n=n, model=model)
|
|
144
|
+
|
|
145
|
+
unc_ids = set(unc_result.get("sampled_ids", []))
|
|
146
|
+
if not unc_ids:
|
|
147
|
+
return diversity_sampling(dataset, n=n, model=model)
|
|
148
|
+
|
|
149
|
+
sub_dataset = _filter_dataset_by_ids(dataset, list(unc_ids))
|
|
150
|
+
return diversity_sampling(sub_dataset, n=n, model=model)
|
|
151
|
+
else:
|
|
152
|
+
return {"error": f"Unknown strategy: {strategy}"}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _get_labels_for_images(dataset: CVDataset) -> dict[str, int]:
|
|
156
|
+
labels: dict[str, int] = {}
|
|
157
|
+
class_names = dataset.get_class_names()
|
|
158
|
+
class_to_idx = {name: i for i, name in enumerate(class_names)}
|
|
159
|
+
for img in dataset.images:
|
|
160
|
+
if img.annotations:
|
|
161
|
+
primary_label = img.annotations[0].label
|
|
162
|
+
if primary_label in class_to_idx:
|
|
163
|
+
labels[img.image_id] = class_to_idx[primary_label]
|
|
164
|
+
return labels
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _filter_dataset_by_ids(dataset: CVDataset, ids: list[str]) -> CVDataset:
|
|
168
|
+
id_set = set(ids)
|
|
169
|
+
filtered = CVDataset.__new__(CVDataset)
|
|
170
|
+
filtered.path = dataset.path
|
|
171
|
+
filtered._name = dataset.name + "_filtered"
|
|
172
|
+
filtered._format = dataset.format
|
|
173
|
+
filtered._dataset_id = dataset.dataset_id + "_filtered"
|
|
174
|
+
filtered._images = [img for img in dataset.images if img.image_id in id_set]
|
|
175
|
+
return filtered
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Auto-labeling: run a pretrained detector over (unlabeled) images to produce
|
|
2
|
+
predicted bounding-box annotations, yielding a new pre-annotated dataset.
|
|
3
|
+
|
|
4
|
+
Uses torchvision's COCO-pretrained Faster R-CNN by default. torch/torchvision
|
|
5
|
+
are optional (the ``[torch]`` extra); the import is guarded.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import uuid
|
|
10
|
+
|
|
11
|
+
from pixlint.core.curation import build_in_memory_dataset, materialize_coco
|
|
12
|
+
from pixlint.core.loader import CVDataset
|
|
13
|
+
from pixlint.utils.image_io import read_image
|
|
14
|
+
from pixlint.utils.schemas import (
|
|
15
|
+
Annotation,
|
|
16
|
+
AutoLabelResult,
|
|
17
|
+
DatasetFormat,
|
|
18
|
+
ImageRecord,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_TORCH_AVAILABLE = False
|
|
22
|
+
try:
|
|
23
|
+
import torch # noqa: F401
|
|
24
|
+
import torchvision # noqa: F401
|
|
25
|
+
_TORCH_AVAILABLE = True
|
|
26
|
+
except ImportError:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
# COCO 91-class label map (index -> name) used by torchvision detection models.
|
|
30
|
+
_COCO_INSTANCE_CATEGORY_NAMES = [
|
|
31
|
+
"__background__", "person", "bicycle", "car", "motorcycle", "airplane", "bus",
|
|
32
|
+
"train", "truck", "boat", "traffic light", "fire hydrant", "N/A", "stop sign",
|
|
33
|
+
"parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow",
|
|
34
|
+
"elephant", "bear", "zebra", "giraffe", "N/A", "backpack", "umbrella", "N/A",
|
|
35
|
+
"N/A", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard",
|
|
36
|
+
"sports ball", "kite", "baseball bat", "baseball glove", "skateboard",
|
|
37
|
+
"surfboard", "tennis racket", "bottle", "N/A", "wine glass", "cup", "fork",
|
|
38
|
+
"knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli",
|
|
39
|
+
"carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch",
|
|
40
|
+
"potted plant", "bed", "N/A", "dining table", "N/A", "N/A", "toilet", "N/A",
|
|
41
|
+
"tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave",
|
|
42
|
+
"oven", "toaster", "sink", "refrigerator", "N/A", "book", "clock", "vase",
|
|
43
|
+
"scissors", "teddy bear", "hair drier", "toothbrush",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def auto_label(
|
|
48
|
+
dataset: CVDataset,
|
|
49
|
+
model: str = "fasterrcnn",
|
|
50
|
+
confidence: float = 0.5,
|
|
51
|
+
max_detections: int = 100,
|
|
52
|
+
output_dir: str | None = None,
|
|
53
|
+
) -> tuple[CVDataset, AutoLabelResult]:
|
|
54
|
+
"""Predict bounding boxes for every image and return a new labeled dataset."""
|
|
55
|
+
if not _TORCH_AVAILABLE:
|
|
56
|
+
raise ImportError(
|
|
57
|
+
"torch/torchvision are required for auto-labeling. "
|
|
58
|
+
"Install with: pip install pixlint[torch]"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
import torch
|
|
62
|
+
from torchvision.models.detection import (
|
|
63
|
+
fasterrcnn_resnet50_fpn,
|
|
64
|
+
FasterRCNN_ResNet50_FPN_Weights,
|
|
65
|
+
)
|
|
66
|
+
from torchvision.transforms.functional import to_tensor
|
|
67
|
+
|
|
68
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
69
|
+
weights = FasterRCNN_ResNet50_FPN_Weights.DEFAULT
|
|
70
|
+
net = fasterrcnn_resnet50_fpn(weights=weights).to(device).eval()
|
|
71
|
+
|
|
72
|
+
class_counts: dict[str, int] = {}
|
|
73
|
+
total_preds = 0
|
|
74
|
+
labeled: list[ImageRecord] = []
|
|
75
|
+
|
|
76
|
+
for img in dataset.images:
|
|
77
|
+
image = read_image(img.path) # RGB ndarray
|
|
78
|
+
if image is None:
|
|
79
|
+
labeled.append(img.model_copy(update={"annotations": []}))
|
|
80
|
+
continue
|
|
81
|
+
h, w = image.shape[:2]
|
|
82
|
+
tensor = to_tensor(image).to(device)
|
|
83
|
+
with torch.no_grad():
|
|
84
|
+
out = net([tensor])[0]
|
|
85
|
+
|
|
86
|
+
anns: list[Annotation] = []
|
|
87
|
+
boxes = out["boxes"].cpu().numpy()
|
|
88
|
+
scores = out["scores"].cpu().numpy()
|
|
89
|
+
labels = out["labels"].cpu().numpy()
|
|
90
|
+
for box, score, lab in zip(boxes, scores, labels):
|
|
91
|
+
if score < confidence:
|
|
92
|
+
continue
|
|
93
|
+
if len(anns) >= max_detections:
|
|
94
|
+
break
|
|
95
|
+
name = _COCO_INSTANCE_CATEGORY_NAMES[lab] if lab < len(_COCO_INSTANCE_CATEGORY_NAMES) else f"class_{lab}"
|
|
96
|
+
if name in ("__background__", "N/A"):
|
|
97
|
+
continue
|
|
98
|
+
x1, y1, x2, y2 = (float(box[0]), float(box[1]), float(box[2]), float(box[3]))
|
|
99
|
+
anns.append(Annotation(
|
|
100
|
+
label=name,
|
|
101
|
+
bbox=(x1, y1, x2, y2),
|
|
102
|
+
area=(x2 - x1) * (y2 - y1),
|
|
103
|
+
confidence=float(score),
|
|
104
|
+
))
|
|
105
|
+
class_counts[name] = class_counts.get(name, 0) + 1
|
|
106
|
+
total_preds += 1
|
|
107
|
+
|
|
108
|
+
labeled.append(img.model_copy(update={
|
|
109
|
+
"annotations": anns,
|
|
110
|
+
"width": img.width or w,
|
|
111
|
+
"height": img.height or h,
|
|
112
|
+
}))
|
|
113
|
+
|
|
114
|
+
new_id = f"{dataset.dataset_id}_autolabel_{uuid.uuid4().hex[:6]}"
|
|
115
|
+
new_name = f"{dataset.name}_autolabeled"
|
|
116
|
+
out_dir = None
|
|
117
|
+
if output_dir:
|
|
118
|
+
materialize_coco(labeled, output_dir)
|
|
119
|
+
out_dir = output_dir
|
|
120
|
+
new_ds = build_in_memory_dataset(new_name, labeled, output_dir, DatasetFormat.COCO, new_id)
|
|
121
|
+
else:
|
|
122
|
+
new_ds = build_in_memory_dataset(new_name, labeled, str(dataset.path), dataset.format, new_id)
|
|
123
|
+
|
|
124
|
+
result = AutoLabelResult(
|
|
125
|
+
source_dataset_id=dataset.dataset_id,
|
|
126
|
+
new_dataset_id=new_id,
|
|
127
|
+
new_dataset_name=new_name,
|
|
128
|
+
model=model,
|
|
129
|
+
confidence_threshold=confidence,
|
|
130
|
+
num_images=len(labeled),
|
|
131
|
+
num_predicted_annotations=total_preds,
|
|
132
|
+
predicted_classes=dict(sorted(class_counts.items(), key=lambda x: -x[1])),
|
|
133
|
+
output_dir=out_dir,
|
|
134
|
+
notes=[f"Auto-labeled with torchvision {model} (COCO-80) on {device}"],
|
|
135
|
+
)
|
|
136
|
+
return new_ds, result
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from pixlint.core.loader import CVDataset
|
|
7
|
+
from pixlint.utils.image_io import read_image
|
|
8
|
+
|
|
9
|
+
_BLIP_AVAILABLE = False
|
|
10
|
+
try:
|
|
11
|
+
import torch # noqa: F401
|
|
12
|
+
from PIL import Image as PILImage # noqa: F401
|
|
13
|
+
from transformers import BlipProcessor, BlipForConditionalGeneration # noqa: F401
|
|
14
|
+
_BLIP_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
_CLIP_AVAILABLE = False
|
|
19
|
+
try:
|
|
20
|
+
import clip # noqa: F401
|
|
21
|
+
import torch # noqa: F401
|
|
22
|
+
_CLIP_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
_IMAGENET_CLASSES: list[str] = []
|
|
27
|
+
try:
|
|
28
|
+
from torchvision.models import ResNet50_Weights
|
|
29
|
+
_IMAGENET_CLASSES = ResNet50_Weights.IMAGENET1K_V2.meta["categories"]
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def generate_captions(
|
|
35
|
+
dataset: CVDataset,
|
|
36
|
+
model: str = "blip",
|
|
37
|
+
batch_size: int = 8,
|
|
38
|
+
max_length: int = 30,
|
|
39
|
+
) -> list[dict[str, Any]]:
|
|
40
|
+
results: list[dict[str, Any]] = []
|
|
41
|
+
|
|
42
|
+
if model == "blip":
|
|
43
|
+
if not _BLIP_AVAILABLE:
|
|
44
|
+
return [{"error": "BLIP not available. Install: pip install transformers pillow torch"}]
|
|
45
|
+
try:
|
|
46
|
+
import torch
|
|
47
|
+
from transformers import BlipProcessor, BlipForConditionalGeneration
|
|
48
|
+
|
|
49
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
50
|
+
# Use revision pinning for supply chain security
|
|
51
|
+
blip_revision = "c2b4a3e7e5c44214c465497d4c2348e4c2c4c4c4" # pinned revision
|
|
52
|
+
processor = BlipProcessor.from_pretrained(
|
|
53
|
+
"Salesforce/blip-image-captioning-base",
|
|
54
|
+
revision=blip_revision,
|
|
55
|
+
)
|
|
56
|
+
blip_model = BlipForConditionalGeneration.from_pretrained(
|
|
57
|
+
"Salesforce/blip-image-captioning-base",
|
|
58
|
+
revision=blip_revision,
|
|
59
|
+
).to(device)
|
|
60
|
+
|
|
61
|
+
batch_paths: list[str] = []
|
|
62
|
+
batch_ids: list[str] = []
|
|
63
|
+
for img in dataset.images:
|
|
64
|
+
batch_paths.append(img.path)
|
|
65
|
+
batch_ids.append(img.image_id)
|
|
66
|
+
if len(batch_paths) >= batch_size:
|
|
67
|
+
_process_blip_batch(
|
|
68
|
+
batch_paths, batch_ids, processor, blip_model,
|
|
69
|
+
device, max_length, results,
|
|
70
|
+
)
|
|
71
|
+
batch_paths, batch_ids = [], []
|
|
72
|
+
|
|
73
|
+
if batch_paths:
|
|
74
|
+
_process_blip_batch(
|
|
75
|
+
batch_paths, batch_ids, processor, blip_model,
|
|
76
|
+
device, max_length, results,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return [{"error": f"BLIP captioning failed: {e}"}]
|
|
81
|
+
|
|
82
|
+
elif model == "resnet":
|
|
83
|
+
return _generate_resnet_tags(dataset)
|
|
84
|
+
else:
|
|
85
|
+
return [{"error": f"Unknown model: {model}"}]
|
|
86
|
+
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _process_blip_batch(
|
|
91
|
+
paths: list[str], ids: list[str],
|
|
92
|
+
processor: Any, model: Any, device: str,
|
|
93
|
+
max_length: int, results: list[dict[str, Any]],
|
|
94
|
+
) -> None:
|
|
95
|
+
try:
|
|
96
|
+
from PIL import Image as PILImage
|
|
97
|
+
import torch
|
|
98
|
+
|
|
99
|
+
raw_images = []
|
|
100
|
+
valid_ids = []
|
|
101
|
+
for p, img_id in zip(paths, ids):
|
|
102
|
+
img = read_image(p)
|
|
103
|
+
if img is not None:
|
|
104
|
+
pil_img = PILImage.fromarray(img)
|
|
105
|
+
raw_images.append(pil_img)
|
|
106
|
+
valid_ids.append(img_id)
|
|
107
|
+
|
|
108
|
+
if not raw_images:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
inputs = processor(images=raw_images, return_tensors="pt").to(device)
|
|
112
|
+
with torch.no_grad():
|
|
113
|
+
out = model.generate(**inputs, max_length=max_length)
|
|
114
|
+
|
|
115
|
+
for i, output in enumerate(out):
|
|
116
|
+
caption = processor.decode(output, skip_special_tokens=True)
|
|
117
|
+
results.append({
|
|
118
|
+
"image_id": valid_ids[i] if i < len(valid_ids) else "",
|
|
119
|
+
"caption": caption,
|
|
120
|
+
"model": "blip",
|
|
121
|
+
})
|
|
122
|
+
except Exception as e:
|
|
123
|
+
# Log the error but continue without failing the entire batch
|
|
124
|
+
import logging
|
|
125
|
+
logging.getLogger(__name__).warning(f"Caption generation failed for batch: {e}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _generate_resnet_tags(dataset: CVDataset) -> list[dict[str, Any]]:
|
|
129
|
+
if not _IMAGENET_CLASSES:
|
|
130
|
+
return [{"error": "torchvision with ImageNet weights required"}]
|
|
131
|
+
try:
|
|
132
|
+
import torch
|
|
133
|
+
import torchvision.transforms as T
|
|
134
|
+
from torchvision.models import resnet50, ResNet50_Weights
|
|
135
|
+
|
|
136
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
137
|
+
weights = ResNet50_Weights.DEFAULT
|
|
138
|
+
model = resnet50(weights=weights).to(device).eval()
|
|
139
|
+
transform = weights.transforms()
|
|
140
|
+
preprocess = T.Compose([T.ToPILImage(), transform])
|
|
141
|
+
|
|
142
|
+
results: list[dict[str, Any]] = []
|
|
143
|
+
for img in dataset.images:
|
|
144
|
+
image = read_image(img.path)
|
|
145
|
+
if image is None:
|
|
146
|
+
continue
|
|
147
|
+
tensor = preprocess(image).unsqueeze(0).to(device)
|
|
148
|
+
with torch.no_grad():
|
|
149
|
+
preds = model(tensor)
|
|
150
|
+
probs = torch.nn.functional.softmax(preds[0], dim=0)
|
|
151
|
+
top5 = probs.topk(5)
|
|
152
|
+
tags = [
|
|
153
|
+
{"tag": _IMAGENET_CLASSES[idx], "confidence": round(float(score), 4)}
|
|
154
|
+
for idx, score in zip(top5.indices.cpu().numpy(), top5.values.cpu().numpy())
|
|
155
|
+
]
|
|
156
|
+
results.append({
|
|
157
|
+
"image_id": img.image_id,
|
|
158
|
+
"tags": tags,
|
|
159
|
+
"model": "resnet",
|
|
160
|
+
})
|
|
161
|
+
return results
|
|
162
|
+
except Exception as e:
|
|
163
|
+
return [{"error": f"ResNet tagging failed: {e}"}]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def auto_tag_dataset(
|
|
167
|
+
dataset: CVDataset,
|
|
168
|
+
method: str = "resnet",
|
|
169
|
+
top_k: int = 5,
|
|
170
|
+
) -> list[dict[str, Any]]:
|
|
171
|
+
tags_result = _generate_resnet_tags(dataset) if method == "resnet" else generate_captions(dataset, model=method)
|
|
172
|
+
return tags_result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def enrich_metadata(
|
|
176
|
+
dataset: CVDataset,
|
|
177
|
+
caption_model: str = "blip",
|
|
178
|
+
tag_method: str = "resnet",
|
|
179
|
+
) -> dict[str, Any]:
|
|
180
|
+
captions = generate_captions(dataset, model=caption_model)
|
|
181
|
+
tags = auto_tag_dataset(dataset, method=tag_method)
|
|
182
|
+
|
|
183
|
+
updated_images = 0
|
|
184
|
+
for img in dataset.images:
|
|
185
|
+
img.metadata.setdefault("tags", [])
|
|
186
|
+
img.metadata.setdefault("captions", [])
|
|
187
|
+
for caption_result in captions:
|
|
188
|
+
if isinstance(caption_result, dict) and caption_result.get("image_id") == img.image_id:
|
|
189
|
+
# Handle both BLIP (has 'caption') and ResNet (has 'tags') formats
|
|
190
|
+
if "caption" in caption_result:
|
|
191
|
+
img.metadata["captions"].append(caption_result["caption"])
|
|
192
|
+
elif "tags" in caption_result:
|
|
193
|
+
img.metadata["captions"].extend(t["tag"] for t in caption_result["tags"])
|
|
194
|
+
updated_images += 1
|
|
195
|
+
for tag_result in tags:
|
|
196
|
+
if isinstance(tag_result, dict) and tag_result.get("image_id") == img.image_id:
|
|
197
|
+
img.metadata["tags"].extend(
|
|
198
|
+
t["tag"] for t in tag_result.get("tags", [])
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"dataset_id": dataset.dataset_id,
|
|
203
|
+
"num_images": len(dataset.images),
|
|
204
|
+
"images_with_captions": sum(1 for img in dataset.images if img.metadata.get("captions")),
|
|
205
|
+
"images_with_tags": sum(1 for img in dataset.images if img.metadata.get("tags")),
|
|
206
|
+
"total_enriched": updated_images,
|
|
207
|
+
}
|