Spaces:
Sleeping
Sleeping
File size: 4,443 Bytes
fb91fdc | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 | """
Guarded background masking with U^2-Net (rembg).
For every image: strip near-black padding, run salient-object segmentation,
then ACCEPT the mask only if it clearly removes a plain photographic backdrop
around the artwork β never content. Five checks, all must pass:
1. kept fraction in [0.20, 0.90] β mask keeps a plausible artwork share
2. no interior holes (> 2%) β artwork regions are never punched out
3. convex solidity >= 0.97 β one solid blob, not scattered figures
4. mask must not touch-fill the border β something around it was removed
5. removed pixels are uniform (std <= 28) β what's removed looks like backdrop
Accepted masks ("applied") are saved as PNGs + a bbox; everything else is
"rejected" and downstream features use the full image. On our gold set this
applies to ~10% of images (museum photos of framed/mounted works).
Output: data/masks/<stem>.png + data/masks/verdicts.csv
(filename, verdict, y0, y1, x0, x1)
Usage: python preprocessing/generate_masks.py
"""
import csv
from pathlib import Path
import cv2
import numpy as np
import pandas as pd
from PIL import Image
from rembg import new_session, remove
from tqdm import tqdm
IMAGES = Path("data/images")
SELECTED = Path("data/artwork_metadata.csv")
MASK_DIR = Path("data/masks")
VERDICTS = MASK_DIR / "verdicts.csv"
Image.MAX_IMAGE_PIXELS = None
def crop_padding(img_rgb, threshold=5):
gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
rows = np.where(gray.max(axis=1) > threshold)[0]
cols = np.where(gray.max(axis=0) > threshold)[0]
if len(rows) == 0 or len(cols) == 0:
return img_rgb
return img_rgb[rows[0]:rows[-1] + 1, cols[0]:cols[-1] + 1]
def mask_verdict(raw, img, lo=0.20, hi=0.90, max_bg_std=28):
"""True (apply) only if the mask removes a solid, uniform border region."""
if raw is None:
return False
m = (raw > 0).astype(np.uint8)
kept = m.mean()
if not (lo <= kept <= hi):
return False
cnts, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not cnts:
return False
filled = m.copy()
cv2.drawContours(filled, cnts, -1, 1, -1)
if (filled - m).sum() / max(m.sum(), 1) > 0.02:
return False
hull = cv2.convexHull(np.vstack([c.reshape(-1, 2) for c in cnts]))
if cv2.contourArea(hull) == 0 or m.sum() / cv2.contourArea(hull) < 0.97:
return False
border = np.zeros_like(m)
border[0, :] = border[-1, :] = border[:, 0] = border[:, -1] = 1
if (border & (1 - m)).sum() == 0:
return False
removed = img[m == 0]
if removed.std(axis=0).mean() > max_bg_std:
return False
return True
def main():
MASK_DIR.mkdir(parents=True, exist_ok=True)
sel = pd.read_csv(SELECTED, dtype=str).drop_duplicates("filename")
done = set()
if VERDICTS.exists():
done = set(pd.read_csv(VERDICTS, dtype=str)["filename"])
todo = [f for f in sel["filename"] if f not in done]
print(f"total={len(sel)} done={len(done)} todo={len(todo)}")
session = new_session("u2net")
mode = "a" if VERDICTS.exists() else "w"
with open(VERDICTS, mode, newline="") as fh:
writer = csv.writer(fh)
if mode == "w":
writer.writerow(["filename", "verdict", "y0", "y1", "x0", "x1"])
applied = rejected = 0
for fn in tqdm(todo):
try:
img = crop_padding(np.array(Image.open(IMAGES / fn).convert("RGB")))
raw = np.array(remove(Image.fromarray(img), session=session,
only_mask=True))
if mask_verdict(raw, img):
m = (raw > 0).astype(np.uint8)
ys, xs = np.where(m > 0)
y0, y1, x0, x1 = ys.min(), ys.max(), xs.min(), xs.max()
cv2.imwrite(str(MASK_DIR / (Path(fn).stem + ".png")), m * 255)
writer.writerow([fn, "applied", y0, y1, x0, x1])
applied += 1
else:
writer.writerow([fn, "rejected", "", "", "", ""])
rejected += 1
except Exception as e:
print(f"FAIL {fn}: {e}")
writer.writerow([fn, "rejected", "", "", "", ""])
rejected += 1
fh.flush()
print(f"applied={applied} rejected={rejected}")
if __name__ == "__main__":
main()
|