Skip to content

Commit a25ff3d

Browse files
committed
Improve prefetch caching
1 parent 2de42c8 commit a25ff3d

5 files changed

Lines changed: 1015 additions & 992 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## [Unreleased]
44
### Changed
5-
- Detectorist now prefetches adjacent images in both directions, so stepping forward or backward feels faster.
5+
- Detectorist prefetches and processes the next 3 images in the current direction. Cached images & detections are displayed almost instantly so the app feels snappier.
66
- The "Loading image..." placeholder only appears when loading actually takes noticeable time.
77
- Batch runs load the next image in the background while the current one is processed, cutting batch time (up to 2x for HIF files).
88
- A batch run no longer aborts when one image fails to load. The image is skipped and recorded as "load-error" in detections.csv.

src/detectorist/detectorist_app.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def __init__(self):
5151
super().__init__()
5252

5353
self.current_image_path = None
54+
self._previous_row: int | None = None
5455
self._all_detection_results: list = []
5556
self._last_detection_time_ms: float = 0.0
5657

@@ -269,6 +270,7 @@ def _load_images_from_paths(self, file_paths: list[str]):
269270
# Clear existing list and main image
270271
self.model.clear()
271272
self.current_image_path = None
273+
self._previous_row = None
272274
self.ui.image_label.clear()
273275
# Files on disk may have changed, so cached decodes are not trustworthy.
274276
# The queued invocation runs clear_cache on the worker thread, which is
@@ -423,6 +425,11 @@ def on_image_selected(self, index):
423425
if new_image_path == self.current_image_path:
424426
return # No need to reload the same image
425427

428+
try:
429+
self._previous_row = self.model.imagePaths().index(self.current_image_path)
430+
except ValueError:
431+
self._previous_row = None
432+
426433
self.current_image_path = new_image_path
427434
self._all_detection_results = []
428435

@@ -466,21 +473,32 @@ def trigger_processing(self):
466473

467474
def _prefetch_hints(self) -> list[str]:
468475
"""
469-
Returns the paths worth decoding ahead of time: one step forward and
470-
one step backward. During forward browsing the previous image is
471-
already cached (no-op), so the cost is one extra decode. During
472-
backward browsing the backward hint populates the slot that would
473-
otherwise miss two steps back.
476+
Returns paths to decode ahead of time. In both directions: protect the
477+
from-image (so it survives the upcoming evictions) then prefetch 3 in the
478+
direction of travel, giving 3 instant steps ahead and 1 instant step back.
479+
Falls back to 2 ahead and 2 behind (n+1, n-1, n+2, n-2) for the first
480+
selection or a jump.
474481
"""
475482
paths = self.model.imagePaths()
476483
try:
477484
row = paths.index(self.current_image_path)
478485
except ValueError:
479486
return []
480-
hints = paths[row + 1:row + 2]
481-
if row > 0:
482-
hints = hints + [paths[row - 1]]
483-
return hints
487+
488+
prev_row = self._previous_row
489+
if prev_row is not None and abs(row - prev_row) == 1:
490+
if row > prev_row:
491+
# Promote the from-image first so it survives the forward loads.
492+
return [paths[prev_row]] + paths[row + 1:row + 4]
493+
else:
494+
# Same pattern as forward: protect the from-image first, then
495+
# prefetch 3 in the direction of travel.
496+
return [paths[prev_row]] + list(reversed(paths[max(0, row - 3):row]))
497+
498+
# No clear direction: nearest 2 in each direction, alternating so the
499+
# closest neighbors are always decoded first.
500+
return [paths[i] for i in [row + 1, row - 1, row + 2, row - 2]
501+
if 0 <= i < len(paths)]
484502

485503
def handle_model_loaded(self, success: bool, message: str, class_names: list):
486504
"""Handles the result of loading a model in the worker."""

src/detectorist/image_cache.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ class ImageCache:
2525
the image file and the loaded model, so entries stay valid until the
2626
model changes or the files on disk may have (callers clear it then).
2727
28-
The default capacity of 3 keeps two previously viewed images plus the
29-
prefetched next image resident, so both a forward and a backward step
30-
land in the cache. Each decoded 33 MP 16-bit image occupies roughly
31-
200 MB, which is why the capacity is this small.
28+
The default capacity of 5 keeps 3 images ahead in the travel direction and
29+
1 image behind alongside the current image, so three consecutive steps in
30+
the browsing direction are served without decoding. Each decoded 33 MP
31+
16-bit image occupies roughly 200 MB, so the capacity is kept small.
3232
"""
3333

34-
def __init__(self, max_entries: int = 3):
34+
def __init__(self, max_entries: int = 5):
3535
self._max_entries = max_entries
3636
# dicts preserve insertion order; the first key is the least recently used
3737
self._entries: dict[str, CacheEntry] = {}

0 commit comments

Comments
 (0)