Skip to content

Commit 83b8c49

Browse files
committed
Add local file path and network path options.
1 parent 6c1ee21 commit 83b8c49

4 files changed

Lines changed: 184 additions & 64 deletions

File tree

docs/environment.rst

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,22 @@ and configurations.
3535
to no debug logging. Setting this value to anything will turn on debug logging
3636
(e.g., ``VERBOSE=1``).
3737
- ``SCOUTBOT_MODEL_URL`` (default: https://wildbookiarepository.azureedge.net/models)
38-
The base URL for downloading ONNX model files. This allows organizations to host models on their
39-
own infrastructure instead of using the default CDN.
38+
The base URL or path for accessing ONNX model files. This can be:
39+
40+
* An HTTP/HTTPS URL (e.g., ``https://example.com/models``)
41+
* A local file path (e.g., ``/opt/models`` or ``C:\models``)
42+
* A network path (e.g., ``//server/share/models`` or ``\\server\share\models``)
43+
44+
When using file paths, models will be copied to a local cache for consistency.
45+
This allows organizations to host models on their own infrastructure, CDN, or
46+
local/network storage.
4047
- ``SCOUTBOT_DATA_URL`` (default: https://wildbookiarepository.azureedge.net/data)
41-
The base URL for downloading test data files. This allows organizations to host test data on their
42-
own infrastructure instead of using the default CDN.
48+
The base URL or path for downloading test data files.
49+
50+
As with SCOUTBOT_MODEL_URL, this can be:
51+
* An HTTP/HTTPS URL or
52+
* A local file path or
53+
* A network path
54+
55+
This allows organizations to host test data on their own infrastructure, CDN, or
56+
local/network storage.

scoutbot/__init__.py

Lines changed: 136 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@
5151
'''
5252
import os
5353
import cv2
54+
import shutil
5455
from os.path import exists
56+
from urllib.parse import urlparse
57+
from pathlib import Path
5558

5659
import pooch
5760
import utool as ut
@@ -69,9 +72,34 @@
6972
'SCOUTBOT_DATA_URL', 'https://wildbookiarepository.azureedge.net/data'
7073
).rstrip('/')
7174

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}")
72100

73101
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
75103

76104
# from tile_batched.models import Yolov8DetectionModel
77105
# from tile_batched import get_sliced_prediction_batched
@@ -81,6 +109,68 @@
81109
__version__ = VERSION
82110

83111

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+
84174
def fetch(pull=False, config=None):
85175
"""
86176
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):
108198

109199

110200
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,
120210
):
121211
"""
122212
Run the ML pipeline on a given image filepath and return the detections
@@ -202,17 +292,17 @@ def pipeline(
202292

203293

204294
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,
216306
):
217307
"""
218308
Run the ML pipeline on a given image filepath and return the detections
@@ -283,15 +373,15 @@ def pipeline_v3(
283373

284374

285375
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,
295385
):
296386
"""
297387
Run the ML pipeline on a given batch of image filepaths and return the detections
@@ -390,7 +480,7 @@ def batch(
390480
assert len(loc_tile_grids) == len(loc_outputs)
391481

392482
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
394484
):
395485
batch[filepath]['loc']['grids'].append(loc_tile_grid)
396486
batch[filepath]['loc']['outputs'].append(loc_output)
@@ -429,16 +519,16 @@ def batch(
429519

430520

431521
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,
442532
):
443533
yolov8_model_path = loc.fetch(config=config)
444534

@@ -499,10 +589,11 @@ def example():
499589
'786a940b062a90961f409539292f09144c3dbdbc6b6faa64c3e764d63d55c988' # NOQA
500590
)
501591

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
506597
)
507598
assert exists(img_filepath)
508599

scoutbot/loc/__init__.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import tqdm
2222
import utool as ut
2323

24-
from scoutbot import MODEL_BASE_URL, QUIET, log
24+
from scoutbot import log
2525
from scoutbot.loc.transforms import (
2626
Compose,
2727
GetBoundingBoxes,
@@ -144,13 +144,13 @@
144144

145145
def fetch(pull=False, config=DEFAULT_CONFIG):
146146
"""
147-
Fetch the Localizer ONNX model file from a CDN if it does not exist locally.
147+
Fetch the Localizer ONNX model file from a CDN, local path, or network path.
148148
149149
This function will throw an AssertionError if the download fails or the
150150
file otherwise does not exists locally on disk.
151151
152152
Args:
153-
pull (bool, optional): If :obj:`True`, force using the downloaded versions
153+
pull (bool, optional): If :obj:`True`, force using the downloaded/copied versions
154154
stored in the local system's cache. Defaults to :obj:`False`.
155155
config (str or None, optional): the configuration to use, one of ``phase1``
156156
or ``mvp``. Defaults to :obj:`None`.
@@ -160,6 +160,7 @@ def fetch(pull=False, config=DEFAULT_CONFIG):
160160
161161
Raises:
162162
AssertionError: If the model cannot be fetched.
163+
FileNotFoundError: If the model file doesn't exist at the specified path.
163164
"""
164165
if config is None:
165166
config = DEFAULT_CONFIG
@@ -171,10 +172,14 @@ def fetch(pull=False, config=DEFAULT_CONFIG):
171172
if not pull and exists(model_path):
172173
onnx_model = model_path
173174
else:
174-
onnx_model = pooch.retrieve(
175-
url=f'{MODEL_BASE_URL}/{model_name}',
176-
known_hash=model_hash,
177-
progressbar=not QUIET,
175+
# Import the utility function from parent module
176+
from scoutbot import MODEL_BASE_URL, QUIET, get_model_from_source
177+
178+
onnx_model = get_model_from_source(
179+
model_name=model_name,
180+
model_hash=model_hash,
181+
source_base=MODEL_BASE_URL,
182+
use_cache=True
178183
)
179184
assert exists(onnx_model)
180185

@@ -256,6 +261,9 @@ def predict(gen):
256261
"""
257262
log.debug('Running LOC inference')
258263

264+
# Import QUIET from parent module
265+
from scoutbot import QUIET
266+
259267
ort_sessions = {}
260268

261269
for chunk, sizes, trim, config in tqdm.tqdm(gen, disable=QUIET):

0 commit comments

Comments
 (0)