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
DEFAULT_MATTE_DETECTION_BACKGROUND = (255, 255, 255)
class tailor:
  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.
tailor( input_dir: str, model_dir: str, output_dir: str, class_dict: dict, do_derive: bool, do_refine: bool, classification_model_path: str, classification_model_class: Type[torch.nn.modules.module.Module], classification_model_args: Dict, segmentation_model_path: str, segmentation_model_class: Type[torch.nn.modules.module.Module], segmentation_model_args: Dict, landmark_detection_model_path: str, landmark_detection_model_class: Type[torch.nn.modules.module.Module], landmark_detection_model_args: Dict, refinement_args: Optional[Dict] = None, derivation_dict: Optional[Dict] = None, device: Union[str, torch.device] = 'cpu', do_matte: bool = False, matting_model_path: Optional[str] = None, matting_model_class: Optional[Type[torch.nn.modules.module.Module]] = None, matting_model_args: Optional[Dict] = None)
 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, and prompt (with "points", "labels", "boxes", and/or "text"), plus optional grounding_model and grounding_processor when using a text prompt with SAM 1 or SAM 2. An optional background_color triggers 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_derive is 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 when do_matte is 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 when do_matte is 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 optional trimap_args and background_color. Alternatively pass a preconstructed model as model, which is how Matting Anything is supplied since it pairs a decoder with a SAM instance. Defaults to None.
Raises:
  • ValueError: If do_derive is True but derivation_dict is None, if do_matte is True but no matting model is provided, or if the requested device is invalid or unavailable on this machine.
device
input_dir
model_dir
output_dir
class_dict
classes
do_derive
do_refine
classification_model_path
classification_model_args
classification_model_class
classification_model
segmentation_model_path
segmentation_model_class
segmentation_model_args
segmentation_has_bg_color
segmentation_model
landmark_detection_model_path
landmark_detection_model_class
landmark_detection_model_args
landmark_detection_model
do_matte
matting_model_path
matting_model_class
matting_model_args
matting_model
def summary(self):
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.

def classify(self, image: str, verbose=False):
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.

def segment(self, image: str):
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_color is provided in the segmentation arguments.

def matte(self, image: str, mask: numpy.ndarray):
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 mask is None.
Returns:

numpy.ndarray: The alpha matte as uint8 in [0, 255], matching the image size.

def detect(self, class_name: str, image: Union[str, numpy.ndarray]):
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.

def derive( self, class_name: str, detection_dict: dict, derivation_dict: dict, landmark_coords: <built-in function array>, np_mask: <built-in function array>):
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.

def refine( self, class_name: str, detection_np: <built-in function array>, detection_conf: <built-in function array>, detection_dict: dict, mask: <built-in function array>, window_size: int = 5, ksize: tuple = (11, 11), sigmaX: float = 0.0):
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_np with refined coordinates. - detection_dict (dict): Updated detection dictionary with refined landmark coordinates.

def measure( self, save_segmentation_image: bool = False, save_measurement_image: bool = False, save_matting_image: bool = False):
 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_color is set in matting_model_args. Only has an effect when the agent was constructed with do_matte=True. Defaults to False.
Raises:
  • ValueError: If save_matting_image is True but the agent was not constructed with do_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"
                }
            }
        }
    }
}