agrobot_base/Python/OAK/datasets/generate_labels_from_masks.py

345 lines
12 KiB
Python

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):
"""
Lê 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()