| """
|
| Reusable Inference Module for Text-Conditioned Segmentation
|
| Supports prompt-based image segmentation using trained ResNet18 + UNet model
|
| """
|
|
|
| import torch
|
| import torch.nn as nn
|
| import numpy as np
|
| from PIL import Image, ImageDraw
|
| from torchvision import transforms, models
|
| import os
|
| import cv2
|
| import numpy as np
|
|
|
|
|
|
|
|
|
| DEVICE = "cpu"
|
|
|
|
|
|
|
|
|
| PROMPT_TO_MODE = {
|
| "segment crack": "crack",
|
| "segment wall crack": "crack",
|
| "segment taping area": "taping",
|
| "segment joint": "taping",
|
| "segment drywall seam": "taping",
|
| }
|
|
|
|
|
|
|
|
|
|
|
|
|
| MODE_THRESHOLDS = {
|
| "crack": 0.485,
|
| "taping": 0.486,
|
| }
|
|
|
|
|
|
|
|
|
| from model import ResNetSegmentation
|
|
|
|
|
|
|
|
|
| def draw_rectangles_on_mask(image_pil, mask_binary, thickness=2, color=(0, 255, 0)):
|
| """
|
| Draw rectangles around detected regions (connected components) on the image.
|
| """
|
|
|
| mask_uint8 = np.uint8(mask_binary)
|
|
|
|
|
| contours, _ = cv2.findContours(mask_uint8, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
|
|
|
|
| image_marked = image_pil.copy()
|
| draw = ImageDraw.Draw(image_marked)
|
|
|
|
|
| for contour in contours:
|
| if len(contour) < 3:
|
| continue
|
|
|
|
|
| x, y, w, h = cv2.boundingRect(contour)
|
|
|
|
|
| if w < 10 or h < 10:
|
| continue
|
|
|
|
|
| draw.rectangle(
|
| [(x, y), (x + w, y + h)],
|
| outline=color,
|
| width=thickness
|
| )
|
|
|
| return image_marked
|
|
|
|
|
|
|
|
|
|
|
| transform = transforms.Compose([
|
| transforms.Resize((256, 256)),
|
| transforms.ToTensor(),
|
| transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
|
| ])
|
|
|
|
|
| def predict(image_path, prompt, model_path="best_model.pth"):
|
|
|
| model = ResNetSegmentation().to(DEVICE)
|
|
|
| if not os.path.exists(model_path):
|
| raise FileNotFoundError(f"Model file not found: {model_path}")
|
|
|
| model.load_state_dict(torch.load(model_path, map_location=DEVICE))
|
| model.eval()
|
|
|
|
|
| if not os.path.exists(image_path):
|
| raise FileNotFoundError(f"Image file not found: {image_path}")
|
|
|
| image = Image.open(image_path).convert("RGB")
|
| original_size = image.size
|
|
|
|
|
| img_tensor = transform(image).unsqueeze(0).to(DEVICE)
|
|
|
|
|
| with torch.no_grad():
|
| logits = model(img_tensor)
|
| probs = torch.sigmoid(logits)
|
|
|
| probs_np = probs[0, 0].cpu().numpy()
|
|
|
|
|
| threshold = 0.5
|
| mask_binary = (probs_np > threshold).astype(np.uint8) * 255
|
|
|
|
|
| mask_pil = Image.fromarray(mask_binary)
|
| mask_resized = mask_pil.resize(original_size, Image.Resampling.NEAREST)
|
| mask = np.array(mask_resized)
|
|
|
| return image, mask
|
|
|
|
|
| def predict_and_save(image_path, prompt, output_dir="outputs", model_path="best_model.pth"):
|
| """
|
| Run inference and save the predicted mask as PNG.
|
|
|
| Args:
|
| image_path (str): Path to input image
|
| prompt (str): Natural language prompt
|
| output_dir (str): Directory to save output PNG
|
| model_path (str): Path to trained model weights
|
|
|
| Returns:
|
| str: Path to saved mask PNG file
|
| """
|
|
|
|
|
| os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
| image, mask = predict(image_path, prompt, model_path)
|
|
|
|
|
| image_name = os.path.splitext(os.path.basename(image_path))[0]
|
| prompt_slug = prompt.replace(" ", "_").lower()
|
| output_filename = f"{image_name}__{prompt_slug}.png"
|
| output_path = os.path.join(output_dir, output_filename)
|
|
|
|
|
| Image.fromarray(mask).save(output_path)
|
|
|
| return output_path
|
|
|
|
|
|
|
|
|
|
|
| def compute_dice(pred, gt, threshold=0.5):
|
| """Compute Dice Score"""
|
| pred = (pred > threshold).astype(np.float32)
|
| gt = gt.astype(np.float32)
|
|
|
| inter = (pred * gt).sum()
|
| dice = (2 * inter) / (pred.sum() + gt.sum() + 1e-6)
|
|
|
| return dice
|
|
|
|
|
| def compute_iou(pred, gt, threshold=0.5):
|
| """Compute Intersection over Union"""
|
| pred = (pred > threshold).astype(np.float32)
|
| gt = gt.astype(np.float32)
|
|
|
| inter = (pred * gt).sum()
|
| union = pred.sum() + gt.sum() - inter
|
| iou = inter / (union + 1e-6)
|
|
|
| return iou
|
|
|
|
|
| if __name__ == "__main__":
|
|
|
| print("Inference module ready. Import and use predict() function.")
|
| print(f"Supported prompts: {list(PROMPT_TO_CLASS.keys())}")
|
|
|