Instructions to use phi-lab-rice/GRADE with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use phi-lab-rice/GRADE with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("phi-lab-rice/GRADE", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Download evaluation/utils/robustness_range_precompute.py from phi-lab-rice/GRADE: direct link, hf CLI and curl.
- Browser
- Download file 13.2 kB
-
https://huggingface.co/phi-lab-rice/GRADE/resolve/main/evaluation/utils/robustness_range_precompute.py
- Command line
-
hf download hf://phi-lab-rice/GRADE/evaluation/utils/robustness_range_precompute.py
-
curl -L -o robustness_range_precompute.py https://huggingface.co/phi-lab-rice/GRADE/resolve/main/evaluation/utils/robustness_range_precompute.py
13.2 kB
| """Precompute the pixel-level absolute-error distribution against ground-truth | |
| depth, as a fine-grained histogram that coarser bin widths are derived from. | |
| The unit of analysis is the *pixel*, not the frame. Every valid ground-truth | |
| pixel in the test set is assigned to a 20 cm ground-truth-depth bin by its own | |
| depth value, and its absolute error is recorded there. Frame identity plays no | |
| part: a frame contributes as many samples to a bin as it has pixels in that | |
| depth range, and a frame contributing three pixels is not upweighted to parity | |
| with one contributing five thousand. This is the discretised form of the | |
| underlying scatter of per-pixel error against per-pixel ground-truth depth. | |
| Storage. The population is ~3.8e9 pixels per model, far too large to keep as a | |
| list of errors (~7.6 GB as int16 millimetres) merely to take a quantile from | |
| it. Each 20 cm bin therefore stores a histogram of its errors in 1 mm buckets. | |
| Ground truth is recorded in millimetres and both prediction and ground truth | |
| are clamped to MAX_DEPTH, so errors are integers bounded by 11200 mm and the | |
| histogram is a *lossless* stand-in for the sample multiset -- it discards only | |
| which pixel produced which error. Cost: 56 x 11201 int64, ~5 MB per model. | |
| Recombination. Histograms add. A coarser bin is the element-wise sum of its | |
| constituent 20 cm rows, and quantiles read off that sum are exactly the | |
| quantiles of the pooled pixel population, not an approximation of them. Stored | |
| summary statistics could never be merged that way -- there is no way to | |
| combine two medians. robustness_range_summarize.py does the collapse. | |
| Two error histograms are stored per depth bin: absolute error in 1 mm buckets, | |
| and AbsRel (|pred-gt|/gt) in 0.001 buckets. AbsRel is *not* redundant with | |
| MAE/bin_centre once bins are merged: inside a merged 0-2 m bin ground truth | |
| spans an order of magnitude, so a fixed absolute error is a wildly different | |
| relative error at either end, and only the per-pixel ratio captures that. | |
| Outputs: | |
| data/robustness/range_hist_20cm_{variant}.npz | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| SCRIPT_DIR = Path(__file__).resolve().parent | |
| EVALUATION_DIR = SCRIPT_DIR.parent | |
| if str(EVALUATION_DIR) not in sys.path: | |
| sys.path.insert(0, str(EVALUATION_DIR)) | |
| # This import follows the explicit package-root setup above. | |
| from utils import eval_compute_simple as E # noqa: E402 | |
| DEFAULT_DATA_ROOT = EVALUATION_DIR.parent / "evaluation_dataset" / "Smoke-Eval" | |
| DEFAULT_RESULTS_ROOT = EVALUATION_DIR / "outputs" / "inference" | |
| # This optional, expensive precomputation always writes newly derived data. | |
| # It must never target evaluation/reference_results/. | |
| DATA_DIR = EVALUATION_DIR / "metric_results" / "radar_robustness" | |
| # Only the three series the range figure draws. The per-frame tables for the | |
| # other baselines are left untouched in data/robustness/; nothing here reads | |
| # or overwrites them. | |
| MODELS = [ | |
| ("ours_radar", "Ours_radar"), | |
| ("ours_diffusion", "Ours_diffusion"), | |
| ("ours_full", "GRADE"), | |
| ] | |
| MAX_DEPTH_MM = int(round(E.MAX_DEPTH * 1000.0)) # 11200 | |
| BATCH_SIZE = 16 | |
| # Base ground-truth depth resolution. Any reported bin width must be a | |
| # multiple of this and is built by summing consecutive base bins. | |
| BASE_BIN_MM = 200 # 20 cm | |
| N_GT_BINS = MAX_DEPTH_MM // BASE_BIN_MM # 56, covering (0, 11.2] m exactly | |
| # Error histogram resolution. Both prediction and ground truth are clamped to | |
| # MAX_DEPTH, so |pred - gt| cannot exceed it; bucket k counts errors rounding | |
| # to k millimetres, and the last bucket is a real value, not a catch-all. | |
| N_ERR_BUCKETS = MAX_DEPTH_MM + 1 # 0..11200 mm inclusive | |
| # AbsRel histogram. Unlike the absolute error, |pred-gt|/gt has no natural | |
| # bound: a 0.1 m error is AbsRel 0.05 at 2 m but 10.0 at 1 cm, so the near-zero | |
| # ground-truth tail can push it arbitrarily high. Buckets are 0.001 wide up to | |
| # ABSREL_MAX, and the final bucket collects everything above it. Quantiles that | |
| # land in that bucket are reported as NaN rather than as ABSREL_MAX, so a | |
| # saturated statistic can never be mistaken for a measured one. | |
| ABSREL_BUCKET = 0.001 | |
| ABSREL_MAX = 10.0 | |
| N_ABSREL_BUCKETS = int(round(ABSREL_MAX / ABSREL_BUCKET)) + 1 # 10001 | |
| def sequence_names(data_root: Path, selected: list[str] | None) -> list[str]: | |
| sequences = sorted( | |
| path.name | |
| for path in data_root.iterdir() | |
| if path.is_dir() and (path / "zed_depth.npy").is_file() | |
| ) | |
| if selected: | |
| requested = set(selected) | |
| sequences = [name for name in sequences if name in requested] | |
| return sequences | |
| def compute_model_histogram( | |
| model_dir: str, | |
| label: str, | |
| data_root: Path, | |
| results_root: Path, | |
| sequences: list[str], | |
| device: torch.device, | |
| ) -> dict[str, np.ndarray]: | |
| """Accumulate the (gt_bin, error_bucket) counts over every valid pixel. | |
| Counts are kept per sequence rather than only in aggregate. Pixels within a | |
| sequence are heavily correlated -- a billion pixels come from ~26k frames of | |
| a few corridors -- so a bootstrap that resamples pixels would report | |
| absurdly tight intervals. Keeping the sequence axis allows a cluster | |
| bootstrap over sequences, which is the honest unit of independence here. | |
| Cost is one histogram per sequence, ~5 MB each. | |
| """ | |
| # Held flat so a single bincount per batch does all the accumulation. The | |
| # alternative -- one boolean mask per bin -- is 56 full passes over every | |
| # batch instead of one. | |
| histogram = torch.zeros( | |
| N_GT_BINS * N_ERR_BUCKETS, dtype=torch.int64, device=device | |
| ) | |
| # Exact running error total per bin, kept alongside the counts so the | |
| # reported mean does not inherit the 1 mm bucket rounding. | |
| error_sum_mm = torch.zeros(N_GT_BINS, dtype=torch.float64, device=device) | |
| absrel_histogram = torch.zeros( | |
| N_GT_BINS * N_ABSREL_BUCKETS, dtype=torch.int64, device=device | |
| ) | |
| absrel_sum = torch.zeros(N_GT_BINS, dtype=torch.float64, device=device) | |
| per_sequence: list[np.ndarray] = [] | |
| per_sequence_absrel: list[np.ndarray] = [] | |
| kept_sequences: list[str] = [] | |
| scale = E.PRED_SCALE[model_dir] | |
| for sequence in sequences: | |
| sequence_start = histogram.clone() | |
| absrel_start = absrel_histogram.clone() | |
| sequence_dir = data_root / sequence | |
| try: | |
| gt_raw = E.load_zed_depth(sequence_dir) | |
| predictions = E.load_model_predictions(model_dir, sequence, results_root) | |
| gt_index = E.load_pred_frame_index(model_dir, sequence, results_root) | |
| except FileNotFoundError as exc: | |
| print(f"SKIP {model_dir}/{sequence}: {exc}") | |
| continue | |
| # Same common scoring grid as eval_compute_simple. Without it a model's | |
| # pixel count -- and so its weight in the pooled population -- would | |
| # depend on its native output resolution rather than on its accuracy. | |
| (target_h, target_w), _ = E.eval_resolution( | |
| preds=predictions, model=model_dir, seq_name=sequence | |
| ) | |
| if gt_index is not None: | |
| frame_count = min(len(predictions), len(gt_index)) | |
| indices = gt_index[:frame_count] | |
| else: | |
| frame_count = min(len(gt_raw), len(predictions)) | |
| indices = np.arange(frame_count, dtype=np.int64) | |
| for start in range(0, frame_count, BATCH_SIZE): | |
| end = min(start + BATCH_SIZE, frame_count) | |
| selected = indices[start:end] | |
| raw = np.ascontiguousarray(gt_raw[selected], dtype=np.float32) | |
| gt_mm = torch.from_numpy(raw).to(device).unsqueeze(1) | |
| gt_mm = E.resize_depth_gpu(gt_mm, target_h, target_w) | |
| valid = (gt_mm > 0) & (gt_mm <= MAX_DEPTH_MM) | |
| prediction_raw = np.asarray(predictions[start:end]) | |
| if prediction_raw.ndim == 4 and prediction_raw.shape[1] == 1: | |
| prediction_raw = prediction_raw[:, 0] | |
| prediction_m = E.pred_batch_to_gpu(prediction_raw, scale, device) | |
| prediction_m = E.resize_depth_gpu(prediction_m, target_h, target_w) | |
| error_mm = (prediction_m * 1000.0 - gt_mm).abs() | |
| # ceil(gt / width) - 1 gives half-open (low, high] bins, matching | |
| # the convention the earlier tables used: a pixel at exactly | |
| # 200 mm belongs to bin 0, (0, 20] cm, not to bin 1. | |
| gt_bin = torch.ceil(gt_mm / BASE_BIN_MM).long() - 1 | |
| gt_bin.clamp_(0, N_GT_BINS - 1) | |
| error_bucket = error_mm.round().long().clamp_(0, N_ERR_BUCKETS - 1) | |
| # gt_mm is strictly positive wherever valid is set, so this ratio | |
| # never divides by zero. | |
| absrel = error_mm / gt_mm.clamp(min=1.0) | |
| absrel_bucket = (absrel / ABSREL_BUCKET).round().long() | |
| absrel_bucket.clamp_(0, N_ABSREL_BUCKETS - 1) | |
| flat_bin = gt_bin[valid] | |
| histogram += torch.bincount( | |
| flat_bin * N_ERR_BUCKETS + error_bucket[valid], | |
| minlength=N_GT_BINS * N_ERR_BUCKETS, | |
| ) | |
| error_sum_mm.scatter_add_( | |
| 0, flat_bin, error_mm[valid].to(torch.float64) | |
| ) | |
| absrel_histogram += torch.bincount( | |
| flat_bin * N_ABSREL_BUCKETS + absrel_bucket[valid], | |
| minlength=N_GT_BINS * N_ABSREL_BUCKETS, | |
| ) | |
| absrel_sum.scatter_add_(0, flat_bin, absrel[valid].to(torch.float64)) | |
| per_sequence.append( | |
| (histogram - sequence_start) | |
| .reshape(N_GT_BINS, N_ERR_BUCKETS).cpu().numpy() | |
| ) | |
| per_sequence_absrel.append( | |
| (absrel_histogram - absrel_start) | |
| .reshape(N_GT_BINS, N_ABSREL_BUCKETS).cpu().numpy() | |
| ) | |
| kept_sequences.append(sequence) | |
| print(f"{label:12s} {sequence} done") | |
| counts = histogram.reshape(N_GT_BINS, N_ERR_BUCKETS).cpu().numpy() | |
| absrel_counts = absrel_histogram.reshape( | |
| N_GT_BINS, N_ABSREL_BUCKETS | |
| ).cpu().numpy() | |
| return { | |
| "counts": counts, | |
| "counts_by_sequence": np.stack(per_sequence) if per_sequence | |
| else np.zeros((0, N_GT_BINS, N_ERR_BUCKETS), dtype=np.int64), | |
| "absrel_counts": absrel_counts, | |
| "absrel_counts_by_sequence": np.stack(per_sequence_absrel) | |
| if per_sequence_absrel | |
| else np.zeros((0, N_GT_BINS, N_ABSREL_BUCKETS), dtype=np.int64), | |
| "sequences": np.array(kept_sequences), | |
| "error_sum_mm": error_sum_mm.cpu().numpy(), | |
| "absrel_sum": absrel_sum.cpu().numpy(), | |
| "pixel_count": counts.sum(axis=1), | |
| } | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Precompute the pixel-level error histogram against " | |
| "ground-truth depth, in 20 cm depth bins." | |
| ) | |
| parser.add_argument( | |
| "--model", nargs="+", choices=[name for name, _ in MODELS], | |
| default=[name for name, _ in MODELS], | |
| help="Restrict computation to these models (default: all three).", | |
| ) | |
| parser.add_argument("--sequence", nargs="+") | |
| parser.add_argument("--data-root", type=Path, default=DEFAULT_DATA_ROOT) | |
| parser.add_argument("--results-root", type=Path, default=DEFAULT_RESULTS_ROOT) | |
| parser.add_argument("--data-dir", type=Path, default=DATA_DIR) | |
| return parser.parse_args() | |
| def main() -> None: | |
| args = parse_args() | |
| selected_models = [item for item in MODELS if item[0] in args.model] | |
| sequences = sequence_names(args.data_root, args.sequence) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print( | |
| f"Device: {device}; sequences: {len(sequences)}; " | |
| f"models: {[m for m, _ in selected_models]}; " | |
| f"{N_GT_BINS} x {BASE_BIN_MM}mm depth bins" | |
| ) | |
| args.data_dir.mkdir(parents=True, exist_ok=True) | |
| edges_mm = np.arange(N_GT_BINS + 1, dtype=np.int64) * BASE_BIN_MM | |
| for model_dir, label in selected_models: | |
| result = compute_model_histogram( | |
| model_dir, label, args.data_root, args.results_root, | |
| sequences, device, | |
| ) | |
| # One file per model, so recomputing one never disturbs the other. | |
| output = args.data_dir / f"range_hist_20cm_{model_dir}.npz" | |
| np.savez_compressed( | |
| output, | |
| counts=result["counts"], | |
| counts_by_sequence=result["counts_by_sequence"], | |
| absrel_counts=result["absrel_counts"], | |
| absrel_counts_by_sequence=result["absrel_counts_by_sequence"], | |
| sequences=result["sequences"], | |
| error_sum_mm=result["error_sum_mm"], | |
| absrel_sum=result["absrel_sum"], | |
| pixel_count=result["pixel_count"], | |
| edges_mm=edges_mm, | |
| base_bin_mm=np.int64(BASE_BIN_MM), | |
| max_depth_mm=np.int64(MAX_DEPTH_MM), | |
| absrel_bucket=np.float64(ABSREL_BUCKET), | |
| absrel_max=np.float64(ABSREL_MAX), | |
| model=np.str_(label), | |
| variant=np.str_(model_dir), | |
| ) | |
| total = int(result["pixel_count"].sum()) | |
| print(f"Saved {output} ({total:,} valid pixels)") | |
| if __name__ == "__main__": | |
| main() | |