Day 12: Building the Catalog Tool pipeline + CLI
Composing the pipeline
Days 8-11 gave you four separable operations: color-space conversion, thresholding/edges, contour/morphology cleanup, and GrabCut segmentation. Today they compose into one pipeline function — background removal, resize, normalize — plus a CLI so it's runnable as a real tool, not a notebook cell.
import cv2
import numpy as np
def remove_background(img_bgr: np.ndarray) -> np.ndarray:
mask = np.zeros(img_bgr.shape[:2], np.uint8)
bgd_model = np.zeros((1, 65), np.float64)
fgd_model = np.zeros((1, 65), np.float64)
h, w = img_bgr.shape[:2]
rect = (int(w * 0.05), int(h * 0.05), int(w * 0.9), int(h * 0.9))
cv2.grabCut(img_bgr, mask, rect, bgd_model, fgd_model, 5, cv2.GC_INIT_WITH_RECT)
fg_mask = np.where((mask == 1) | (mask == 3), 255, 0).astype("uint8")
# Day 10 cleanup: close small holes, open away stray noise
kernel = np.ones((5, 5), np.uint8)
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel)
fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_OPEN, kernel)
b, g, r = cv2.split(img_bgr)
return cv2.merge([b, g, r, fg_mask]) # BGRA — mask becomes the alpha channel
def resize_and_normalize(img_rgba: np.ndarray, target: int = 1024) -> np.ndarray:
h, w = img_rgba.shape[:2]
scale = target / max(h, w)
resized = cv2.resize(img_rgba, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
# pad to a square canvas so every catalog image has consistent dimensions
canvas = np.zeros((target, target, 4), dtype=np.uint8)
y_off = (target - resized.shape[0]) // 2
x_off = (target - resized.shape[1]) // 2
canvas[y_off:y_off + resized.shape[0], x_off:x_off + resized.shape[1]] = resized
return canvas
def process_image(path: str, target: int = 1024) -> np.ndarray:
img = cv2.imread(path)
cutout = remove_background(img)
return resize_and_normalize(cutout, target)The CLI
argparse (stdlib) is the pragmatic choice for a tool this size — no new dependency, and every backend engineer already knows the shape of a CLI (flags, positional args, --help). Iterate over a directory, process each image, report successes and failures rather than crashing on the first bad file.
import argparse
from pathlib import Path
import cv2
from .pipeline import process_image
def main():
parser = argparse.ArgumentParser(description="FitXpert Catalog Tool")
parser.add_argument("input_dir", type=Path)
parser.add_argument("output_dir", type=Path)
parser.add_argument("--size", type=int, default=1024)
args = parser.parse_args()
args.output_dir.mkdir(parents=True, exist_ok=True)
ok, failed = 0, []
for path in args.input_dir.glob("*.jpg"):
try:
result = process_image(str(path), args.size)
cv2.imwrite(str(args.output_dir / f"{path.stem}.png"), result)
ok += 1
except Exception as e:
failed.append((path.name, str(e)))
print(f"Processed {ok} images, {len(failed)} failed")
for name, err in failed:
print(f" {name}: {err}")
if __name__ == "__main__":
main()Fail loud per-item, not for the whole batch
A batch tool that crashes on image 3 of 20 and silently loses images 4-20's results is worse than one that logs 17 successes and 3 named failures. This pattern — catch per-item, report at the end — is the same instinct as a good batch job in any backend system, just applied to a CV pipeline.
Key terms
- argparse
- Python's standard-library module for building command-line interfaces with flags, positional arguments, and --help.
- BGRA
- BGR color channels plus an Alpha (transparency) channel — how a background-removed image is typically saved as a PNG.
Processing a directory of 20 images, image #7 is corrupt and raises an exception inside the pipeline. What should the CLI do?