From 69df7215dfbed62fe82fb9bbb50f1b9eb952b44f Mon Sep 17 00:00:00 2001 From: bw4sz Date: Wed, 13 May 2026 10:43:42 -0700 Subject: [PATCH 1/2] add classifier metadata workflow Co-authored-by: Cursor --- boem_conf/classification_model/USGS.yaml | 6 +- boem_conf/classification_model/finetune.yaml | 6 +- pyproject.toml | 1 + src/active_learning.py | 3 + src/classification.py | 68 +++- src/detection.py | 54 +++- src/pipeline.py | 42 ++- src/spatiotemporal_metadata.py | 129 ++++++++ tests/test_classification.py | 4 +- tests/test_spatiotemporal_metadata.py | 66 ++++ uv.lock | 308 ++++++++++++++++--- 11 files changed, 623 insertions(+), 64 deletions(-) create mode 100644 src/spatiotemporal_metadata.py create mode 100644 tests/test_spatiotemporal_metadata.py diff --git a/boem_conf/classification_model/USGS.yaml b/boem_conf/classification_model/USGS.yaml index 4471c2c..82373d3 100644 --- a/boem_conf/classification_model/USGS.yaml +++ b/boem_conf/classification_model/USGS.yaml @@ -10,4 +10,8 @@ lr: 0.00001 batch_size: 96 workers: 8 expand: 30 -balance_classes: false \ No newline at end of file +balance_classes: false +use_metadata: false +metadata_dim: 32 +metadata_dropout: 0.5 +# metadata_dir defaults to report.metadata_dir when use_metadata is true. diff --git a/boem_conf/classification_model/finetune.yaml b/boem_conf/classification_model/finetune.yaml index b41a63e..a90c9a4 100644 --- a/boem_conf/classification_model/finetune.yaml +++ b/boem_conf/classification_model/finetune.yaml @@ -10,4 +10,8 @@ lr: 0.00001 batch_size: 72 workers: 5 expand: 30 -balance_classes: false \ No newline at end of file +balance_classes: false +use_metadata: false +metadata_dim: 32 +metadata_dropout: 0.5 +# metadata_dir defaults to report.metadata_dir when use_metadata is true. diff --git a/pyproject.toml b/pyproject.toml index 19b80f1..fbe92c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ override-dependencies = [ ] [tool.uv.sources] +deepforest = { git = "https://github.com/weecology/DeepForest.git", rev = "b37826f0d35adc535e53344b462d996b042401e4" } torch = [ { index = "pytorch-cu128", marker = "sys_platform != 'darwin'" }, ] diff --git a/src/active_learning.py b/src/active_learning.py index b5a459c..8aae005 100644 --- a/src/active_learning.py +++ b/src/active_learning.py @@ -81,6 +81,7 @@ def generate_pool_predictions( hcast_batch_size=None, hcast_workers=None, workers=0, + metadata_lookup=None, ): """ Generate predictions for the flight pool. @@ -99,6 +100,7 @@ def generate_pool_predictions( hcast_batch_size (int, optional): Batch size for H-CAST classification. hcast_workers (int, optional): Number of workers for H-CAST DataLoader. workers (int, optional): Number of DataLoader workers for detection. Defaults to 0. + metadata_lookup (dict, optional): Per-image metadata for metadata-aware CropModel inference. Returns: pd.DataFrame: A DataFrame of predictions (with hcast columns if hcast_model provided). @@ -115,6 +117,7 @@ def generate_pool_predictions( batch_size=batch_size, crop_model=crop_model, workers=workers, + metadata_lookup=metadata_lookup, ) if preannotations is None: diff --git a/src/classification.py b/src/classification.py index bd27ef6..00d25c5 100644 --- a/src/classification.py +++ b/src/classification.py @@ -9,6 +9,8 @@ import datetime import pandas as pd +from src.spatiotemporal_metadata import build_crop_metadata_rows + def get_latest_checkpoint(checkpoint_dir, num_classes): #Get model with latest checkpoint dir, if none exist make a new model if os.path.exists(checkpoint_dir): @@ -128,6 +130,11 @@ def preprocess_and_train( workers=0, comet_logger=None, balance_classes=False, + use_metadata=False, + metadata_dir=None, + flight_name=None, + metadata_dim=32, + metadata_dropout=0.5, **kwargs, ): """Preprocess data and train a crop model. @@ -144,9 +151,27 @@ def preprocess_and_train( loaded_model = CropModel.load_from_checkpoint(checkpoint_path=checkpoint) else: num_classes = len(pd.concat([train_df, validation_df]).label.unique()) - loaded_model = CropModel() + if use_metadata: + try: + loaded_model = CropModel(config_args={ + "use_metadata": True, + "metadata_dim": metadata_dim, + "metadata_dropout": metadata_dropout, + }) + except TypeError as exc: + raise TypeError( + "classification_model.use_metadata requires DeepForest PR #1334 " + "or a DeepForest release with CropModel(config_args=...)." + ) from exc + else: + loaded_model = CropModel() loaded_model.config["cropmodel"]["balance_classes"] = balance_classes + if use_metadata and not loaded_model.config["cropmodel"].get("use_metadata", False): + raise ValueError( + "classification_model.use_metadata=True requires training from scratch " + "or loading a metadata-enabled CropModel checkpoint." + ) loaded_model.create_trainer() # Preprocess train and validation data @@ -166,9 +191,44 @@ def preprocess_and_train( expand_pixels=expand_pixels, ) - loaded_model.load_from_disk( - train_dir=str(train_crop_image_dir), val_dir=str(val_crop_image_dir) - ) + metadata_csv = None + if use_metadata: + if metadata_dir is None: + raise ValueError( + "classification_model.use_metadata=True requires report.metadata_dir " + "or classification_model.metadata_dir" + ) + if flight_name is None: + flight_name = os.path.basename(os.path.normpath(image_dir)) + metadata_rows = pd.concat( + [ + build_crop_metadata_rows(train_df, metadata_dir, flight_name), + build_crop_metadata_rows(validation_df, metadata_dir, flight_name), + ], + ignore_index=True, + ).drop_duplicates(subset=["filename"]) + if metadata_rows.empty: + raise ValueError(f"No crop metadata rows were created for flight {flight_name}") + metadata_csv = os.path.join(checkpoint_dir, "classification_crop_metadata.csv") + os.makedirs(checkpoint_dir, exist_ok=True) + metadata_rows.to_csv(metadata_csv, index=False) + print(f"[preprocess_and_train] wrote crop metadata sidecar {metadata_csv}") + + load_kwargs = { + "train_dir": str(train_crop_image_dir), + "val_dir": str(val_crop_image_dir), + } + if metadata_csv is not None: + load_kwargs["metadata_csv"] = metadata_csv + try: + loaded_model.load_from_disk(**load_kwargs) + except TypeError as exc: + if metadata_csv is not None: + raise TypeError( + "classification_model.use_metadata requires DeepForest PR #1334 " + "or a DeepForest release with CropModel.load_from_disk(..., metadata_csv=...)." + ) from exc + raise # Assert that the label_dict, the train_ds classes and val_ds classes are the same assert loaded_model.label_dict == loaded_model.train_ds.class_to_idx diff --git a/src/detection.py b/src/detection.py index f27e224..40fc222 100644 --- a/src/detection.py +++ b/src/detection.py @@ -293,7 +293,16 @@ def get_latest_checkpoint(checkpoint_dir): -def predict(m, image_paths, patch_size, patch_overlap, crop_model=None, batch_size=6, workers=5): +def predict( + m, + image_paths, + patch_size, + patch_overlap, + crop_model=None, + batch_size=6, + workers=5, + metadata_lookup=None, +): """Predict bounding boxes for images Args: m (main.deepforest): A trained deepforest model. @@ -301,6 +310,7 @@ def predict(m, image_paths, patch_size, patch_overlap, crop_model=None, batch_si crop_model (main.deepforest): A trained deepforest model for classification. model_path (str): The path to a model checkpoint. batch_size (int): The batch size for prediction. + metadata_lookup (dict): Optional basename/stem keyed metadata for CropModel. Returns: list: A list of image predictions. """ @@ -308,13 +318,41 @@ def predict(m, image_paths, patch_size, patch_overlap, crop_model=None, batch_si m.config["batch_size"] = batch_size m.config["workers"] = workers - predictions = m.predict_tile( - image_paths, - patch_size=patch_size, - patch_overlap=patch_overlap, - dataloader_strategy="batch", - crop_model=crop_model, - ) + if metadata_lookup: + predictions = [] + for image_path in image_paths: + basename = os.path.basename(str(image_path)) + stem = os.path.splitext(basename)[0] + metadata = metadata_lookup.get(basename) or metadata_lookup.get(stem) + predict_kwargs = { + "path": [image_path], + "patch_size": patch_size, + "patch_overlap": patch_overlap, + "dataloader_strategy": "batch", + "crop_model": crop_model, + } + if metadata is not None: + predict_kwargs["metadata"] = metadata + try: + image_predictions = m.predict_tile(**predict_kwargs) + except TypeError as exc: + if metadata is not None: + raise TypeError( + "metadata_lookup requires DeepForest PR #1334 or a " + "DeepForest release with predict_tile(..., metadata=...)." + ) from exc + raise + if image_predictions is not None: + predictions.append(image_predictions) + predictions = pd.concat(predictions, ignore_index=True) if predictions else None + else: + predictions = m.predict_tile( + image_paths, + patch_size=patch_size, + patch_overlap=patch_overlap, + dataloader_strategy="batch", + crop_model=crop_model, + ) if predictions is None: return None diff --git a/src/pipeline.py b/src/pipeline.py index 8158cc0..e152657 100644 --- a/src/pipeline.py +++ b/src/pipeline.py @@ -16,6 +16,7 @@ from src.report import generate_report, load_geospatial_metadata, georeference_predictions from src import bulk_annotations as bulk_mod from src.pipeline_evaluation import PipelineEvaluation +from src.spatiotemporal_metadata import metadata_lookup_for_images from pytorch_lightning.loggers import CometLogger import glob import pandas as pd @@ -57,6 +58,37 @@ def _comet_experiment_id(self) -> str: eid = self.comet_logger.experiment.id return eid() if callable(eid) else eid + def _classification_metadata_dir(self): + cls_metadata_dir = getattr(self.config.classification_model, "metadata_dir", None) + if cls_metadata_dir: + return cls_metadata_dir + report_cfg = getattr(self.config, "report", None) + return getattr(report_cfg, "metadata_dir", None) if report_cfg else None + + def _use_classification_metadata(self) -> bool: + return bool(getattr(self.config.classification_model, "use_metadata", False)) + + def _metadata_lookup_for_pool(self, pool): + if not self._use_classification_metadata(): + return None + metadata_dir = self._classification_metadata_dir() + if not metadata_dir: + raise ValueError( + "classification_model.use_metadata=True requires report.metadata_dir " + "or classification_model.metadata_dir" + ) + lookup = metadata_lookup_for_images( + image_paths=pool, + metadata_dir=metadata_dir, + default_flight_name=os.path.basename(self.config.image_dir), + ) + if pool and not lookup: + raise ValueError( + "classification_model.use_metadata=True but no image metadata " + f"matched flight {os.path.basename(self.config.image_dir)}" + ) + return lookup + def check_new_annotations(self, instance_name): return self.annotator.check_for_new_annotations(instance_name, image_dir=self.config.image_dir) @@ -204,6 +236,7 @@ def upload_full_flight(self, project_name=None): hcast_batch_size=hcast_batch_size, hcast_workers=hcast_workers, workers=self.config.predict.workers, + metadata_lookup=self._metadata_lookup_for_pool(unannotated), ) if pool_predictions is not None and not pool_predictions.empty: pool_predictions.to_csv(full_flight_cache, index=False) @@ -343,7 +376,13 @@ def run(self): lr=self.config.classification_model.lr, batch_size=self.config.classification_model.batch_size, workers=self.config.classification_model.workers, - comet_logger=self.comet_logger) + comet_logger=self.comet_logger, + balance_classes=getattr(self.config.classification_model, "balance_classes", False), + use_metadata=self._use_classification_metadata(), + metadata_dir=self._classification_metadata_dir(), + flight_name=os.path.basename(self.config.image_dir), + metadata_dim=getattr(self.config.classification_model, "metadata_dim", 32), + metadata_dropout=getattr(self.config.classification_model, "metadata_dropout", 0.5)) else: trained_classification_model = CropModel.load_from_checkpoint(self.config.classification_model.checkpoint) else: @@ -431,6 +470,7 @@ def run(self): hcast_batch_size=hcast_batch_size, hcast_workers=hcast_workers, workers=self.config.predict.workers, + metadata_lookup=self._metadata_lookup_for_pool(prediction_pool), ) if flightline_predictions is None: diff --git a/src/spatiotemporal_metadata.py b/src/spatiotemporal_metadata.py new file mode 100644 index 0000000..f556a7e --- /dev/null +++ b/src/spatiotemporal_metadata.py @@ -0,0 +1,129 @@ +import os +from pathlib import Path +from typing import Iterable + +import pandas as pd + + +def flight_datetime_key(flight_name: str) -> str: + """Strip JPG_ prefix to get the metadata CSV key for a flight.""" + key = str(flight_name) + return key[4:] if key.startswith("JPG_") else key + + +def flight_date(flight_name: str) -> str: + """Parse YYYY-MM-DD from flight names like JPG_20241220_104800.""" + key = flight_datetime_key(flight_name) + if len(key) >= 8 and key[:8].isdigit(): + return f"{key[:4]}-{key[4:6]}-{key[6:8]}" + return "" + + +def _image_stem(image_path: str) -> str: + return os.path.splitext(os.path.basename(str(image_path)))[0] + + +def load_flight_metadata(flight_name: str, metadata_dir: str | Path) -> dict[str, dict]: + """Return image-stem keyed metadata dicts for one flight.""" + metadata_dir = Path(metadata_dir) + captures_path = metadata_dir / f"{flight_datetime_key(flight_name)}_captures.csv" + captures = pd.read_csv(captures_path) + required = {"Basename", "Lat", "Lon"} + missing = required - set(captures.columns) + if missing: + raise ValueError(f"{captures_path} missing required columns: {sorted(missing)}") + + date = flight_date(flight_name) + rows = captures.drop_duplicates(subset=["Basename"]) + return { + str(row.Basename): { + "lat": float(row.Lat), + "lon": float(row.Lon), + "date": date, + } + for row in rows.itertuples(index=False) + } + + +def metadata_for_image( + image_path: str, + metadata_dir: str | Path, + flight_name: str, + cache: dict[str, dict[str, dict]] | None = None, +) -> dict | None: + """Look up spatial-temporal metadata for an image path.""" + cache = cache if cache is not None else {} + if flight_name not in cache: + cache[flight_name] = load_flight_metadata(flight_name, metadata_dir) + return cache[flight_name].get(_image_stem(image_path)) + + +def build_crop_metadata_rows( + annotations: pd.DataFrame, + metadata_dir: str | Path, + default_flight_name: str, +) -> pd.DataFrame: + """Build DeepForest CropModel metadata rows for crops written by classification.write_crops.""" + cache: dict[str, dict[str, dict]] = {} + rows = [] + for crop_index, row in enumerate(annotations.itertuples(index=False)): + image_path = getattr(row, "image_path") + flight_name = getattr(row, "flight_name", default_flight_name) + if pd.isna(flight_name) or not flight_name: + flight_name = default_flight_name + metadata = metadata_for_image( + image_path=image_path, + metadata_dir=metadata_dir, + flight_name=str(flight_name), + cache=cache, + ) + if metadata is None or not metadata["date"]: + continue + rows.append({ + "filename": f"{_image_stem(image_path)}_{crop_index}.png", + "lat": metadata["lat"], + "lon": metadata["lon"], + "date": metadata["date"], + }) + return pd.DataFrame(rows, columns=["filename", "lat", "lon", "date"]) + + +def write_crop_metadata_csv( + annotations: pd.DataFrame, + metadata_dir: str | Path, + default_flight_name: str, + output_csv: str | Path, +) -> str: + """Write the DeepForest metadata sidecar CSV for classification crops.""" + rows = build_crop_metadata_rows(annotations, metadata_dir, default_flight_name) + if rows.empty: + raise ValueError( + "No crop metadata rows were created. Check report.metadata_dir and " + f"captures metadata for flight {default_flight_name}." + ) + output_csv = Path(output_csv) + output_csv.parent.mkdir(parents=True, exist_ok=True) + rows.to_csv(output_csv, index=False) + return str(output_csv) + + +def metadata_lookup_for_images( + image_paths: Iterable[str], + metadata_dir: str | Path, + default_flight_name: str, +) -> dict[str, dict]: + """Return basename/stem keyed metadata for prediction images.""" + cache: dict[str, dict[str, dict]] = {} + lookup = {} + for image_path in image_paths: + metadata = metadata_for_image( + image_path=image_path, + metadata_dir=metadata_dir, + flight_name=default_flight_name, + cache=cache, + ) + if metadata is None: + continue + lookup[os.path.basename(str(image_path))] = metadata + lookup[_image_stem(str(image_path))] = metadata + return lookup diff --git a/tests/test_classification.py b/tests/test_classification.py index 3a03d7e..a377efa 100644 --- a/tests/test_classification.py +++ b/tests/test_classification.py @@ -23,7 +23,9 @@ def sample_annotations(): def sample_model(): from deepforest.model import CropModel - return CropModel(num_classes=2) + model = CropModel() + model.create_model(num_classes=2) + return model def test_preprocess_images(sample_model, sample_annotations, tmp_path): diff --git a/tests/test_spatiotemporal_metadata.py b/tests/test_spatiotemporal_metadata.py new file mode 100644 index 0000000..03c7cd7 --- /dev/null +++ b/tests/test_spatiotemporal_metadata.py @@ -0,0 +1,66 @@ +import pandas as pd + +from src.spatiotemporal_metadata import ( + build_crop_metadata_rows, + flight_date, + metadata_lookup_for_images, +) + + +def test_flight_date_from_jpg_flight_name(): + assert flight_date("JPG_20241220_104800") == "2024-12-20" + + +def test_build_crop_metadata_rows(tmp_path): + metadata_dir = tmp_path / "metadata" + metadata_dir.mkdir() + pd.DataFrame({ + "Basename": ["C1_L1_F0001_T20241220_104801_000"], + "Lat": [28.5], + "Lon": [-89.25], + }).to_csv(metadata_dir / "20241220_104800_captures.csv", index=False) + + annotations = pd.DataFrame({ + "image_path": ["C1_L1_F0001_T20241220_104801_000.jpg"], + "label": ["genus species"], + "xmin": [10], + "ymin": [10], + "xmax": [20], + "ymax": [20], + }) + + rows = build_crop_metadata_rows( + annotations=annotations, + metadata_dir=metadata_dir, + default_flight_name="JPG_20241220_104800", + ) + + assert rows.to_dict(orient="records") == [{ + "filename": "C1_L1_F0001_T20241220_104801_000_0.png", + "lat": 28.5, + "lon": -89.25, + "date": "2024-12-20", + }] + + +def test_metadata_lookup_for_images(tmp_path): + metadata_dir = tmp_path / "metadata" + metadata_dir.mkdir() + pd.DataFrame({ + "Basename": ["ATL_0001"], + "Lat": [38.1], + "Lon": [-74.2], + }).to_csv(metadata_dir / "20250105_090000_captures.csv", index=False) + + lookup = metadata_lookup_for_images( + image_paths=["/tmp/ATL_0001.jpg"], + metadata_dir=metadata_dir, + default_flight_name="JPG_20250105_090000", + ) + + assert lookup["ATL_0001.jpg"] == { + "lat": 38.1, + "lon": -74.2, + "date": "2025-01-05", + } + assert lookup["ATL_0001"] == lookup["ATL_0001.jpg"] diff --git a/uv.lock b/uv.lock index 40f7cd3..8472030 100644 --- a/uv.lock +++ b/uv.lock @@ -436,7 +436,7 @@ requires-dist = [ { name = "contextily" }, { name = "dask" }, { name = "dask-jobqueue" }, - { name = "deepforest" }, + { name = "deepforest", git = "https://github.com/weecology/DeepForest.git?rev=b37826f0d35adc535e53344b462d996b042401e4" }, { name = "dotenv" }, { name = "folium" }, { name = "geopandas" }, @@ -1175,7 +1175,7 @@ wheels = [ [[package]] name = "deepforest" version = "2.1.0" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/weecology/DeepForest.git?rev=b37826f0d35adc535e53344b462d996b042401e4#b37826f0d35adc535e53344b462d996b042401e4" } dependencies = [ { name = "aiohttp" }, { name = "aiolimiter" }, @@ -1199,6 +1199,8 @@ dependencies = [ { name = "rasterio", version = "1.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "rasterio", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "safetensors" }, + { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "setuptools" }, { name = "shapely" }, { name = "slidingwindow" }, @@ -1213,10 +1215,6 @@ dependencies = [ { name = "transformers" }, { name = "xmltodict" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/1e/3057d557a92ec20b9e1d2f86f21753614c78cb048b708ee1581b077b2e44/deepforest-2.1.0.tar.gz", hash = "sha256:fa224f3ae3c099876296f3b821de759a3be780162a19d49b42b313e0fc328d45", size = 20636788, upload-time = "2026-02-26T12:30:59.963Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/a8/67e66685c81ec070372db546f933312c6d7401f845e200e5d900107ff51e/deepforest-2.1.0-py3-none-any.whl", hash = "sha256:8c2798d8e9be7f0004b194fe207b76c1d9fff6e711b3db67b9508dcd5d00ae54", size = 20639595, upload-time = "2026-02-26T12:30:57.147Z" }, -] [[package]] name = "defusedxml" @@ -1949,6 +1947,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/7f/6076db1a2f6a40ec16bca9e4786034e661ca49fb3886c44a449e6f61cb64/ijson-3.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:0567e8c833825b119e74e10a7c29761dc65fcd155f5d4cb10f9d3b8916ef9912", size = 48472, upload-time = "2023-10-10T17:25:07.014Z" }, ] +[[package]] +name = "imageio" +version = "2.37.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, +] + [[package]] name = "importlib-metadata" version = "9.0.0" @@ -2311,6 +2323,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/74/da/0c507101089564ba8db9a6ddad176ab5f51b30a4f0b11ff2864b39bc6bcd/label_studio_tools-0.0.4-py3-none-any.whl", hash = "sha256:5b18420e3f9e6850785451588cc90330233a4afeaffae88c17b449e1bbe412ca", size = 16942, upload-time = "2024-04-29T12:32:33.899Z" }, ] +[[package]] +name = "lazy-loader" +version = "0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, +] + [[package]] name = "lightning-utilities" version = "0.15.3" @@ -4823,6 +4847,134 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/69/e2/b011c38e5394c4c18fb5500778a55ec43ad6106126e74723ffaee246f56e/safetensors-0.5.3-cp38-abi3-win_amd64.whl", hash = "sha256:836cbbc320b47e80acd40e44c8682db0e8ad7123209f69b093def21ec7cafd11", size = 308878, upload-time = "2025-02-26T09:15:14.99Z" }, ] +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "imageio", marker = "python_full_version < '3.11'" }, + { name = "lazy-loader", marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pillow", marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '4' and sys_platform == 'win32'", + "python_full_version >= '4' and sys_platform == 'emscripten'", + "python_full_version >= '4' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '4' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'win32'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "imageio", marker = "python_full_version >= '3.11'" }, + { name = "lazy-loader", marker = "python_full_version >= '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pillow", marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "tifffile", version = "2026.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, + { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, + { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, + { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, + { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, + { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, + { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, + { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, + { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, + { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, + { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, + { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, + { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, + { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, + { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, + { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, + { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, + { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, + { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, +] + [[package]] name = "scikit-learn" version = "1.7.2" @@ -5404,6 +5556,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform != 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'emscripten'", + "python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '4' and sys_platform == 'win32'", + "python_full_version >= '4' and sys_platform == 'emscripten'", + "python_full_version >= '4' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '4' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'win32'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.14' and python_full_version < '4' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/3e/695c7ab56be57814e369c1f38bc3f64b9dea0a83e867d00c0c9d613a9929/tifffile-2026.5.2.tar.gz", hash = "sha256:21b10227ede8493814a34676774797f721f487e36cb0530e7c3bd882caa87f5a", size = 429140, upload-time = "2026-05-02T20:19:31.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/af/ce4df3ca29122d219c45d3e86e5ff9a9df03b8cf31afd76817b662c803a3/tifffile-2026.5.2-py3-none-any.whl", hash = "sha256:5129b53b826e768a5b1af26b765eeea75c2d0a227d2d12849617e0737588e105", size = 266420, upload-time = "2026-05-02T20:19:29.814Z" }, +] + [[package]] name = "timm" version = "1.0.26" @@ -5592,27 +5804,27 @@ dependencies = [ { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp310-cp310-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp311-cp311-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-win_amd64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-win_amd64.whl" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5ac6e34681d5a0e527edb741b38254899cd03087a7dd7e841791a4ee0a5e7011", upload-time = "2026-04-27T17:32:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:72d53f3176a69cc20710c4ecb95f7dc4c6ba10c4e4eda45b8396ee79ee40f75a", upload-time = "2026-04-27T17:33:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:7c792fe95ad5edaf622cf9e4f5573f5aecf2bc0654c7e866eda6134088f95d72", upload-time = "2026-04-27T17:34:55Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d76f08e212285bd84c4c5a3472417f8eb4ee72e4067a604f7508dbfa2119771f", upload-time = "2026-04-27T17:36:45Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c9a7ca4c74fae10a58e6175b4b2cea953f9322bb6562bbf339ad6a05f52190ad", upload-time = "2026-04-27T17:37:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:90ef0c2454e5296a9fb021ddd42252e4ce1abe2c0a4988a173ef90a6cded0bf5", upload-time = "2026-04-27T17:39:29Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9c8f38efee365cb9d334de8a83ce52fc7e5fc9e5a7b0853285efa1b69e00b0f2", upload-time = "2026-04-27T17:41:30Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d252cf975fb18c94a85336323ad425f473df56dab35a44b00399bd70c7a3b997", upload-time = "2026-04-27T17:42:06Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:7c78215c3af4f62e63f2b2e360f1722fc719b0853c7ac22666483d9810613a4c", upload-time = "2026-04-27T17:43:49Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:7db3580106bba044da5b8950f3fb8fe5f31999eaab3f6a3aa2ac5d202c3684d2", upload-time = "2026-04-27T17:45:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:db964b33c55035a72ab3e2162287af8f1cc276039c65d015740cc88c26dcedf7", upload-time = "2026-04-27T17:46:18Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:6f367e62fd81b75cdf23ca4b75ced834d2db2cf98d1588ac935bde345de9de23", upload-time = "2026-04-27T17:48:09Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd1cf1005c5fe419194ee294b7b584ba5ad0f2fb1778b3fe5a7b9c3f4617ddbc", upload-time = "2026-04-27T17:50:01Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:74b628dbc71603977b09f4e140792c6e997081a35ef3421555f3f6e201b81210", upload-time = "2026-04-27T17:50:42Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:c2a5984deba8e001d166bf9cb83b8351f63a28b009e1a2fa0e4bbf08c90b259b", upload-time = "2026-04-27T17:52:32Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:baa52f7b8a53cab16587b10f1c27d1000ca033f97236878b685b75d5a1b92408", upload-time = "2026-04-27T17:54:24Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d389a850677f0d24dafae1573644034428d8d3b9c80b51d55ba62fed7e6c8777", upload-time = "2026-04-27T17:55:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314-win_amd64.whl", hash = "sha256:d6c21797ff75271b4fbdd905e2d703be4ecea5ea5bbdde4d1c201e9c71bc411d", upload-time = "2026-04-27T17:56:46Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:06849e9311dbb0617c97557d9c26c99a9e1c4f2ac9cb8e9b6d9b420d522acb91", upload-time = "2026-04-27T17:58:48Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:169a9987e1f84f0c5eee07544b3a34827a163ac9180e23abf0c3548f1335762c", upload-time = "2026-04-27T17:59:26Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.11.0%2Bcu128-cp314-cp314t-win_amd64.whl", hash = "sha256:d86c125d720c2c368c53bd1a4ef062916d91fa965c10448c74c78b5d039faf2d", upload-time = "2026-04-27T18:01:14Z" }, ] [[package]] @@ -5707,27 +5919,27 @@ dependencies = [ { name = "torch", version = "2.11.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform != 'darwin'" }, ] wheels = [ - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2a04bf491eb22e6487defe6cead6ffaabbce13fb5981b8eb3540050f96cb0599" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f44bfc61b9be80bcf52a762d34da363cea3125d10c01f37e271583803c7bb97b" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:ed0770e00b96d8aa675718e20db69c740c927f027c9c8b1330251f8a973221b6" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ed1324dbbbecb5a0149ed4ce8f9308465a1eef85ca2d2370dbb14805bf1c90aa" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f2629d056570c929b0a1d5473d9cb0320b90bda1764bda353553a72cc6b2069" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:d26091b15cd6e3c74c148d9b68c9a901ad6fb9b0f66fa3ea3ab09f04132a07d3" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63e35234aed13b6edda37056f417b5c281249669db631e706811917af36b21d7" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ccf26b4b659cfce6f2208cb8326071d51c70219a34856dfdf468d1e19af52c0d" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:8c0d1c4fbb2c9a4d5d41d0aaa87da20e525bcb2a154ce405725b0be59456804b" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c4a9cacd521f2a4df0bcd9d8e96704771b928f478f1f3067e4085bb53a1da298" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cb1f6184a7ba30fba40580e1a01a6604a86c55e79fdda187f40116ee680441ec" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:0232cb219927a52d6c98ff202f32d1cdf4802c2195a85fc1f1a0c1b0b4983a4d" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e594732552a8c2fee2ace9c6475c6c6904fc44ccca622ee6765a89a045416a44" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6168abc019803ac9e97efce27eafd2fdb33db04dcc54a86039537729e5047b29" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:367d42ea703844ecdb516e9d5eb09929012a58705d2622cf4e9e3c37f278cb85" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b3865fa227661dd75b7b28c96d3d14e739bd08bf0614132758922fe0e7206f91" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:aac647c9130f1f25f5c8f5bca3d95cfd96bdfac93ab54529690b088e64e4fa64" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-win_amd64.whl", hash = "sha256:6319e1ba49c6f62ac9902f73d0eab207b8a4dc6b4d3392fe9edd9903fff1be0a" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2ee9e16ee4518292694537fcbd20d2d27044e381d92b864f637e82795796a84" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b5772c55bfda4377df8f1930d43c4e0231ef231b0228eade4b227c8d3ba6e34e" }, - { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-win_amd64.whl", hash = "sha256:f160dc552a086244f7102c898f7be8ef46a41b36bce5ea80a4f2493cb30ca1fc" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2a04bf491eb22e6487defe6cead6ffaabbce13fb5981b8eb3540050f96cb0599", upload-time = "2026-04-09T23:21:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f44bfc61b9be80bcf52a762d34da363cea3125d10c01f37e271583803c7bb97b", upload-time = "2026-03-23T15:36:21Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp310-cp310-win_amd64.whl", hash = "sha256:ed0770e00b96d8aa675718e20db69c740c927f027c9c8b1330251f8a973221b6", upload-time = "2026-04-09T23:21:33Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ed1324dbbbecb5a0149ed4ce8f9308465a1eef85ca2d2370dbb14805bf1c90aa", upload-time = "2026-04-09T23:21:34Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f2629d056570c929b0a1d5473d9cb0320b90bda1764bda353553a72cc6b2069", upload-time = "2026-03-23T15:36:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp311-cp311-win_amd64.whl", hash = "sha256:d26091b15cd6e3c74c148d9b68c9a901ad6fb9b0f66fa3ea3ab09f04132a07d3", upload-time = "2026-04-09T23:21:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63e35234aed13b6edda37056f417b5c281249669db631e706811917af36b21d7", upload-time = "2026-04-09T23:21:35Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ccf26b4b659cfce6f2208cb8326071d51c70219a34856dfdf468d1e19af52c0d", upload-time = "2026-03-23T15:36:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp312-cp312-win_amd64.whl", hash = "sha256:8c0d1c4fbb2c9a4d5d41d0aaa87da20e525bcb2a154ce405725b0be59456804b", upload-time = "2026-04-09T23:21:36Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c4a9cacd521f2a4df0bcd9d8e96704771b928f478f1f3067e4085bb53a1da298", upload-time = "2026-04-09T23:21:37Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cb1f6184a7ba30fba40580e1a01a6604a86c55e79fdda187f40116ee680441ec", upload-time = "2026-03-23T15:36:22Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313-win_amd64.whl", hash = "sha256:0232cb219927a52d6c98ff202f32d1cdf4802c2195a85fc1f1a0c1b0b4983a4d", upload-time = "2026-04-09T23:21:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e594732552a8c2fee2ace9c6475c6c6904fc44ccca622ee6765a89a045416a44", upload-time = "2026-04-09T23:21:38Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6168abc019803ac9e97efce27eafd2fdb33db04dcc54a86039537729e5047b29", upload-time = "2026-03-23T15:36:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp313-cp313t-win_amd64.whl", hash = "sha256:367d42ea703844ecdb516e9d5eb09929012a58705d2622cf4e9e3c37f278cb85", upload-time = "2026-04-09T23:21:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:b3865fa227661dd75b7b28c96d3d14e739bd08bf0614132758922fe0e7206f91", upload-time = "2026-04-09T23:21:39Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:aac647c9130f1f25f5c8f5bca3d95cfd96bdfac93ab54529690b088e64e4fa64", upload-time = "2026-03-23T15:36:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314-win_amd64.whl", hash = "sha256:6319e1ba49c6f62ac9902f73d0eab207b8a4dc6b4d3392fe9edd9903fff1be0a", upload-time = "2026-04-09T23:21:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2ee9e16ee4518292694537fcbd20d2d27044e381d92b864f637e82795796a84", upload-time = "2026-04-09T23:21:40Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b5772c55bfda4377df8f1930d43c4e0231ef231b0228eade4b227c8d3ba6e34e", upload-time = "2026-03-23T15:36:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.26.0%2Bcu128-cp314-cp314t-win_amd64.whl", hash = "sha256:f160dc552a086244f7102c898f7be8ef46a41b36bce5ea80a4f2493cb30ca1fc", upload-time = "2026-04-09T23:21:41Z" }, ] [[package]] From d2ea192d8fcbb1320d5ca51540f2cb265413e13a Mon Sep 17 00:00:00 2001 From: bw4sz Date: Thu, 14 May 2026 06:25:35 -0700 Subject: [PATCH 2/2] add metadata prior visualization Co-authored-by: Cursor --- scripts/visualize_metadata_priors.py | 310 +++++++++++++++++++++++++++ 1 file changed, 310 insertions(+) create mode 100644 scripts/visualize_metadata_priors.py diff --git a/scripts/visualize_metadata_priors.py b/scripts/visualize_metadata_priors.py new file mode 100644 index 0000000..34f1dec --- /dev/null +++ b/scripts/visualize_metadata_priors.py @@ -0,0 +1,310 @@ +"""Map metadata-only class priors from a metadata-enabled CropModel checkpoint. + +This script visualizes what the spatial-temporal embedding branch contributes +to each class, independent of image content. It evaluates a coarse lat/lon grid +for one or more dates, then writes CSV score rasters and PNG maps. +""" + +import argparse +import datetime as dt +import os +import sys +from pathlib import Path + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import rasterio +from rasterio.transform import from_origin +import torch + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from deepforest.model import CropModel + +try: + import contextily as ctx +except ImportError: # pragma: no cover - contextily is an optional visual enhancement. + ctx = None + + +SPECIES_ALIASES = { + "Northern Gannet": "Morus bassanus", + "Common Eider": "Somateria mollissima", +} + + +DEFAULT_SPECIES = ["Morus bassanus", "Somateria mollissima"] +DEFAULT_DATES = ["2024-01-15", "2024-04-15", "2024-07-15", "2024-10-15"] +DEFAULT_BOUNDS = (-98.0, 18.0, -55.0, 48.0) # Gulf of Mexico + western Atlantic + + +def day_of_year(date: str) -> float: + return float(dt.datetime.strptime(date, "%Y-%m-%d").timetuple().tm_yday) + + +def resolve_species(species: list[str]) -> list[str]: + return [SPECIES_ALIASES.get(name, name) for name in species] + + +def make_grid(bounds: tuple[float, float, float, float], cell_degrees: float) -> pd.DataFrame: + min_lon, min_lat, max_lon, max_lat = bounds + lons = np.arange(min_lon + cell_degrees / 2, max_lon, cell_degrees) + lats = np.arange(min_lat + cell_degrees / 2, max_lat, cell_degrees) + lon_grid, lat_grid = np.meshgrid(lons, lats) + return pd.DataFrame({ + "lon": lon_grid.ravel(), + "lat": lat_grid.ravel(), + }) + + +def load_metadata_model(checkpoint: str, device: str) -> CropModel: + model = CropModel.load_from_checkpoint(checkpoint, map_location=device) + model.eval() + model.to(device) + if getattr(model, "metadata_encoder", None) is None or getattr(model, "classifier", None) is None: + raise ValueError( + "Checkpoint is not metadata-enabled. Expected CropModel.metadata_encoder " + "and CropModel.classifier from the DeepForest metadata PR." + ) + return model + + +def metadata_prior_scores( + model: CropModel, + grid: pd.DataFrame, + date: str, + device: str, +) -> pd.DataFrame: + """Compute metadata-only logits and probabilities for every grid cell/class.""" + metadata = torch.tensor( + np.column_stack([ + grid["lat"].to_numpy(), + grid["lon"].to_numpy(), + np.full(len(grid), day_of_year(date)), + ]), + dtype=torch.float32, + device=device, + ) + + with torch.no_grad(): + meta_features = model.metadata_encoder(metadata) + meta_dim = meta_features.shape[1] + classifier = model.classifier + meta_weights = classifier.weight[:, -meta_dim:] + logits = meta_features @ meta_weights.T + if classifier.bias is not None: + logits = logits + classifier.bias + probabilities = torch.softmax(logits, dim=1) + + labels = model.numeric_to_label_dict + rows = [] + logits_np = logits.cpu().numpy() + probs_np = probabilities.cpu().numpy() + for class_idx, label in labels.items(): + class_scores = pd.DataFrame({ + "date": date, + "class_idx": class_idx, + "species": label, + "lat": grid["lat"].to_numpy(), + "lon": grid["lon"].to_numpy(), + "metadata_logit": logits_np[:, class_idx], + "metadata_probability": probs_np[:, class_idx], + }) + rows.append(class_scores) + return pd.concat(rows, ignore_index=True) + + +def select_species_scores(scores: pd.DataFrame, species: list[str]) -> pd.DataFrame: + available = set(scores["species"].unique()) + missing = [name for name in species if name not in available] + if missing: + examples = sorted(available)[:20] + raise ValueError( + f"Species not found in checkpoint label_dict: {missing}. " + f"First available labels: {examples}" + ) + + selected = scores[scores["species"].isin(species)].copy() + selected["relative_score"] = selected.groupby(["date", "species"])["metadata_logit"].transform( + lambda x: (x - x.min()) / (x.max() - x.min()) if x.max() > x.min() else 0.0 + ) + return selected + + +def _safe_name(value: str) -> str: + return value.lower().replace(" ", "_").replace("/", "_") + + +def plot_species_map( + scores: pd.DataFrame, + species: str, + date: str, + bounds: tuple[float, float, float, float], + output_path: Path, + plot_column: str, + cell_degrees: float, + cmap: str, + use_basemap: bool, +) -> None: + subset = scores[(scores["species"] == species) & (scores["date"] == date)] + pivot = subset.pivot(index="lat", columns="lon", values=plot_column).sort_index() + min_lon, min_lat, max_lon, max_lat = bounds + + fig, ax = plt.subplots(figsize=(12, 8)) + ax.set_xlim(min_lon, max_lon) + ax.set_ylim(min_lat, max_lat) + ax.set_aspect("equal") + + if use_basemap and ctx is not None: + try: + ctx.add_basemap( + ax, + crs="EPSG:4326", + source=ctx.providers.Esri.OceanBasemap, + attribution_size=5, + zorder=0, + ) + except Exception as exc: + print(f"Could not add basemap tiles: {exc}") + + image = ax.imshow( + pivot.to_numpy(), + extent=[ + pivot.columns.min() - cell_degrees / 2, + pivot.columns.max() + cell_degrees / 2, + pivot.index.min() - cell_degrees / 2, + pivot.index.max() + cell_degrees / 2, + ], + origin="lower", + cmap=cmap, + alpha=0.75, + zorder=2, + vmin=0 if plot_column == "relative_score" else None, + vmax=1 if plot_column == "relative_score" else None, + ) + fig.colorbar(image, ax=ax, label=plot_column.replace("_", " ")) + ax.set_title(f"{species} metadata prior, {date}") + ax.set_xlabel("Longitude") + ax.set_ylabel("Latitude") + ax.grid(color="white", linewidth=0.3, alpha=0.4) + fig.savefig(output_path, dpi=250, bbox_inches="tight") + plt.close(fig) + + +def write_species_geotiff( + scores: pd.DataFrame, + species: str, + date: str, + output_path: Path, + plot_column: str, + cell_degrees: float, +) -> None: + subset = scores[(scores["species"] == species) & (scores["date"] == date)] + pivot = subset.pivot(index="lat", columns="lon", values=plot_column).sort_index() + array = np.flipud(pivot.to_numpy()).astype("float32") + transform = from_origin( + pivot.columns.min() - cell_degrees / 2, + pivot.index.max() + cell_degrees / 2, + cell_degrees, + cell_degrees, + ) + with rasterio.open( + output_path, + "w", + driver="GTiff", + height=array.shape[0], + width=array.shape[1], + count=1, + dtype="float32", + crs="EPSG:4326", + transform=transform, + ) as dst: + dst.write(array, 1) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Visualize metadata-only species priors from a CropModel checkpoint." + ) + parser.add_argument("--checkpoint", required=True, help="Metadata-enabled CropModel checkpoint.") + parser.add_argument( + "--species", + nargs="+", + default=DEFAULT_SPECIES, + help="Scientific names to map. Common aliases supported: Northern Gannet, Common Eider.", + ) + parser.add_argument("--dates", nargs="+", default=DEFAULT_DATES, help="YYYY-MM-DD dates to map.") + parser.add_argument( + "--bounds", + nargs=4, + type=float, + default=DEFAULT_BOUNDS, + metavar=("MIN_LON", "MIN_LAT", "MAX_LON", "MAX_LAT"), + ) + parser.add_argument("--cell-degrees", type=float, default=1.0, help="Grid cell size in degrees.") + parser.add_argument("--output-dir", default="outputs/metadata_prior_maps") + parser.add_argument( + "--plot-column", + default="relative_score", + choices=["relative_score", "metadata_probability", "metadata_logit"], + help="Score column used for PNG coloring. CSV always contains all score columns.", + ) + parser.add_argument("--cmap", default="viridis") + parser.add_argument("--device", default="cpu") + parser.add_argument("--no-basemap", action="store_true") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + output_dir = Path(args.output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + species = resolve_species(args.species) + grid = make_grid(tuple(args.bounds), args.cell_degrees) + model = load_metadata_model(args.checkpoint, args.device) + + all_scores = [] + for date in args.dates: + scores = metadata_prior_scores(model=model, grid=grid, date=date, device=args.device) + selected = select_species_scores(scores, species) + all_scores.append(selected) + + for species_name in species: + output_stem = output_dir / f"{_safe_name(species_name)}_{date}" + plot_species_map( + scores=selected, + species=species_name, + date=date, + bounds=tuple(args.bounds), + output_path=output_stem.with_suffix(".png"), + plot_column=args.plot_column, + cell_degrees=args.cell_degrees, + cmap=args.cmap, + use_basemap=not args.no_basemap, + ) + write_species_geotiff( + scores=selected, + species=species_name, + date=date, + output_path=output_stem.with_suffix(".tif"), + plot_column=args.plot_column, + cell_degrees=args.cell_degrees, + ) + print(f"Wrote {output_stem.with_suffix('.png')}") + print(f"Wrote {output_stem.with_suffix('.tif')}") + + combined = pd.concat(all_scores, ignore_index=True) + csv_path = output_dir / "metadata_prior_scores.csv" + combined.to_csv(csv_path, index=False) + print(f"Wrote {csv_path}") + + +if __name__ == "__main__": + main()