|
51 | 51 | ''' |
52 | 52 | import os |
53 | 53 | import cv2 |
| 54 | +import shutil |
54 | 55 | from os.path import exists |
| 56 | +from urllib.parse import urlparse |
| 57 | +from pathlib import Path |
55 | 58 |
|
56 | 59 | import pooch |
57 | 60 | import utool as ut |
|
69 | 72 | 'SCOUTBOT_DATA_URL', 'https://wildbookiarepository.azureedge.net/data' |
70 | 73 | ).rstrip('/') |
71 | 74 |
|
| 75 | +# Validate MODEL_BASE_URL |
| 76 | +parsed = urlparse(MODEL_BASE_URL) |
| 77 | +if parsed.scheme in ('http', 'https', 'ftp'): |
| 78 | + # It's a URL, no further validation needed |
| 79 | + pass |
| 80 | +elif parsed.scheme and parsed.scheme not in ('file', ''): |
| 81 | + log.warning(f"Unrecognized scheme in MODEL_BASE_URL: {parsed.scheme}") |
| 82 | +else: |
| 83 | + # It's a path - check if it exists |
| 84 | + model_path = Path(MODEL_BASE_URL) |
| 85 | + if not model_path.exists(): |
| 86 | + log.warning(f"Model source path does not exist: {MODEL_BASE_URL}") |
| 87 | + |
| 88 | +# Validate DATA_BASE_URL |
| 89 | +parsed = urlparse(DATA_BASE_URL) |
| 90 | +if parsed.scheme in ('http', 'https', 'ftp'): |
| 91 | + # It's a URL, no further validation needed |
| 92 | + pass |
| 93 | +elif parsed.scheme and parsed.scheme not in ('file', ''): |
| 94 | + log.warning(f"Unrecognized scheme in DATA_BASE_URL: {parsed.scheme}") |
| 95 | +else: |
| 96 | + # It's a path - check if it exists |
| 97 | + data_path = Path(DATA_BASE_URL) |
| 98 | + if not data_path.exists(): |
| 99 | + log.warning(f"Data source path does not exist: {DATA_BASE_URL}") |
72 | 100 |
|
73 | 101 | from scoutbot import agg, loc, tile, wic, tile_batched # NOQA |
74 | | -from scoutbot.loc import CONFIGS as LOC_CONFIGS # NOQA |
| 102 | +from scoutbot.loc import CONFIGS as LOC_CONFIGS # NOQA |
75 | 103 |
|
76 | 104 | # from tile_batched.models import Yolov8DetectionModel |
77 | 105 | # from tile_batched import get_sliced_prediction_batched |
|
81 | 109 | __version__ = VERSION |
82 | 110 |
|
83 | 111 |
|
| 112 | +def get_resource_from_source(resource_name, resource_hash, source_base, resource_type='model', use_cache=True): |
| 113 | + """ |
| 114 | + Retrieve a resource (model or data) from URL, local path, or network path. |
| 115 | +
|
| 116 | + Args: |
| 117 | + resource_name: Name of the resource file |
| 118 | + resource_hash: Expected hash of the resource (used for URL downloads) |
| 119 | + source_base: Base URL or path (from MODEL_BASE_URL or DATA_BASE_URL) |
| 120 | + resource_type: Type of resource ('model' or 'data') for cache organization |
| 121 | + use_cache: Whether to use cached version for URLs |
| 122 | +
|
| 123 | + Returns: |
| 124 | + str: Path to the resource file |
| 125 | +
|
| 126 | + Raises: |
| 127 | + FileNotFoundError: If the resource file doesn't exist at the specified path |
| 128 | + """ |
| 129 | + # Parse the source to determine if it's a URL or path |
| 130 | + parsed = urlparse(source_base) |
| 131 | + |
| 132 | + if parsed.scheme in ('http', 'https', 'ftp'): |
| 133 | + # It's a URL - use pooch as before |
| 134 | + resource_url = f'{source_base}/{resource_name}' |
| 135 | + return pooch.retrieve( |
| 136 | + url=resource_url, |
| 137 | + known_hash=resource_hash, |
| 138 | + progressbar=not QUIET, |
| 139 | + ) |
| 140 | + else: |
| 141 | + # It's a local or network path |
| 142 | + source_path = Path(source_base) / resource_name |
| 143 | + |
| 144 | + if not source_path.exists(): |
| 145 | + raise FileNotFoundError(f"{resource_type.capitalize()} file not found: {source_path}") |
| 146 | + |
| 147 | + if use_cache: |
| 148 | + # Copy to cache directory to maintain consistency |
| 149 | + cache_dir = Path(pooch.os_cache(f"scoutbot/{resource_type}s")) |
| 150 | + cache_dir.mkdir(parents=True, exist_ok=True) |
| 151 | + cache_path = cache_dir / resource_name |
| 152 | + |
| 153 | + # Only copy if not already cached or file has changed |
| 154 | + if not cache_path.exists() or os.path.getmtime(source_path) > os.path.getmtime(cache_path): |
| 155 | + log.debug(f"Copying {resource_type} from {source_path} to cache {cache_path}") |
| 156 | + shutil.copy2(source_path, cache_path) |
| 157 | + |
| 158 | + return str(cache_path) |
| 159 | + else: |
| 160 | + # Use directly from source |
| 161 | + return str(source_path) |
| 162 | + |
| 163 | + |
| 164 | +def get_model_from_source(model_name, model_hash, source_base, use_cache=True): |
| 165 | + """Backward compatibility wrapper for get_resource_from_source""" |
| 166 | + return get_resource_from_source(model_name, model_hash, source_base, 'model', use_cache) |
| 167 | + |
| 168 | + |
| 169 | +def get_data_from_source(data_name, data_hash, source_base, use_cache=True): |
| 170 | + """Convenience wrapper for get_resource_from_source for data files""" |
| 171 | + return get_resource_from_source(data_name, data_hash, source_base, 'data', use_cache) |
| 172 | + |
| 173 | + |
84 | 174 | def fetch(pull=False, config=None): |
85 | 175 | """ |
86 | 176 | Fetch the WIC and Localizer ONNX model files from a CDN if they do not exist locally. |
@@ -108,15 +198,15 @@ def fetch(pull=False, config=None): |
108 | 198 |
|
109 | 199 |
|
110 | 200 | def pipeline( |
111 | | - filepath, |
112 | | - config=None, |
113 | | - backend_device='cuda:0', |
114 | | - wic_thresh=wic.CONFIGS[None]['thresh'], |
115 | | - loc_thresh=loc.CONFIGS[None]['thresh'], |
116 | | - loc_nms_thresh=loc.CONFIGS[None]['nms'], |
117 | | - agg_thresh=agg.CONFIGS[None]['thresh'], |
118 | | - agg_nms_thresh=agg.CONFIGS[None]['nms'], |
119 | | - clean=True, |
| 201 | + filepath, |
| 202 | + config=None, |
| 203 | + backend_device='cuda:0', |
| 204 | + wic_thresh=wic.CONFIGS[None]['thresh'], |
| 205 | + loc_thresh=loc.CONFIGS[None]['thresh'], |
| 206 | + loc_nms_thresh=loc.CONFIGS[None]['nms'], |
| 207 | + agg_thresh=agg.CONFIGS[None]['thresh'], |
| 208 | + agg_nms_thresh=agg.CONFIGS[None]['nms'], |
| 209 | + clean=True, |
120 | 210 | ): |
121 | 211 | """ |
122 | 212 | Run the ML pipeline on a given image filepath and return the detections |
@@ -202,17 +292,17 @@ def pipeline( |
202 | 292 |
|
203 | 293 |
|
204 | 294 | def pipeline_v3( |
205 | | - filepath, |
206 | | - config, |
207 | | - batched_detection_model=None, |
208 | | - backend_device='cuda:0', |
209 | | - loc_thresh=0.45, |
210 | | - slice_height=512, |
211 | | - slice_width=512, |
212 | | - overlap_height_ratio=0.25, |
213 | | - overlap_width_ratio=0.25, |
214 | | - perform_standard_pred=False, |
215 | | - postprocess_class_agnostic=True, |
| 295 | + filepath, |
| 296 | + config, |
| 297 | + batched_detection_model=None, |
| 298 | + backend_device='cuda:0', |
| 299 | + loc_thresh=0.45, |
| 300 | + slice_height=512, |
| 301 | + slice_width=512, |
| 302 | + overlap_height_ratio=0.25, |
| 303 | + overlap_width_ratio=0.25, |
| 304 | + perform_standard_pred=False, |
| 305 | + postprocess_class_agnostic=True, |
216 | 306 | ): |
217 | 307 | """ |
218 | 308 | Run the ML pipeline on a given image filepath and return the detections |
@@ -283,15 +373,15 @@ def pipeline_v3( |
283 | 373 |
|
284 | 374 |
|
285 | 375 | def batch( |
286 | | - filepaths, |
287 | | - config=None, |
288 | | - backend_device='cuda:0', |
289 | | - wic_thresh=wic.CONFIGS[None]['thresh'], |
290 | | - loc_thresh=loc.CONFIGS[None]['thresh'], |
291 | | - loc_nms_thresh=loc.CONFIGS[None]['nms'], |
292 | | - agg_thresh=agg.CONFIGS[None]['thresh'], |
293 | | - agg_nms_thresh=agg.CONFIGS[None]['nms'], |
294 | | - clean=True, |
| 376 | + filepaths, |
| 377 | + config=None, |
| 378 | + backend_device='cuda:0', |
| 379 | + wic_thresh=wic.CONFIGS[None]['thresh'], |
| 380 | + loc_thresh=loc.CONFIGS[None]['thresh'], |
| 381 | + loc_nms_thresh=loc.CONFIGS[None]['nms'], |
| 382 | + agg_thresh=agg.CONFIGS[None]['thresh'], |
| 383 | + agg_nms_thresh=agg.CONFIGS[None]['nms'], |
| 384 | + clean=True, |
295 | 385 | ): |
296 | 386 | """ |
297 | 387 | Run the ML pipeline on a given batch of image filepaths and return the detections |
@@ -390,7 +480,7 @@ def batch( |
390 | 480 | assert len(loc_tile_grids) == len(loc_outputs) |
391 | 481 |
|
392 | 482 | for filepath, loc_tile_grid, loc_output in zip( |
393 | | - loc_tile_img_filepaths, loc_tile_grids, loc_outputs |
| 483 | + loc_tile_img_filepaths, loc_tile_grids, loc_outputs |
394 | 484 | ): |
395 | 485 | batch[filepath]['loc']['grids'].append(loc_tile_grid) |
396 | 486 | batch[filepath]['loc']['outputs'].append(loc_output) |
@@ -429,16 +519,16 @@ def batch( |
429 | 519 |
|
430 | 520 |
|
431 | 521 | def batch_v3( |
432 | | - filepaths, |
433 | | - config, |
434 | | - backend_device, |
435 | | - loc_thresh=0.45, |
436 | | - slice_height=512, |
437 | | - slice_width=512, |
438 | | - overlap_height_ratio=0.25, |
439 | | - overlap_width_ratio=0.25, |
440 | | - perform_standard_pred=False, |
441 | | - postprocess_class_agnostic=True, |
| 522 | + filepaths, |
| 523 | + config, |
| 524 | + backend_device, |
| 525 | + loc_thresh=0.45, |
| 526 | + slice_height=512, |
| 527 | + slice_width=512, |
| 528 | + overlap_height_ratio=0.25, |
| 529 | + overlap_width_ratio=0.25, |
| 530 | + perform_standard_pred=False, |
| 531 | + postprocess_class_agnostic=True, |
442 | 532 | ): |
443 | 533 | yolov8_model_path = loc.fetch(config=config) |
444 | 534 |
|
@@ -499,10 +589,11 @@ def example(): |
499 | 589 | '786a940b062a90961f409539292f09144c3dbdbc6b6faa64c3e764d63d55c988' # NOQA |
500 | 590 | ) |
501 | 591 |
|
502 | | - img_filepath = pooch.retrieve( |
503 | | - url=f'{DATA_BASE_URL}/{TEST_IMAGE}', |
504 | | - known_hash=TEST_IMAGE_HASH, |
505 | | - progressbar=True, |
| 592 | + img_filepath = get_data_from_source( |
| 593 | + data_name=TEST_IMAGE, |
| 594 | + data_hash=TEST_IMAGE_HASH, |
| 595 | + source_base=DATA_BASE_URL, |
| 596 | + use_cache=True |
506 | 597 | ) |
507 | 598 | assert exists(img_filepath) |
508 | 599 |
|
|
0 commit comments