agrobot_base/Python/OAK/datasets/generate_labels_from_masks.py

345 lines
12 KiB
Python
Raw Normal View History

2026-02-24 13:38:14 +00:00
import json
import os
import argparse
from pathlib import Path
import numpy as np
import cv2
from PIL import Image, ImageDraw
import matplotlib.pyplot as plt
from utils import carregar_labelmap_completo
# =========================
# CONFIGURAÇÃO DE CLASSES
# =========================
# Ajuste aqui conforme suas máscaras:
# - Se sua máscara for "indexada" (modo P) ou grayscale com IDs por pixel:
# class_ids = {0: 1, 1: 2} # exemplo: erva=1, cana=2
# - Se sua máscara for RGB com cores fixas:
# class_colors = {0: (0,255,0), 1: (0,0,255)} # exemplo
#
# Por padrão abaixo: tenta RGB primeiro; se a máscara vier indexada, usa IDs.
def build_maps_from_labelmap(alpha: int = 90, ignore_names=None):
"""
labelmap e constrói maps dinâmicos:
- class_names: {new_id: name}
- class_colors_rgb: {new_id: (r,g,b)}
- class_ids: {new_id: new_id} (para máscaras indexed alinhadas com o new_id)
- overlay_rgba: {new_id: (r,g,b,alpha)}
- ignore_rgb: cor da classe "ignore" (se existir no labelmap original)
- id_old_to_new: {old_id: new_id} (útil se sua máscara indexed usa ids antigos)
- id_new_to_old: {new_id: old_id}
"""
if ignore_names is None:
ignore_names = []
ignore_set = {n.strip().lower() for n in ignore_names if n and n.strip()}
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
MODELO = config["camera"]
labelmap_path = os.path.join(MODELO, "dataset", "labelmap.txt")
# Lê tudo do labelmap (mantém seus ids originais)
cor_para_id, cores_rgb, id_para_nome, ignore_rgb = carregar_labelmap_completo(labelmap_path)
# --- Filtra classes por nome ---
kept_old_ids = []
for old_id, name in id_para_nome.items():
if name.strip().lower() in ignore_set:
continue
kept_old_ids.append(old_id)
# Reindexa para ficar 0..N-1
kept_old_ids = sorted(kept_old_ids)
id_old_to_new = {old_id: new_id for new_id, old_id in enumerate(kept_old_ids)}
id_new_to_old = {new_id: old_id for old_id, new_id in id_old_to_new.items()}
# Constrói maps novos (compactos)
class_names = {}
class_colors_rgb = {}
overlay_rgba = {}
# cor_para_id: { (r,g,b): old_id }
# id_para_nome: { old_id: name }
for cor_rgb, old_id in cor_para_id.items():
if old_id not in id_old_to_new:
continue
new_id = id_old_to_new[old_id]
class_names[new_id] = id_para_nome[old_id]
class_colors_rgb[new_id] = cor_rgb
overlay_rgba[new_id] = (cor_rgb[0], cor_rgb[1], cor_rgb[2], alpha)
# Para máscara indexed:
# - Se sua máscara indexed já usa os IDs NOVOS (compactos), isso aqui está ok.
# - Se ela usa IDs ANTIGOS, você precisa mapear (old -> new) antes de extrair polígonos.
class_ids = {new_id: new_id for new_id in class_names.keys()}
return {
"class_names": class_names,
"class_colors_rgb": class_colors_rgb,
"class_ids": class_ids,
"overlay_rgba": overlay_rgba,
"ignore_rgb": ignore_rgb,
"id_old_to_new": id_old_to_new,
"id_new_to_old": id_new_to_old,
"labelmap_path": labelmap_path,
}
maps = build_maps_from_labelmap(ignore_names=["chao", "ignore"])
CLASS_NAMES = maps["class_names"]
CLASS_IDS = maps["class_ids"]
CLASS_COLORS_RGB = maps["class_colors_rgb"]
OVERLAY_RGBA = maps["overlay_rgba"]
IGNORE_RGB = maps["ignore_rgb"]
# =========================
# UTILITÁRIOS
# =========================
def imread_unicode(path: Path) -> np.ndarray:
"""Lê imagem com caminho unicode no Windows."""
data = np.fromfile(str(path), dtype=np.uint8)
img = cv2.imdecode(data, cv2.IMREAD_UNCHANGED)
return img
def load_mask(mask_path: Path):
"""
Retorna:
mask_type: 'indexed' ou 'rgb'
mask_data:
- indexed: np.ndarray (H,W) int
- rgb: np.ndarray (H,W,3) uint8 em RGB
"""
pil = Image.open(mask_path)
if pil.mode == "P":
arr = np.array(pil, dtype=np.int32)
return "indexed", arr
if pil.mode in ("L", "I;16"):
arr = np.array(pil, dtype=np.int32)
return "indexed", arr
# RGB/RGBA
pil = pil.convert("RGBA")
rgba = np.array(pil, dtype=np.uint8)
rgb = rgba[:, :, :3]
return "rgb", rgb
def class_binary_from_mask(mask_type, mask_data, cls, class_ids, class_colors, rgb_tol=10):
"""Gera máscara binária (uint8 0/255) para uma classe."""
if mask_type == "indexed":
target_id = class_ids[cls]
bin_mask = (mask_data == target_id).astype(np.uint8) * 255
return bin_mask
# rgb
target = np.array(class_colors[cls], dtype=np.int16)
img = mask_data.astype(np.int16)
diff = np.abs(img - target[None, None, :])
ok = (diff[:, :, 0] <= rgb_tol) & (diff[:, :, 1] <= rgb_tol) & (diff[:, :, 2] <= rgb_tol)
return ok.astype(np.uint8) * 255
def simplify_contour(cnt, epsilon_px=1.0, epsilon_rel=0.001):
peri = cv2.arcLength(cnt, True)
eps = max(epsilon_px, epsilon_rel * peri)
return cv2.approxPolyDP(cnt, eps, True)
def contours_to_polygons(bin_mask, min_area_px=50, epsilon_px=2.0):
"""
bin_mask: uint8 0/255
Retorna lista de polígonos, cada um como array (N,2) em pixels (float).
"""
# limpa ruído e fecha pequenos buracos
kernel = np.ones((3, 3), np.uint8)
m = cv2.morphologyEx(bin_mask, cv2.MORPH_OPEN, kernel, iterations=1)
m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, kernel, iterations=1)
contours, _hier = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
polys = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < min_area_px:
continue
approx = simplify_contour(cnt, epsilon_px=epsilon_px)
if len(approx) < 3:
continue
pts = approx.reshape(-1, 2).astype(np.float32)
polys.append(pts)
return polys
def polygon_px_to_yolo(poly_px, w, h):
"""(N,2) px -> lista [x1,y1,x2,y2,...] normalizada 0..1"""
xs = np.clip(poly_px[:, 0] / float(w), 0.0, 1.0)
ys = np.clip(poly_px[:, 1] / float(h), 0.0, 1.0)
coords = []
for x, y in zip(xs, ys):
coords.append(float(x))
coords.append(float(y))
return coords
def draw_polygons_on_preview(preview_path: Path, polygons_by_class, out_path: Path):
"""Cria overlay (PIL) com polígonos extraídos por cima do preview."""
img = Image.open(preview_path).convert("RGB")
w, h = img.size
draw = ImageDraw.Draw(img, "RGBA")
for cls, polys in polygons_by_class.items():
color = OVERLAY_RGBA.get(cls, (255, 255, 255, 90))
outline = color[:3] + (255,)
for poly in polys:
pts = [(float(x), float(y)) for x, y in poly]
if len(pts) >= 3:
draw.polygon(pts, fill=color, outline=outline)
img.save(out_path)
def make_triview(preview_path: Path, mask_path: Path, overlay_path: Path, out_path: Path, title: str = ""):
"""Salva uma imagem com 3 colunas: preview | mask | overlay."""
prev = Image.open(preview_path).convert("RGB")
msk = Image.open(mask_path).convert("RGB")
ovl = Image.open(overlay_path).convert("RGB")
fig = plt.figure(figsize=(16, 6))
fig.suptitle(title, fontsize=12)
ax1 = fig.add_subplot(1, 3, 1)
ax1.imshow(prev)
ax1.set_title("Preview")
ax1.axis("off")
ax2 = fig.add_subplot(1, 3, 2)
ax2.imshow(msk)
ax2.set_title("Mask (manual)")
ax2.axis("off")
ax3 = fig.add_subplot(1, 3, 3)
ax3.imshow(ovl)
ax3.set_title("Overlay (polígonos extraídos)")
ax3.axis("off")
plt.tight_layout()
fig.savefig(out_path, dpi=140)
plt.close(fig)
def find_matching_preview(previews_dir: Path, stem: str):
"""Procura preview com mesmo stem em extensões comuns."""
for ext in [".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"]:
p = previews_dir / f"{stem}{ext}"
if p.exists():
return p
return None
# =========================
# PIPELINE PRINCIPAL
# =========================
def process_dataset(root_dir: Path, out_labels_dir: Path, out_vis_dir: Path,
class_ids, class_colors, rgb_tol=10,
min_area_px=50, epsilon_px=2.0):
previews_dir = root_dir / "previews"
masks_dir = root_dir / "masks"
if not previews_dir.exists() or not masks_dir.exists():
raise FileNotFoundError(f"Esperado encontrar previews/ e masks/ dentro de {root_dir}")
out_labels_dir.mkdir(parents=True, exist_ok=True)
out_vis_dir.mkdir(parents=True, exist_ok=True)
mask_files = sorted(list(masks_dir.glob("*.png")) + list(masks_dir.glob("*.jpg")) + list(masks_dir.glob("*.jpeg")))
if not mask_files:
print(f"[WARN] Nenhuma máscara encontrada em: {masks_dir}")
return
total = 0
for mask_path in mask_files:
stem = mask_path.stem
preview_path = find_matching_preview(previews_dir, stem)
if preview_path is None:
print(f"[WARN] Sem preview para máscara: {mask_path.name}")
continue
# tamanhos
prev_img = Image.open(preview_path)
w, h = prev_img.size
mask_type, mask_data = load_mask(mask_path)
polygons_by_class = {}
yolo_lines = []
for cls in sorted(CLASS_NAMES.keys()):
if cls not in class_ids or cls not in class_colors:
continue
bin_mask = class_binary_from_mask(mask_type, mask_data, cls, class_ids, class_colors, rgb_tol=rgb_tol)
polys = contours_to_polygons(bin_mask, min_area_px=min_area_px, epsilon_px=epsilon_px)
if not polys:
continue
polygons_by_class[cls] = polys
for poly_px in polys:
coords = polygon_px_to_yolo(poly_px, w, h)
# YOLOv8-seg exige pelo menos 3 pontos (6 nums)
if len(coords) >= 6:
line = str(cls) + " " + " ".join(f"{v:.6f}" for v in coords)
yolo_lines.append(line)
# salva label
label_path = out_labels_dir / f"{stem}.txt"
label_path.write_text("\n".join(yolo_lines) + ("\n" if yolo_lines else ""), encoding="utf-8")
# gera overlay e triview
overlay_path = out_vis_dir / f"{stem}_overlay.png"
triview_path = out_vis_dir / f"{stem}_triview.png"
draw_polygons_on_preview(preview_path, polygons_by_class, overlay_path)
make_triview(preview_path, mask_path, overlay_path, triview_path, title=stem)
total += 1
print(f"[OK] {stem}: polys={sum(len(v) for v in polygons_by_class.values())} -> {label_path.name}")
print(f"\nFeito ✅ Processados: {total} arquivos")
print(f"Labels: {out_labels_dir}")
print(f"Vis: {out_vis_dir}")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--root", type=str, required=True, help="Pasta raiz no formato antigo (contendo previews/ masks/ raws/ metas/)")
ap.add_argument("--rgb_tol", type=int, default=10, help="Tolerância p/ match de cor RGB na máscara")
ap.add_argument("--min_area", type=int, default=50, help="Área mínima (px) pra descartar sujeira")
ap.add_argument("--eps", type=float, default=2.0, help="Epsilon (px) pra simplificar polígonos")
args = ap.parse_args()
root_dir = Path(args.root)
out_labels_dir = Path(f"{args.root}/labels")
out_vis_dir = Path(f"{args.root}/vis")
process_dataset(
root_dir=root_dir,
out_labels_dir=out_labels_dir,
out_vis_dir=out_vis_dir,
class_ids=CLASS_IDS,
class_colors=CLASS_COLORS_RGB,
rgb_tol=args.rgb_tol,
min_area_px=args.min_area,
epsilon_px=args.eps
)
if __name__ == "__main__":
main()