Skip to content

Commit e97cfe2

Browse files
committed
Refactor Dataset and search_image
1 parent 8a6009a commit e97cfe2

6 files changed

Lines changed: 149 additions & 123 deletions

File tree

abraia/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from dotenv import load_dotenv
33
load_dotenv()
44

5-
__version__ = '0.27.0'
5+
__version__ = '0.27.1'
66

77
from . import config
88
from .client import Abraia, APIError

abraia/training/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from PIL import Image
66
from typing import Dict, Any
7+
from tqdm import tqdm
78
from tqdm.contrib.concurrent import process_map
89

910
from ..utils import save_text
@@ -80,6 +81,7 @@ def __init__(self, project: str, task: str, classes: list, imgsz: int = None):
8081
self.project = project
8182
self.task = task
8283
self.classes = classes
84+
self.pbar = None
8385
imgsz = imgsz or (224 if task == 'classify' else 640)
8486
if task == 'classify':
8587
from . import classify
@@ -88,9 +90,18 @@ def __init__(self, project: str, task: str, classes: list, imgsz: int = None):
8890
from . import detect
8991
self.model = detect.Model(task, imgsz=imgsz)
9092

93+
def _progress_callback(self, progress):
94+
if self.pbar is None:
95+
self.pbar = tqdm(total=progress['epochs'], initial=progress['epoch'])
96+
self.pbar.set_description(f"Loss: {progress['loss']:.4f} Acc: {progress['acc']:.4f}")
97+
self.pbar.update(1)
98+
9199
def train(self, epochs: int = None, batch: int = 32, callback=None) -> None:
92100
epochs = epochs or (30 if self.task == 'classify' else 300)
101+
callback = callback or self._progress_callback
93102
self.model.train(self.project, epochs=epochs, batch=batch, callback=callback)
103+
if self.pbar:
104+
self.pbar.close()
94105

95106
def test(self, split: str = 'val') -> Dict[str, Any]:
96107
return self.model.test(split=split)

abraia/training/dataset.py

Lines changed: 124 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import os
22
import re
33
import io
4+
import json
45
import urllib
56
import requests
67
import filetype
@@ -9,10 +10,12 @@
910
from tqdm import tqdm
1011
from PIL import Image
1112

13+
from abraia.inference.ops import mask_to_polygon
14+
1215
from ..client import Abraia
1316
from ..utils import HEADERS, load_image, load_url, list_dir, url_path
1417
from .ops import train_test_split
15-
from ..inference.detect import segment_objects
18+
from ..inference.sam import SAM
1619

1720

1821
abraia = Abraia()
@@ -28,7 +31,9 @@ def convert_to_jpg(src, save_output, max_size=1920):
2831
im = Image.open(src).convert('RGB')
2932
im.thumbnail([max_size, max_size], Image.LANCZOS)
3033
phash = str(imagehash.phash(im))
31-
im.save(os.path.join(save_output, phash + '.jpg'))
34+
filename = phash + '.jpg'
35+
im.save(os.path.join(save_output, filename))
36+
return filename
3237

3338

3439
def download_page(url):
@@ -37,12 +42,19 @@ def download_page(url):
3742
return resp.text
3843

3944

40-
def save_image_file(link, save_output, timeout=10, max_size=1920):
45+
def save_image_file(link, upload_folder, existing_filenames=None, timeout=10, max_size=1920):
4146
resp = requests.get(link, headers=HEADERS, allow_redirects=True, timeout=timeout)
4247
kind = filetype.guess(resp.content)
4348
if kind and kind.mime.startswith('image'):
4449
d = io.BytesIO(resp.content)
45-
convert_to_jpg(d, save_output, max_size)
50+
import tempfile
51+
with tempfile.TemporaryDirectory() as temp_dir:
52+
filename = convert_to_jpg(d, temp_dir, max_size)
53+
if existing_filenames is None or filename not in existing_filenames:
54+
local_path = os.path.join(temp_dir, filename)
55+
abraia.upload_file(local_path, upload_folder)
56+
return True, filename
57+
return False, filename
4658
else:
4759
raise ValueError(f'Invalid image, not saving')
4860

@@ -89,31 +101,45 @@ def search_images(query, limit=100, save_output='dataset', callback=None):
89101
"""Search and download images from Google and Bing."""
90102
seen = set()
91103
download_count = 0
92-
os.makedirs(save_output, exist_ok=True)
104+
try:
105+
files = abraia.list_files(f"{save_output}")[0]
106+
existing_filenames = {f['name'] for f in files}
107+
except:
108+
existing_filenames = set()
109+
93110
links = [search_google(query), search_bing(query)]
94111
ends = [False] * len(links)
112+
113+
pbar = tqdm(total=limit, desc="Downloading images") if callback is None else None
114+
95115
for id in itertools.cycle(range(len(links))):
96116
try:
97117
link = next(links[id])
98118
if link not in seen:
99119
seen.add(link)
100120
if download_count < limit:
101121
try:
102-
save_image_file(link, save_output)
103-
download_count += 1
104-
if callback:
105-
callback({'current': download_count, 'total': limit, 'link': link})
106-
else:
107-
print(f"[%] Downloaded Image #{download_count} from {link}")
122+
uploaded, filename = save_image_file(link, save_output, existing_filenames=existing_filenames)
123+
if uploaded:
124+
download_count += 1
125+
if callback:
126+
callback({'current': download_count, 'total': limit, 'filename': filename})
127+
elif pbar:
128+
pbar.set_description(f"Downloaded {filename}")
129+
pbar.update(1)
108130
except Exception:
109-
print(f"[!] Error Image #{download_count} from {link}")
131+
pass
110132
else:
111133
break
112134
except StopIteration:
113135
ends[id] = True
114136
if set(ends) == {True}:
115137
break
116-
return list_dir(save_output)
138+
139+
if pbar:
140+
pbar.close()
141+
142+
return abraia.list_files(f"{save_output}/")[0]
117143

118144

119145
def download_file(path, folder):
@@ -123,24 +149,6 @@ def download_file(path, folder):
123149
return dest
124150

125151

126-
def detect_dino(img, classes, threshold=0.3, pipe=None):
127-
"""Detect objects in an image using Grounding Dino."""
128-
classes = [label.lower().strip() for label in classes]
129-
labels = [f"{label}." if not label.endswith('.') else label for label in classes]
130-
if pipe is None:
131-
from transformers import pipeline
132-
pipe = pipeline(task="zero-shot-object-detection", model="IDEA-Research/grounding-dino-tiny")
133-
results = pipe(Image.fromarray(img), candidate_labels=labels, threshold=threshold)
134-
objects = []
135-
for result in results:
136-
score = result["score"]
137-
if score > threshold:
138-
label = result["label"].rpartition('.')[0]
139-
xmin, ymin, xmax, ymax = result['box'].values()
140-
objects.append({"label": label, "score": score, "box": [xmin, ymin, xmax - xmin, ymax - ymin]})
141-
return objects
142-
143-
144152
def list_datasets():
145153
folders = abraia.list_files()[1]
146154
return [folder['name'] for folder in folders if abraia.check_file(f"{folder['name']}/annotations.json")]
@@ -151,47 +159,43 @@ def list_models(project):
151159
return [f['name'] for f in files if f['name'].endswith('.onnx')]
152160

153161

154-
def load_annotations(project):
155-
annotations = abraia.load_json(f"{project}/annotations.json")
156-
for annotation in annotations:
157-
annotation['path'] = f"{project}/{annotation['filename']}"
158-
annotation['url'] = url_path(f"{abraia.userid}/{annotation['path']}")
159-
return annotations
160-
161-
162-
def load_labels(annotations):
163-
labels = []
164-
for annotation in annotations:
165-
for object in annotation.get('objects', []):
166-
label = object.get('label')
167-
if label and label not in labels:
168-
labels.append(label)
169-
return list(set(labels))
170-
171-
172-
def load_task(annotations):
173-
classify, detect, segment = False, False, False
174-
for annotation in annotations:
175-
for object in annotation.get('objects', []):
176-
if 'polygon' in object:
177-
segment = True
178-
elif 'box' in object:
179-
detect = True
180-
elif 'label' in object:
181-
classify = True
182-
return 'segment' if segment else 'detect' if detect else 'classify' if classify else ''
183-
184-
185-
def list_images(project):
186-
files = abraia.list_files(f"{project}/")[0]
187-
files = [f for f in files if f['type'] in ['image/jpeg', 'image/png']]
188-
for data in files:
189-
data['url'] = url_path(f"{abraia.userid}/{data['path']}")
190-
return files
191-
192-
193-
def save_annotations(project, annotations):
194-
abraia.save_json(f"{project}/annotations.json", annotations)
162+
class Annotator:
163+
def __init__(self, model="IDEA-Research/grounding-dino-tiny", segment=False):
164+
from transformers import pipeline
165+
self.pipe = pipeline(task="zero-shot-object-detection", model=model)
166+
self.segment_enabled = segment
167+
if self.segment_enabled:
168+
self.sam = SAM()
169+
170+
def detect(self, img, classes, threshold=0.3):
171+
classes = [label.lower().strip() for label in classes]
172+
labels = [f"{label}." if not label.endswith('.') else label for label in classes]
173+
results = self.pipe(Image.fromarray(img), candidate_labels=labels, threshold=threshold)
174+
objects = []
175+
for result in results:
176+
score = result["score"]
177+
if score > threshold:
178+
label = result["label"].rpartition('.')[0]
179+
xmin, ymin, xmax, ymax = result['box'].values()
180+
objects.append({"label": label, "score": score, "box": [xmin, ymin, xmax - xmin, ymax - ymin]})
181+
return objects
182+
183+
def segment(self, img, objects):
184+
self.sam.encode(img)
185+
for result in objects:
186+
x, y, w, h = result['box']
187+
mask = self.sam.predict(img, prompt=json.dumps([{"type": "rectangle", "data": [x, y, x+w, y+h]}]))
188+
result['polygon'] = mask_to_polygon(mask[y:y+h, x:x+w], (x, y))
189+
return objects
190+
191+
def annotate(self, img, label, threshold=0.3):
192+
objects = self.detect(img, [label], threshold=threshold)
193+
if objects and self.segment_enabled:
194+
try:
195+
objects = self.segment(img, objects)
196+
except:
197+
return None
198+
return objects
195199

196200

197201
class Dataset:
@@ -204,37 +208,64 @@ def __init__(self, project):
204208

205209
def load(self):
206210
if self.project in list_datasets():
207-
self.annotations = load_annotations(self.project)
208-
self.classes = load_labels(self.annotations)
209-
self.task = load_task(self.annotations)
210-
self.images = list_images(self.project)
211+
self.annotations = self._load_annotations(self.project)
212+
self.classes, self.task = self._process_annotations(self.annotations)
213+
self.images = self._list_images(self.project)
211214
return self
212215

216+
def _load_annotations(self, project):
217+
annotations = abraia.load_json(f"{project}/annotations.json")
218+
for annotation in annotations:
219+
annotation['path'] = f"{project}/{annotation['filename']}"
220+
annotation['url'] = url_path(f"{abraia.userid}/{annotation['path']}")
221+
return annotations
222+
223+
def _process_annotations(self, annotations):
224+
labels = set()
225+
classify, detect, segment = False, False, False
226+
for annotation in annotations:
227+
for obj in annotation.get('objects', []):
228+
label = obj.get('label')
229+
if label:
230+
labels.add(label)
231+
classify = True
232+
if 'polygon' in obj:
233+
segment = True
234+
elif 'box' in obj:
235+
detect = True
236+
return list(labels), 'segment' if segment else 'detect' if detect else 'classify' if classify else ''
237+
238+
def _list_images(self, project):
239+
files = abraia.list_files(f"{project}/")[0]
240+
files = [f for f in files if f['type'] in ['image/jpeg', 'image/png']]
241+
for data in files:
242+
data['url'] = url_path(f"{abraia.userid}/{data['path']}")
243+
return files
244+
213245
def annotate(self, label, segment=False, callback=None):
214-
from transformers import pipeline
215246
annotated_filenames = {a['filename'] for a in self.annotations}
216247
images = [img for img in self.images if img['name'] not in annotated_filenames]
217-
pipe = pipeline(task="zero-shot-object-detection", model="IDEA-Research/grounding-dino-tiny")
218-
for i, row in enumerate(tqdm(images)):
248+
annotator = Annotator(segment=segment)
249+
250+
pbar = tqdm(images) if callback is None else None
251+
iterable = pbar if pbar else images
252+
for i, row in enumerate(iterable):
253+
if pbar:
254+
pbar.set_description(f"Annotating {row['name']}")
219255
url, filename = row['url'], row['name']
220256
img = load_image(load_url(url))
221-
objects = detect_dino(img, [label], pipe=pipe)
257+
objects = annotator.annotate(img, label)
258+
annotation = {'url': url, 'filename': filename, 'objects': objects}
259+
self.annotations.append(annotation)
260+
self.save()
222261
if callback:
223-
callback({'current': i + 1, 'total': len(images)})
224-
if objects:
225-
if segment:
226-
try:
227-
objects = segment_objects(img, objects)
228-
except:
229-
continue
230-
annotation = {'url': url, 'filename': filename, 'objects': objects}
231-
self.annotations.append(annotation)
262+
callback({'current': i + 1, 'total': len(images), 'filename': filename})
263+
if pbar:
264+
pbar.close()
232265
return self.annotations
233266

234-
def save(self, annotations=None):
235-
if annotations is not None:
236-
self.annotations = annotations
237-
save_annotations(self.project, self.annotations)
267+
def save(self):
268+
abraia.save_json(f"{self.project}/annotations.json", self.annotations)
238269

239270
def split(self):
240271
# TODO: Split dataset by classes to avoid class imbalance

abraia/training/detect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import contextlib
77
import numpy as np
88

9-
# os.environ['YOLO_VERBOSE'] = 'False'
9+
os.environ['YOLO_VERBOSE'] = 'False'
1010

1111
from ultralytics import YOLO
1212

0 commit comments

Comments
 (0)