garmentiq.tailor
The tailor agent, which runs the whole GarmentIQ pipeline.
Processes a folder of images end to end: classification, segmentation, optional alpha matting, landmark detection, refinement, derivation, and measurement. Results are written to an output directory and summarised in a metadata table.
1"""The tailor agent, which runs the whole GarmentIQ pipeline. 2 3Processes a folder of images end to end: classification, segmentation, optional alpha 4matting, landmark detection, refinement, derivation, and measurement. Results are 5written to an output directory and summarised in a metadata table. 6""" 7import os 8from typing import List, Dict, Type, Any, Optional, Union 9import torch 10import torch.nn as nn 11import numpy as np 12from pathlib import Path 13import pandas as pd 14from tqdm.auto import tqdm 15import textwrap 16from PIL import Image, ImageDraw, ImageFont 17from . import classification 18from . import segmentation 19from . import landmark 20from . import matting 21from . import utils 22 23 24# Background colour used to composite the alpha matte for landmark detection when neither 25# the matting nor the segmentation stage specifies one. Detection needs an RGB image, and a 26# plain neutral background measurably helps the pose model compared with the raw photo. 27DEFAULT_MATTE_DETECTION_BACKGROUND = (255, 255, 255) 28 29 30class tailor: 31 """ 32 The `tailor` class acts as a central agent for the GarmentIQ pipeline, 33 orchestrating garment measurement from classification to landmark derivation. 34 35 It integrates functionalities from other modules (classification, segmentation, landmark) 36 to provide a smooth end-to-end process for automated garment measurement from images. 37 38 Attributes: 39 input_dir (str): Directory containing input images. 40 model_dir (str): Directory where models are stored. 41 output_dir (str): Directory to save processed outputs. 42 class_dict (dict): Dictionary defining garment classes and their properties. 43 do_derive (bool): Flag to enable landmark derivation. 44 do_refine (bool): Flag to enable landmark refinement. 45 classification_model_path (str): Path to the classification model. 46 classification_model_class (Type[nn.Module]): Class definition for the classification model. 47 classification_model_args (Dict): Arguments for the classification model. 48 segmentation_model_path (str): Name or path for the segmentation model. 49 segmentation_model_class (Type[nn.Module]): Class definition for the segmentation model. 50 segmentation_model_args (Dict): Arguments for the segmentation model. 51 landmark_detection_model_path (str): Path to the landmark detection model. 52 landmark_detection_model_class (Type[nn.Module]): Class definition for the landmark detection model. 53 landmark_detection_model_args (Dict): Arguments for the landmark detection model. 54 refinement_args (Optional[Dict]): Arguments for landmark refinement. 55 derivation_dict (Optional[Dict]): Dictionary for landmark derivation rules. 56 device (torch.device): The device all models are loaded onto and run on. 57 do_matte (bool): Flag to enable the alpha matting stage. 58 matting_model_args (Optional[Dict]): Arguments for the matting model. 59 """ 60 61 def __init__( 62 self, 63 input_dir: str, 64 model_dir: str, 65 output_dir: str, 66 class_dict: dict, 67 do_derive: bool, 68 do_refine: bool, 69 classification_model_path: str, 70 classification_model_class: Type[nn.Module], 71 classification_model_args: Dict, 72 segmentation_model_path: str, 73 segmentation_model_class: Type[nn.Module], 74 segmentation_model_args: Dict, 75 landmark_detection_model_path: str, 76 landmark_detection_model_class: Type[nn.Module], 77 landmark_detection_model_args: Dict, 78 refinement_args: Optional[Dict] = None, 79 derivation_dict: Optional[Dict] = None, 80 device: Union[str, torch.device] = "cpu", 81 do_matte: bool = False, 82 matting_model_path: Optional[str] = None, 83 matting_model_class: Optional[Type[nn.Module]] = None, 84 matting_model_args: Optional[Dict] = None, 85 ): 86 """ 87 Initializes the `tailor` agent with paths, model configurations, and processing flags. 88 89 Args: 90 input_dir (str): Path to the directory containing input images. 91 model_dir (str): Path to the directory where all required models are stored. 92 output_dir (str): Path to the directory where all processed outputs will be saved. 93 class_dict (dict): A dictionary defining the garment classes, their predefined points, 94 index ranges, and instruction JSON file paths. 95 do_derive (bool): If True, enables the landmark derivation step. 96 do_refine (bool): If True, enables the landmark refinement step. 97 classification_model_path (str): The filename or relative path to the classification model. 98 classification_model_class (Type[nn.Module]): The Python class of the classification model. 99 classification_model_args (Dict): A dictionary of arguments to initialize the classification model. 100 segmentation_model_path (str): The filename or relative path of the segmentation model. 101 segmentation_model_class (Type[nn.Module]): The Python class of the segmentation model. 102 segmentation_model_args (Dict): A dictionary of arguments for the segmentation model. 103 For SAM this typically holds `model_config`, `processor`, 104 and `prompt` (with `"points"`, `"labels"`, `"boxes"`, 105 and/or `"text"`), plus optional `grounding_model` and 106 `grounding_processor` when using a text prompt with 107 SAM 1 or SAM 2. An optional `background_color` triggers 108 background replacement. 109 landmark_detection_model_path (str): The filename or relative path to the landmark detection model. 110 landmark_detection_model_class (Type[nn.Module]): The Python class of the landmark detection model. 111 landmark_detection_model_args (Dict): A dictionary of arguments for the landmark detection model. 112 refinement_args (Optional[Dict]): Optional arguments for the refinement process, 113 e.g., `window_size`, `ksize`, `sigmaX`. Defaults to None. 114 derivation_dict (Optional[Dict]): A dictionary defining derivation rules for non-predefined landmarks. 115 Required if `do_derive` is True. 116 device (Union[str, torch.device], optional): The device that every model in the pipeline 117 is loaded onto and run on, e.g. `"cpu"`, 118 `"cuda"`, `"cuda:0"`, or `"mps"`. Hardware 119 acceleration is opt-in; pass it explicitly 120 to use a GPU or Apple Silicon. 121 Defaults to `"cpu"`. 122 do_matte (bool, optional): If True, enables the alpha matting stage, which refines the 123 hard segmentation mask into a soft alpha matte. Matting in 124 the pipeline is deliberately built on top of segmentation: 125 the segmentation mask supplies the trimap (ViTMatte) or the 126 guidance mask (Matting Anything), so enabling this forces the 127 segmentation stage to run. Defaults to False. 128 matting_model_path (str, optional): The filename or relative path to the matting model 129 weights, relative to `model_dir`. Required when 130 `do_matte` is True and the model is loaded by 131 GarmentIQ. Defaults to None. 132 matting_model_class (Type[nn.Module], optional): The Python class of the matting model, 133 e.g. `VitMatteForImageMatting`. 134 Required when `do_matte` is True. 135 Defaults to None. 136 matting_model_args (Dict, optional): Arguments for the matting model. For ViTMatte this 137 holds `model_config` (e.g. 138 `{"config": load_vitmatte_config(...)}`), 139 `processor`, and optional `trimap_args` and 140 `background_color`. Alternatively pass a 141 preconstructed model as `model`, which is how 142 Matting Anything is supplied since it pairs a 143 decoder with a SAM instance. Defaults to None. 144 145 Raises: 146 ValueError: If `do_derive` is True but `derivation_dict` is None, if `do_matte` is True 147 but no matting model is provided, or if the requested `device` is invalid 148 or unavailable on this machine. 149 """ 150 # Device (resolved once and reused by every stage of the pipeline) 151 self.device = utils.resolve_device(device) 152 153 # Directories 154 self.input_dir = input_dir 155 self.model_dir = model_dir 156 self.output_dir = output_dir 157 158 # Classes 159 self.class_dict = class_dict 160 self.classes = sorted(list(class_dict.keys())) 161 162 # Derivation 163 self.do_derive = do_derive 164 if self.do_derive: 165 if derivation_dict is None: 166 raise ValueError( 167 "`derivation_dict` must be provided if `do_derive=True`." 168 ) 169 self.derivation_dict = derivation_dict 170 else: 171 self.derivation_dict = None 172 173 # Refinement setup 174 self.do_refine = do_refine 175 176 if self.do_refine: 177 if refinement_args is None: 178 self.refinement_args = {} 179 self.refinement_args = refinement_args 180 else: 181 self.refinement_args = None 182 183 # Classification model setup 184 self.classification_model_path = classification_model_path 185 self.classification_model_args = classification_model_args 186 self.classification_model_class = classification_model_class 187 filtered_model_args = { 188 k: v 189 for k, v in self.classification_model_args.items() 190 if k not in ("pretrained", "resize_dim", "normalize_mean", "normalize_std") 191 } 192 193 # Load the model using the filtered arguments 194 self.classification_model = classification.load_model( 195 model_path=f"{self.model_dir}/{self.classification_model_path}", 196 model_class=self.classification_model_class, 197 model_args=filtered_model_args, 198 device=self.device, 199 ) 200 201 # Segmentation model setup 202 self.segmentation_model_path = segmentation_model_path 203 self.segmentation_model_class = segmentation_model_class 204 self.segmentation_model_args = segmentation_model_args 205 self.segmentation_has_bg_color = "background_color" in segmentation_model_args 206 self.segmentation_model = segmentation.load_model( 207 model_path=f"{self.model_dir}/{self.segmentation_model_path}", 208 model_class=self.segmentation_model_class, 209 model_args=self.segmentation_model_args.get("model_config"), 210 device=self.device, 211 ) 212 213 # Landmark detection model setup 214 self.landmark_detection_model_path = landmark_detection_model_path 215 self.landmark_detection_model_class = landmark_detection_model_class 216 self.landmark_detection_model_args = landmark_detection_model_args 217 self.landmark_detection_model = landmark.detection.load_model( 218 model_path=f"{self.model_dir}/{self.landmark_detection_model_path}", 219 model_class=self.landmark_detection_model_class, 220 device=self.device, 221 ) 222 223 # Matting setup (optional, and always layered on top of segmentation) 224 self.do_matte = do_matte 225 self.matting_model_path = matting_model_path 226 self.matting_model_class = matting_model_class 227 self.matting_model_args = matting_model_args or {} 228 self.matting_model = None 229 230 if self.do_matte: 231 # Matting refines a segmentation mask, so the pipeline cannot run it 232 # without a working segmentation stage. 233 if self.segmentation_model is None: 234 raise ValueError( 235 "`do_matte=True` requires segmentation, because the segmentation mask " 236 "supplies the trimap (ViTMatte) or guidance mask (Matting Anything). " 237 "Configure the segmentation model, or set `do_matte=False`." 238 ) 239 240 preloaded = self.matting_model_args.get("model") 241 if preloaded is not None: 242 # Matting Anything is assembled by the caller because it pairs a 243 # decoder with an existing SAM instance. 244 self.matting_model = preloaded.to(self.device) 245 elif matting_model_class is not None and matting_model_path is not None: 246 self.matting_model = matting.load_model( 247 model_class=self.matting_model_class, 248 model_path=f"{self.model_dir}/{self.matting_model_path}", 249 model_args=self.matting_model_args.get("model_config"), 250 device=self.device, 251 ) 252 else: 253 missing = [] 254 if matting_model_class is None: 255 missing.append("`matting_model_class`") 256 if matting_model_path is None: 257 missing.append("`matting_model_path`") 258 raise ValueError( 259 f"`do_matte=True` requires a matting model, but {' and '.join(missing)} " 260 f"{'was' if len(missing) == 1 else 'were'} not provided. Either pass " 261 f"`matting_model_class` and `matting_model_path` (for ViTMatte), or pass " 262 f"an already constructed model as `matting_model_args={{'model': ...}}` " 263 f"(for Matting Anything)." 264 ) 265 266 # ViTMatte consumes a stacked image+trimap tensor built by its processor, 267 # so a missing processor would only fail deep inside inference. 268 model_names = {c.__name__ for c in type(self.matting_model).__mro__} 269 is_mam = "MattingAnything" in model_names 270 if not is_mam and self.matting_model_args.get("processor") is None: 271 raise ValueError( 272 "`do_matte=True` with a trimap-based model such as ViTMatte requires a " 273 "processor. Pass it as " 274 "`matting_model_args={'processor': load_vitmatte_processor(...)}`." 275 ) 276 if is_mam and not self.matting_model_args.get("prompt"): 277 raise ValueError( 278 "`do_matte=True` with Matting Anything requires a prompt for its internal " 279 "SAM. Pass it as " 280 "`matting_model_args={'prompt': {'boxes': [[[x0, y0, x1, y1]]]}}`." 281 ) 282 283 def summary(self): 284 """ 285 Prints a summary of the `tailor` agent's configuration, including directory paths, 286 defined classes, processing options (refine, derive, device), and loaded models. 287 """ 288 width = 80 289 sep = "=" * width 290 291 print(sep) 292 print("TAILOR AGENT SUMMARY".center(width)) 293 print(sep) 294 295 # Directories 296 print("DIRECTORY PATHS".center(width, "-")) 297 print(f"{'Input directory:':25} {self.input_dir}") 298 print(f"{'Model directory:':25} {self.model_dir}") 299 print(f"{'Output directory:':25} {self.output_dir}") 300 print() 301 302 # Classes 303 print("CLASSES".center(width, "-")) 304 print(f"{'Class Index':<11} | Class Name") 305 print(f"{'-'*11} | {'-'*66}") 306 for i, cls in enumerate(self.classes): 307 print(f"{i:<11} | {cls}") 308 print() 309 310 # Flags 311 print("OPTIONS".center(width, "-")) 312 print(f"{'Do refine?:':25} {self.do_refine}") 313 print(f"{'Do derive?:':25} {self.do_derive}") 314 print(f"{'Do matte?:':25} {self.do_matte}") 315 print(f"{'Device:':25} {self.device}") 316 print() 317 318 # Models 319 print("MODELS".center(width, "-")) 320 print( 321 f"{'Classification Model:':25} {self.classification_model_class.__name__}" 322 ) 323 print(f"{'Segmentation Model:':25} {self.segmentation_model_class.__name__}") 324 print(f"{' └─ Change BG color?:':25} {self.segmentation_has_bg_color}") 325 print( 326 f"{'Landmark Detection Model:':25} {self.landmark_detection_model_class.__class__.__name__}" 327 ) 328 if self.do_matte: 329 print(f"{'Matting Model:':25} {type(self.matting_model).__name__}") 330 matte_bg = self.matting_model_args.get("background_color") 331 print(f"{' └─ Composite BG color?:':25} {matte_bg is not None}") 332 print(sep) 333 334 def classify(self, image: str, verbose=False): 335 """ 336 Classifies a single garment image using the configured classification model. 337 338 Args: 339 image (str): The filename of the image to classify, located in `self.input_dir`. 340 verbose (bool): If True, prints detailed classification output. Defaults to False. 341 342 Returns: 343 tuple: 344 - label (str): The predicted class label of the garment. 345 - probabilities (List[float]): A list of probabilities for each class. 346 """ 347 label, probablities = classification.predict( 348 model=self.classification_model, 349 image_path=f"{self.input_dir}/{image}", 350 classes=self.classes, 351 resize_dim=self.classification_model_args.get("resize_dim"), 352 normalize_mean=self.classification_model_args.get("normalize_mean"), 353 normalize_std=self.classification_model_args.get("normalize_std"), 354 device=self.device, 355 verbose=verbose, 356 ) 357 return label, probablities 358 359 def segment(self, image: str): 360 """ 361 Segments a single garment image to extract its mask and optionally modifies the background color. 362 363 This method acts as an intelligent router for your segmentation arguments. It automatically 364 filters out initialization keys (e.g., `model_config`) and post-processing keys 365 (e.g., `background_color`) from `self.segmentation_model_args`. The remaining arguments 366 (such as `processor` and `prompt` for SAM or `resize_dim` for standard models such as BiRefNet) 367 are dynamically passed into the extraction pipeline. 368 369 For Segment Anything models the prompt is supplied via the `prompt` dictionary, which accepts 370 `"points"`, `"labels"`, `"boxes"`, and/or `"text"`. When a text prompt is used with SAM 1 or 371 SAM 2, also provide `grounding_model` and `grounding_processor` in the segmentation arguments, 372 because those families have no text encoder and need the phrase grounded into boxes first. 373 374 Args: 375 image (str): The filename of the image to segment, located in `self.input_dir`. 376 377 Raises: 378 ValueError: If a SAM model is configured without any prompt, or if a text prompt is used 379 with SAM 1 or SAM 2 without a grounding model. 380 381 Returns: 382 tuple: 383 - original_img (np.ndarray): The original input image converted to a numpy array. 384 - mask (np.ndarray): The extracted binary segmentation mask as a numpy array. 385 - bg_modified_img (np.ndarray, optional): The image with the background color replaced. 386 This third element is only returned if 387 `background_color` is provided in the 388 segmentation arguments. 389 """ 390 # 1. Filter out initialization and post-processing arguments 391 extraction_kwargs = { 392 k: v for k, v in self.segmentation_model_args.items() 393 if k not in ["model_config", "background_color"] 394 } 395 396 # 2. Extract using the unified function and unpacked kwargs 397 original_img, mask = segmentation.extract( 398 model=self.segmentation_model, 399 image_path=f"{self.input_dir}/{image}", 400 device=self.device, 401 **extraction_kwargs 402 ) 403 404 # 3. Handle optional background color modification 405 background_color = self.segmentation_model_args.get("background_color") 406 407 if background_color is None: 408 return original_img, mask 409 else: 410 bg_modified_img = segmentation.change_background_color( 411 image_np=original_img, mask_np=mask, background_color=background_color 412 ) 413 return original_img, mask, bg_modified_img 414 415 def matte(self, image: str, mask: np.ndarray): 416 """ 417 Refines a segmentation mask into a soft alpha matte for a single image. 418 419 Matting in the pipeline is intentionally built on top of segmentation rather than 420 run standalone: the segmentation mask is what supplies the trimap for ViTMatte or 421 the guidance mask for Matting Anything. Calling this without a mask therefore 422 raises, which is why `do_matte=True` forces the segmentation stage to run. 423 424 Standalone matting has no such requirement; call `garmentiq.matting.matte` directly 425 with whatever image and trimap you already have. 426 427 Args: 428 image (str): The filename of the image to matte, located in `self.input_dir`. 429 mask (numpy.ndarray): The segmentation mask produced for the same image. 430 431 Raises: 432 ValueError: If matting is not configured, or if `mask` is None. 433 434 Returns: 435 numpy.ndarray: The alpha matte as `uint8` in `[0, 255]`, matching the image size. 436 """ 437 if not self.do_matte or self.matting_model is None: 438 raise ValueError( 439 "Matting is not configured on this tailor agent. Construct it with " 440 "`do_matte=True` and a matting model." 441 ) 442 if mask is None: 443 raise ValueError( 444 "Matting in the tailor pipeline requires a segmentation mask. Run the " 445 "segmentation stage first, then pass its mask here." 446 ) 447 448 _, alpha = matting.matte( 449 model=self.matting_model, 450 image_path=f"{self.input_dir}/{image}", 451 processor=self.matting_model_args.get("processor"), 452 mask=mask, 453 prompt=self.matting_model_args.get("prompt"), 454 trimap_args=self.matting_model_args.get("trimap_args"), 455 device=self.device, 456 ) 457 return alpha 458 459 def detect(self, class_name: str, image: Union[str, np.ndarray]): 460 """ 461 Detects predefined landmarks on a garment image based on its classified class. 462 463 Args: 464 class_name (str): The classified name of the garment. 465 image (Union[str, np.ndarray]): The path to the image file or a NumPy array of the image. 466 467 Returns: 468 tuple: 469 - coords (np.array): Detected landmark coordinates. 470 - maxval (np.array): Confidence scores for detected landmarks. 471 - detection_dict (dict): A dictionary containing detailed landmark detection data. 472 """ 473 if isinstance(image, str): 474 image = f"{self.input_dir}/{image}" 475 476 coords, maxval, detection_dict = landmark.detect( 477 class_name=class_name, 478 class_dict=self.class_dict, 479 image_path=image, 480 model=self.landmark_detection_model, 481 scale_std=self.landmark_detection_model_args.get("scale_std"), 482 resize_dim=self.landmark_detection_model_args.get("resize_dim"), 483 normalize_mean=self.landmark_detection_model_args.get("normalize_mean"), 484 normalize_std=self.landmark_detection_model_args.get("normalize_std"), 485 device=self.device, 486 ) 487 return coords, maxval, detection_dict 488 489 def derive( 490 self, 491 class_name: str, 492 detection_dict: dict, 493 derivation_dict: dict, 494 landmark_coords: np.array, 495 np_mask: np.array, 496 ): 497 """ 498 Derives non-predefined landmark coordinates based on predefined landmarks and a mask. 499 500 Args: 501 class_name (str): The name of the garment class. 502 detection_dict (dict): The dictionary containing detected landmarks. 503 derivation_dict (dict): The dictionary defining derivation rules. 504 landmark_coords (np.array): NumPy array of initial landmark coordinates. 505 np_mask (np.array): NumPy array of the segmentation mask. 506 507 Returns: 508 tuple: 509 - derived_coords (dict): A dictionary of the newly derived landmark coordinates. 510 - updated_detection_dict (dict): The detection dictionary updated with derived landmarks. 511 """ 512 derived_coords, updated_detection_dict = landmark.derive( 513 class_name=class_name, 514 detection_dict=detection_dict, 515 derivation_dict=derivation_dict, 516 landmark_coords=landmark_coords, 517 np_mask=np_mask, 518 ) 519 return derived_coords, updated_detection_dict 520 521 def refine( 522 self, 523 class_name: str, 524 detection_np: np.array, 525 detection_conf: np.array, 526 detection_dict: dict, 527 mask: np.array, 528 window_size: int = 5, 529 ksize: tuple = (11, 11), 530 sigmaX: float = 0.0, 531 ): 532 """ 533 Refines detected landmark coordinates using a blurred segmentation mask. 534 535 Args: 536 class_name (str): The name of the garment class. 537 detection_np (np.array): NumPy array of initial landmark predictions. 538 detection_conf (np.array): NumPy array of confidence scores for each predicted landmark. 539 detection_dict (dict): Dictionary containing landmark data for each class. 540 mask (np.array): Grayscale mask image used to guide refinement. 541 window_size (int, optional): Size of the window used in the refinement algorithm. Defaults to 5. 542 ksize (tuple, optional): Kernel size for Gaussian blur. Must be odd integers. Defaults to (11, 11). 543 sigmaX (float, optional): Gaussian kernel standard deviation in the X direction. Defaults to 0.0. 544 545 Returns: 546 tuple: 547 - refined_detection_np (np.array): Array of the same shape as `detection_np` with refined coordinates. 548 - detection_dict (dict): Updated detection dictionary with refined landmark coordinates. 549 """ 550 if self.refinement_args: 551 if self.refinement_args.get("window_size") is not None: 552 window_size = self.refinement_args["window_size"] 553 if self.refinement_args.get("ksize") is not None: 554 ksize = self.refinement_args["ksize"] 555 if self.refinement_args.get("sigmaX") is not None: 556 sigmaX = self.refinement_args["sigmaX"] 557 558 refined_detection_np, refined_detection_dict = landmark.refine( 559 class_name=class_name, 560 detection_np=detection_np, 561 detection_conf=detection_conf, 562 detection_dict=detection_dict, 563 mask=mask, 564 window_size=window_size, 565 ksize=ksize, 566 sigmaX=sigmaX, 567 ) 568 569 return refined_detection_np, refined_detection_dict 570 571 def measure( 572 self, 573 save_segmentation_image: bool = False, 574 save_measurement_image: bool = False, 575 save_matting_image: bool = False, 576 ): 577 """ 578 Executes the full garment measurement pipeline for all images in the input directory. 579 580 This method processes each image through a multi-stage pipeline that includes garment classification, 581 segmentation, landmark detection, optional refinement, and measurement derivation. During classification, 582 the system identifies the type of garment (e.g., shirt, dress, pants). Segmentation follows, producing 583 binary or instance masks that separate the garment from the background. When matting is 584 enabled, the mask is then refined into a soft alpha matte, and the resulting alpha-composited 585 image replaces the hard background-modified image as the input to landmark detection. 586 Landmark detection is then 587 performed to locate anatomical or garment-specific keypoints such as shoulders or waist positions. If 588 enabled, an optional refinement step applies post-processing or model-based corrections to improve the 589 accuracy of detected keypoints. Finally, the system calculates key garment dimensions - such as chest width, 590 waist width, and full length - based on the detected landmarks. In addition to this processing pipeline, 591 the method also manages data and visual output exports. For each input image, a cleaned JSON file is 592 generated containing the predicted garment class, landmark coordinates, and the resulting measurements. 593 Optionally, visual outputs such as segmentation masks and images annotated with landmarks and measurements 594 can be saved to assist in inspection or debugging. 595 596 Args: 597 save_segmentation_image (bool): If True, saves segmentation masks and background-modified images. 598 Defaults to False. 599 save_measurement_image (bool): If True, saves images overlaid with detected landmarks and measurements. 600 Defaults to False. 601 save_matting_image (bool): If True, saves the alpha mattes produced by the matting stage, 602 and the alpha-composited images when a `background_color` is 603 set in `matting_model_args`. Only has an effect when the agent 604 was constructed with `do_matte=True`. Defaults to False. 605 606 Raises: 607 ValueError: If `save_matting_image` is True but the agent was not constructed with 608 `do_matte=True`, or if the matting stage cannot find a segmentation mask. 609 610 Returns: 611 tuple: 612 - metadata (pd.DataFrame): A DataFrame containing metadata for each processed image, such as: 613 - Original image path 614 - Paths to any saved segmentation or annotated images 615 - Class and measurement results 616 - outputs (dict): A dictionary mapping image filenames to their detailed processing results, including: 617 - Predicted class 618 - Detected landmarks with coordinates and confidence scores 619 - Calculated measurements 620 - File paths to any saved images (if applicable) 621 622 Example of exported JSON: 623 ``` 624 { 625 "cloth_3.jpg": { 626 "class": "vest dress", 627 "landmarks": { 628 "10": { 629 "conf": 0.7269417643547058, 630 "x": 611.0, 631 "y": 861.0 632 }, 633 "16": { 634 "conf": 0.6769524812698364, 635 "x": 1226.0, 636 "y": 838.0 637 }, 638 "17": { 639 "conf": 0.7472652196884155, 640 "x": 1213.0, 641 "y": 726.0 642 }, 643 "18": { 644 "conf": 0.7360446453094482, 645 "x": 1238.0, 646 "y": 613.0 647 }, 648 "2": { 649 "conf": 0.9256571531295776, 650 "x": 703.0, 651 "y": 264.0 652 }, 653 "20": { 654 "x": 700.936, 655 "y": 2070.0 656 }, 657 "8": { 658 "conf": 0.7129100561141968, 659 "x": 563.0, 660 "y": 613.0 661 }, 662 "9": { 663 "conf": 0.8203497529029846, 664 "x": 598.0, 665 "y": 726.0 666 } 667 }, 668 "measurements": { 669 "chest": { 670 "distance": 675.0, 671 "landmarks": { 672 "end": "18", 673 "start": "8" 674 } 675 }, 676 "full length": { 677 "distance": 1806.0011794281863, 678 "landmarks": { 679 "end": "20", 680 "start": "2" 681 } 682 }, 683 "hips": { 684 "distance": 615.4299310238331, 685 "landmarks": { 686 "end": "16", 687 "start": "10" 688 } 689 }, 690 "waist": { 691 "distance": 615.0, 692 "landmarks": { 693 "end": "17", 694 "start": "9" 695 } 696 } 697 } 698 } 699 } 700 ``` 701 """ 702 # Some helper variables 703 use_bg_color = self.segmentation_model_args.get("background_color") is not None 704 use_matte_bg = ( 705 self.do_matte 706 and self.matting_model_args.get("background_color") is not None 707 ) 708 outputs = {} 709 710 if save_matting_image and not self.do_matte: 711 raise ValueError( 712 "`save_matting_image=True` but this tailor agent was not constructed with " 713 "`do_matte=True`, so there is no matting stage to save output from." 714 ) 715 716 # Step 1: Create the output directory 717 Path(self.output_dir).mkdir(parents=True, exist_ok=True) 718 Path(f"{self.output_dir}/measurement_json").mkdir(parents=True, exist_ok=True) 719 720 if save_segmentation_image and ( 721 use_bg_color or self.do_derive or self.do_refine 722 ): 723 Path(f"{self.output_dir}/mask_image").mkdir(parents=True, exist_ok=True) 724 if use_bg_color: 725 Path(f"{self.output_dir}/bg_modified_image").mkdir( 726 parents=True, exist_ok=True 727 ) 728 729 if save_measurement_image: 730 Path(f"{self.output_dir}/measurement_image").mkdir( 731 parents=True, exist_ok=True 732 ) 733 734 if save_matting_image and self.do_matte: 735 Path(f"{self.output_dir}/matte_image").mkdir(parents=True, exist_ok=True) 736 if use_matte_bg: 737 Path(f"{self.output_dir}/matte_composite_image").mkdir( 738 parents=True, exist_ok=True 739 ) 740 741 # Step 2: Collect image filenames from input_dir 742 image_extensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.tiff"] 743 input_path = Path(self.input_dir) 744 745 image_files = [] 746 for ext in image_extensions: 747 image_files.extend(input_path.glob(ext)) 748 749 # Step 3: Determine column structure 750 columns = [ 751 "filename", 752 "class", 753 "mask_image" if use_bg_color or self.do_derive or self.do_refine else None, 754 "bg_modified_image" if use_bg_color else None, 755 "matte_image" if save_matting_image and self.do_matte else None, 756 "matte_composite_image" 757 if save_matting_image and use_matte_bg 758 else None, 759 "measurement_image", 760 "measurement_json", 761 ] 762 columns = [col for col in columns if col is not None] 763 764 metadata = pd.DataFrame(columns=columns) 765 metadata["filename"] = [img.name for img in image_files] 766 767 # Step 4: Print start message and information 768 print(f"Start measuring {len(metadata['filename'])} garment images ...") 769 770 # Build the step list dynamically so every enabled stage is reported. 771 steps = ["classification"] 772 if use_bg_color or self.do_derive or self.do_refine or self.do_matte: 773 steps.append("segmentation") 774 if self.do_matte: 775 steps.append("matting") 776 steps.append("landmark detection") 777 if self.do_refine: 778 steps.append("landmark refinement") 779 if self.do_derive: 780 steps.append("landmark derivation") 781 782 if len(steps) == 1: 783 listed = steps[0] 784 elif len(steps) == 2: 785 listed = f"{steps[0]} and {steps[1]}" 786 else: 787 listed = ", ".join(steps[:-1]) + f", and {steps[-1]}" 788 message = f"There are {len(steps)} measurement steps: {listed}." 789 790 print(textwrap.fill(message, width=80)) 791 792 # Step 5: Classification 793 for idx, image in tqdm( 794 enumerate(metadata["filename"]), total=len(metadata), desc="Classification" 795 ): 796 label, _ = self.classify(image=image, verbose=False) 797 metadata.at[idx, "class"] = label 798 outputs[image] = {} 799 800 # Step 6: Segmentation 801 # Matting consumes the segmentation mask, so enabling it forces this stage. 802 if use_bg_color or self.do_derive or self.do_refine or self.do_matte: 803 for idx, image in tqdm( 804 enumerate(metadata["filename"]), 805 total=len(metadata), 806 desc="Segmentation", 807 ): 808 if use_bg_color: 809 original_img, mask, bg_modified_image = self.segment(image=image) 810 outputs[image] = { 811 "mask": mask, 812 "bg_modified_image": bg_modified_image, 813 } 814 else: 815 original_img, mask = self.segment(image=image) 816 outputs[image] = { 817 "mask": mask, 818 } 819 820 # Step 6b: Matting (always after segmentation, using its mask as guidance) 821 if self.do_matte: 822 matte_bg_color = self.matting_model_args.get("background_color") 823 # Landmark detection needs an RGB image, so the alpha matte is always 824 # composited onto some background. The colour is chosen in order of 825 # specificity: the matting colour, then the segmentation colour, then a 826 # neutral default. Compositing always happens when matting is enabled, so 827 # that the matte genuinely drives detection even when neither stage was 828 # asked to replace the background in its saved output. 829 detect_bg_color = matte_bg_color 830 if detect_bg_color is None: 831 detect_bg_color = self.segmentation_model_args.get("background_color") 832 if detect_bg_color is None: 833 detect_bg_color = DEFAULT_MATTE_DETECTION_BACKGROUND 834 835 for idx, image in tqdm( 836 enumerate(metadata["filename"]), 837 total=len(metadata), 838 desc="Matting", 839 ): 840 if outputs[image].get("mask") is None: 841 raise ValueError( 842 f"Matting requires a segmentation mask, but none was produced for " 843 f"{image!r}. The segmentation stage must run before matting." 844 ) 845 alpha = self.matte(image=image, mask=outputs[image]["mask"]) 846 outputs[image]["alpha"] = alpha 847 848 composited = matting.composite( 849 image_np=np.array( 850 Image.open(f"{self.input_dir}/{image}").convert("RGB") 851 ), 852 alpha_np=alpha, 853 background_color=detect_bg_color, 854 ) 855 outputs[image]["matte_detect_image"] = composited 856 # The composited image is only offered as a saved output when the user 857 # explicitly asked for a matting background colour, keeping that output 858 # optional in the same way segmentation's background replacement is. 859 if matte_bg_color is not None: 860 outputs[image]["matte_composite"] = composited 861 862 # Step 7: Landmark detection 863 # Detection runs on a background-replaced image when one was requested, because 864 # a clean background helps the pose model. When matting is enabled its softer, 865 # more accurate composite is used in place of the hard segmentation composite. 866 for idx, image in tqdm( 867 enumerate(metadata["filename"]), 868 total=len(metadata), 869 desc="Landmark detection", 870 ): 871 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 872 873 if self.do_matte and outputs[image].get("matte_detect_image") is not None: 874 detect_input = outputs[image]["matte_detect_image"] 875 elif use_bg_color: 876 detect_input = outputs[image]["bg_modified_image"] 877 else: 878 detect_input = image 879 880 coords, maxvals, detection_dict = self.detect( 881 class_name=label, image=detect_input 882 ) 883 outputs[image]["detection_dict"] = detection_dict 884 if self.do_derive or self.do_refine: 885 outputs[image]["coords"] = coords 886 outputs[image]["maxvals"] = maxvals 887 888 # Step 8: Landmark refinement 889 if self.do_refine: 890 for idx, image in tqdm( 891 enumerate(metadata["filename"]), 892 total=len(metadata), 893 desc="Landmark refinement", 894 ): 895 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 896 updated_coords, updated_detection_dict = self.refine( 897 class_name=label, 898 detection_np=outputs[image]["coords"], 899 detection_conf=outputs[image]["maxvals"], 900 detection_dict=outputs[image]["detection_dict"], 901 mask=outputs[image]["mask"], 902 ) 903 outputs[image]["coords"] = updated_coords 904 outputs[image]["detection_dict"] = updated_detection_dict 905 906 # Step 9: Landmark derivation 907 if self.do_derive: 908 for idx, image in tqdm( 909 enumerate(metadata["filename"]), 910 total=len(metadata), 911 desc="Landmark derivation", 912 ): 913 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 914 derived_coords, updated_detection_dict = self.derive( 915 class_name=label, 916 detection_dict=outputs[image]["detection_dict"], 917 derivation_dict=self.derivation_dict, 918 landmark_coords=outputs[image]["coords"], 919 np_mask=outputs[image]["mask"], 920 ) 921 outputs[image]["detection_dict"] = updated_detection_dict 922 923 # Step 10: Save segmentation image 924 if save_segmentation_image and ( 925 use_bg_color or self.do_derive or self.do_refine 926 ): 927 for idx, image in tqdm( 928 enumerate(metadata["filename"]), 929 total=len(metadata), 930 desc="Save segmentation image", 931 ): 932 transformed_name = os.path.splitext(image)[0] 933 Image.fromarray(outputs[image]["mask"]).save( 934 f"{self.output_dir}/mask_image/{transformed_name}_mask.png" 935 ) 936 metadata.at[ 937 idx, "mask_image" 938 ] = f"{self.output_dir}/mask_image/{transformed_name}_mask.png" 939 if use_bg_color: 940 Image.fromarray(outputs[image]["bg_modified_image"]).save( 941 f"{self.output_dir}/bg_modified_image/{transformed_name}_bg_modified.png" 942 ) 943 metadata.at[ 944 idx, "bg_modified_image" 945 ] = f"{self.output_dir}/bg_modified_image/{transformed_name}_bg_modified.png" 946 947 # Step 10b: Save matting image 948 if save_matting_image and self.do_matte: 949 for idx, image in tqdm( 950 enumerate(metadata["filename"]), 951 total=len(metadata), 952 desc="Save matting image", 953 ): 954 transformed_name = os.path.splitext(image)[0] 955 alpha_path = ( 956 f"{self.output_dir}/matte_image/{transformed_name}_matte.png" 957 ) 958 Image.fromarray(outputs[image]["alpha"]).save(alpha_path) 959 metadata.at[idx, "matte_image"] = alpha_path 960 961 if use_matte_bg: 962 composite_path = ( 963 f"{self.output_dir}/matte_composite_image/" 964 f"{transformed_name}_matte_composite.png" 965 ) 966 Image.fromarray(outputs[image]["matte_composite"]).save( 967 composite_path 968 ) 969 metadata.at[idx, "matte_composite_image"] = composite_path 970 971 # Step 11: Save measurement image 972 if save_measurement_image: 973 for idx, image in tqdm( 974 enumerate(metadata["filename"]), 975 total=len(metadata), 976 desc="Save measurement image", 977 ): 978 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 979 transformed_name = os.path.splitext(image)[0] 980 981 image_to_save = Image.open(f"{self.input_dir}/{image}").convert("RGB") 982 draw = ImageDraw.Draw(image_to_save) 983 font = ImageFont.load_default() 984 landmarks = outputs[image]["detection_dict"][label]["landmarks"] 985 986 for lm_id, lm_data in landmarks.items(): 987 x, y = lm_data["x"], lm_data["y"] 988 radius = 5 989 draw.ellipse( 990 (x - radius, y - radius, x + radius, y + radius), fill="green" 991 ) 992 draw.text((x + 8, y - 8), lm_id, fill="green", font=font) 993 994 image_to_save.save( 995 f"{self.output_dir}/measurement_image/{transformed_name}_measurement.png" 996 ) 997 metadata.at[ 998 idx, "measurement_image" 999 ] = f"{self.output_dir}/measurement_image/{transformed_name}_measurement.png" 1000 1001 # Step 12: Save measurement json 1002 for idx, image in tqdm( 1003 enumerate(metadata["filename"]), 1004 total=len(metadata), 1005 desc="Save measurement json", 1006 ): 1007 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 1008 transformed_name = os.path.splitext(image)[0] 1009 1010 # Clean the detection dictionary 1011 final_dict = utils.clean_detection_dict( 1012 class_name=label, 1013 image_name=image, 1014 detection_dict=outputs[image]["detection_dict"], 1015 ) 1016 1017 # Export JSON 1018 utils.export_dict_to_json( 1019 data=final_dict, 1020 filename=f"{self.output_dir}/measurement_json/{transformed_name}_measurement.json", 1021 ) 1022 1023 metadata.at[ 1024 idx, "measurement_json" 1025 ] = f"{self.output_dir}/measurement_json/{transformed_name}_measurement.json" 1026 1027 # Step 13: Save metadata as a CSV 1028 metadata.to_csv(f"{self.output_dir}/metadata.csv", index=False) 1029 1030 return metadata, outputs
31class tailor: 32 """ 33 The `tailor` class acts as a central agent for the GarmentIQ pipeline, 34 orchestrating garment measurement from classification to landmark derivation. 35 36 It integrates functionalities from other modules (classification, segmentation, landmark) 37 to provide a smooth end-to-end process for automated garment measurement from images. 38 39 Attributes: 40 input_dir (str): Directory containing input images. 41 model_dir (str): Directory where models are stored. 42 output_dir (str): Directory to save processed outputs. 43 class_dict (dict): Dictionary defining garment classes and their properties. 44 do_derive (bool): Flag to enable landmark derivation. 45 do_refine (bool): Flag to enable landmark refinement. 46 classification_model_path (str): Path to the classification model. 47 classification_model_class (Type[nn.Module]): Class definition for the classification model. 48 classification_model_args (Dict): Arguments for the classification model. 49 segmentation_model_path (str): Name or path for the segmentation model. 50 segmentation_model_class (Type[nn.Module]): Class definition for the segmentation model. 51 segmentation_model_args (Dict): Arguments for the segmentation model. 52 landmark_detection_model_path (str): Path to the landmark detection model. 53 landmark_detection_model_class (Type[nn.Module]): Class definition for the landmark detection model. 54 landmark_detection_model_args (Dict): Arguments for the landmark detection model. 55 refinement_args (Optional[Dict]): Arguments for landmark refinement. 56 derivation_dict (Optional[Dict]): Dictionary for landmark derivation rules. 57 device (torch.device): The device all models are loaded onto and run on. 58 do_matte (bool): Flag to enable the alpha matting stage. 59 matting_model_args (Optional[Dict]): Arguments for the matting model. 60 """ 61 62 def __init__( 63 self, 64 input_dir: str, 65 model_dir: str, 66 output_dir: str, 67 class_dict: dict, 68 do_derive: bool, 69 do_refine: bool, 70 classification_model_path: str, 71 classification_model_class: Type[nn.Module], 72 classification_model_args: Dict, 73 segmentation_model_path: str, 74 segmentation_model_class: Type[nn.Module], 75 segmentation_model_args: Dict, 76 landmark_detection_model_path: str, 77 landmark_detection_model_class: Type[nn.Module], 78 landmark_detection_model_args: Dict, 79 refinement_args: Optional[Dict] = None, 80 derivation_dict: Optional[Dict] = None, 81 device: Union[str, torch.device] = "cpu", 82 do_matte: bool = False, 83 matting_model_path: Optional[str] = None, 84 matting_model_class: Optional[Type[nn.Module]] = None, 85 matting_model_args: Optional[Dict] = None, 86 ): 87 """ 88 Initializes the `tailor` agent with paths, model configurations, and processing flags. 89 90 Args: 91 input_dir (str): Path to the directory containing input images. 92 model_dir (str): Path to the directory where all required models are stored. 93 output_dir (str): Path to the directory where all processed outputs will be saved. 94 class_dict (dict): A dictionary defining the garment classes, their predefined points, 95 index ranges, and instruction JSON file paths. 96 do_derive (bool): If True, enables the landmark derivation step. 97 do_refine (bool): If True, enables the landmark refinement step. 98 classification_model_path (str): The filename or relative path to the classification model. 99 classification_model_class (Type[nn.Module]): The Python class of the classification model. 100 classification_model_args (Dict): A dictionary of arguments to initialize the classification model. 101 segmentation_model_path (str): The filename or relative path of the segmentation model. 102 segmentation_model_class (Type[nn.Module]): The Python class of the segmentation model. 103 segmentation_model_args (Dict): A dictionary of arguments for the segmentation model. 104 For SAM this typically holds `model_config`, `processor`, 105 and `prompt` (with `"points"`, `"labels"`, `"boxes"`, 106 and/or `"text"`), plus optional `grounding_model` and 107 `grounding_processor` when using a text prompt with 108 SAM 1 or SAM 2. An optional `background_color` triggers 109 background replacement. 110 landmark_detection_model_path (str): The filename or relative path to the landmark detection model. 111 landmark_detection_model_class (Type[nn.Module]): The Python class of the landmark detection model. 112 landmark_detection_model_args (Dict): A dictionary of arguments for the landmark detection model. 113 refinement_args (Optional[Dict]): Optional arguments for the refinement process, 114 e.g., `window_size`, `ksize`, `sigmaX`. Defaults to None. 115 derivation_dict (Optional[Dict]): A dictionary defining derivation rules for non-predefined landmarks. 116 Required if `do_derive` is True. 117 device (Union[str, torch.device], optional): The device that every model in the pipeline 118 is loaded onto and run on, e.g. `"cpu"`, 119 `"cuda"`, `"cuda:0"`, or `"mps"`. Hardware 120 acceleration is opt-in; pass it explicitly 121 to use a GPU or Apple Silicon. 122 Defaults to `"cpu"`. 123 do_matte (bool, optional): If True, enables the alpha matting stage, which refines the 124 hard segmentation mask into a soft alpha matte. Matting in 125 the pipeline is deliberately built on top of segmentation: 126 the segmentation mask supplies the trimap (ViTMatte) or the 127 guidance mask (Matting Anything), so enabling this forces the 128 segmentation stage to run. Defaults to False. 129 matting_model_path (str, optional): The filename or relative path to the matting model 130 weights, relative to `model_dir`. Required when 131 `do_matte` is True and the model is loaded by 132 GarmentIQ. Defaults to None. 133 matting_model_class (Type[nn.Module], optional): The Python class of the matting model, 134 e.g. `VitMatteForImageMatting`. 135 Required when `do_matte` is True. 136 Defaults to None. 137 matting_model_args (Dict, optional): Arguments for the matting model. For ViTMatte this 138 holds `model_config` (e.g. 139 `{"config": load_vitmatte_config(...)}`), 140 `processor`, and optional `trimap_args` and 141 `background_color`. Alternatively pass a 142 preconstructed model as `model`, which is how 143 Matting Anything is supplied since it pairs a 144 decoder with a SAM instance. Defaults to None. 145 146 Raises: 147 ValueError: If `do_derive` is True but `derivation_dict` is None, if `do_matte` is True 148 but no matting model is provided, or if the requested `device` is invalid 149 or unavailable on this machine. 150 """ 151 # Device (resolved once and reused by every stage of the pipeline) 152 self.device = utils.resolve_device(device) 153 154 # Directories 155 self.input_dir = input_dir 156 self.model_dir = model_dir 157 self.output_dir = output_dir 158 159 # Classes 160 self.class_dict = class_dict 161 self.classes = sorted(list(class_dict.keys())) 162 163 # Derivation 164 self.do_derive = do_derive 165 if self.do_derive: 166 if derivation_dict is None: 167 raise ValueError( 168 "`derivation_dict` must be provided if `do_derive=True`." 169 ) 170 self.derivation_dict = derivation_dict 171 else: 172 self.derivation_dict = None 173 174 # Refinement setup 175 self.do_refine = do_refine 176 177 if self.do_refine: 178 if refinement_args is None: 179 self.refinement_args = {} 180 self.refinement_args = refinement_args 181 else: 182 self.refinement_args = None 183 184 # Classification model setup 185 self.classification_model_path = classification_model_path 186 self.classification_model_args = classification_model_args 187 self.classification_model_class = classification_model_class 188 filtered_model_args = { 189 k: v 190 for k, v in self.classification_model_args.items() 191 if k not in ("pretrained", "resize_dim", "normalize_mean", "normalize_std") 192 } 193 194 # Load the model using the filtered arguments 195 self.classification_model = classification.load_model( 196 model_path=f"{self.model_dir}/{self.classification_model_path}", 197 model_class=self.classification_model_class, 198 model_args=filtered_model_args, 199 device=self.device, 200 ) 201 202 # Segmentation model setup 203 self.segmentation_model_path = segmentation_model_path 204 self.segmentation_model_class = segmentation_model_class 205 self.segmentation_model_args = segmentation_model_args 206 self.segmentation_has_bg_color = "background_color" in segmentation_model_args 207 self.segmentation_model = segmentation.load_model( 208 model_path=f"{self.model_dir}/{self.segmentation_model_path}", 209 model_class=self.segmentation_model_class, 210 model_args=self.segmentation_model_args.get("model_config"), 211 device=self.device, 212 ) 213 214 # Landmark detection model setup 215 self.landmark_detection_model_path = landmark_detection_model_path 216 self.landmark_detection_model_class = landmark_detection_model_class 217 self.landmark_detection_model_args = landmark_detection_model_args 218 self.landmark_detection_model = landmark.detection.load_model( 219 model_path=f"{self.model_dir}/{self.landmark_detection_model_path}", 220 model_class=self.landmark_detection_model_class, 221 device=self.device, 222 ) 223 224 # Matting setup (optional, and always layered on top of segmentation) 225 self.do_matte = do_matte 226 self.matting_model_path = matting_model_path 227 self.matting_model_class = matting_model_class 228 self.matting_model_args = matting_model_args or {} 229 self.matting_model = None 230 231 if self.do_matte: 232 # Matting refines a segmentation mask, so the pipeline cannot run it 233 # without a working segmentation stage. 234 if self.segmentation_model is None: 235 raise ValueError( 236 "`do_matte=True` requires segmentation, because the segmentation mask " 237 "supplies the trimap (ViTMatte) or guidance mask (Matting Anything). " 238 "Configure the segmentation model, or set `do_matte=False`." 239 ) 240 241 preloaded = self.matting_model_args.get("model") 242 if preloaded is not None: 243 # Matting Anything is assembled by the caller because it pairs a 244 # decoder with an existing SAM instance. 245 self.matting_model = preloaded.to(self.device) 246 elif matting_model_class is not None and matting_model_path is not None: 247 self.matting_model = matting.load_model( 248 model_class=self.matting_model_class, 249 model_path=f"{self.model_dir}/{self.matting_model_path}", 250 model_args=self.matting_model_args.get("model_config"), 251 device=self.device, 252 ) 253 else: 254 missing = [] 255 if matting_model_class is None: 256 missing.append("`matting_model_class`") 257 if matting_model_path is None: 258 missing.append("`matting_model_path`") 259 raise ValueError( 260 f"`do_matte=True` requires a matting model, but {' and '.join(missing)} " 261 f"{'was' if len(missing) == 1 else 'were'} not provided. Either pass " 262 f"`matting_model_class` and `matting_model_path` (for ViTMatte), or pass " 263 f"an already constructed model as `matting_model_args={{'model': ...}}` " 264 f"(for Matting Anything)." 265 ) 266 267 # ViTMatte consumes a stacked image+trimap tensor built by its processor, 268 # so a missing processor would only fail deep inside inference. 269 model_names = {c.__name__ for c in type(self.matting_model).__mro__} 270 is_mam = "MattingAnything" in model_names 271 if not is_mam and self.matting_model_args.get("processor") is None: 272 raise ValueError( 273 "`do_matte=True` with a trimap-based model such as ViTMatte requires a " 274 "processor. Pass it as " 275 "`matting_model_args={'processor': load_vitmatte_processor(...)}`." 276 ) 277 if is_mam and not self.matting_model_args.get("prompt"): 278 raise ValueError( 279 "`do_matte=True` with Matting Anything requires a prompt for its internal " 280 "SAM. Pass it as " 281 "`matting_model_args={'prompt': {'boxes': [[[x0, y0, x1, y1]]]}}`." 282 ) 283 284 def summary(self): 285 """ 286 Prints a summary of the `tailor` agent's configuration, including directory paths, 287 defined classes, processing options (refine, derive, device), and loaded models. 288 """ 289 width = 80 290 sep = "=" * width 291 292 print(sep) 293 print("TAILOR AGENT SUMMARY".center(width)) 294 print(sep) 295 296 # Directories 297 print("DIRECTORY PATHS".center(width, "-")) 298 print(f"{'Input directory:':25} {self.input_dir}") 299 print(f"{'Model directory:':25} {self.model_dir}") 300 print(f"{'Output directory:':25} {self.output_dir}") 301 print() 302 303 # Classes 304 print("CLASSES".center(width, "-")) 305 print(f"{'Class Index':<11} | Class Name") 306 print(f"{'-'*11} | {'-'*66}") 307 for i, cls in enumerate(self.classes): 308 print(f"{i:<11} | {cls}") 309 print() 310 311 # Flags 312 print("OPTIONS".center(width, "-")) 313 print(f"{'Do refine?:':25} {self.do_refine}") 314 print(f"{'Do derive?:':25} {self.do_derive}") 315 print(f"{'Do matte?:':25} {self.do_matte}") 316 print(f"{'Device:':25} {self.device}") 317 print() 318 319 # Models 320 print("MODELS".center(width, "-")) 321 print( 322 f"{'Classification Model:':25} {self.classification_model_class.__name__}" 323 ) 324 print(f"{'Segmentation Model:':25} {self.segmentation_model_class.__name__}") 325 print(f"{' └─ Change BG color?:':25} {self.segmentation_has_bg_color}") 326 print( 327 f"{'Landmark Detection Model:':25} {self.landmark_detection_model_class.__class__.__name__}" 328 ) 329 if self.do_matte: 330 print(f"{'Matting Model:':25} {type(self.matting_model).__name__}") 331 matte_bg = self.matting_model_args.get("background_color") 332 print(f"{' └─ Composite BG color?:':25} {matte_bg is not None}") 333 print(sep) 334 335 def classify(self, image: str, verbose=False): 336 """ 337 Classifies a single garment image using the configured classification model. 338 339 Args: 340 image (str): The filename of the image to classify, located in `self.input_dir`. 341 verbose (bool): If True, prints detailed classification output. Defaults to False. 342 343 Returns: 344 tuple: 345 - label (str): The predicted class label of the garment. 346 - probabilities (List[float]): A list of probabilities for each class. 347 """ 348 label, probablities = classification.predict( 349 model=self.classification_model, 350 image_path=f"{self.input_dir}/{image}", 351 classes=self.classes, 352 resize_dim=self.classification_model_args.get("resize_dim"), 353 normalize_mean=self.classification_model_args.get("normalize_mean"), 354 normalize_std=self.classification_model_args.get("normalize_std"), 355 device=self.device, 356 verbose=verbose, 357 ) 358 return label, probablities 359 360 def segment(self, image: str): 361 """ 362 Segments a single garment image to extract its mask and optionally modifies the background color. 363 364 This method acts as an intelligent router for your segmentation arguments. It automatically 365 filters out initialization keys (e.g., `model_config`) and post-processing keys 366 (e.g., `background_color`) from `self.segmentation_model_args`. The remaining arguments 367 (such as `processor` and `prompt` for SAM or `resize_dim` for standard models such as BiRefNet) 368 are dynamically passed into the extraction pipeline. 369 370 For Segment Anything models the prompt is supplied via the `prompt` dictionary, which accepts 371 `"points"`, `"labels"`, `"boxes"`, and/or `"text"`. When a text prompt is used with SAM 1 or 372 SAM 2, also provide `grounding_model` and `grounding_processor` in the segmentation arguments, 373 because those families have no text encoder and need the phrase grounded into boxes first. 374 375 Args: 376 image (str): The filename of the image to segment, located in `self.input_dir`. 377 378 Raises: 379 ValueError: If a SAM model is configured without any prompt, or if a text prompt is used 380 with SAM 1 or SAM 2 without a grounding model. 381 382 Returns: 383 tuple: 384 - original_img (np.ndarray): The original input image converted to a numpy array. 385 - mask (np.ndarray): The extracted binary segmentation mask as a numpy array. 386 - bg_modified_img (np.ndarray, optional): The image with the background color replaced. 387 This third element is only returned if 388 `background_color` is provided in the 389 segmentation arguments. 390 """ 391 # 1. Filter out initialization and post-processing arguments 392 extraction_kwargs = { 393 k: v for k, v in self.segmentation_model_args.items() 394 if k not in ["model_config", "background_color"] 395 } 396 397 # 2. Extract using the unified function and unpacked kwargs 398 original_img, mask = segmentation.extract( 399 model=self.segmentation_model, 400 image_path=f"{self.input_dir}/{image}", 401 device=self.device, 402 **extraction_kwargs 403 ) 404 405 # 3. Handle optional background color modification 406 background_color = self.segmentation_model_args.get("background_color") 407 408 if background_color is None: 409 return original_img, mask 410 else: 411 bg_modified_img = segmentation.change_background_color( 412 image_np=original_img, mask_np=mask, background_color=background_color 413 ) 414 return original_img, mask, bg_modified_img 415 416 def matte(self, image: str, mask: np.ndarray): 417 """ 418 Refines a segmentation mask into a soft alpha matte for a single image. 419 420 Matting in the pipeline is intentionally built on top of segmentation rather than 421 run standalone: the segmentation mask is what supplies the trimap for ViTMatte or 422 the guidance mask for Matting Anything. Calling this without a mask therefore 423 raises, which is why `do_matte=True` forces the segmentation stage to run. 424 425 Standalone matting has no such requirement; call `garmentiq.matting.matte` directly 426 with whatever image and trimap you already have. 427 428 Args: 429 image (str): The filename of the image to matte, located in `self.input_dir`. 430 mask (numpy.ndarray): The segmentation mask produced for the same image. 431 432 Raises: 433 ValueError: If matting is not configured, or if `mask` is None. 434 435 Returns: 436 numpy.ndarray: The alpha matte as `uint8` in `[0, 255]`, matching the image size. 437 """ 438 if not self.do_matte or self.matting_model is None: 439 raise ValueError( 440 "Matting is not configured on this tailor agent. Construct it with " 441 "`do_matte=True` and a matting model." 442 ) 443 if mask is None: 444 raise ValueError( 445 "Matting in the tailor pipeline requires a segmentation mask. Run the " 446 "segmentation stage first, then pass its mask here." 447 ) 448 449 _, alpha = matting.matte( 450 model=self.matting_model, 451 image_path=f"{self.input_dir}/{image}", 452 processor=self.matting_model_args.get("processor"), 453 mask=mask, 454 prompt=self.matting_model_args.get("prompt"), 455 trimap_args=self.matting_model_args.get("trimap_args"), 456 device=self.device, 457 ) 458 return alpha 459 460 def detect(self, class_name: str, image: Union[str, np.ndarray]): 461 """ 462 Detects predefined landmarks on a garment image based on its classified class. 463 464 Args: 465 class_name (str): The classified name of the garment. 466 image (Union[str, np.ndarray]): The path to the image file or a NumPy array of the image. 467 468 Returns: 469 tuple: 470 - coords (np.array): Detected landmark coordinates. 471 - maxval (np.array): Confidence scores for detected landmarks. 472 - detection_dict (dict): A dictionary containing detailed landmark detection data. 473 """ 474 if isinstance(image, str): 475 image = f"{self.input_dir}/{image}" 476 477 coords, maxval, detection_dict = landmark.detect( 478 class_name=class_name, 479 class_dict=self.class_dict, 480 image_path=image, 481 model=self.landmark_detection_model, 482 scale_std=self.landmark_detection_model_args.get("scale_std"), 483 resize_dim=self.landmark_detection_model_args.get("resize_dim"), 484 normalize_mean=self.landmark_detection_model_args.get("normalize_mean"), 485 normalize_std=self.landmark_detection_model_args.get("normalize_std"), 486 device=self.device, 487 ) 488 return coords, maxval, detection_dict 489 490 def derive( 491 self, 492 class_name: str, 493 detection_dict: dict, 494 derivation_dict: dict, 495 landmark_coords: np.array, 496 np_mask: np.array, 497 ): 498 """ 499 Derives non-predefined landmark coordinates based on predefined landmarks and a mask. 500 501 Args: 502 class_name (str): The name of the garment class. 503 detection_dict (dict): The dictionary containing detected landmarks. 504 derivation_dict (dict): The dictionary defining derivation rules. 505 landmark_coords (np.array): NumPy array of initial landmark coordinates. 506 np_mask (np.array): NumPy array of the segmentation mask. 507 508 Returns: 509 tuple: 510 - derived_coords (dict): A dictionary of the newly derived landmark coordinates. 511 - updated_detection_dict (dict): The detection dictionary updated with derived landmarks. 512 """ 513 derived_coords, updated_detection_dict = landmark.derive( 514 class_name=class_name, 515 detection_dict=detection_dict, 516 derivation_dict=derivation_dict, 517 landmark_coords=landmark_coords, 518 np_mask=np_mask, 519 ) 520 return derived_coords, updated_detection_dict 521 522 def refine( 523 self, 524 class_name: str, 525 detection_np: np.array, 526 detection_conf: np.array, 527 detection_dict: dict, 528 mask: np.array, 529 window_size: int = 5, 530 ksize: tuple = (11, 11), 531 sigmaX: float = 0.0, 532 ): 533 """ 534 Refines detected landmark coordinates using a blurred segmentation mask. 535 536 Args: 537 class_name (str): The name of the garment class. 538 detection_np (np.array): NumPy array of initial landmark predictions. 539 detection_conf (np.array): NumPy array of confidence scores for each predicted landmark. 540 detection_dict (dict): Dictionary containing landmark data for each class. 541 mask (np.array): Grayscale mask image used to guide refinement. 542 window_size (int, optional): Size of the window used in the refinement algorithm. Defaults to 5. 543 ksize (tuple, optional): Kernel size for Gaussian blur. Must be odd integers. Defaults to (11, 11). 544 sigmaX (float, optional): Gaussian kernel standard deviation in the X direction. Defaults to 0.0. 545 546 Returns: 547 tuple: 548 - refined_detection_np (np.array): Array of the same shape as `detection_np` with refined coordinates. 549 - detection_dict (dict): Updated detection dictionary with refined landmark coordinates. 550 """ 551 if self.refinement_args: 552 if self.refinement_args.get("window_size") is not None: 553 window_size = self.refinement_args["window_size"] 554 if self.refinement_args.get("ksize") is not None: 555 ksize = self.refinement_args["ksize"] 556 if self.refinement_args.get("sigmaX") is not None: 557 sigmaX = self.refinement_args["sigmaX"] 558 559 refined_detection_np, refined_detection_dict = landmark.refine( 560 class_name=class_name, 561 detection_np=detection_np, 562 detection_conf=detection_conf, 563 detection_dict=detection_dict, 564 mask=mask, 565 window_size=window_size, 566 ksize=ksize, 567 sigmaX=sigmaX, 568 ) 569 570 return refined_detection_np, refined_detection_dict 571 572 def measure( 573 self, 574 save_segmentation_image: bool = False, 575 save_measurement_image: bool = False, 576 save_matting_image: bool = False, 577 ): 578 """ 579 Executes the full garment measurement pipeline for all images in the input directory. 580 581 This method processes each image through a multi-stage pipeline that includes garment classification, 582 segmentation, landmark detection, optional refinement, and measurement derivation. During classification, 583 the system identifies the type of garment (e.g., shirt, dress, pants). Segmentation follows, producing 584 binary or instance masks that separate the garment from the background. When matting is 585 enabled, the mask is then refined into a soft alpha matte, and the resulting alpha-composited 586 image replaces the hard background-modified image as the input to landmark detection. 587 Landmark detection is then 588 performed to locate anatomical or garment-specific keypoints such as shoulders or waist positions. If 589 enabled, an optional refinement step applies post-processing or model-based corrections to improve the 590 accuracy of detected keypoints. Finally, the system calculates key garment dimensions - such as chest width, 591 waist width, and full length - based on the detected landmarks. In addition to this processing pipeline, 592 the method also manages data and visual output exports. For each input image, a cleaned JSON file is 593 generated containing the predicted garment class, landmark coordinates, and the resulting measurements. 594 Optionally, visual outputs such as segmentation masks and images annotated with landmarks and measurements 595 can be saved to assist in inspection or debugging. 596 597 Args: 598 save_segmentation_image (bool): If True, saves segmentation masks and background-modified images. 599 Defaults to False. 600 save_measurement_image (bool): If True, saves images overlaid with detected landmarks and measurements. 601 Defaults to False. 602 save_matting_image (bool): If True, saves the alpha mattes produced by the matting stage, 603 and the alpha-composited images when a `background_color` is 604 set in `matting_model_args`. Only has an effect when the agent 605 was constructed with `do_matte=True`. Defaults to False. 606 607 Raises: 608 ValueError: If `save_matting_image` is True but the agent was not constructed with 609 `do_matte=True`, or if the matting stage cannot find a segmentation mask. 610 611 Returns: 612 tuple: 613 - metadata (pd.DataFrame): A DataFrame containing metadata for each processed image, such as: 614 - Original image path 615 - Paths to any saved segmentation or annotated images 616 - Class and measurement results 617 - outputs (dict): A dictionary mapping image filenames to their detailed processing results, including: 618 - Predicted class 619 - Detected landmarks with coordinates and confidence scores 620 - Calculated measurements 621 - File paths to any saved images (if applicable) 622 623 Example of exported JSON: 624 ``` 625 { 626 "cloth_3.jpg": { 627 "class": "vest dress", 628 "landmarks": { 629 "10": { 630 "conf": 0.7269417643547058, 631 "x": 611.0, 632 "y": 861.0 633 }, 634 "16": { 635 "conf": 0.6769524812698364, 636 "x": 1226.0, 637 "y": 838.0 638 }, 639 "17": { 640 "conf": 0.7472652196884155, 641 "x": 1213.0, 642 "y": 726.0 643 }, 644 "18": { 645 "conf": 0.7360446453094482, 646 "x": 1238.0, 647 "y": 613.0 648 }, 649 "2": { 650 "conf": 0.9256571531295776, 651 "x": 703.0, 652 "y": 264.0 653 }, 654 "20": { 655 "x": 700.936, 656 "y": 2070.0 657 }, 658 "8": { 659 "conf": 0.7129100561141968, 660 "x": 563.0, 661 "y": 613.0 662 }, 663 "9": { 664 "conf": 0.8203497529029846, 665 "x": 598.0, 666 "y": 726.0 667 } 668 }, 669 "measurements": { 670 "chest": { 671 "distance": 675.0, 672 "landmarks": { 673 "end": "18", 674 "start": "8" 675 } 676 }, 677 "full length": { 678 "distance": 1806.0011794281863, 679 "landmarks": { 680 "end": "20", 681 "start": "2" 682 } 683 }, 684 "hips": { 685 "distance": 615.4299310238331, 686 "landmarks": { 687 "end": "16", 688 "start": "10" 689 } 690 }, 691 "waist": { 692 "distance": 615.0, 693 "landmarks": { 694 "end": "17", 695 "start": "9" 696 } 697 } 698 } 699 } 700 } 701 ``` 702 """ 703 # Some helper variables 704 use_bg_color = self.segmentation_model_args.get("background_color") is not None 705 use_matte_bg = ( 706 self.do_matte 707 and self.matting_model_args.get("background_color") is not None 708 ) 709 outputs = {} 710 711 if save_matting_image and not self.do_matte: 712 raise ValueError( 713 "`save_matting_image=True` but this tailor agent was not constructed with " 714 "`do_matte=True`, so there is no matting stage to save output from." 715 ) 716 717 # Step 1: Create the output directory 718 Path(self.output_dir).mkdir(parents=True, exist_ok=True) 719 Path(f"{self.output_dir}/measurement_json").mkdir(parents=True, exist_ok=True) 720 721 if save_segmentation_image and ( 722 use_bg_color or self.do_derive or self.do_refine 723 ): 724 Path(f"{self.output_dir}/mask_image").mkdir(parents=True, exist_ok=True) 725 if use_bg_color: 726 Path(f"{self.output_dir}/bg_modified_image").mkdir( 727 parents=True, exist_ok=True 728 ) 729 730 if save_measurement_image: 731 Path(f"{self.output_dir}/measurement_image").mkdir( 732 parents=True, exist_ok=True 733 ) 734 735 if save_matting_image and self.do_matte: 736 Path(f"{self.output_dir}/matte_image").mkdir(parents=True, exist_ok=True) 737 if use_matte_bg: 738 Path(f"{self.output_dir}/matte_composite_image").mkdir( 739 parents=True, exist_ok=True 740 ) 741 742 # Step 2: Collect image filenames from input_dir 743 image_extensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.tiff"] 744 input_path = Path(self.input_dir) 745 746 image_files = [] 747 for ext in image_extensions: 748 image_files.extend(input_path.glob(ext)) 749 750 # Step 3: Determine column structure 751 columns = [ 752 "filename", 753 "class", 754 "mask_image" if use_bg_color or self.do_derive or self.do_refine else None, 755 "bg_modified_image" if use_bg_color else None, 756 "matte_image" if save_matting_image and self.do_matte else None, 757 "matte_composite_image" 758 if save_matting_image and use_matte_bg 759 else None, 760 "measurement_image", 761 "measurement_json", 762 ] 763 columns = [col for col in columns if col is not None] 764 765 metadata = pd.DataFrame(columns=columns) 766 metadata["filename"] = [img.name for img in image_files] 767 768 # Step 4: Print start message and information 769 print(f"Start measuring {len(metadata['filename'])} garment images ...") 770 771 # Build the step list dynamically so every enabled stage is reported. 772 steps = ["classification"] 773 if use_bg_color or self.do_derive or self.do_refine or self.do_matte: 774 steps.append("segmentation") 775 if self.do_matte: 776 steps.append("matting") 777 steps.append("landmark detection") 778 if self.do_refine: 779 steps.append("landmark refinement") 780 if self.do_derive: 781 steps.append("landmark derivation") 782 783 if len(steps) == 1: 784 listed = steps[0] 785 elif len(steps) == 2: 786 listed = f"{steps[0]} and {steps[1]}" 787 else: 788 listed = ", ".join(steps[:-1]) + f", and {steps[-1]}" 789 message = f"There are {len(steps)} measurement steps: {listed}." 790 791 print(textwrap.fill(message, width=80)) 792 793 # Step 5: Classification 794 for idx, image in tqdm( 795 enumerate(metadata["filename"]), total=len(metadata), desc="Classification" 796 ): 797 label, _ = self.classify(image=image, verbose=False) 798 metadata.at[idx, "class"] = label 799 outputs[image] = {} 800 801 # Step 6: Segmentation 802 # Matting consumes the segmentation mask, so enabling it forces this stage. 803 if use_bg_color or self.do_derive or self.do_refine or self.do_matte: 804 for idx, image in tqdm( 805 enumerate(metadata["filename"]), 806 total=len(metadata), 807 desc="Segmentation", 808 ): 809 if use_bg_color: 810 original_img, mask, bg_modified_image = self.segment(image=image) 811 outputs[image] = { 812 "mask": mask, 813 "bg_modified_image": bg_modified_image, 814 } 815 else: 816 original_img, mask = self.segment(image=image) 817 outputs[image] = { 818 "mask": mask, 819 } 820 821 # Step 6b: Matting (always after segmentation, using its mask as guidance) 822 if self.do_matte: 823 matte_bg_color = self.matting_model_args.get("background_color") 824 # Landmark detection needs an RGB image, so the alpha matte is always 825 # composited onto some background. The colour is chosen in order of 826 # specificity: the matting colour, then the segmentation colour, then a 827 # neutral default. Compositing always happens when matting is enabled, so 828 # that the matte genuinely drives detection even when neither stage was 829 # asked to replace the background in its saved output. 830 detect_bg_color = matte_bg_color 831 if detect_bg_color is None: 832 detect_bg_color = self.segmentation_model_args.get("background_color") 833 if detect_bg_color is None: 834 detect_bg_color = DEFAULT_MATTE_DETECTION_BACKGROUND 835 836 for idx, image in tqdm( 837 enumerate(metadata["filename"]), 838 total=len(metadata), 839 desc="Matting", 840 ): 841 if outputs[image].get("mask") is None: 842 raise ValueError( 843 f"Matting requires a segmentation mask, but none was produced for " 844 f"{image!r}. The segmentation stage must run before matting." 845 ) 846 alpha = self.matte(image=image, mask=outputs[image]["mask"]) 847 outputs[image]["alpha"] = alpha 848 849 composited = matting.composite( 850 image_np=np.array( 851 Image.open(f"{self.input_dir}/{image}").convert("RGB") 852 ), 853 alpha_np=alpha, 854 background_color=detect_bg_color, 855 ) 856 outputs[image]["matte_detect_image"] = composited 857 # The composited image is only offered as a saved output when the user 858 # explicitly asked for a matting background colour, keeping that output 859 # optional in the same way segmentation's background replacement is. 860 if matte_bg_color is not None: 861 outputs[image]["matte_composite"] = composited 862 863 # Step 7: Landmark detection 864 # Detection runs on a background-replaced image when one was requested, because 865 # a clean background helps the pose model. When matting is enabled its softer, 866 # more accurate composite is used in place of the hard segmentation composite. 867 for idx, image in tqdm( 868 enumerate(metadata["filename"]), 869 total=len(metadata), 870 desc="Landmark detection", 871 ): 872 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 873 874 if self.do_matte and outputs[image].get("matte_detect_image") is not None: 875 detect_input = outputs[image]["matte_detect_image"] 876 elif use_bg_color: 877 detect_input = outputs[image]["bg_modified_image"] 878 else: 879 detect_input = image 880 881 coords, maxvals, detection_dict = self.detect( 882 class_name=label, image=detect_input 883 ) 884 outputs[image]["detection_dict"] = detection_dict 885 if self.do_derive or self.do_refine: 886 outputs[image]["coords"] = coords 887 outputs[image]["maxvals"] = maxvals 888 889 # Step 8: Landmark refinement 890 if self.do_refine: 891 for idx, image in tqdm( 892 enumerate(metadata["filename"]), 893 total=len(metadata), 894 desc="Landmark refinement", 895 ): 896 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 897 updated_coords, updated_detection_dict = self.refine( 898 class_name=label, 899 detection_np=outputs[image]["coords"], 900 detection_conf=outputs[image]["maxvals"], 901 detection_dict=outputs[image]["detection_dict"], 902 mask=outputs[image]["mask"], 903 ) 904 outputs[image]["coords"] = updated_coords 905 outputs[image]["detection_dict"] = updated_detection_dict 906 907 # Step 9: Landmark derivation 908 if self.do_derive: 909 for idx, image in tqdm( 910 enumerate(metadata["filename"]), 911 total=len(metadata), 912 desc="Landmark derivation", 913 ): 914 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 915 derived_coords, updated_detection_dict = self.derive( 916 class_name=label, 917 detection_dict=outputs[image]["detection_dict"], 918 derivation_dict=self.derivation_dict, 919 landmark_coords=outputs[image]["coords"], 920 np_mask=outputs[image]["mask"], 921 ) 922 outputs[image]["detection_dict"] = updated_detection_dict 923 924 # Step 10: Save segmentation image 925 if save_segmentation_image and ( 926 use_bg_color or self.do_derive or self.do_refine 927 ): 928 for idx, image in tqdm( 929 enumerate(metadata["filename"]), 930 total=len(metadata), 931 desc="Save segmentation image", 932 ): 933 transformed_name = os.path.splitext(image)[0] 934 Image.fromarray(outputs[image]["mask"]).save( 935 f"{self.output_dir}/mask_image/{transformed_name}_mask.png" 936 ) 937 metadata.at[ 938 idx, "mask_image" 939 ] = f"{self.output_dir}/mask_image/{transformed_name}_mask.png" 940 if use_bg_color: 941 Image.fromarray(outputs[image]["bg_modified_image"]).save( 942 f"{self.output_dir}/bg_modified_image/{transformed_name}_bg_modified.png" 943 ) 944 metadata.at[ 945 idx, "bg_modified_image" 946 ] = f"{self.output_dir}/bg_modified_image/{transformed_name}_bg_modified.png" 947 948 # Step 10b: Save matting image 949 if save_matting_image and self.do_matte: 950 for idx, image in tqdm( 951 enumerate(metadata["filename"]), 952 total=len(metadata), 953 desc="Save matting image", 954 ): 955 transformed_name = os.path.splitext(image)[0] 956 alpha_path = ( 957 f"{self.output_dir}/matte_image/{transformed_name}_matte.png" 958 ) 959 Image.fromarray(outputs[image]["alpha"]).save(alpha_path) 960 metadata.at[idx, "matte_image"] = alpha_path 961 962 if use_matte_bg: 963 composite_path = ( 964 f"{self.output_dir}/matte_composite_image/" 965 f"{transformed_name}_matte_composite.png" 966 ) 967 Image.fromarray(outputs[image]["matte_composite"]).save( 968 composite_path 969 ) 970 metadata.at[idx, "matte_composite_image"] = composite_path 971 972 # Step 11: Save measurement image 973 if save_measurement_image: 974 for idx, image in tqdm( 975 enumerate(metadata["filename"]), 976 total=len(metadata), 977 desc="Save measurement image", 978 ): 979 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 980 transformed_name = os.path.splitext(image)[0] 981 982 image_to_save = Image.open(f"{self.input_dir}/{image}").convert("RGB") 983 draw = ImageDraw.Draw(image_to_save) 984 font = ImageFont.load_default() 985 landmarks = outputs[image]["detection_dict"][label]["landmarks"] 986 987 for lm_id, lm_data in landmarks.items(): 988 x, y = lm_data["x"], lm_data["y"] 989 radius = 5 990 draw.ellipse( 991 (x - radius, y - radius, x + radius, y + radius), fill="green" 992 ) 993 draw.text((x + 8, y - 8), lm_id, fill="green", font=font) 994 995 image_to_save.save( 996 f"{self.output_dir}/measurement_image/{transformed_name}_measurement.png" 997 ) 998 metadata.at[ 999 idx, "measurement_image" 1000 ] = f"{self.output_dir}/measurement_image/{transformed_name}_measurement.png" 1001 1002 # Step 12: Save measurement json 1003 for idx, image in tqdm( 1004 enumerate(metadata["filename"]), 1005 total=len(metadata), 1006 desc="Save measurement json", 1007 ): 1008 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 1009 transformed_name = os.path.splitext(image)[0] 1010 1011 # Clean the detection dictionary 1012 final_dict = utils.clean_detection_dict( 1013 class_name=label, 1014 image_name=image, 1015 detection_dict=outputs[image]["detection_dict"], 1016 ) 1017 1018 # Export JSON 1019 utils.export_dict_to_json( 1020 data=final_dict, 1021 filename=f"{self.output_dir}/measurement_json/{transformed_name}_measurement.json", 1022 ) 1023 1024 metadata.at[ 1025 idx, "measurement_json" 1026 ] = f"{self.output_dir}/measurement_json/{transformed_name}_measurement.json" 1027 1028 # Step 13: Save metadata as a CSV 1029 metadata.to_csv(f"{self.output_dir}/metadata.csv", index=False) 1030 1031 return metadata, outputs
The tailor class acts as a central agent for the GarmentIQ pipeline,
orchestrating garment measurement from classification to landmark derivation.
It integrates functionalities from other modules (classification, segmentation, landmark) to provide a smooth end-to-end process for automated garment measurement from images.
Attributes:
- input_dir (str): Directory containing input images.
- model_dir (str): Directory where models are stored.
- output_dir (str): Directory to save processed outputs.
- class_dict (dict): Dictionary defining garment classes and their properties.
- do_derive (bool): Flag to enable landmark derivation.
- do_refine (bool): Flag to enable landmark refinement.
- classification_model_path (str): Path to the classification model.
- classification_model_class (Type[nn.Module]): Class definition for the classification model.
- classification_model_args (Dict): Arguments for the classification model.
- segmentation_model_path (str): Name or path for the segmentation model.
- segmentation_model_class (Type[nn.Module]): Class definition for the segmentation model.
- segmentation_model_args (Dict): Arguments for the segmentation model.
- landmark_detection_model_path (str): Path to the landmark detection model.
- landmark_detection_model_class (Type[nn.Module]): Class definition for the landmark detection model.
- landmark_detection_model_args (Dict): Arguments for the landmark detection model.
- refinement_args (Optional[Dict]): Arguments for landmark refinement.
- derivation_dict (Optional[Dict]): Dictionary for landmark derivation rules.
- device (torch.device): The device all models are loaded onto and run on.
- do_matte (bool): Flag to enable the alpha matting stage.
- matting_model_args (Optional[Dict]): Arguments for the matting model.
62 def __init__( 63 self, 64 input_dir: str, 65 model_dir: str, 66 output_dir: str, 67 class_dict: dict, 68 do_derive: bool, 69 do_refine: bool, 70 classification_model_path: str, 71 classification_model_class: Type[nn.Module], 72 classification_model_args: Dict, 73 segmentation_model_path: str, 74 segmentation_model_class: Type[nn.Module], 75 segmentation_model_args: Dict, 76 landmark_detection_model_path: str, 77 landmark_detection_model_class: Type[nn.Module], 78 landmark_detection_model_args: Dict, 79 refinement_args: Optional[Dict] = None, 80 derivation_dict: Optional[Dict] = None, 81 device: Union[str, torch.device] = "cpu", 82 do_matte: bool = False, 83 matting_model_path: Optional[str] = None, 84 matting_model_class: Optional[Type[nn.Module]] = None, 85 matting_model_args: Optional[Dict] = None, 86 ): 87 """ 88 Initializes the `tailor` agent with paths, model configurations, and processing flags. 89 90 Args: 91 input_dir (str): Path to the directory containing input images. 92 model_dir (str): Path to the directory where all required models are stored. 93 output_dir (str): Path to the directory where all processed outputs will be saved. 94 class_dict (dict): A dictionary defining the garment classes, their predefined points, 95 index ranges, and instruction JSON file paths. 96 do_derive (bool): If True, enables the landmark derivation step. 97 do_refine (bool): If True, enables the landmark refinement step. 98 classification_model_path (str): The filename or relative path to the classification model. 99 classification_model_class (Type[nn.Module]): The Python class of the classification model. 100 classification_model_args (Dict): A dictionary of arguments to initialize the classification model. 101 segmentation_model_path (str): The filename or relative path of the segmentation model. 102 segmentation_model_class (Type[nn.Module]): The Python class of the segmentation model. 103 segmentation_model_args (Dict): A dictionary of arguments for the segmentation model. 104 For SAM this typically holds `model_config`, `processor`, 105 and `prompt` (with `"points"`, `"labels"`, `"boxes"`, 106 and/or `"text"`), plus optional `grounding_model` and 107 `grounding_processor` when using a text prompt with 108 SAM 1 or SAM 2. An optional `background_color` triggers 109 background replacement. 110 landmark_detection_model_path (str): The filename or relative path to the landmark detection model. 111 landmark_detection_model_class (Type[nn.Module]): The Python class of the landmark detection model. 112 landmark_detection_model_args (Dict): A dictionary of arguments for the landmark detection model. 113 refinement_args (Optional[Dict]): Optional arguments for the refinement process, 114 e.g., `window_size`, `ksize`, `sigmaX`. Defaults to None. 115 derivation_dict (Optional[Dict]): A dictionary defining derivation rules for non-predefined landmarks. 116 Required if `do_derive` is True. 117 device (Union[str, torch.device], optional): The device that every model in the pipeline 118 is loaded onto and run on, e.g. `"cpu"`, 119 `"cuda"`, `"cuda:0"`, or `"mps"`. Hardware 120 acceleration is opt-in; pass it explicitly 121 to use a GPU or Apple Silicon. 122 Defaults to `"cpu"`. 123 do_matte (bool, optional): If True, enables the alpha matting stage, which refines the 124 hard segmentation mask into a soft alpha matte. Matting in 125 the pipeline is deliberately built on top of segmentation: 126 the segmentation mask supplies the trimap (ViTMatte) or the 127 guidance mask (Matting Anything), so enabling this forces the 128 segmentation stage to run. Defaults to False. 129 matting_model_path (str, optional): The filename or relative path to the matting model 130 weights, relative to `model_dir`. Required when 131 `do_matte` is True and the model is loaded by 132 GarmentIQ. Defaults to None. 133 matting_model_class (Type[nn.Module], optional): The Python class of the matting model, 134 e.g. `VitMatteForImageMatting`. 135 Required when `do_matte` is True. 136 Defaults to None. 137 matting_model_args (Dict, optional): Arguments for the matting model. For ViTMatte this 138 holds `model_config` (e.g. 139 `{"config": load_vitmatte_config(...)}`), 140 `processor`, and optional `trimap_args` and 141 `background_color`. Alternatively pass a 142 preconstructed model as `model`, which is how 143 Matting Anything is supplied since it pairs a 144 decoder with a SAM instance. Defaults to None. 145 146 Raises: 147 ValueError: If `do_derive` is True but `derivation_dict` is None, if `do_matte` is True 148 but no matting model is provided, or if the requested `device` is invalid 149 or unavailable on this machine. 150 """ 151 # Device (resolved once and reused by every stage of the pipeline) 152 self.device = utils.resolve_device(device) 153 154 # Directories 155 self.input_dir = input_dir 156 self.model_dir = model_dir 157 self.output_dir = output_dir 158 159 # Classes 160 self.class_dict = class_dict 161 self.classes = sorted(list(class_dict.keys())) 162 163 # Derivation 164 self.do_derive = do_derive 165 if self.do_derive: 166 if derivation_dict is None: 167 raise ValueError( 168 "`derivation_dict` must be provided if `do_derive=True`." 169 ) 170 self.derivation_dict = derivation_dict 171 else: 172 self.derivation_dict = None 173 174 # Refinement setup 175 self.do_refine = do_refine 176 177 if self.do_refine: 178 if refinement_args is None: 179 self.refinement_args = {} 180 self.refinement_args = refinement_args 181 else: 182 self.refinement_args = None 183 184 # Classification model setup 185 self.classification_model_path = classification_model_path 186 self.classification_model_args = classification_model_args 187 self.classification_model_class = classification_model_class 188 filtered_model_args = { 189 k: v 190 for k, v in self.classification_model_args.items() 191 if k not in ("pretrained", "resize_dim", "normalize_mean", "normalize_std") 192 } 193 194 # Load the model using the filtered arguments 195 self.classification_model = classification.load_model( 196 model_path=f"{self.model_dir}/{self.classification_model_path}", 197 model_class=self.classification_model_class, 198 model_args=filtered_model_args, 199 device=self.device, 200 ) 201 202 # Segmentation model setup 203 self.segmentation_model_path = segmentation_model_path 204 self.segmentation_model_class = segmentation_model_class 205 self.segmentation_model_args = segmentation_model_args 206 self.segmentation_has_bg_color = "background_color" in segmentation_model_args 207 self.segmentation_model = segmentation.load_model( 208 model_path=f"{self.model_dir}/{self.segmentation_model_path}", 209 model_class=self.segmentation_model_class, 210 model_args=self.segmentation_model_args.get("model_config"), 211 device=self.device, 212 ) 213 214 # Landmark detection model setup 215 self.landmark_detection_model_path = landmark_detection_model_path 216 self.landmark_detection_model_class = landmark_detection_model_class 217 self.landmark_detection_model_args = landmark_detection_model_args 218 self.landmark_detection_model = landmark.detection.load_model( 219 model_path=f"{self.model_dir}/{self.landmark_detection_model_path}", 220 model_class=self.landmark_detection_model_class, 221 device=self.device, 222 ) 223 224 # Matting setup (optional, and always layered on top of segmentation) 225 self.do_matte = do_matte 226 self.matting_model_path = matting_model_path 227 self.matting_model_class = matting_model_class 228 self.matting_model_args = matting_model_args or {} 229 self.matting_model = None 230 231 if self.do_matte: 232 # Matting refines a segmentation mask, so the pipeline cannot run it 233 # without a working segmentation stage. 234 if self.segmentation_model is None: 235 raise ValueError( 236 "`do_matte=True` requires segmentation, because the segmentation mask " 237 "supplies the trimap (ViTMatte) or guidance mask (Matting Anything). " 238 "Configure the segmentation model, or set `do_matte=False`." 239 ) 240 241 preloaded = self.matting_model_args.get("model") 242 if preloaded is not None: 243 # Matting Anything is assembled by the caller because it pairs a 244 # decoder with an existing SAM instance. 245 self.matting_model = preloaded.to(self.device) 246 elif matting_model_class is not None and matting_model_path is not None: 247 self.matting_model = matting.load_model( 248 model_class=self.matting_model_class, 249 model_path=f"{self.model_dir}/{self.matting_model_path}", 250 model_args=self.matting_model_args.get("model_config"), 251 device=self.device, 252 ) 253 else: 254 missing = [] 255 if matting_model_class is None: 256 missing.append("`matting_model_class`") 257 if matting_model_path is None: 258 missing.append("`matting_model_path`") 259 raise ValueError( 260 f"`do_matte=True` requires a matting model, but {' and '.join(missing)} " 261 f"{'was' if len(missing) == 1 else 'were'} not provided. Either pass " 262 f"`matting_model_class` and `matting_model_path` (for ViTMatte), or pass " 263 f"an already constructed model as `matting_model_args={{'model': ...}}` " 264 f"(for Matting Anything)." 265 ) 266 267 # ViTMatte consumes a stacked image+trimap tensor built by its processor, 268 # so a missing processor would only fail deep inside inference. 269 model_names = {c.__name__ for c in type(self.matting_model).__mro__} 270 is_mam = "MattingAnything" in model_names 271 if not is_mam and self.matting_model_args.get("processor") is None: 272 raise ValueError( 273 "`do_matte=True` with a trimap-based model such as ViTMatte requires a " 274 "processor. Pass it as " 275 "`matting_model_args={'processor': load_vitmatte_processor(...)}`." 276 ) 277 if is_mam and not self.matting_model_args.get("prompt"): 278 raise ValueError( 279 "`do_matte=True` with Matting Anything requires a prompt for its internal " 280 "SAM. Pass it as " 281 "`matting_model_args={'prompt': {'boxes': [[[x0, y0, x1, y1]]]}}`." 282 )
Initializes the tailor agent with paths, model configurations, and processing flags.
Arguments:
- input_dir (str): Path to the directory containing input images.
- model_dir (str): Path to the directory where all required models are stored.
- output_dir (str): Path to the directory where all processed outputs will be saved.
- class_dict (dict): A dictionary defining the garment classes, their predefined points, index ranges, and instruction JSON file paths.
- do_derive (bool): If True, enables the landmark derivation step.
- do_refine (bool): If True, enables the landmark refinement step.
- classification_model_path (str): The filename or relative path to the classification model.
- classification_model_class (Type[nn.Module]): The Python class of the classification model.
- classification_model_args (Dict): A dictionary of arguments to initialize the classification model.
- segmentation_model_path (str): The filename or relative path of the segmentation model.
- segmentation_model_class (Type[nn.Module]): The Python class of the segmentation model.
- segmentation_model_args (Dict): A dictionary of arguments for the segmentation model.
For SAM this typically holds
model_config,processor, andprompt(with"points","labels","boxes", and/or"text"), plus optionalgrounding_modelandgrounding_processorwhen using a text prompt with SAM 1 or SAM 2. An optionalbackground_colortriggers background replacement. - landmark_detection_model_path (str): The filename or relative path to the landmark detection model.
- landmark_detection_model_class (Type[nn.Module]): The Python class of the landmark detection model.
- landmark_detection_model_args (Dict): A dictionary of arguments for the landmark detection model.
- refinement_args (Optional[Dict]): Optional arguments for the refinement process,
e.g.,
window_size,ksize,sigmaX. Defaults to None. - derivation_dict (Optional[Dict]): A dictionary defining derivation rules for non-predefined landmarks.
Required if
do_deriveis True. - device (Union[str, torch.device], optional): The device that every model in the pipeline
is loaded onto and run on, e.g.
"cpu","cuda","cuda:0", or"mps". Hardware acceleration is opt-in; pass it explicitly to use a GPU or Apple Silicon. Defaults to"cpu". - do_matte (bool, optional): If True, enables the alpha matting stage, which refines the hard segmentation mask into a soft alpha matte. Matting in the pipeline is deliberately built on top of segmentation: the segmentation mask supplies the trimap (ViTMatte) or the guidance mask (Matting Anything), so enabling this forces the segmentation stage to run. Defaults to False.
- matting_model_path (str, optional): The filename or relative path to the matting model
weights, relative to
model_dir. Required whendo_matteis True and the model is loaded by GarmentIQ. Defaults to None. - matting_model_class (Type[nn.Module], optional): The Python class of the matting model,
e.g.
VitMatteForImageMatting. Required whendo_matteis True. Defaults to None. - matting_model_args (Dict, optional): Arguments for the matting model. For ViTMatte this
holds
model_config(e.g.{"config": load_vitmatte_config(...)}),processor, and optionaltrimap_argsandbackground_color. Alternatively pass a preconstructed model asmodel, which is how Matting Anything is supplied since it pairs a decoder with a SAM instance. Defaults to None.
Raises:
284 def summary(self): 285 """ 286 Prints a summary of the `tailor` agent's configuration, including directory paths, 287 defined classes, processing options (refine, derive, device), and loaded models. 288 """ 289 width = 80 290 sep = "=" * width 291 292 print(sep) 293 print("TAILOR AGENT SUMMARY".center(width)) 294 print(sep) 295 296 # Directories 297 print("DIRECTORY PATHS".center(width, "-")) 298 print(f"{'Input directory:':25} {self.input_dir}") 299 print(f"{'Model directory:':25} {self.model_dir}") 300 print(f"{'Output directory:':25} {self.output_dir}") 301 print() 302 303 # Classes 304 print("CLASSES".center(width, "-")) 305 print(f"{'Class Index':<11} | Class Name") 306 print(f"{'-'*11} | {'-'*66}") 307 for i, cls in enumerate(self.classes): 308 print(f"{i:<11} | {cls}") 309 print() 310 311 # Flags 312 print("OPTIONS".center(width, "-")) 313 print(f"{'Do refine?:':25} {self.do_refine}") 314 print(f"{'Do derive?:':25} {self.do_derive}") 315 print(f"{'Do matte?:':25} {self.do_matte}") 316 print(f"{'Device:':25} {self.device}") 317 print() 318 319 # Models 320 print("MODELS".center(width, "-")) 321 print( 322 f"{'Classification Model:':25} {self.classification_model_class.__name__}" 323 ) 324 print(f"{'Segmentation Model:':25} {self.segmentation_model_class.__name__}") 325 print(f"{' └─ Change BG color?:':25} {self.segmentation_has_bg_color}") 326 print( 327 f"{'Landmark Detection Model:':25} {self.landmark_detection_model_class.__class__.__name__}" 328 ) 329 if self.do_matte: 330 print(f"{'Matting Model:':25} {type(self.matting_model).__name__}") 331 matte_bg = self.matting_model_args.get("background_color") 332 print(f"{' └─ Composite BG color?:':25} {matte_bg is not None}") 333 print(sep)
Prints a summary of the tailor agent's configuration, including directory paths,
defined classes, processing options (refine, derive, device), and loaded models.
335 def classify(self, image: str, verbose=False): 336 """ 337 Classifies a single garment image using the configured classification model. 338 339 Args: 340 image (str): The filename of the image to classify, located in `self.input_dir`. 341 verbose (bool): If True, prints detailed classification output. Defaults to False. 342 343 Returns: 344 tuple: 345 - label (str): The predicted class label of the garment. 346 - probabilities (List[float]): A list of probabilities for each class. 347 """ 348 label, probablities = classification.predict( 349 model=self.classification_model, 350 image_path=f"{self.input_dir}/{image}", 351 classes=self.classes, 352 resize_dim=self.classification_model_args.get("resize_dim"), 353 normalize_mean=self.classification_model_args.get("normalize_mean"), 354 normalize_std=self.classification_model_args.get("normalize_std"), 355 device=self.device, 356 verbose=verbose, 357 ) 358 return label, probablities
Classifies a single garment image using the configured classification model.
Arguments:
- image (str): The filename of the image to classify, located in
self.input_dir. - verbose (bool): If True, prints detailed classification output. Defaults to False.
Returns:
tuple: - label (str): The predicted class label of the garment. - probabilities (List[float]): A list of probabilities for each class.
360 def segment(self, image: str): 361 """ 362 Segments a single garment image to extract its mask and optionally modifies the background color. 363 364 This method acts as an intelligent router for your segmentation arguments. It automatically 365 filters out initialization keys (e.g., `model_config`) and post-processing keys 366 (e.g., `background_color`) from `self.segmentation_model_args`. The remaining arguments 367 (such as `processor` and `prompt` for SAM or `resize_dim` for standard models such as BiRefNet) 368 are dynamically passed into the extraction pipeline. 369 370 For Segment Anything models the prompt is supplied via the `prompt` dictionary, which accepts 371 `"points"`, `"labels"`, `"boxes"`, and/or `"text"`. When a text prompt is used with SAM 1 or 372 SAM 2, also provide `grounding_model` and `grounding_processor` in the segmentation arguments, 373 because those families have no text encoder and need the phrase grounded into boxes first. 374 375 Args: 376 image (str): The filename of the image to segment, located in `self.input_dir`. 377 378 Raises: 379 ValueError: If a SAM model is configured without any prompt, or if a text prompt is used 380 with SAM 1 or SAM 2 without a grounding model. 381 382 Returns: 383 tuple: 384 - original_img (np.ndarray): The original input image converted to a numpy array. 385 - mask (np.ndarray): The extracted binary segmentation mask as a numpy array. 386 - bg_modified_img (np.ndarray, optional): The image with the background color replaced. 387 This third element is only returned if 388 `background_color` is provided in the 389 segmentation arguments. 390 """ 391 # 1. Filter out initialization and post-processing arguments 392 extraction_kwargs = { 393 k: v for k, v in self.segmentation_model_args.items() 394 if k not in ["model_config", "background_color"] 395 } 396 397 # 2. Extract using the unified function and unpacked kwargs 398 original_img, mask = segmentation.extract( 399 model=self.segmentation_model, 400 image_path=f"{self.input_dir}/{image}", 401 device=self.device, 402 **extraction_kwargs 403 ) 404 405 # 3. Handle optional background color modification 406 background_color = self.segmentation_model_args.get("background_color") 407 408 if background_color is None: 409 return original_img, mask 410 else: 411 bg_modified_img = segmentation.change_background_color( 412 image_np=original_img, mask_np=mask, background_color=background_color 413 ) 414 return original_img, mask, bg_modified_img
Segments a single garment image to extract its mask and optionally modifies the background color.
This method acts as an intelligent router for your segmentation arguments. It automatically
filters out initialization keys (e.g., model_config) and post-processing keys
(e.g., background_color) from self.segmentation_model_args. The remaining arguments
(such as processor and prompt for SAM or resize_dim for standard models such as BiRefNet)
are dynamically passed into the extraction pipeline.
For Segment Anything models the prompt is supplied via the prompt dictionary, which accepts
"points", "labels", "boxes", and/or "text". When a text prompt is used with SAM 1 or
SAM 2, also provide grounding_model and grounding_processor in the segmentation arguments,
because those families have no text encoder and need the phrase grounded into boxes first.
Arguments:
- image (str): The filename of the image to segment, located in
self.input_dir.
Raises:
- ValueError: If a SAM model is configured without any prompt, or if a text prompt is used with SAM 1 or SAM 2 without a grounding model.
Returns:
tuple: - original_img (np.ndarray): The original input image converted to a numpy array. - mask (np.ndarray): The extracted binary segmentation mask as a numpy array. - bg_modified_img (np.ndarray, optional): The image with the background color replaced. This third element is only returned if
background_coloris provided in the segmentation arguments.
416 def matte(self, image: str, mask: np.ndarray): 417 """ 418 Refines a segmentation mask into a soft alpha matte for a single image. 419 420 Matting in the pipeline is intentionally built on top of segmentation rather than 421 run standalone: the segmentation mask is what supplies the trimap for ViTMatte or 422 the guidance mask for Matting Anything. Calling this without a mask therefore 423 raises, which is why `do_matte=True` forces the segmentation stage to run. 424 425 Standalone matting has no such requirement; call `garmentiq.matting.matte` directly 426 with whatever image and trimap you already have. 427 428 Args: 429 image (str): The filename of the image to matte, located in `self.input_dir`. 430 mask (numpy.ndarray): The segmentation mask produced for the same image. 431 432 Raises: 433 ValueError: If matting is not configured, or if `mask` is None. 434 435 Returns: 436 numpy.ndarray: The alpha matte as `uint8` in `[0, 255]`, matching the image size. 437 """ 438 if not self.do_matte or self.matting_model is None: 439 raise ValueError( 440 "Matting is not configured on this tailor agent. Construct it with " 441 "`do_matte=True` and a matting model." 442 ) 443 if mask is None: 444 raise ValueError( 445 "Matting in the tailor pipeline requires a segmentation mask. Run the " 446 "segmentation stage first, then pass its mask here." 447 ) 448 449 _, alpha = matting.matte( 450 model=self.matting_model, 451 image_path=f"{self.input_dir}/{image}", 452 processor=self.matting_model_args.get("processor"), 453 mask=mask, 454 prompt=self.matting_model_args.get("prompt"), 455 trimap_args=self.matting_model_args.get("trimap_args"), 456 device=self.device, 457 ) 458 return alpha
Refines a segmentation mask into a soft alpha matte for a single image.
Matting in the pipeline is intentionally built on top of segmentation rather than
run standalone: the segmentation mask is what supplies the trimap for ViTMatte or
the guidance mask for Matting Anything. Calling this without a mask therefore
raises, which is why do_matte=True forces the segmentation stage to run.
Standalone matting has no such requirement; call garmentiq.matting.matte directly
with whatever image and trimap you already have.
Arguments:
- image (str): The filename of the image to matte, located in
self.input_dir. - mask (numpy.ndarray): The segmentation mask produced for the same image.
Raises:
- ValueError: If matting is not configured, or if
maskis None.
Returns:
numpy.ndarray: The alpha matte as
uint8in[0, 255], matching the image size.
460 def detect(self, class_name: str, image: Union[str, np.ndarray]): 461 """ 462 Detects predefined landmarks on a garment image based on its classified class. 463 464 Args: 465 class_name (str): The classified name of the garment. 466 image (Union[str, np.ndarray]): The path to the image file or a NumPy array of the image. 467 468 Returns: 469 tuple: 470 - coords (np.array): Detected landmark coordinates. 471 - maxval (np.array): Confidence scores for detected landmarks. 472 - detection_dict (dict): A dictionary containing detailed landmark detection data. 473 """ 474 if isinstance(image, str): 475 image = f"{self.input_dir}/{image}" 476 477 coords, maxval, detection_dict = landmark.detect( 478 class_name=class_name, 479 class_dict=self.class_dict, 480 image_path=image, 481 model=self.landmark_detection_model, 482 scale_std=self.landmark_detection_model_args.get("scale_std"), 483 resize_dim=self.landmark_detection_model_args.get("resize_dim"), 484 normalize_mean=self.landmark_detection_model_args.get("normalize_mean"), 485 normalize_std=self.landmark_detection_model_args.get("normalize_std"), 486 device=self.device, 487 ) 488 return coords, maxval, detection_dict
Detects predefined landmarks on a garment image based on its classified class.
Arguments:
- class_name (str): The classified name of the garment.
- image (Union[str, np.ndarray]): The path to the image file or a NumPy array of the image.
Returns:
tuple: - coords (np.array): Detected landmark coordinates. - maxval (np.array): Confidence scores for detected landmarks. - detection_dict (dict): A dictionary containing detailed landmark detection data.
490 def derive( 491 self, 492 class_name: str, 493 detection_dict: dict, 494 derivation_dict: dict, 495 landmark_coords: np.array, 496 np_mask: np.array, 497 ): 498 """ 499 Derives non-predefined landmark coordinates based on predefined landmarks and a mask. 500 501 Args: 502 class_name (str): The name of the garment class. 503 detection_dict (dict): The dictionary containing detected landmarks. 504 derivation_dict (dict): The dictionary defining derivation rules. 505 landmark_coords (np.array): NumPy array of initial landmark coordinates. 506 np_mask (np.array): NumPy array of the segmentation mask. 507 508 Returns: 509 tuple: 510 - derived_coords (dict): A dictionary of the newly derived landmark coordinates. 511 - updated_detection_dict (dict): The detection dictionary updated with derived landmarks. 512 """ 513 derived_coords, updated_detection_dict = landmark.derive( 514 class_name=class_name, 515 detection_dict=detection_dict, 516 derivation_dict=derivation_dict, 517 landmark_coords=landmark_coords, 518 np_mask=np_mask, 519 ) 520 return derived_coords, updated_detection_dict
Derives non-predefined landmark coordinates based on predefined landmarks and a mask.
Arguments:
- class_name (str): The name of the garment class.
- detection_dict (dict): The dictionary containing detected landmarks.
- derivation_dict (dict): The dictionary defining derivation rules.
- landmark_coords (np.array): NumPy array of initial landmark coordinates.
- np_mask (np.array): NumPy array of the segmentation mask.
Returns:
tuple: - derived_coords (dict): A dictionary of the newly derived landmark coordinates. - updated_detection_dict (dict): The detection dictionary updated with derived landmarks.
522 def refine( 523 self, 524 class_name: str, 525 detection_np: np.array, 526 detection_conf: np.array, 527 detection_dict: dict, 528 mask: np.array, 529 window_size: int = 5, 530 ksize: tuple = (11, 11), 531 sigmaX: float = 0.0, 532 ): 533 """ 534 Refines detected landmark coordinates using a blurred segmentation mask. 535 536 Args: 537 class_name (str): The name of the garment class. 538 detection_np (np.array): NumPy array of initial landmark predictions. 539 detection_conf (np.array): NumPy array of confidence scores for each predicted landmark. 540 detection_dict (dict): Dictionary containing landmark data for each class. 541 mask (np.array): Grayscale mask image used to guide refinement. 542 window_size (int, optional): Size of the window used in the refinement algorithm. Defaults to 5. 543 ksize (tuple, optional): Kernel size for Gaussian blur. Must be odd integers. Defaults to (11, 11). 544 sigmaX (float, optional): Gaussian kernel standard deviation in the X direction. Defaults to 0.0. 545 546 Returns: 547 tuple: 548 - refined_detection_np (np.array): Array of the same shape as `detection_np` with refined coordinates. 549 - detection_dict (dict): Updated detection dictionary with refined landmark coordinates. 550 """ 551 if self.refinement_args: 552 if self.refinement_args.get("window_size") is not None: 553 window_size = self.refinement_args["window_size"] 554 if self.refinement_args.get("ksize") is not None: 555 ksize = self.refinement_args["ksize"] 556 if self.refinement_args.get("sigmaX") is not None: 557 sigmaX = self.refinement_args["sigmaX"] 558 559 refined_detection_np, refined_detection_dict = landmark.refine( 560 class_name=class_name, 561 detection_np=detection_np, 562 detection_conf=detection_conf, 563 detection_dict=detection_dict, 564 mask=mask, 565 window_size=window_size, 566 ksize=ksize, 567 sigmaX=sigmaX, 568 ) 569 570 return refined_detection_np, refined_detection_dict
Refines detected landmark coordinates using a blurred segmentation mask.
Arguments:
- class_name (str): The name of the garment class.
- detection_np (np.array): NumPy array of initial landmark predictions.
- detection_conf (np.array): NumPy array of confidence scores for each predicted landmark.
- detection_dict (dict): Dictionary containing landmark data for each class.
- mask (np.array): Grayscale mask image used to guide refinement.
- window_size (int, optional): Size of the window used in the refinement algorithm. Defaults to 5.
- ksize (tuple, optional): Kernel size for Gaussian blur. Must be odd integers. Defaults to (11, 11).
- sigmaX (float, optional): Gaussian kernel standard deviation in the X direction. Defaults to 0.0.
Returns:
tuple: - refined_detection_np (np.array): Array of the same shape as
detection_npwith refined coordinates. - detection_dict (dict): Updated detection dictionary with refined landmark coordinates.
572 def measure( 573 self, 574 save_segmentation_image: bool = False, 575 save_measurement_image: bool = False, 576 save_matting_image: bool = False, 577 ): 578 """ 579 Executes the full garment measurement pipeline for all images in the input directory. 580 581 This method processes each image through a multi-stage pipeline that includes garment classification, 582 segmentation, landmark detection, optional refinement, and measurement derivation. During classification, 583 the system identifies the type of garment (e.g., shirt, dress, pants). Segmentation follows, producing 584 binary or instance masks that separate the garment from the background. When matting is 585 enabled, the mask is then refined into a soft alpha matte, and the resulting alpha-composited 586 image replaces the hard background-modified image as the input to landmark detection. 587 Landmark detection is then 588 performed to locate anatomical or garment-specific keypoints such as shoulders or waist positions. If 589 enabled, an optional refinement step applies post-processing or model-based corrections to improve the 590 accuracy of detected keypoints. Finally, the system calculates key garment dimensions - such as chest width, 591 waist width, and full length - based on the detected landmarks. In addition to this processing pipeline, 592 the method also manages data and visual output exports. For each input image, a cleaned JSON file is 593 generated containing the predicted garment class, landmark coordinates, and the resulting measurements. 594 Optionally, visual outputs such as segmentation masks and images annotated with landmarks and measurements 595 can be saved to assist in inspection or debugging. 596 597 Args: 598 save_segmentation_image (bool): If True, saves segmentation masks and background-modified images. 599 Defaults to False. 600 save_measurement_image (bool): If True, saves images overlaid with detected landmarks and measurements. 601 Defaults to False. 602 save_matting_image (bool): If True, saves the alpha mattes produced by the matting stage, 603 and the alpha-composited images when a `background_color` is 604 set in `matting_model_args`. Only has an effect when the agent 605 was constructed with `do_matte=True`. Defaults to False. 606 607 Raises: 608 ValueError: If `save_matting_image` is True but the agent was not constructed with 609 `do_matte=True`, or if the matting stage cannot find a segmentation mask. 610 611 Returns: 612 tuple: 613 - metadata (pd.DataFrame): A DataFrame containing metadata for each processed image, such as: 614 - Original image path 615 - Paths to any saved segmentation or annotated images 616 - Class and measurement results 617 - outputs (dict): A dictionary mapping image filenames to their detailed processing results, including: 618 - Predicted class 619 - Detected landmarks with coordinates and confidence scores 620 - Calculated measurements 621 - File paths to any saved images (if applicable) 622 623 Example of exported JSON: 624 ``` 625 { 626 "cloth_3.jpg": { 627 "class": "vest dress", 628 "landmarks": { 629 "10": { 630 "conf": 0.7269417643547058, 631 "x": 611.0, 632 "y": 861.0 633 }, 634 "16": { 635 "conf": 0.6769524812698364, 636 "x": 1226.0, 637 "y": 838.0 638 }, 639 "17": { 640 "conf": 0.7472652196884155, 641 "x": 1213.0, 642 "y": 726.0 643 }, 644 "18": { 645 "conf": 0.7360446453094482, 646 "x": 1238.0, 647 "y": 613.0 648 }, 649 "2": { 650 "conf": 0.9256571531295776, 651 "x": 703.0, 652 "y": 264.0 653 }, 654 "20": { 655 "x": 700.936, 656 "y": 2070.0 657 }, 658 "8": { 659 "conf": 0.7129100561141968, 660 "x": 563.0, 661 "y": 613.0 662 }, 663 "9": { 664 "conf": 0.8203497529029846, 665 "x": 598.0, 666 "y": 726.0 667 } 668 }, 669 "measurements": { 670 "chest": { 671 "distance": 675.0, 672 "landmarks": { 673 "end": "18", 674 "start": "8" 675 } 676 }, 677 "full length": { 678 "distance": 1806.0011794281863, 679 "landmarks": { 680 "end": "20", 681 "start": "2" 682 } 683 }, 684 "hips": { 685 "distance": 615.4299310238331, 686 "landmarks": { 687 "end": "16", 688 "start": "10" 689 } 690 }, 691 "waist": { 692 "distance": 615.0, 693 "landmarks": { 694 "end": "17", 695 "start": "9" 696 } 697 } 698 } 699 } 700 } 701 ``` 702 """ 703 # Some helper variables 704 use_bg_color = self.segmentation_model_args.get("background_color") is not None 705 use_matte_bg = ( 706 self.do_matte 707 and self.matting_model_args.get("background_color") is not None 708 ) 709 outputs = {} 710 711 if save_matting_image and not self.do_matte: 712 raise ValueError( 713 "`save_matting_image=True` but this tailor agent was not constructed with " 714 "`do_matte=True`, so there is no matting stage to save output from." 715 ) 716 717 # Step 1: Create the output directory 718 Path(self.output_dir).mkdir(parents=True, exist_ok=True) 719 Path(f"{self.output_dir}/measurement_json").mkdir(parents=True, exist_ok=True) 720 721 if save_segmentation_image and ( 722 use_bg_color or self.do_derive or self.do_refine 723 ): 724 Path(f"{self.output_dir}/mask_image").mkdir(parents=True, exist_ok=True) 725 if use_bg_color: 726 Path(f"{self.output_dir}/bg_modified_image").mkdir( 727 parents=True, exist_ok=True 728 ) 729 730 if save_measurement_image: 731 Path(f"{self.output_dir}/measurement_image").mkdir( 732 parents=True, exist_ok=True 733 ) 734 735 if save_matting_image and self.do_matte: 736 Path(f"{self.output_dir}/matte_image").mkdir(parents=True, exist_ok=True) 737 if use_matte_bg: 738 Path(f"{self.output_dir}/matte_composite_image").mkdir( 739 parents=True, exist_ok=True 740 ) 741 742 # Step 2: Collect image filenames from input_dir 743 image_extensions = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.tiff"] 744 input_path = Path(self.input_dir) 745 746 image_files = [] 747 for ext in image_extensions: 748 image_files.extend(input_path.glob(ext)) 749 750 # Step 3: Determine column structure 751 columns = [ 752 "filename", 753 "class", 754 "mask_image" if use_bg_color or self.do_derive or self.do_refine else None, 755 "bg_modified_image" if use_bg_color else None, 756 "matte_image" if save_matting_image and self.do_matte else None, 757 "matte_composite_image" 758 if save_matting_image and use_matte_bg 759 else None, 760 "measurement_image", 761 "measurement_json", 762 ] 763 columns = [col for col in columns if col is not None] 764 765 metadata = pd.DataFrame(columns=columns) 766 metadata["filename"] = [img.name for img in image_files] 767 768 # Step 4: Print start message and information 769 print(f"Start measuring {len(metadata['filename'])} garment images ...") 770 771 # Build the step list dynamically so every enabled stage is reported. 772 steps = ["classification"] 773 if use_bg_color or self.do_derive or self.do_refine or self.do_matte: 774 steps.append("segmentation") 775 if self.do_matte: 776 steps.append("matting") 777 steps.append("landmark detection") 778 if self.do_refine: 779 steps.append("landmark refinement") 780 if self.do_derive: 781 steps.append("landmark derivation") 782 783 if len(steps) == 1: 784 listed = steps[0] 785 elif len(steps) == 2: 786 listed = f"{steps[0]} and {steps[1]}" 787 else: 788 listed = ", ".join(steps[:-1]) + f", and {steps[-1]}" 789 message = f"There are {len(steps)} measurement steps: {listed}." 790 791 print(textwrap.fill(message, width=80)) 792 793 # Step 5: Classification 794 for idx, image in tqdm( 795 enumerate(metadata["filename"]), total=len(metadata), desc="Classification" 796 ): 797 label, _ = self.classify(image=image, verbose=False) 798 metadata.at[idx, "class"] = label 799 outputs[image] = {} 800 801 # Step 6: Segmentation 802 # Matting consumes the segmentation mask, so enabling it forces this stage. 803 if use_bg_color or self.do_derive or self.do_refine or self.do_matte: 804 for idx, image in tqdm( 805 enumerate(metadata["filename"]), 806 total=len(metadata), 807 desc="Segmentation", 808 ): 809 if use_bg_color: 810 original_img, mask, bg_modified_image = self.segment(image=image) 811 outputs[image] = { 812 "mask": mask, 813 "bg_modified_image": bg_modified_image, 814 } 815 else: 816 original_img, mask = self.segment(image=image) 817 outputs[image] = { 818 "mask": mask, 819 } 820 821 # Step 6b: Matting (always after segmentation, using its mask as guidance) 822 if self.do_matte: 823 matte_bg_color = self.matting_model_args.get("background_color") 824 # Landmark detection needs an RGB image, so the alpha matte is always 825 # composited onto some background. The colour is chosen in order of 826 # specificity: the matting colour, then the segmentation colour, then a 827 # neutral default. Compositing always happens when matting is enabled, so 828 # that the matte genuinely drives detection even when neither stage was 829 # asked to replace the background in its saved output. 830 detect_bg_color = matte_bg_color 831 if detect_bg_color is None: 832 detect_bg_color = self.segmentation_model_args.get("background_color") 833 if detect_bg_color is None: 834 detect_bg_color = DEFAULT_MATTE_DETECTION_BACKGROUND 835 836 for idx, image in tqdm( 837 enumerate(metadata["filename"]), 838 total=len(metadata), 839 desc="Matting", 840 ): 841 if outputs[image].get("mask") is None: 842 raise ValueError( 843 f"Matting requires a segmentation mask, but none was produced for " 844 f"{image!r}. The segmentation stage must run before matting." 845 ) 846 alpha = self.matte(image=image, mask=outputs[image]["mask"]) 847 outputs[image]["alpha"] = alpha 848 849 composited = matting.composite( 850 image_np=np.array( 851 Image.open(f"{self.input_dir}/{image}").convert("RGB") 852 ), 853 alpha_np=alpha, 854 background_color=detect_bg_color, 855 ) 856 outputs[image]["matte_detect_image"] = composited 857 # The composited image is only offered as a saved output when the user 858 # explicitly asked for a matting background colour, keeping that output 859 # optional in the same way segmentation's background replacement is. 860 if matte_bg_color is not None: 861 outputs[image]["matte_composite"] = composited 862 863 # Step 7: Landmark detection 864 # Detection runs on a background-replaced image when one was requested, because 865 # a clean background helps the pose model. When matting is enabled its softer, 866 # more accurate composite is used in place of the hard segmentation composite. 867 for idx, image in tqdm( 868 enumerate(metadata["filename"]), 869 total=len(metadata), 870 desc="Landmark detection", 871 ): 872 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 873 874 if self.do_matte and outputs[image].get("matte_detect_image") is not None: 875 detect_input = outputs[image]["matte_detect_image"] 876 elif use_bg_color: 877 detect_input = outputs[image]["bg_modified_image"] 878 else: 879 detect_input = image 880 881 coords, maxvals, detection_dict = self.detect( 882 class_name=label, image=detect_input 883 ) 884 outputs[image]["detection_dict"] = detection_dict 885 if self.do_derive or self.do_refine: 886 outputs[image]["coords"] = coords 887 outputs[image]["maxvals"] = maxvals 888 889 # Step 8: Landmark refinement 890 if self.do_refine: 891 for idx, image in tqdm( 892 enumerate(metadata["filename"]), 893 total=len(metadata), 894 desc="Landmark refinement", 895 ): 896 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 897 updated_coords, updated_detection_dict = self.refine( 898 class_name=label, 899 detection_np=outputs[image]["coords"], 900 detection_conf=outputs[image]["maxvals"], 901 detection_dict=outputs[image]["detection_dict"], 902 mask=outputs[image]["mask"], 903 ) 904 outputs[image]["coords"] = updated_coords 905 outputs[image]["detection_dict"] = updated_detection_dict 906 907 # Step 9: Landmark derivation 908 if self.do_derive: 909 for idx, image in tqdm( 910 enumerate(metadata["filename"]), 911 total=len(metadata), 912 desc="Landmark derivation", 913 ): 914 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 915 derived_coords, updated_detection_dict = self.derive( 916 class_name=label, 917 detection_dict=outputs[image]["detection_dict"], 918 derivation_dict=self.derivation_dict, 919 landmark_coords=outputs[image]["coords"], 920 np_mask=outputs[image]["mask"], 921 ) 922 outputs[image]["detection_dict"] = updated_detection_dict 923 924 # Step 10: Save segmentation image 925 if save_segmentation_image and ( 926 use_bg_color or self.do_derive or self.do_refine 927 ): 928 for idx, image in tqdm( 929 enumerate(metadata["filename"]), 930 total=len(metadata), 931 desc="Save segmentation image", 932 ): 933 transformed_name = os.path.splitext(image)[0] 934 Image.fromarray(outputs[image]["mask"]).save( 935 f"{self.output_dir}/mask_image/{transformed_name}_mask.png" 936 ) 937 metadata.at[ 938 idx, "mask_image" 939 ] = f"{self.output_dir}/mask_image/{transformed_name}_mask.png" 940 if use_bg_color: 941 Image.fromarray(outputs[image]["bg_modified_image"]).save( 942 f"{self.output_dir}/bg_modified_image/{transformed_name}_bg_modified.png" 943 ) 944 metadata.at[ 945 idx, "bg_modified_image" 946 ] = f"{self.output_dir}/bg_modified_image/{transformed_name}_bg_modified.png" 947 948 # Step 10b: Save matting image 949 if save_matting_image and self.do_matte: 950 for idx, image in tqdm( 951 enumerate(metadata["filename"]), 952 total=len(metadata), 953 desc="Save matting image", 954 ): 955 transformed_name = os.path.splitext(image)[0] 956 alpha_path = ( 957 f"{self.output_dir}/matte_image/{transformed_name}_matte.png" 958 ) 959 Image.fromarray(outputs[image]["alpha"]).save(alpha_path) 960 metadata.at[idx, "matte_image"] = alpha_path 961 962 if use_matte_bg: 963 composite_path = ( 964 f"{self.output_dir}/matte_composite_image/" 965 f"{transformed_name}_matte_composite.png" 966 ) 967 Image.fromarray(outputs[image]["matte_composite"]).save( 968 composite_path 969 ) 970 metadata.at[idx, "matte_composite_image"] = composite_path 971 972 # Step 11: Save measurement image 973 if save_measurement_image: 974 for idx, image in tqdm( 975 enumerate(metadata["filename"]), 976 total=len(metadata), 977 desc="Save measurement image", 978 ): 979 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 980 transformed_name = os.path.splitext(image)[0] 981 982 image_to_save = Image.open(f"{self.input_dir}/{image}").convert("RGB") 983 draw = ImageDraw.Draw(image_to_save) 984 font = ImageFont.load_default() 985 landmarks = outputs[image]["detection_dict"][label]["landmarks"] 986 987 for lm_id, lm_data in landmarks.items(): 988 x, y = lm_data["x"], lm_data["y"] 989 radius = 5 990 draw.ellipse( 991 (x - radius, y - radius, x + radius, y + radius), fill="green" 992 ) 993 draw.text((x + 8, y - 8), lm_id, fill="green", font=font) 994 995 image_to_save.save( 996 f"{self.output_dir}/measurement_image/{transformed_name}_measurement.png" 997 ) 998 metadata.at[ 999 idx, "measurement_image" 1000 ] = f"{self.output_dir}/measurement_image/{transformed_name}_measurement.png" 1001 1002 # Step 12: Save measurement json 1003 for idx, image in tqdm( 1004 enumerate(metadata["filename"]), 1005 total=len(metadata), 1006 desc="Save measurement json", 1007 ): 1008 label = metadata.loc[metadata["filename"] == image, "class"].values[0] 1009 transformed_name = os.path.splitext(image)[0] 1010 1011 # Clean the detection dictionary 1012 final_dict = utils.clean_detection_dict( 1013 class_name=label, 1014 image_name=image, 1015 detection_dict=outputs[image]["detection_dict"], 1016 ) 1017 1018 # Export JSON 1019 utils.export_dict_to_json( 1020 data=final_dict, 1021 filename=f"{self.output_dir}/measurement_json/{transformed_name}_measurement.json", 1022 ) 1023 1024 metadata.at[ 1025 idx, "measurement_json" 1026 ] = f"{self.output_dir}/measurement_json/{transformed_name}_measurement.json" 1027 1028 # Step 13: Save metadata as a CSV 1029 metadata.to_csv(f"{self.output_dir}/metadata.csv", index=False) 1030 1031 return metadata, outputs
Executes the full garment measurement pipeline for all images in the input directory.
This method processes each image through a multi-stage pipeline that includes garment classification, segmentation, landmark detection, optional refinement, and measurement derivation. During classification, the system identifies the type of garment (e.g., shirt, dress, pants). Segmentation follows, producing binary or instance masks that separate the garment from the background. When matting is enabled, the mask is then refined into a soft alpha matte, and the resulting alpha-composited image replaces the hard background-modified image as the input to landmark detection. Landmark detection is then performed to locate anatomical or garment-specific keypoints such as shoulders or waist positions. If enabled, an optional refinement step applies post-processing or model-based corrections to improve the accuracy of detected keypoints. Finally, the system calculates key garment dimensions - such as chest width, waist width, and full length - based on the detected landmarks. In addition to this processing pipeline, the method also manages data and visual output exports. For each input image, a cleaned JSON file is generated containing the predicted garment class, landmark coordinates, and the resulting measurements. Optionally, visual outputs such as segmentation masks and images annotated with landmarks and measurements can be saved to assist in inspection or debugging.
Arguments:
- save_segmentation_image (bool): If True, saves segmentation masks and background-modified images. Defaults to False.
- save_measurement_image (bool): If True, saves images overlaid with detected landmarks and measurements. Defaults to False.
- save_matting_image (bool): If True, saves the alpha mattes produced by the matting stage,
and the alpha-composited images when a
background_coloris set inmatting_model_args. Only has an effect when the agent was constructed withdo_matte=True. Defaults to False.
Raises:
- ValueError: If
save_matting_imageis True but the agent was not constructed withdo_matte=True, or if the matting stage cannot find a segmentation mask.
Returns:
tuple: - metadata (pd.DataFrame): A DataFrame containing metadata for each processed image, such as: - Original image path - Paths to any saved segmentation or annotated images - Class and measurement results - outputs (dict): A dictionary mapping image filenames to their detailed processing results, including: - Predicted class - Detected landmarks with coordinates and confidence scores - Calculated measurements - File paths to any saved images (if applicable)
Example of exported JSON:
{ "cloth_3.jpg": { "class": "vest dress", "landmarks": { "10": { "conf": 0.7269417643547058, "x": 611.0, "y": 861.0 }, "16": { "conf": 0.6769524812698364, "x": 1226.0, "y": 838.0 }, "17": { "conf": 0.7472652196884155, "x": 1213.0, "y": 726.0 }, "18": { "conf": 0.7360446453094482, "x": 1238.0, "y": 613.0 }, "2": { "conf": 0.9256571531295776, "x": 703.0, "y": 264.0 }, "20": { "x": 700.936, "y": 2070.0 }, "8": { "conf": 0.7129100561141968, "x": 563.0, "y": 613.0 }, "9": { "conf": 0.8203497529029846, "x": 598.0, "y": 726.0 } }, "measurements": { "chest": { "distance": 675.0, "landmarks": { "end": "18", "start": "8" } }, "full length": { "distance": 1806.0011794281863, "landmarks": { "end": "20", "start": "2" } }, "hips": { "distance": 615.4299310238331, "landmarks": { "end": "16", "start": "10" } }, "waist": { "distance": 615.0, "landmarks": { "end": "17", "start": "9" } } } } }