anatools 5.1.28__py3-none-any.whl → 6.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.
- anatools/__init__.py +1 -1
- anatools/anaclient/anaclient.py +16 -15
- anatools/anaclient/api/api.py +7 -2
- anatools/anaclient/api/datasets.py +21 -3
- anatools/anaclient/api/handlers.py +1 -1
- anatools/anaclient/channels.py +50 -25
- anatools/anaclient/datasets.py +94 -7
- anatools/anaclient/helpers.py +11 -10
- anatools/anaclient/services.py +46 -20
- anatools/anaclient/volumes.py +19 -18
- anatools/annotations/annotations.py +39 -18
- anatools/annotations/draw.py +34 -18
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anadeploy +4 -0
- anatools-6.0.1.data/scripts/renderedai +4166 -0
- {anatools-5.1.28.dist-info → anatools-6.0.1.dist-info}/METADATA +1 -1
- {anatools-5.1.28.dist-info → anatools-6.0.1.dist-info}/RECORD +27 -26
- {anatools-5.1.28.dist-info → anatools-6.0.1.dist-info}/WHEEL +1 -1
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/ana +0 -0
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anamount +0 -0
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anaprofile +0 -0
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anarules +0 -0
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anaserver +0 -0
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anatransfer +0 -0
- {anatools-5.1.28.data → anatools-6.0.1.data}/scripts/anautils +0 -0
- {anatools-5.1.28.dist-info → anatools-6.0.1.dist-info}/entry_points.txt +0 -0
- {anatools-5.1.28.dist-info → anatools-6.0.1.dist-info}/licenses/LICENSE +0 -0
- {anatools-5.1.28.dist-info → anatools-6.0.1.dist-info}/top_level.txt +0 -0
anatools/anaclient/services.py
CHANGED
|
@@ -174,15 +174,40 @@ def build_service(self, servicefile):
|
|
|
174
174
|
disk_usage = shutil.disk_usage('/')
|
|
175
175
|
gb_free = disk_usage.free / (1024**3) # Convert bytes to GB
|
|
176
176
|
if self.verbose == 'debug': print(f"Disk space left: {gb_free:.3f}GB")
|
|
177
|
-
if gb_free < 20: print_color(f'\nWarning: Low disk space detected! Only {gb_free:.1f}GB available.', 'ffff00')
|
|
177
|
+
if gb_free < 20 and self.interactive: print_color(f'\nWarning: Low disk space detected! Only {gb_free:.1f}GB available.', 'ffff00')
|
|
178
178
|
docker_space_cmd = "docker system df --format json"
|
|
179
179
|
result = subprocess.run(docker_space_cmd, shell=True, capture_output=True, text=True)
|
|
180
180
|
if result.returncode == 0:
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
# Parse Docker output which may be NDJSON (newline-delimited) or concatenated JSON objects
|
|
182
|
+
# Use JSONDecoder.raw_decode() to handle both formats robustly
|
|
183
|
+
import re
|
|
184
|
+
def parse_docker_json(data):
|
|
185
|
+
results = []
|
|
186
|
+
decoder = json.JSONDecoder()
|
|
187
|
+
idx = 0
|
|
188
|
+
data = data.strip()
|
|
189
|
+
while idx < len(data):
|
|
190
|
+
obj, end_idx = decoder.raw_decode(data, idx)
|
|
191
|
+
results.append(obj)
|
|
192
|
+
idx = end_idx
|
|
193
|
+
# Skip any whitespace between objects
|
|
194
|
+
while idx < len(data) and data[idx].isspace():
|
|
195
|
+
idx += 1
|
|
196
|
+
return results
|
|
197
|
+
docker_stats = parse_docker_json(result.stdout)
|
|
198
|
+
# Parse human-readable size strings (e.g., "85.19GB", "2.087MB", "0B") to GB
|
|
199
|
+
def parse_size_to_gb(size_str):
|
|
200
|
+
if not size_str or size_str == '0B':
|
|
201
|
+
return 0.0
|
|
202
|
+
match = re.match(r'^([\d.]+)([KMGT]?B)$', size_str)
|
|
203
|
+
if not match:
|
|
204
|
+
return 0.0
|
|
205
|
+
value, unit = float(match.group(1)), match.group(2)
|
|
206
|
+
multipliers = {'B': 1/(1024**3), 'KB': 1/(1024**2), 'MB': 1/1024, 'GB': 1, 'TB': 1024}
|
|
207
|
+
return value * multipliers.get(unit, 0)
|
|
208
|
+
docker_gb = sum(parse_size_to_gb(item.get('Size', '0B')) for item in docker_stats)
|
|
184
209
|
if self.verbose == 'debug': print(f"Docker space used: {docker_gb:.3f}GB")
|
|
185
|
-
if docker_gb > 20:
|
|
210
|
+
if docker_gb > 20 and self.interactive:
|
|
186
211
|
print_color(f'\nWarning: Docker is using {docker_gb:.1f}GB of disk space!', 'ffff00')
|
|
187
212
|
print_color('Consider running "docker system prune -a" to free up space.', 'ffff00')
|
|
188
213
|
print_color('This will remove:', 'ffff00')
|
|
@@ -191,10 +216,11 @@ def build_service(self, servicefile):
|
|
|
191
216
|
print_color(' - All dangling images', 'ffff00')
|
|
192
217
|
print_color(' - All dangling build cache', 'ffff00')
|
|
193
218
|
except Exception as e:
|
|
219
|
+
if self.verbose == 'debug': print(f"Docker space check failed: {e}")
|
|
194
220
|
pass
|
|
195
221
|
|
|
196
222
|
# check for service Dockerfile
|
|
197
|
-
print('Building Service Image...', end='', flush=True)
|
|
223
|
+
if self.interactive: print('Building Service Image...', end='', flush=True)
|
|
198
224
|
if not os.path.isfile(servicefile): raise Exception(f'No service file {servicefile} found.')
|
|
199
225
|
servicedir, servicefile = os.path.split(servicefile)
|
|
200
226
|
imgtime = int(time.time())
|
|
@@ -207,12 +233,12 @@ def build_service(self, servicefile):
|
|
|
207
233
|
try:
|
|
208
234
|
start = time.time()
|
|
209
235
|
streamer = dockerclient.build(
|
|
210
|
-
path=servicedir,
|
|
211
|
-
dockerfile=os.path.join(servicedir, '.devcontainer/Dockerfile'),
|
|
212
|
-
tag=image,
|
|
213
|
-
rm=True,
|
|
214
|
-
decode=True,
|
|
215
|
-
platform='linux/amd64',
|
|
236
|
+
path=servicedir,
|
|
237
|
+
dockerfile=os.path.join(servicedir, '.devcontainer/Dockerfile'),
|
|
238
|
+
tag=image,
|
|
239
|
+
rm=True,
|
|
240
|
+
decode=True,
|
|
241
|
+
platform='linux/amd64',
|
|
216
242
|
nocache=False,
|
|
217
243
|
pull=False)
|
|
218
244
|
logfilepath = os.path.join(tempfile.gettempdir(), 'dockerbuild.log')
|
|
@@ -225,7 +251,7 @@ def build_service(self, servicefile):
|
|
|
225
251
|
if 'error' in output: print_color(f'{output["error"]}', 'ff0000')
|
|
226
252
|
if 'stream' in output: logfile.write(output['stream'])
|
|
227
253
|
if 'error' in output: logfile.write(output["error"])
|
|
228
|
-
print(f'\rBuilding Service Image... [{time.time()-start:.3f}s]', end='', flush=True)
|
|
254
|
+
if self.interactive: print(f'\rBuilding Service Image... [{time.time()-start:.3f}s]', end='', flush=True)
|
|
229
255
|
except StopIteration:
|
|
230
256
|
time.sleep(1)
|
|
231
257
|
try:
|
|
@@ -238,7 +264,7 @@ def build_service(self, servicefile):
|
|
|
238
264
|
logfile.close()
|
|
239
265
|
raise Exception(f'Error encountered while building Docker image. Please check logfile {logfilepath}.')
|
|
240
266
|
if self.verbose != 'debug' and os.path.exists(logfilepath): os.remove(logfilepath)
|
|
241
|
-
print(f"\rBuilding Service Image...done. [{time.time()-start:.3f}s]", flush=True)
|
|
267
|
+
if self.interactive: print(f"\rBuilding Service Image...done. [{time.time()-start:.3f}s]", flush=True)
|
|
242
268
|
return image
|
|
243
269
|
|
|
244
270
|
|
|
@@ -296,13 +322,13 @@ def deploy_service(self, serviceId=None, servicefile=None):
|
|
|
296
322
|
progressDetail = line['progressDetail']
|
|
297
323
|
if progressDetail['total'] >= largest:
|
|
298
324
|
largest = progressDetail['total']
|
|
299
|
-
print(f"\rPushing Service Image... [{time.time()-start:.3f}s, {min(100,round((progressDetail['current']/progressDetail['total']) * 100))}%]", end='', flush=True)
|
|
300
|
-
if 'error' in line:
|
|
325
|
+
if self.interactive: print(f"\rPushing Service Image... [{time.time()-start:.3f}s, {min(100,round((progressDetail['current']/progressDetail['total']) * 100))}%]", end='', flush=True)
|
|
326
|
+
if 'error' in line:
|
|
301
327
|
if 'HTTP 403' in line['error']: raise Exception('You do not have permission to push to this repository.')
|
|
302
328
|
else: raise Exception(line['error'])
|
|
303
329
|
logfile.close()
|
|
304
330
|
if self.verbose != 'debug' and os.path.exists(logfilepath): os.remove(logfilepath)
|
|
305
|
-
print(f"\rPushing Service Image...done. [{time.time()-start:.3f}s] ", flush=True)
|
|
331
|
+
if self.interactive: print(f"\rPushing Service Image...done. [{time.time()-start:.3f}s] ", flush=True)
|
|
306
332
|
|
|
307
333
|
# cleanup docker and update services
|
|
308
334
|
dockerclient.images.remove(reponame)
|
|
@@ -331,13 +357,13 @@ def get_service_deployment(self, deploymentId, stream=False):
|
|
|
331
357
|
if deploymentId is None: raise Exception('DeploymentId must be specified.')
|
|
332
358
|
if stream:
|
|
333
359
|
data = self.ana_api.getServiceDeployment(deploymentId=deploymentId)
|
|
334
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
360
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
335
361
|
while (data['status']['state'] not in ['Service Deployment Complete','Service Deployment Failed']):
|
|
336
362
|
time.sleep(10)
|
|
337
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
363
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", end='', flush=True)
|
|
338
364
|
if self.check_logout(): return
|
|
339
365
|
data = self.ana_api.getServiceDeployment(deploymentId=deploymentId)
|
|
340
|
-
print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", flush=True)
|
|
366
|
+
if self.interactive: print(f"\r\tStep {data['status']['step']} - {data['status']['message']}", flush=True)
|
|
341
367
|
return data
|
|
342
368
|
else: return self.ana_api.getServiceDeployment(deploymentId=deploymentId)
|
|
343
369
|
|
anatools/anaclient/volumes.py
CHANGED
|
@@ -274,8 +274,8 @@ def download_volume_data(self, volumeId, files=[], localDir=None, recursive=True
|
|
|
274
274
|
if self.interactive:
|
|
275
275
|
print(f"\x1b[1K\rdownload: {response[index]['key']} to {filename}", flush=True)
|
|
276
276
|
except:
|
|
277
|
-
traceback.print_exc()
|
|
278
|
-
print(f"\x1b[1K\rdownload: failed to download {response[index]['key']}", flush=True)
|
|
277
|
+
if self.verbose: traceback.print_exc()
|
|
278
|
+
if self.interactive: print(f"\x1b[1K\rdownload: failed to download {response[index]['key']}", flush=True)
|
|
279
279
|
return
|
|
280
280
|
|
|
281
281
|
|
|
@@ -330,7 +330,8 @@ def upload_volume_data(self, volumeId, files=None, localDir=None, destinationDir
|
|
|
330
330
|
if sync == True:
|
|
331
331
|
file_hash = generate_etag(filepath)
|
|
332
332
|
source_hashes.append(file + file_hash)
|
|
333
|
-
else:
|
|
333
|
+
else:
|
|
334
|
+
if self.interactive: print(f"Could not find {filepath}.")
|
|
334
335
|
else:
|
|
335
336
|
for root, dirs, files in os.walk(localDir):
|
|
336
337
|
for file in files:
|
|
@@ -371,37 +372,37 @@ def upload_volume_data(self, volumeId, files=None, localDir=None, destinationDir
|
|
|
371
372
|
delete_files.append(destination_file)
|
|
372
373
|
|
|
373
374
|
if (len(delete_files)):
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
375
|
+
if self.interactive:
|
|
376
|
+
print(f"The following files will be deleted:", end='\n', flush=True)
|
|
377
|
+
for file in delete_files:
|
|
378
|
+
print(f" {file}", end='\n', flush=True)
|
|
379
|
+
answer = input("Delete these files [Y/n]: ")
|
|
380
|
+
if answer.lower() == "y":
|
|
381
|
+
self.refresh_token()
|
|
382
|
+
self.ana_api.deleteVolumeData(volumeId=volumeId, keys=delete_files)
|
|
381
383
|
|
|
382
384
|
for index, file in enumerate(source_files):
|
|
383
385
|
destination_key = (destinationDir or '') + file
|
|
384
|
-
print(f"\x1b[1K\rUploading {file} to the volume [{index+1} / {len(source_files)}]", end='\n' if self.verbose else '', flush=True)
|
|
386
|
+
if self.interactive: print(f"\x1b[1K\rUploading {file} to the volume [{index+1} / {len(source_files)}]", end='\n' if self.verbose else '', flush=True)
|
|
385
387
|
if (sync == True and (source_hashes[index] in destination_hashes)):
|
|
386
|
-
print(f"\x1b[1K\rsync: {file}'s hash exists", end='\n' if self.verbose else '', flush=True)
|
|
388
|
+
if self.interactive: print(f"\x1b[1K\rsync: {file}'s hash exists", end='\n' if self.verbose else '', flush=True)
|
|
387
389
|
elif sync == False or (source_hashes[index] not in destination_hashes):
|
|
388
390
|
try:
|
|
389
391
|
self.refresh_token()
|
|
390
392
|
filepath = os.path.join(localDir, file)
|
|
391
393
|
filesize = os.path.getsize(filepath)
|
|
392
394
|
fileinfo = self.ana_api.uploadVolumeData(volumeId=volumeId, key=destination_key, size=filesize)
|
|
393
|
-
|
|
394
|
-
parts = multipart_upload_file(filepath, int(fileinfo["partSize"]), fileinfo["urls"], f"Uploading {file} to the volume [{index+1} / {len(source_files)}]")
|
|
395
|
+
parts = multipart_upload_file(filepath, int(fileinfo["partSize"]), fileinfo["urls"], f"Uploading {file} to the volume [{index+1} / {len(source_files)}]", interactive=self.interactive)
|
|
395
396
|
self.refresh_token()
|
|
396
397
|
finalize_success = self.ana_api.uploadVolumeDataFinalizer(uploadId=fileinfo['uploadId'], key=fileinfo['key'], parts=parts)
|
|
397
398
|
if not finalize_success:
|
|
398
399
|
faileduploads.append(file)
|
|
399
400
|
except:
|
|
400
|
-
traceback.print_exc()
|
|
401
|
+
if self.verbose: traceback.print_exc()
|
|
401
402
|
faileduploads.append(file)
|
|
402
|
-
print(f"\x1b[1K\rupload: {file} failed", end='\n' if self.verbose else '', flush=True)
|
|
403
|
-
print("\x1b[1K\rUploading files completed.", flush=True)
|
|
404
|
-
if len(faileduploads): print('The following files failed to upload:', faileduploads, flush=True)
|
|
403
|
+
if self.interactive: print(f"\x1b[1K\rupload: {file} failed", end='\n' if self.verbose else '', flush=True)
|
|
404
|
+
if self.interactive: print("\x1b[1K\rUploading files completed.", flush=True)
|
|
405
|
+
if len(faileduploads) and self.interactive: print('The following files failed to upload:', faileduploads, flush=True)
|
|
405
406
|
return
|
|
406
407
|
|
|
407
408
|
|
|
@@ -14,15 +14,15 @@ class annotations:
|
|
|
14
14
|
Examples of mapfiles are in the example channel at /ana/channels/example/mapfiles/.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
def bounding_box_2d(self, image_path, out_dir, object_ids=None, object_types=None, line_thickness=1, colors=None):
|
|
17
|
+
def bounding_box_2d(self, image_path, out_dir, object_ids=None, object_types=None, line_thickness=1, colors=None, quiet=False):
|
|
18
18
|
"""
|
|
19
|
-
Generates images annotated with 2d bounding boxes for datasets downloaded from the Platform. Optional filter on
|
|
19
|
+
Generates images annotated with 2d bounding boxes for datasets downloaded from the Platform. Optional filter on
|
|
20
20
|
object_ids or object_types (must choose a single filter).
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Parameters
|
|
23
23
|
----------
|
|
24
24
|
image_path : str
|
|
25
|
-
Path to of specific image file to draw the boxes for.
|
|
25
|
+
Path to of specific image file to draw the boxes for.
|
|
26
26
|
out_dir : str
|
|
27
27
|
File path to directory where the image should be saved to.
|
|
28
28
|
object_ids : list[int]
|
|
@@ -30,22 +30,29 @@ class annotations:
|
|
|
30
30
|
object_types: list[str]
|
|
31
31
|
Filter for the object types to annotate. If not provided, all object types will get annotated. Choose either id or type filter.
|
|
32
32
|
line_thickness: int
|
|
33
|
-
Desired line thickness for box outline.
|
|
34
|
-
|
|
33
|
+
Desired line thickness for box outline.
|
|
34
|
+
colors: dict
|
|
35
35
|
Dictionary of colors to use for each object type where each color is a tuple of 3 integers (R, G, B). For example: colors={'Car': (255, 0, 0)}
|
|
36
|
+
quiet: bool
|
|
37
|
+
If True, suppress print output. Useful for CLI/automation.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
str or None
|
|
42
|
+
Path to the saved annotated image, or None if an error occurred.
|
|
36
43
|
"""
|
|
37
|
-
draw(image_path, out_dir, draw_type='box_2d', object_ids=object_ids, object_types=object_types, line_thickness=line_thickness, colors=colors)
|
|
44
|
+
return draw(image_path, out_dir, draw_type='box_2d', object_ids=object_ids, object_types=object_types, line_thickness=line_thickness, colors=colors, quiet=quiet)
|
|
38
45
|
|
|
39
46
|
|
|
40
|
-
def bounding_box_3d(self, image_path, out_dir, object_ids=None, object_types=None, line_thickness=1, colors=None):
|
|
47
|
+
def bounding_box_3d(self, image_path, out_dir, object_ids=None, object_types=None, line_thickness=1, colors=None, quiet=False):
|
|
41
48
|
"""
|
|
42
|
-
Generates images annotated with 3d bounding boxes for datasets downloaded from the Platform. Optional filter on
|
|
49
|
+
Generates images annotated with 3d bounding boxes for datasets downloaded from the Platform. Optional filter on
|
|
43
50
|
object_ids or object_types (must choose a single filter).
|
|
44
|
-
|
|
51
|
+
|
|
45
52
|
Parameters
|
|
46
53
|
----------
|
|
47
54
|
image_path : str
|
|
48
|
-
Path to of specific image file to draw the boxes for.
|
|
55
|
+
Path to of specific image file to draw the boxes for.
|
|
49
56
|
out_dir : str
|
|
50
57
|
File path to directory where the image should be saved to.
|
|
51
58
|
object_ids : list[int]
|
|
@@ -53,22 +60,29 @@ class annotations:
|
|
|
53
60
|
object_types: list[str]
|
|
54
61
|
Filter for the object types to annotate. If not provided, all object types will get annotated. Choose either id or type filter.
|
|
55
62
|
line_thickness: int
|
|
56
|
-
Desired line thickness for box outline.
|
|
63
|
+
Desired line thickness for box outline.
|
|
57
64
|
colors: dict
|
|
58
65
|
Dictionary of colors to use for each object type where each color is a tuple of 3 integers (R, G, B). For example: colors={'Car': (255, 0, 0)}
|
|
66
|
+
quiet: bool
|
|
67
|
+
If True, suppress print output. Useful for CLI/automation.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
str or None
|
|
72
|
+
Path to the saved annotated image, or None if an error occurred.
|
|
59
73
|
"""
|
|
60
|
-
draw(image_path, out_dir, draw_type='box_3d', object_ids=object_ids, object_types=object_types, line_thickness=line_thickness, colors=colors)
|
|
74
|
+
return draw(image_path, out_dir, draw_type='box_3d', object_ids=object_ids, object_types=object_types, line_thickness=line_thickness, colors=colors, quiet=quiet)
|
|
61
75
|
|
|
62
76
|
|
|
63
|
-
def segmentation(self, image_path, out_dir, object_ids=None, object_types=None, line_thickness=1, colors=None):
|
|
77
|
+
def segmentation(self, image_path, out_dir, object_ids=None, object_types=None, line_thickness=1, colors=None, quiet=False):
|
|
64
78
|
"""
|
|
65
|
-
Generates images annotated with outlines around objects for datasets downloaded from the Platform. Optional filter on
|
|
79
|
+
Generates images annotated with outlines around objects for datasets downloaded from the Platform. Optional filter on
|
|
66
80
|
object_ids or object_types (must choose a single filter).
|
|
67
|
-
|
|
81
|
+
|
|
68
82
|
Parameters
|
|
69
83
|
----------
|
|
70
84
|
image_path : str
|
|
71
|
-
Path to of specific image file to draw the boxes for.
|
|
85
|
+
Path to of specific image file to draw the boxes for.
|
|
72
86
|
out_dir : str
|
|
73
87
|
File path to directory where the image should be saved to.
|
|
74
88
|
object_ids : list[int]
|
|
@@ -79,8 +93,15 @@ class annotations:
|
|
|
79
93
|
Desired line thickness for object segmentation outline.
|
|
80
94
|
colors: dict
|
|
81
95
|
Dictionary of colors to use for each object type where each color is a tuple of 3 integers (R, G, B). For example: colors={'Car': (255, 0, 0)}
|
|
96
|
+
quiet: bool
|
|
97
|
+
If True, suppress print output. Useful for CLI/automation.
|
|
98
|
+
|
|
99
|
+
Returns
|
|
100
|
+
-------
|
|
101
|
+
str or None
|
|
102
|
+
Path to the saved annotated image, or None if an error occurred.
|
|
82
103
|
"""
|
|
83
|
-
draw(image_path, out_dir, draw_type='segmentation', object_ids=object_ids, object_types=object_types, line_thickness=line_thickness, colors=colors)
|
|
104
|
+
return draw(image_path, out_dir, draw_type='segmentation', object_ids=object_ids, object_types=object_types, line_thickness=line_thickness, colors=colors, quiet=quiet)
|
|
84
105
|
|
|
85
106
|
|
|
86
107
|
def dump_classification(self, datadir, outdir, mapfile=None):
|
anatools/annotations/draw.py
CHANGED
|
@@ -4,14 +4,14 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=None, line_thickness=1, colors=None):
|
|
7
|
+
def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=None, line_thickness=1, colors=None, quiet=False):
|
|
8
8
|
"""
|
|
9
9
|
This function handles the io and draws the right type of annotation on the image.
|
|
10
10
|
|
|
11
11
|
Parameters
|
|
12
12
|
----------
|
|
13
13
|
image_path : str
|
|
14
|
-
Path to of specific image file to draw the boxes for.
|
|
14
|
+
Path to of specific image file to draw the boxes for.
|
|
15
15
|
out_dir : str
|
|
16
16
|
File path to directory where the image should be saved to.
|
|
17
17
|
draw_type : str
|
|
@@ -24,15 +24,24 @@ def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=
|
|
|
24
24
|
Desired line thickness for box outline.
|
|
25
25
|
colors: dict
|
|
26
26
|
Dictionary of colors to use for each object type. Must provide a color for each object type specified in the filter.
|
|
27
|
+
quiet: bool
|
|
28
|
+
If True, suppress print output. Useful for CLI/automation.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
str or None
|
|
33
|
+
Path to the saved annotated image, or None if an error occurred.
|
|
27
34
|
"""
|
|
28
35
|
|
|
29
36
|
if not os.path.exists(image_path):
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
if not quiet:
|
|
38
|
+
print('Incorrect path to images: ' + image_path)
|
|
39
|
+
return None
|
|
32
40
|
|
|
33
41
|
if object_types and object_ids:
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
if not quiet:
|
|
43
|
+
print('Provide either object_types OR object_ids. ')
|
|
44
|
+
return None
|
|
36
45
|
|
|
37
46
|
if not os.path.exists(out_dir):
|
|
38
47
|
os.makedirs(out_dir)
|
|
@@ -47,8 +56,8 @@ def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=
|
|
|
47
56
|
file.close()
|
|
48
57
|
|
|
49
58
|
annotation_ids = [data['id'] for data in annotations['annotations']]
|
|
50
|
-
if object_ids is not None and not check_lists(annotation_ids, object_ids, 'object_ids'):
|
|
51
|
-
return
|
|
59
|
+
if object_ids is not None and not check_lists(annotation_ids, object_ids, 'object_ids', quiet=quiet):
|
|
60
|
+
return None
|
|
52
61
|
|
|
53
62
|
|
|
54
63
|
metadata_file = root_dir+'/metadata/'+image_name+'-metadata.json'
|
|
@@ -62,8 +71,8 @@ def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=
|
|
|
62
71
|
if object_ids:
|
|
63
72
|
object_data = [data for data in metadata['objects'] if data['id'] in object_ids]
|
|
64
73
|
elif object_types is not None:
|
|
65
|
-
if not check_lists(metadata_types, object_types, 'object_types'):
|
|
66
|
-
return
|
|
74
|
+
if not check_lists(metadata_types, object_types, 'object_types', quiet=quiet):
|
|
75
|
+
return None
|
|
67
76
|
object_data = [data for data in metadata['objects'] if data['type'] in object_types]
|
|
68
77
|
else:
|
|
69
78
|
object_data = [data for data in metadata['objects']]
|
|
@@ -74,14 +83,15 @@ def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=
|
|
|
74
83
|
generate_color = [int(val) for val in np.random.randint(0, 255, 3)]
|
|
75
84
|
type_colors[type] = tuple(generate_color)
|
|
76
85
|
else: type_colors = colors
|
|
77
|
-
|
|
86
|
+
|
|
78
87
|
|
|
79
88
|
draw_img = Image.open(image_path)
|
|
80
89
|
for object in annotations['annotations']:
|
|
81
90
|
if any(object['id'] == d['id'] for d in object_data):
|
|
82
91
|
color_info = [data for data in object_data if object['id'] == data['id']]
|
|
83
92
|
if color_info[0]['type'] not in type_colors:
|
|
84
|
-
|
|
93
|
+
if not quiet:
|
|
94
|
+
print(f'Color for {color_info[0]["type"]} not found in colors dictionary. Skipping object.')
|
|
85
95
|
continue
|
|
86
96
|
if draw_type == 'box_2d':
|
|
87
97
|
draw_img = box_2d(object['bbox'], draw_img, line_thickness, type_colors[color_info[0]['type']])
|
|
@@ -90,11 +100,14 @@ def draw(image_path, out_dir, draw_type='box_2d', object_ids=None, object_types=
|
|
|
90
100
|
elif draw_type == 'segmentation':
|
|
91
101
|
draw_img = segmentation(object['segmentation'], draw_img, line_thickness, type_colors[color_info[0]['type']])
|
|
92
102
|
else:
|
|
93
|
-
|
|
103
|
+
if not quiet:
|
|
104
|
+
print('Provide either box_2d, box_3d, or segmentation')
|
|
94
105
|
|
|
95
106
|
outimg = out_dir+'/'+image_name+'-annotated-'+draw_type+'.'+image_ext
|
|
96
107
|
draw_img.save(outimg)
|
|
97
|
-
|
|
108
|
+
if not quiet:
|
|
109
|
+
print(f'Image saved to {outimg}')
|
|
110
|
+
return outimg
|
|
98
111
|
|
|
99
112
|
|
|
100
113
|
def box_2d(coordinates, bbox_img, width, outline):
|
|
@@ -193,7 +206,7 @@ def segmentation(coordinates, draw_img, width, color):
|
|
|
193
206
|
return draw_img
|
|
194
207
|
|
|
195
208
|
|
|
196
|
-
def check_lists(actual, expected, name_to_check):
|
|
209
|
+
def check_lists(actual, expected, name_to_check, quiet=False):
|
|
197
210
|
"""
|
|
198
211
|
Helper function that checks if one list is in another for validation on draw inputs.
|
|
199
212
|
|
|
@@ -205,16 +218,19 @@ def check_lists(actual, expected, name_to_check):
|
|
|
205
218
|
The expected list that is provided from the user.
|
|
206
219
|
name_to_check: str
|
|
207
220
|
Name of parameter to check (either object_id or object_type).
|
|
221
|
+
quiet: bool
|
|
222
|
+
If True, suppress print output.
|
|
208
223
|
|
|
209
224
|
Returns
|
|
210
225
|
-------
|
|
211
226
|
bool
|
|
212
227
|
True if lists are matching, False otherwise.
|
|
213
228
|
"""
|
|
214
|
-
|
|
229
|
+
|
|
215
230
|
if not set(expected).issubset(set(actual)):
|
|
216
231
|
out_of_bounds_check= list(set(expected) - set(actual))
|
|
217
|
-
|
|
218
|
-
|
|
232
|
+
if not quiet:
|
|
233
|
+
print(f'Provided {name_to_check} list has the following out of bounds: {out_of_bounds_check}. Please rerun with valid list. \nHere are all the {name_to_check} that can get annotated: ')
|
|
234
|
+
print(list(set(actual)))
|
|
219
235
|
return False
|
|
220
236
|
return True
|
|
@@ -332,6 +332,7 @@ try:
|
|
|
332
332
|
while resp not in [str(i) for i in range(len(channels)+1)]:
|
|
333
333
|
resp = input(f'Invalid input, please enter a number between 0 and {len(channels)}: ')
|
|
334
334
|
remotechannel = channels[int(resp)-1]
|
|
335
|
+
print_color(f"Selected channel: {remotechannel['name']}", color='brand')
|
|
335
336
|
if volumes != remotechannel['volumes']:
|
|
336
337
|
client.edit_channel(channelId=remotechannel['channelId'], volumes=volumes)
|
|
337
338
|
remotechannel = client.get_channels(channelId=remotechannel['channelId'])[0]
|
|
@@ -346,6 +347,7 @@ try:
|
|
|
346
347
|
if channels == False or len(channels) == 0: raise Exception(f"Channel {args.channelId} not found")
|
|
347
348
|
remotechannel = channels[0]
|
|
348
349
|
starttime = time.time()
|
|
350
|
+
client.interactive = True
|
|
349
351
|
deploymentId = client.deploy_channel(channelId=remotechannel['channelId'], channelfile=args.channel)
|
|
350
352
|
print('Registering Channel Image...', flush=True)
|
|
351
353
|
registerstart = time.time()
|
|
@@ -493,6 +495,7 @@ try:
|
|
|
493
495
|
while resp not in [str(i) for i in range(len(services)+1)]:
|
|
494
496
|
resp = input(f'Invalid input, please enter a number between 0 and {len(services)}: ')
|
|
495
497
|
remoteservice = services[int(resp)-1]
|
|
498
|
+
print_color(f"Selected service: {remoteservice['name']}", color='brand')
|
|
496
499
|
if volumes != remoteservice['volumes']:
|
|
497
500
|
client.edit_service(serviceId=remoteservice['serviceId'], volumes=volumes)
|
|
498
501
|
remoteservice = client.get_services(serviceId=remoteservice['serviceId'])[0]
|
|
@@ -510,6 +513,7 @@ try:
|
|
|
510
513
|
client.edit_service(serviceId=remoteservice['serviceId'], volumes=volumes)
|
|
511
514
|
remoteservice = client.get_services(serviceId=remoteservice['serviceId'])[0]
|
|
512
515
|
starttime = time.time()
|
|
516
|
+
client.interactive = True
|
|
513
517
|
deploymentId = client.deploy_service(serviceId=remoteservice['serviceId'], servicefile=args.service)
|
|
514
518
|
print('Registering Service Image...', flush=True)
|
|
515
519
|
registerstart = time.time()
|