Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
# committing it without the exclusion would force every `git clone` to
# download 2.65 GB and spend the repo's LFS bandwidth quota. Fetch it via the
# "BiRefNet setup" page in the app, or:
# git lfs pull --include="weights/birefnet_leaf.pth"
# git lfs pull -X "" -I "weights/birefnet_leaf.pth"
weights/*.pth filter=lfs diff=lfs merge=lfs -text
10 changes: 5 additions & 5 deletions .lfsconfig
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
# Opt in via the "BiRefNet setup" page in the MATS app, `mats fetch-weights
# --only birefnet --source lfs`, or by hand:
#
# git lfs pull --include="weights/birefnet_leaf.pth"
# git lfs pull -X "" -I "weights/birefnet_leaf.pth"
#
# Note that a bare `git lfs pull` will NOT fetch it -- only -I/--include
# overrides fetchexclude. That's deliberate: no accidental 2.65 GB pulls.
# [lfs]
# fetchexclude = weights/birefnet_leaf.pth
# A bare pull and an include-only pull both retain fetchexclude. `-X ""`
# clears that exclusion for this invocation; `-I` limits the pull to BiRefNet.
[lfs]
fetchexclude = weights/birefnet_leaf.pth
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ other system libraries (QR codes are decoded with OpenCV).
> fresh checkout fails to detect markers, and it looks like a model problem
> rather than a setup problem. Verify with `ls -l weights/rf_detr_marker.pth`
> (~134 MB, not ~134 bytes) or `mats doctor`; repair with
> `git lfs install && git lfs pull --exclude="weights/birefnet_leaf.pth"` — the `--exclude` matters: a bare `git lfs pull` can also
> fetch the 2.65 GB BiRefNet checkpoint.
> `git lfs install && git lfs pull --exclude="weights/birefnet_leaf.pth"` — the `--exclude` keeps the repair to RF-DETR. With
> `.lfsconfig` active, a bare `git lfs pull` also leaves the 2.65 GB BiRefNet checkpoint out; fetch it only with
> `git lfs pull -X "" -I "weights/birefnet_leaf.pth"` (or `mats fetch-weights --only birefnet --source lfs`).

```bash
git lfs install # one-time, per machine, BEFORE cloning
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# MATS — Morphometric Analysis Toolbox
# MATS — Morphometric Analysis Toolbox for Segmentation

Measure leaf **area, length, and width** in real-world units from a photo of
leaves laid on a printed calibration template.
Expand Down Expand Up @@ -68,8 +68,7 @@ ls -l weights/rf_detr_marker.pth

**Already cloned without Git LFS?** No need to start over — install Git LFS as
above, then repair the checkout in place. The `--exclude` keeps this to the
~134 MB RF-DETR file; a bare `git lfs pull` can also fetch the 2.65 GB BiRefNet
checkpoint:
~134 MB RF-DETR file, avoiding the optional 2.65 GB BiRefNet checkpoint:

```bash
git lfs install && git lfs pull --exclude="weights/birefnet_leaf.pth"
Expand Down Expand Up @@ -148,12 +147,14 @@ checkpoint or install the pyzbar/QReader robust-QR fallbacks.

The required ~134 MB RF-DETR marker checkpoint is different: it is mandatory for
every run, so it ships **in the clone** via Git LFS and needs no separate
download step. If it is ever missing — a clone made without Git LFS, or an
install outside a Git checkout — MATS fetches it once on first use and prints
`Fetching weights/rf_detr_marker.pth via Git LFS ...` while it does. Set
`MATS_NO_AUTO_FETCH=1` to turn that off and require pre-staged weights instead
(recommended on HPC login nodes). The app never does this silently: a missing
RF-DETR checkpoint is a blocking Preflight error.
download step. If it is missing from a Git checkout — for example, after cloning
without Git LFS — MATS can fetch it once on first use and prints
`Fetching weights/rf_detr_marker.pth via Git LFS ...` while it does. An install
outside a Git checkout must use a pre-staged checkpoint (or a separately
configured Hugging Face source). Set `MATS_NO_AUTO_FETCH=1` to turn automatic
fetching off and require pre-staged weights instead (recommended on HPC login
nodes). The app never does this silently: a missing RF-DETR checkpoint is a
blocking Preflight error.

This keeps the initial network and disk footprint predictable, avoids native
`zbar` failures on managed machines, and works better on HPC systems and
Expand Down
12 changes: 8 additions & 4 deletions deploy/ondemand/mats/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ compute node and exposes it through the Open OnDemand reverse proxy.
```
Then set `CONDA_ENV` in `template/script.sh.erb` to that env name (`mats`).
(A plain virtualenv works too — the default install needs no system libs.)
2. **The model checkpoints.** Fetch them once, ideally to a shared location:
2. **The model checkpoints.** Pre-stage them once in a shared location. From a
Git checkout with Git LFS installed, materialize the files, then copy them to
the shared directory:
```bash
export MATS_WEIGHTS_DIR=/shared/models/mats
mats fetch-weights --all # RF-DETR + BiRefNet (the GUI defaults to Otsu, but a
# GPU-backed OOD app is the typical BiRefNet use case)
mats doctor # confirm they resolve
mkdir -p "$MATS_WEIGHTS_DIR"
mats fetch-weights --all # writes RF-DETR + BiRefNet to the checkout's weights/
cp weights/rf_detr_marker.pth "$MATS_WEIGHTS_DIR/"
cp weights/birefnet_leaf.pth "$MATS_WEIGHTS_DIR/"
mats doctor # confirm they resolve
```
Point the same `MATS_WEIGHTS_DIR` at that path in `template/script.sh.erb`.
3. *(Optional)* Enhanced QR reading (`pip install -e ".[qr]"`) adds the `pyzbar`
Expand Down
4 changes: 3 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ mats fetch-weights --only birefnet --source lfs # explicitly fetch just BiRefNet
mats fetch-weights --force # re-download even if present
```

Downloads to `~/.cache/mats/weights` (or `$MATS_WEIGHTS_DIR`). See
Git LFS downloads write to the Git checkout's `weights/` directory.
`MATS_WEIGHTS_DIR` is for pre-staged local or shared checkpoints; a configured
Hugging Face source may use it as its download destination. See
[weights.md](weights.md).

## `mats doctor`
Expand Down
9 changes: 7 additions & 2 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@ Repair an existing clone without re-cloning:
git lfs install && git lfs pull --exclude="weights/birefnet_leaf.pth"
```

The `--exclude` keeps the repair to the ~134 MB RF-DETR file; a bare
`git lfs pull` can also fetch the 2.65 GB BiRefNet checkpoint.
The `--exclude` keeps the repair to the ~134 MB RF-DETR file. With this
repository's `lfs.fetchexclude`, a bare `git lfs pull` also leaves the 2.65 GB
BiRefNet checkpoint out; fetch it explicitly only when needed:

```bash
git lfs pull -X "" -I "weights/birefnet_leaf.pth"
```

**Do I need to download the model first?** No. The clone brings RF-DETR with it,
so there is no separate download step — run `mats doctor` and you're done. (If
Expand Down
14 changes: 10 additions & 4 deletions docs/hpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@ conda env create -f environment.yml
conda activate mats
pip install -e ".[app]"
export MATS_WEIGHTS_DIR=/project/<your_project>/mats_weights # shared, readable
mats fetch-weights --all # populate both checkpoints once, from a data-transfer node
mkdir -p "$MATS_WEIGHTS_DIR"
# Run from a Git checkout with Git LFS installed. It materializes both files
# in that checkout's weights/ directory; copy the verified files to /project.
mats fetch-weights --all
cp weights/rf_detr_marker.pth "$MATS_WEIGHTS_DIR/"
cp weights/birefnet_leaf.pth "$MATS_WEIGHTS_DIR/"
mats doctor
```

On USDA **SCINet** (Ceres/Atlas), a `/project` directory is a mounted filesystem
shared across the project, so every job reads the weights in place — no per-user
copy. Fetch them once to that path and point `MATS_WEIGHTS_DIR` at it for all
users. External collaborators without SCINet accounts can pull the same directory
via a **Globus guest collection** (they need a free Globus login).
copy. Materialize the weights once in a Git checkout, copy the verified files to
that path, and point `MATS_WEIGHTS_DIR` at it for all users. External collaborators
without SCINet accounts can pull the same directory via a **Globus guest collection**
(they need a free Globus login).

Set `MATS_NO_AUTO_FETCH=1` in your jobs so a misconfigured path fails fast with a
clear error instead of triggering a 2.65 GB download on a login or compute node
Expand Down
19 changes: 12 additions & 7 deletions docs/weights.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,11 @@ mats doctor # show resolved paths, channels, and so
```

After a clone made with Git LFS installed, bare `mats fetch-weights` is a no-op
that prints "already present" — RF-DETR arrived with the checkout. The command
exists to repair a checkout made *without* Git LFS, and to populate a shared
`MATS_WEIGHTS_DIR`.
that prints "already present" — RF-DETR arrived with the checkout. The Git LFS
channel repairs a checkout made *without* Git LFS and always writes to that
checkout's `weights/` directory. `MATS_WEIGHTS_DIR` controls where MATS looks
for pre-staged files; provision a shared directory by copying verified
checkpoints there rather than expecting a Git LFS fetch to populate it.

BiRefNet is never downloaded automatically. If it is absent when selected,
MATs reports the missing local checkpoint and leaves Otsu fully usable.
Expand All @@ -78,7 +80,7 @@ only option that needs no per-user download at all:

```bash
export MATS_WEIGHTS_DIR=/project/<your_project>/mats_weights
mats fetch-weights --all # populates it once (from a data-transfer node)
# Pre-stage the verified checkpoint files in this directory.
mats doctor # confirm it resolves
```

Expand All @@ -96,10 +98,12 @@ excluded from the default clone and from a bare `git lfs pull` by `.lfsconfig`
place, not the 2.65 GB file. Pull it explicitly:

```bash
git lfs pull --include="weights/birefnet_leaf.pth"
git lfs pull -X "" -I "weights/birefnet_leaf.pth"
```

or `mats fetch-weights --only birefnet --source lfs`, or the setup page.
The empty `-X` value clears `.lfsconfig`'s exclusion for this invocation, and
`-I` limits the pull to the BiRefNet checkpoint.

This exclusion exists because committing BiRefNet without it would force
*every* `git clone` to download 2.65 GB and spend the repository's Git LFS
Expand All @@ -111,8 +115,9 @@ If your Git LFS version predates the exclusion behavior (needs the
`.lfsconfig` fetchexclude to be read from the repo index/HEAD during the
initial clone — true for modern Git LFS), a clone could pull BiRefNet anyway.
`GIT_LFS_SKIP_SMUDGE=1 git clone ...` is a guaranteed way to skip *all* LFS
content on clone if you want to be certain, then `git lfs pull --include=...`
each file you actually need.
content on clone if you want to be certain. Afterward, fetch RF-DETR with
`git lfs pull --exclude="weights/birefnet_leaf.pth"`; add BiRefNet later with
`git lfs pull -X "" -I "weights/birefnet_leaf.pth"` if needed.

## Manual / air-gapped

Expand Down
2 changes: 1 addition & 1 deletion src/mats/app/pages/2_BiRefNet_Setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def update(phase, completed, total):
"place, no download for anyone who can mount it.\n"
"- Or set `BIREFNET_CHECKPOINT` to an explicit checkpoint path.\n"
"- Or, from a terminal in a Git checkout: "
"`git lfs pull --include=\"weights/birefnet_leaf.pth\"`.\n"
"`git lfs pull -X \"\" -I \"weights/birefnet_leaf.pth\"`.\n"
"- On air-gapped systems, pre-stage the checkpoint and verify its SHA-256 before "
"launching MATS."
)
106 changes: 64 additions & 42 deletions src/mats/weights.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"""Install, verify and resolve the MATs model checkpoints.

The checkpoints are large (RF-DETR ~134 MB, BiRefNet ~2.65 GB) and are
delivered through two independent channels, plus a shared-filesystem escape
hatch -- :mod:`mats.paths` resolves to whichever channel produces a real file:

1. **Hugging Face Hub** -- the default public host. Free, no account needed,
but unreachable on some institutional networks (notably USDA's).
2. **Git LFS** -- By default, MATS will only pull the RF-DETR checkpoint file
when fetching weights via ``git-lfs pull``. To pull the larger BiRefNet file,
you can run ``mats fetch-weights --only birefnet --source lfs`` or just
``git-lfs pull``. This channel exists because Hugging Face is not reachable
from every collaborator's network.
delivered through Git LFS or an optional Hugging Face configuration, plus a
shared-filesystem escape hatch -- :mod:`mats.paths` resolves to whichever
channel produces a real file:

1. **Hugging Face Hub** -- available only when a MATS weights repository is
configured. It is free and needs no account, but is unreachable on some
institutional networks (notably USDA's).
2. **Git LFS** -- The default clone and pull exclude BiRefNet, so RF-DETR is
available without an automatic 2.65 GB download. Install BiRefNet explicitly
with ``mats fetch-weights --only birefnet --source lfs``. This channel exists
because Hugging Face is not reachable from every collaborator's network.
3. **A shared/mounted filesystem** (e.g. USDA SCINet ``/project``) -- point
``MATS_WEIGHTS_DIR`` at it and the weights are read in place, no download,
for anyone who can mount it.
Expand Down Expand Up @@ -205,7 +206,17 @@ def get_weight_status(name):

checkout = _checkout_target(name)
if checkout is not None and looks_like_lfs_pointer(checkout):
detail = "Not yet fetched via Git LFS -- fetch it via the app, `mats fetch-weights --only birefnet` or Hugging Face."
if name == "birefnet":
detail = (
"Excluded from the default Git LFS clone. Fetch it via the app or "
"`mats fetch-weights --only birefnet --source lfs`."
)
else:
detail = (
"Git LFS left an RF-DETR pointer instead of the checkpoint. Run "
"`git lfs install && git lfs pull --exclude=\"weights/birefnet_leaf.pth\"` "
"or `mats fetch-weights --only rf-detr --source lfs`."
)
return WeightStatus(name, checkout, "missing", detail, 0, spec["size_bytes"], sources)

target = _download_target(name)
Expand All @@ -220,18 +231,36 @@ def _emit(progress_callback, phase, completed, total):
progress_callback(phase, completed, total)


def _manual_instructions():
def _lfs_pull_args(name):
"""Return a Git LFS pull command that fetches only the intended weights."""
birefnet_rel = f"weights/{_MANIFEST['birefnet']['filename']}"
if name == "birefnet":
return ["git", "lfs", "pull", "-X", "", "-I", birefnet_rel]
return ["git", "lfs", "pull", "--exclude", birefnet_rel]


def _lfs_manual_command(name):
"""Return the shell form of the checkpoint-specific Git LFS repair."""
if name == "birefnet":
return 'git lfs pull -X "" -I "weights/birefnet_leaf.pth"'
return 'git lfs install && git lfs pull --exclude="weights/birefnet_leaf.pth"'


def _manual_instructions(name):
spec = _MANIFEST[name]
override = (
"RF_DETR_MARKER_CHECKPOINT" if name == "rf-detr" else "BIREFNET_CHECKPOINT"
)
print(
"No automatic download source is available in this build.\n\n"
"Get the checkpoints one of these ways:\n"
f" - Download them and place them here:\n"
f" {WEIGHTS_DIR / RF_DETR_MARKER_FILENAME}\n"
f" {WEIGHTS_DIR / BIREFNET_FILENAME}\n"
" - Or set MATS_WEIGHTS_DIR to a directory that already contains them\n"
f"Get {spec['filename']} one of these ways:\n"
" - Download it and place it here:\n"
f" {WEIGHTS_DIR / spec['filename']}\n"
" - Or set MATS_WEIGHTS_DIR to a directory that already contains it\n"
" (e.g. a shared SCINet /project path).\n"
" - Or set RF_DETR_MARKER_CHECKPOINT / BIREFNET_CHECKPOINT to specific files.\n"
f" - Or set {override} to the specific file.\n"
" - Or, from a Git checkout with Git LFS installed:\n"
" git lfs pull\n\n"
f" {_lfs_manual_command(name)}\n\n"
"See docs/weights.md.",
file=sys.stderr,
)
Expand Down Expand Up @@ -334,11 +363,9 @@ def _emit_lfs_progress(progress_path, progress_callback, fallback_total, last_do
def _download_from_lfs(name, progress_callback=None):
"""Fetch one checkpoint via Git LFS.

For BiRefNet, runs a plain ``git lfs pull`` (no flags) so the large
checkpoint is fetched without affecting other files. For all other
checkpoints, runs ``git lfs pull --exclude weights/birefnet_leaf.pth``
so the 2.65 GB BiRefNet file is never pulled as a side-effect of an
unrelated weight update.
BiRefNet clears the repository exclusion for one invocation and includes
only its checkpoint. All other checkpoints explicitly exclude BiRefNet so
it is never pulled as a side effect of an unrelated weight update.

Writes into the checkout's weights/ directory -- that's where Git LFS
smudges content, and it's tier 3 of paths.py's resolution order, so the
Expand All @@ -364,16 +391,10 @@ def _download_from_lfs(name, progress_callback=None):
rel_path = f"weights/{spec['filename']}"
print(f"Fetching {rel_path} via Git LFS -> {target}")

# For BiRefNet use a plain `git lfs pull` (no flags) -- without a
# fetchexclude in .lfsconfig a bare pull fetches all LFS files, which is
# what we want for this explicit opt-in download.
# For everything else, exclude the large BiRefNet checkpoint so it is
# never pulled as an unintended side-effect.
birefnet_rel = f"weights/{_MANIFEST['birefnet']['filename']}"
if name == "birefnet":
lfs_cmd = ["git", "lfs", "pull"]
else:
lfs_cmd = ["git", "lfs", "pull", "--exclude", birefnet_rel]
# `-X ""` clears .lfsconfig's exclusion for the explicit BiRefNet request;
# `-I` keeps that pull scoped to BiRefNet. Other requests explicitly
# exclude the large optional checkpoint under either repository setting.
lfs_cmd = _lfs_pull_args(name)

with tempfile.TemporaryDirectory() as tmp:
progress_path = Path(tmp) / "progress"
Expand Down Expand Up @@ -495,7 +516,7 @@ def fetch(only=None, force=False, source="auto"):
print(f"error: {by_id[source].label} is unavailable: {by_id[source].reason}",
file=sys.stderr)
else:
_manual_instructions()
_manual_instructions(name)
ok = False
continue

Expand Down Expand Up @@ -530,13 +551,13 @@ def ensure_weight(name):
raise FileNotFoundError(
f"{spec['filename']} not found and auto-fetch is disabled "
f"({_AUTO_FETCH_DISABLED} is set). Pre-stage the weights, or run "
f"`mats fetch-weights --only {name}` after unsetting {_AUTO_FETCH_DISABLED} "
f"(from a Git checkout, `git lfs pull` fetches all weights including birefnet, or "
f"`git lfs pull --exclude weights/{_MANIFEST['birefnet']['filename']}` fetches all others)."
f"`mats fetch-weights --only {name}` after unsetting "
f"{_AUTO_FETCH_DISABLED}. From a Git checkout with Git LFS, run "
f"`{_lfs_manual_command(name)}`."
)

# A pointer stub for BiRefNet means the user hasn't run `git lfs pull`
# for it yet (it's large and opt-in) -- fetch it rather than failing.
# A pointer stub means Git LFS has not materialized this checkpoint yet.
# Fetch the requested checkpoint rather than handing the stub to a model.
checkout = _checkout_target(name)
if checkout is not None and looks_like_lfs_pointer(checkout) and _download_from_lfs(name):
return checkout
Expand Down Expand Up @@ -570,8 +591,9 @@ def require_local_weight(name):
)
raise FileNotFoundError(
f"{_MANIFEST[name]['filename']} is not installed locally. "
f"BiRefNet is optional; install it explicitly with "
f"`mats fetch-weights --only {name} --source lfs`, or place it at {status.path}."
f"BiRefNet is optional; from a Git checkout with Git LFS, install it "
f"explicitly with `mats fetch-weights --only {name} --source lfs`, or "
f"place it at {status.path}."
)


Expand Down
4 changes: 3 additions & 1 deletion tests/test_home_app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pathlib import Path

import numpy as np
import pytest

# Every heavy import is gated: CI installs with `--no-deps`, so a bare
# module-level `import numpy` here is a collection error, not a skip.
np = pytest.importorskip("numpy")
pd = pytest.importorskip("pandas")
pytest.importorskip("streamlit")
cv2 = pytest.importorskip("cv2")
Expand Down
Loading
Loading