Download inference.py from EvanZhongg/StrandSeg-Lite: direct link, hf CLI and curl.
- Browser
- Download file 4.54 kB
-
https://huggingface.co/EvanZhongg/StrandSeg-Lite/resolve/main/inference.py
- Command line
-
hf download hf://EvanZhongg/StrandSeg-Lite/inference.py
-
curl -L -o inference.py https://huggingface.co/EvanZhongg/StrandSeg-Lite/resolve/main/inference.py
4.54 kB
| """Standalone inference API and CLI for StrandSeg-Lite.""" | |
| from __future__ import annotations | |
| import argparse | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import cv2 | |
| import numpy as np | |
| import torch | |
| from model import load_model | |
| IMAGE_MEAN = np.asarray([0.485, 0.456, 0.406], dtype=np.float32) | |
| IMAGE_STD = np.asarray([0.229, 0.224, 0.225], dtype=np.float32) | |
| class Prediction: | |
| probability: np.ndarray | |
| mask: np.ndarray | |
| threshold: float | |
| def _letterbox(image: np.ndarray, width: int, height: int) -> tuple[np.ndarray, tuple[int, int, int, int]]: | |
| source_height, source_width = image.shape[:2] | |
| scale = min(width / source_width, height / source_height) | |
| resized_width = max(1, int(round(source_width * scale))) | |
| resized_height = max(1, int(round(source_height * scale))) | |
| pad_x = (width - resized_width) // 2 | |
| pad_y = (height - resized_height) // 2 | |
| resized = cv2.resize(image, (resized_width, resized_height), interpolation=cv2.INTER_AREA) | |
| canvas = np.zeros((height, width, 3), dtype=np.uint8) | |
| canvas[pad_y : pad_y + resized_height, pad_x : pad_x + resized_width] = resized | |
| return canvas, (pad_x, pad_y, resized_width, resized_height) | |
| class StrandSegLite: | |
| def __init__(self, checkpoint: str | Path = "model.pt", device: str | torch.device = "cpu"): | |
| self.device = torch.device(device) | |
| self.model, self.metadata = load_model(checkpoint, self.device) | |
| config = dict(self.metadata.get("model_config") or {}) | |
| self.input_width = int(config.get("input_width", 960)) | |
| self.input_height = int(config.get("input_height", 544)) | |
| self.threshold = float(self.metadata.get("threshold", 0.9204)) | |
| def predict(self, image: np.ndarray, *, color: str = "rgb", threshold: float | None = None) -> Prediction: | |
| if image.ndim != 3 or image.shape[2] != 3: | |
| raise ValueError("image must be an HxWx3 uint8 array") | |
| if image.dtype != np.uint8: | |
| raise ValueError("image must have dtype uint8") | |
| color = color.lower() | |
| if color == "rgb": | |
| bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) | |
| elif color == "bgr": | |
| bgr = image | |
| else: | |
| raise ValueError("color must be 'rgb' or 'bgr'") | |
| original_height, original_width = bgr.shape[:2] | |
| prepared, (pad_x, pad_y, resized_width, resized_height) = _letterbox( | |
| bgr, self.input_width, self.input_height | |
| ) | |
| rgb = cv2.cvtColor(prepared, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0 | |
| rgb = (rgb - IMAGE_MEAN) / IMAGE_STD | |
| tensor = torch.from_numpy(np.transpose(rgb, (2, 0, 1))).unsqueeze(0).to(self.device) | |
| probability = torch.sigmoid(self.model(tensor))[0, 0].cpu().numpy() | |
| probability = probability[pad_y : pad_y + resized_height, pad_x : pad_x + resized_width] | |
| probability = cv2.resize(probability, (original_width, original_height), interpolation=cv2.INTER_LINEAR) | |
| selected_threshold = self.threshold if threshold is None else float(threshold) | |
| mask = (probability >= selected_threshold).astype(np.uint8) | |
| return Prediction(probability=probability.astype(np.float32), mask=mask, threshold=selected_threshold) | |
| def predict_file(self, path: str | Path, *, threshold: float | None = None) -> Prediction: | |
| encoded = np.fromfile(str(path), dtype=np.uint8) | |
| image = cv2.imdecode(encoded, cv2.IMREAD_COLOR) if encoded.size else None | |
| if image is None: | |
| raise ValueError(f"Could not read image: {path}") | |
| return self.predict(image, color="bgr", threshold=threshold) | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Run StrandSeg-Lite on one image") | |
| parser.add_argument("image") | |
| parser.add_argument("--checkpoint", default="model.pt") | |
| parser.add_argument("--device", default="cpu") | |
| parser.add_argument("--threshold", type=float) | |
| parser.add_argument("--output-mask", default="mask.png") | |
| parser.add_argument("--output-probability", default="probability.png") | |
| args = parser.parse_args() | |
| result = StrandSegLite(args.checkpoint, args.device).predict_file(args.image, threshold=args.threshold) | |
| cv2.imwrite(args.output_mask, result.mask * 255) | |
| cv2.imwrite(args.output_probability, np.clip(result.probability * 255.0, 0, 255).astype(np.uint8)) | |
| print(f"threshold={result.threshold:.4f} foreground_pixels={int(result.mask.sum())}") | |
| if __name__ == "__main__": | |
| main() | |