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.
Files changed (66) hide show
  1. pixlint/__init__.py +132 -0
  2. pixlint/analysis/__init__.py +0 -0
  3. pixlint/analysis/active_learning.py +175 -0
  4. pixlint/analysis/autolabel.py +136 -0
  5. pixlint/analysis/captioning.py +207 -0
  6. pixlint/analysis/diff.py +79 -0
  7. pixlint/analysis/distribution.py +198 -0
  8. pixlint/analysis/duplicates.py +257 -0
  9. pixlint/analysis/embeddings.py +288 -0
  10. pixlint/analysis/health.py +144 -0
  11. pixlint/analysis/integrity.py +85 -0
  12. pixlint/analysis/label_errors.py +135 -0
  13. pixlint/analysis/outliers.py +171 -0
  14. pixlint/analysis/quality.py +242 -0
  15. pixlint/analysis/query.py +185 -0
  16. pixlint/analysis/readiness.py +164 -0
  17. pixlint/analysis/slices.py +157 -0
  18. pixlint/analysis/statistics.py +89 -0
  19. pixlint/analysis/video.py +169 -0
  20. pixlint/augmentation/__init__.py +0 -0
  21. pixlint/augmentation/auto_augment.py +108 -0
  22. pixlint/augmentation/pipeline.py +194 -0
  23. pixlint/augmentation/transforms.py +196 -0
  24. pixlint/core/__init__.py +0 -0
  25. pixlint/core/cache.py +67 -0
  26. pixlint/core/cloud.py +161 -0
  27. pixlint/core/curation.py +321 -0
  28. pixlint/core/index.py +177 -0
  29. pixlint/core/loader.py +339 -0
  30. pixlint/core/merge.py +163 -0
  31. pixlint/core/metadata.py +37 -0
  32. pixlint/core/pipeline.py +361 -0
  33. pixlint/dashboard.py +293 -0
  34. pixlint/export/__init__.py +0 -0
  35. pixlint/export/extra_formats.py +322 -0
  36. pixlint/export/hdf5.py +133 -0
  37. pixlint/export/huggingface.py +105 -0
  38. pixlint/export/pytorch.py +197 -0
  39. pixlint/export/tensorflow.py +190 -0
  40. pixlint/export/ultralytics.py +119 -0
  41. pixlint/server.py +1883 -0
  42. pixlint/splitting/__init__.py +0 -0
  43. pixlint/splitting/cross_validation.py +81 -0
  44. pixlint/splitting/leakage.py +66 -0
  45. pixlint/splitting/splitter.py +157 -0
  46. pixlint/transformation/__init__.py +0 -0
  47. pixlint/transformation/format_converter.py +354 -0
  48. pixlint/transformation/normalize.py +111 -0
  49. pixlint/transformation/resize.py +126 -0
  50. pixlint/utils/__init__.py +0 -0
  51. pixlint/utils/hashing.py +74 -0
  52. pixlint/utils/image_io.py +53 -0
  53. pixlint/utils/parallel.py +24 -0
  54. pixlint/utils/progress.py +74 -0
  55. pixlint/utils/schemas.py +446 -0
  56. pixlint/utils/security.py +871 -0
  57. pixlint/visualization/__init__.py +0 -0
  58. pixlint/visualization/charts.py +284 -0
  59. pixlint/visualization/embeddings_viz.py +166 -0
  60. pixlint/visualization/previews.py +164 -0
  61. pixlint-1.0.dist-info/METADATA +199 -0
  62. pixlint-1.0.dist-info/RECORD +66 -0
  63. pixlint-1.0.dist-info/WHEEL +5 -0
  64. pixlint-1.0.dist-info/entry_points.txt +2 -0
  65. pixlint-1.0.dist-info/licenses/LICENSE +116 -0
  66. 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
+ }