mv / app.py
arioobarzan's picture
Update app.py
409c9d9 verified
Raw
History Blame Contribute Delete
13.7 kB
import gradio as gr
import numpy as np
from PIL import Image
import cv2
from sklearn.cluster import KMeans
from scipy import ndimage as ndi
import tempfile
# ===============================
# ابزارهای پایه
# ===============================
def pil_to_np_rgb(pil_image: Image.Image) -> np.ndarray:
return np.array(pil_image.convert("RGB"))
def save_as_bmp(np_img: np.ndarray, name_prefix: str) -> str:
f = tempfile.NamedTemporaryFile(delete=False, suffix=".bmp", prefix=f"{name_prefix}_")
Image.fromarray(np_img).save(f.name, format="BMP")
return f.name
# ===============================
# KMeans: کاهش رنگ‌ها (15..30)
# ===============================
def quantize_colors_kmeans(img_rgb: np.ndarray, n_clusters: int):
h, w, _ = img_rgb.shape
pixels = img_rgb.reshape(-1, 3)
kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=42).fit(pixels)
labels = kmeans.labels_.reshape(h, w)
palette = kmeans.cluster_centers_.astype(np.uint8)
reduced = np.zeros_like(img_rgb)
for i in range(n_clusters):
reduced[labels == i] = palette[i]
return labels, palette, reduced
# ===============================
# حذف ریزناحیه‌ها (ادغام با همسایه غالب)
# ===============================
def clean_small_regions(labels: np.ndarray, min_area_px: int, connectivity: int = 8) -> np.ndarray:
if min_area_px <= 0:
return labels
labels = labels.copy()
kernel = np.ones((3, 3), np.uint8)
for lbl in np.unique(labels):
mask = (labels == lbl).astype(np.uint8)
num, comp_map, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=connectivity)
for comp_id in range(1, num):
area = int(stats[comp_id, cv2.CC_STAT_AREA])
if area >= min_area_px:
continue
comp_mask = (comp_map == comp_id)
dil = cv2.dilate(comp_mask.astype(np.uint8), kernel, iterations=1).astype(bool)
border = dil & (~comp_mask)
neighbor_labels = labels[border]
neighbor_labels = neighbor_labels[neighbor_labels != lbl]
if neighbor_labels.size == 0:
dil2 = cv2.dilate(comp_mask.astype(np.uint8), kernel, iterations=2).astype(bool)
border2 = dil2 & (~comp_mask)
neighbor_labels = labels[border2]
neighbor_labels = neighbor_labels[neighbor_labels != lbl]
if neighbor_labels.size == 0:
continue
new_lbl = int(np.bincount(neighbor_labels).argmax())
labels[comp_mask] = new_lbl
return labels
# ===============================
# نرم‌سازی برچسب‌ها با «اکثریت همسایه‌ها»
# ===============================
def smooth_labels_majority(labels: np.ndarray, radius: int) -> np.ndarray:
if radius <= 0:
return labels
size = 2 * radius + 1
def mode_func(window):
w = window.astype(np.int32)
return np.bincount(w).argmax()
return ndi.generic_filter(labels, mode_func, size=size, mode="nearest")
# ===============================
# مرز جهانی 1px از روی لیبل‌ها
# ===============================
def build_global_boundary_mask(labels: np.ndarray) -> np.ndarray:
H, W = labels.shape
boundary = np.zeros((H, W), dtype=np.uint8)
h_diff = labels[:, 1:] != labels[:, :-1]
v_diff = labels[1:, :] != labels[:-1, :]
boundary[:, :-1] |= h_diff
boundary[:, 1:] |= h_diff
boundary[:-1, :] |= v_diff
boundary[ 1:, :] |= v_diff
return (boundary.astype(np.uint8) * 255)
# ===============================
# ابزار کانتور + Resample + پایین‌گذر فوریه
# ===============================
def dedupe_consecutive_points(cnt: np.ndarray):
cnt = cnt.squeeze()
if cnt.ndim != 2 or len(cnt) < 5:
return None
keep = np.ones(len(cnt), dtype=bool)
keep[1:] = np.any(np.diff(cnt, axis=0) != 0, axis=1)
cnt = cnt[keep]
if len(cnt) < 5:
return None
return cnt
def resample_by_arclength(points: np.ndarray, step: float = 1.0, closed: bool = True) -> np.ndarray:
pts = points.astype(np.float64)
if closed and not np.array_equal(pts[0], pts[-1]):
pts = np.vstack([pts, pts[0]])
seg = np.sqrt(((pts[1:] - pts[:-1]) ** 2).sum(axis=1))
s = np.hstack([[0.0], np.cumsum(seg)])
total = s[-1]
if total < step:
return pts.astype(np.float32)
n_new = int(np.floor(total / step))
s_new = np.linspace(0, total, n_new, endpoint=False)
x = np.interp(s_new, s, pts[:, 0])
y = np.interp(s_new, s, pts[:, 1])
out = np.vstack([x, y]).T
return out.astype(np.float32)
def fourier_lowpass(points: np.ndarray, min_wavelength_px: int, upsample_factor: float = 2.0) -> np.ndarray:
pts = points
N = len(pts)
if N < 8:
return pts
# سیگنال مختلط
z = pts[:, 0].astype(np.float64) + 1j * pts[:, 1].astype(np.float64)
Z = np.fft.fft(z)
k_max = max(1, int(np.floor(N / max(1, min_wavelength_px))))
Z_lp = np.zeros_like(Z)
Z_lp[:k_max + 1] = Z[:k_max + 1]
if k_max > 0:
Z_lp[-k_max:] = Z[-k_max:]
z_s = np.fft.ifft(Z_lp)
xs, ys = np.real(z_s), np.imag(z_s)
# بازنمونه‌گیری نرم‌تر (upsample)
idx = np.arange(N)
N_new = max(N, int(N * upsample_factor))
idx_new = np.linspace(0, N - 1, N_new)
x_new = np.interp(idx_new, idx, xs)
y_new = np.interp(idx_new, idx, ys)
return np.vstack([x_new, y_new]).T.astype(np.float32)
# ===============================
# رسم مرزهای نرم بدون اورلپ (روی خود ماسک مرز)
# ===============================
def render_boundaries_smooth_no_overlap(labels: np.ndarray,
palette: np.ndarray,
boundary_mask: np.ndarray,
min_wavelength_px: int,
arclen_step: float = 1.0,
upsample_factor: float = 2.0) -> np.ndarray:
H, W = labels.shape
canvas = np.zeros((H, W, 3), dtype=np.uint8)
occupied = np.zeros((H, W), dtype=bool)
cnts_data = cv2.findContours(boundary_mask, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
contours = cnts_data[0] if len(cnts_data) == 2 else cnts_data[1]
kernel = np.ones((3, 3), np.uint8)
for cnt in contours:
cnt_xy = dedupe_consecutive_points(cnt)
if cnt_xy is None or len(cnt_xy) < 12:
continue
# بازنمونه‌گیری به گام ثابت طول‌قوس + پایین‌گذر فوریه
rs = resample_by_arclength(cnt_xy, step=arclen_step, closed=True)
smooth = fourier_lowpass(rs, min_wavelength_px=min_wavelength_px, upsample_factor=upsample_factor)
# رستر کردن روی ماسک موقت (خط 1px)
temp = np.zeros((H, W), dtype=np.uint8)
smooth_i = np.round(smooth).astype(np.int32)
# ensure closed
if not (smooth_i[0] == smooth_i[-1]).all():
smooth_i = np.vstack([smooth_i, smooth_i[0]])
cv2.polylines(temp, [smooth_i], isClosed=True, color=255, thickness=1, lineType=cv2.LINE_8)
# فقط پیکسل‌های مرزی اجازه‌ی رسم دارند (کلاپ روی مرز)
write_mask = (temp > 0) & (boundary_mask > 0) & (~occupied)
if not np.any(write_mask):
continue
# تعیین رنگ هر نقطه‌ی مرز از اکثریت همسایه‌ها
ring = cv2.dilate((write_mask).astype(np.uint8), kernel, iterations=1).astype(bool)
ring = ring & (~write_mask)
neigh_lbls = labels[ring]
if neigh_lbls.size == 0:
# fallback: از خود مرز، یک رشد کوچک
ring2 = cv2.dilate((write_mask).astype(np.uint8), kernel, iterations=2).astype(bool)
ring2 = ring2 & (~write_mask)
neigh_lbls = labels[ring2]
if neigh_lbls.size == 0:
continue
owner_lbl = int(np.bincount(neigh_lbls).argmax())
color = tuple(int(v) for v in palette[owner_lbl])
ys, xs = np.where(write_mask)
canvas[ys, xs] = color
occupied[ys, xs] = True
return canvas
# ===============================
# ترکیب نهایی بدون تولید رنگ جدید
# ===============================
def overlay_contours_on_reduced(reduced_rgb: np.ndarray, contours_rgb: np.ndarray) -> np.ndarray:
final = reduced_rgb.copy()
mask = np.any(contours_rgb != 0, axis=2)
final[mask] = contours_rgb[mask]
return final
# ===============================
# پایپلاین اصلی Gradio
# ===============================
def process_pipeline(pil_image: Image.Image,
n_clusters: int,
min_area_px: int,
label_smooth_r: int,
min_wavelength_px: int,
arc_step: float,
upsample_factor: float):
original = pil_to_np_rgb(pil_image)
# 1) کوانتیزه
labels_raw, palette, _ = quantize_colors_kmeans(original, n_clusters=n_clusters)
# 2) حذف ریزناحیه‌ها
labels = clean_small_regions(labels_raw, min_area_px=min_area_px, connectivity=8)
# 3) نرم‌سازی برچسب‌ها با اکثریت همسایه‌ها (کاهش زیگزاگ پیکسلی)
labels = smooth_labels_majority(labels, radius=label_smooth_r)
# 4) یک پاسِ دیگر حذف ریزناحیه‌ها بعد از نرم‌سازی اختیاری
labels = clean_small_regions(labels, min_area_px=min_area_px, connectivity=8)
# 5) بازسازی تصویر کاهش‌یافته از روی لیبل‌های تمیز
reduced = np.zeros_like(original)
for i in range(len(palette)):
reduced[labels == i] = palette[i]
# 6) مرز 1px سراسری
boundary_mask = build_global_boundary_mask(labels)
# 7) مرزهای نرم، یکپارچه، بدون اورلپ، فقط روی مرز
contours_img = render_boundaries_smooth_no_overlap(
labels=labels,
palette=palette,
boundary_mask=boundary_mask,
min_wavelength_px=min_wavelength_px,
arclen_step=arc_step,
upsample_factor=upsample_factor
)
# 8) خروجی نهایی (نواحی رنگ + مرز 1px)
final_img = overlay_contours_on_reduced(reduced, contours_img)
# BMP برای دانلود
p_ori = save_as_bmp(original, "original")
p_red = save_as_bmp(reduced, "reduced_clean")
p_cnt = save_as_bmp(contours_img, "contours_smooth_no_overlap")
p_fin = save_as_bmp(final_img,"final")
return (
Image.fromarray(original), Image.fromarray(reduced),
Image.fromarray(contours_img), Image.fromarray(final_img),
p_ori, p_red, p_cnt, p_fin
)
# ===============================
# UI با اسکرول + دانلود BMP
# ===============================
CSS = """
.scroll-pane { max-height: 720px; overflow: auto; }
.scroll-pane img, .scroll-pane canvas { width: auto !important; max-width: none !important; }
"""
with gr.Blocks(css=CSS) as demo:
gr.Markdown("## 🎨 مرزهای نرمِ پیوسته (Fourier + Arc-length) بدون اورلپ + حذف ریزناحیه‌ها (BMP)")
with gr.Row():
inp = gr.Image(label="آپلود تصویر", type="pil")
with gr.Row():
n_clusters = gr.Slider(15, 30, value=20, step=1, label="تعداد کلاسترهای رنگ (15–30)")
min_area = gr.Slider(0, 20000, value=1500, step=100, label="حداقل مساحت ناحیه (پیکسل) – حذف ریزناحیه‌ها")
label_smooth_r = gr.Slider(0, 5, value=2, step=1, label="نرم‌سازی برچسب‌ها (شعاع اکثریت همسایه)")
with gr.Row():
min_wave = gr.Slider(10, 400, value=140, step=5, label="حداقل طول‌موج مرز (پیکسل) – صاف‌سازی فوریه")
arc_step = gr.Slider(0.5, 3.0, value=1.0, step=0.5, label="گام بازنمونه‌گیری طول‌قوس (پیکسل)")
upsample = gr.Slider(1.0, 4.0, value=2.0, step=0.5, label="Upsample منحنی پس از فیلتر")
btn = gr.Button("🚀 پردازش")
with gr.Row():
out_ori = gr.Image(label="۱) تصویر اصلی", elem_classes=["scroll-pane"], height=600)
out_red = gr.Image(label="۲) تصویر کاهش‌یافته پس از پاک‌سازی", elem_classes=["scroll-pane"], height=600)
with gr.Row():
out_cnt = gr.Image(label="۳) مرزهای نرمِ پیوسته (۱px، بدون اورلپ)", elem_classes=["scroll-pane"], height=600)
out_fin = gr.Image(label="۴) خروجی نهایی (نواحی رنگ‌شده + مرز ۱px)", elem_classes=["scroll-pane"], height=600)
with gr.Accordion("دانلود فایل‌های BMP", open=True):
with gr.Row():
file_ori = gr.File(label="دانلود: تصویر اصلی (BMP)")
file_red = gr.File(label="دانلود: تصویر کاهش‌یافته پاک‌سازی‌شده (BMP)")
with gr.Row():
file_cnt = gr.File(label="دانلود: مرزهای نرمِ پیوسته (BMP)")
file_fin = gr.File(label="دانلود: خروجی نهایی (BMP)")
btn.click(
process_pipeline,
inputs=[inp, n_clusters, min_area, label_smooth_r, min_wave, arc_step, upsample],
outputs=[out_ori, out_red, out_cnt, out_fin, file_ori, file_red, file_cnt, file_fin]
)
demo.launch()