""" πŸ–₯️ λͺ¨λ‹ˆν„° νŒ¨λ„ μΊ˜λ¦¬λΈŒλ ˆμ΄μ…˜ 계츑 μ‹œμŠ€ν…œ v3 Auto Screen Capture + Marker Detection + ICC Profile 핡심 κ°œμ„ : 1. κΈ°μ€€ νŒ¨ν„΄μ— 4색 μ½”λ„ˆ 마컀 μžλ™ μ‚½μž… 2. JS getDisplayMedia()둜 원클릭 슀크린캑처 3. 캑처 μ΄λ―Έμ§€μ—μ„œ 마컀 μžλ™ 감지 β†’ ROI μΆ”μΆœ 4. 크기/λΉ„μœ¨ 차이 μžλ™ 보정 5. νƒ­ κ°„ μžλ™ 연동 (State) """ import gradio as gr import numpy as np from PIL import Image, ImageDraw import struct import io import base64 import tempfile from datetime import datetime # ============================================================ # 색상 κ³Όν•™ μ—”μ§„ # ============================================================ def srgb_to_linear(c): c = np.asarray(c, dtype=np.float64) / 255.0 return np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4) def srgb_to_xyz(rgb): linear = srgb_to_linear(rgb) M = np.array([[0.4124564,0.3575761,0.1804375], [0.2126729,0.7151522,0.0721750], [0.0193339,0.1191920,0.9503041]]) return M @ linear def xyz_to_lab(xyz, wr=None): if wr is None: wr = np.array([0.95047, 1.0, 1.08883]) r = xyz / wr def f(t): d = 6/29 return np.where(t > d**3, t**(1/3), t/(3*d**2) + 4/29) fx, fy, fz = f(r[0]), f(r[1]), f(r[2]) return np.array([116*fy-16, 500*(fx-fy), 200*(fy-fz)]) def rgb_to_lab(rgb): return xyz_to_lab(srgb_to_xyz(rgb)) def delta_e_2000(lab1, lab2): L1,a1,b1 = lab1; L2,a2,b2 = lab2 C1=np.sqrt(a1**2+b1**2); C2=np.sqrt(a2**2+b2**2) Ca=(C1+C2)/2; G=0.5*(1-np.sqrt(Ca**7/(Ca**7+25**7))) a1p=a1*(1+G); a2p=a2*(1+G) C1p=np.sqrt(a1p**2+b1**2); C2p=np.sqrt(a2p**2+b2**2) h1p=np.degrees(np.arctan2(b1,a1p))%360; h2p=np.degrees(np.arctan2(b2,a2p))%360 dLp=L2-L1; dCp=C2p-C1p if C1p*C2p==0: dhp=0 elif abs(h2p-h1p)<=180: dhp=h2p-h1p elif h2p-h1p>180: dhp=h2p-h1p-360 else: dhp=h2p-h1p+360 dHp=2*np.sqrt(C1p*C2p)*np.sin(np.radians(dhp/2)) Lpa=(L1+L2)/2; Cpa=(C1p+C2p)/2 if C1p*C2p==0: hpa=h1p+h2p elif abs(h1p-h2p)<=180: hpa=(h1p+h2p)/2 elif h1p+h2p<360: hpa=(h1p+h2p+360)/2 else: hpa=(h1p+h2p-360)/2 T=1-0.17*np.cos(np.radians(hpa-30))+0.24*np.cos(np.radians(2*hpa))+0.32*np.cos(np.radians(3*hpa+6))-0.20*np.cos(np.radians(4*hpa-63)) SL=1+0.015*(Lpa-50)**2/np.sqrt(20+(Lpa-50)**2) SC=1+0.045*Cpa; SH=1+0.015*Cpa*T RT=-2*np.sqrt(Cpa**7/(Cpa**7+25**7))*np.sin(np.radians(60*np.exp(-((hpa-275)/25)**2))) return np.sqrt((dLp/SL)**2+(dCp/SC)**2+(dHp/SH)**2+RT*(dCp/SC)*(dHp/SH)) def delta_e_rating(de): if de<1: return "β—Ž μ™„λ²½","#00c853" elif de<2: return "β—‹ 우수","#64dd17" elif de<3: return "β–³ μ–‘ν˜Έ","#ffd600" elif de<5: return "β–½ 보톡","#ff9100" else: return "βœ• λΆˆλŸ‰","#ff1744" # ============================================================ # 마컀 μ‹œμŠ€ν…œ # ============================================================ # μ½”λ„ˆ 마컀 색상 (각각 고유, ν…Œλ‘λ¦¬μ™€λ„ λ‹€λ₯Έ 색상) MARKER_COLORS = { 'TL': (255, 0, 128), # λ‘œμ¦ˆν•‘ν¬ 'TR': (0, 255, 128), # μŠ€ν”„λ§κ·Έλ¦° 'BL': (128, 0, 255), # λ°”μ΄μ˜¬λ › 'BR': (0, 128, 255), # 도저블루 } MARKER_SIZE = 40 # μ½”λ„ˆ 마컀 크기 (px) - 감지 신뒰도λ₯Ό μœ„ν•΄ μΆ©λΆ„νžˆ 크게 BORDER_WIDTH = 8 # ν…Œλ‘λ¦¬ 폭 (px) BORDER_COLOR = (255, 128, 0) # μ˜€λ Œμ§€ ν…Œλ‘λ¦¬ (λ§ˆμ»€μ™€ μ™„μ „νžˆ λ‹€λ₯Έ 색) def add_marker_frame(img_array): """이미지에 κ°μ§€μš© 마컀 ν”„λ ˆμž„ μΆ”κ°€ ꡬ쑰: [λ§ˆμ»€ν”„λ ˆμž„] β†’ [원본 이미지] ν”„λ ˆμž„μ΄ μΆ”κ°€λœ 이미지 λ°˜ν™˜ + ν”„λ ˆμž„ λ‚΄λΆ€(원본) 크기 기둝 """ h, w = img_array.shape[:2] ms = MARKER_SIZE bw = BORDER_WIDTH pad = ms # 마컀 크기만큼 νŒ¨λ”© # ν”„λ ˆμž„ 포함 μƒˆ 이미지 new_h = h + pad * 2 new_w = w + pad * 2 framed = np.zeros((new_h, new_w, 3), dtype=np.uint8) framed[:, :] = (30, 30, 30) # μ–΄λ‘μš΄ λ°°κ²½ # ν…Œλ‘λ¦¬ 그리기 framed[pad-bw:pad, pad-bw:pad+w+bw] = BORDER_COLOR # 상단 framed[pad+h:pad+h+bw, pad-bw:pad+w+bw] = BORDER_COLOR # ν•˜λ‹¨ framed[pad-bw:pad+h+bw, pad-bw:pad] = BORDER_COLOR # 쒌츑 framed[pad-bw:pad+h+bw, pad+w:pad+w+bw] = BORDER_COLOR # 우츑 # 4개 μ½”λ„ˆ 마컀 framed[0:ms, 0:ms] = MARKER_COLORS['TL'] # μ’Œμƒ framed[0:ms, new_w-ms:new_w] = MARKER_COLORS['TR'] # μš°μƒ framed[new_h-ms:new_h, 0:ms] = MARKER_COLORS['BL'] # μ’Œν•˜ framed[new_h-ms:new_h, new_w-ms:new_w] = MARKER_COLORS['BR'] # μš°ν•˜ # 원본 이미지 μ‚½μž… framed[pad:pad+h, pad:pad+w] = img_array[:, :, :3] return framed, (w, h) def color_distance(pixel, target): """RGB μœ ν΄λ¦¬λ“œ 거리""" return np.sqrt(np.sum((np.array(pixel, dtype=float) - np.array(target, dtype=float))**2)) def find_marker_regions(img_array, target_color, threshold=60): """νŠΉμ • μƒ‰μƒμ˜ 마컀 μ˜μ—­ 쀑심 μ’Œν‘œ μ°ΎκΈ°""" h, w = img_array.shape[:2] # 채널별 거리 계산 (벑터화) diff = img_array[:,:,:3].astype(float) - np.array(target_color, dtype=float) dist = np.sqrt(np.sum(diff**2, axis=2)) mask = dist < threshold if np.sum(mask) < 10: return None # 마슀크의 쀑심 μ’Œν‘œ ys, xs = np.where(mask) cy, cx = np.mean(ys), np.mean(xs) # 마컀 λ°”μš΄λ”©λ°•μŠ€ min_y, max_y = np.min(ys), np.max(ys) min_x, max_x = np.min(xs), np.max(xs) return { 'center': (int(cx), int(cy)), 'bbox': (int(min_x), int(min_y), int(max_x), int(max_y)), 'pixel_count': int(np.sum(mask)) } def detect_pattern_roi(capture_array): """캑처 μ΄λ―Έμ§€μ—μ„œ 마컀 ν”„λ ˆμž„μ„ κ°μ§€ν•˜κ³  νŒ¨ν„΄ μ˜μ—­(ROI) μΆ”μΆœ 감지 μ „λž΅: 1. 4색 μ½”λ„ˆ 마컀 탐색 (각각 독립적 색상) 2. 마컀 μœ„μΉ˜λ‘œ νŒ¨ν„΄ λ°”μš΄λ”©λ°•μŠ€ 계산 3. 폴백: μ˜€λ Œμ§€ ν…Œλ‘λ¦¬ 탐색 Returns: (roi_image, detection_info) or (None, error_msg) """ if capture_array is None: return None, "캑처 이미지가 μ—†μŠ΅λ‹ˆλ‹€." h, w = capture_array.shape[:2] cap = capture_array[:,:,:3] # 4개 마컀 탐색 (threshold λ„‰λ„‰ν•˜κ²Œ) markers_found = {} for name, color in MARKER_COLORS.items(): result = find_marker_regions(cap, color, threshold=50) if result and result['pixel_count'] >= 20: markers_found[name] = result info_lines = [f"κ°μ§€λœ 마컀: {len(markers_found)}/4"] for name, data in markers_found.items(): cx, cy = data['center'] bx1, by1, bx2, by2 = data['bbox'] info_lines.append(f" {name}: center=({cx},{cy}) bbox=({bx1},{by1})-({bx2},{by2}) px={data['pixel_count']}") # ── 방법 1: TL + BR 마컀 쌍으둜 ROI ── if 'TL' in markers_found and 'BR' in markers_found: tl_bbox = markers_found['TL']['bbox'] # (min_x, min_y, max_x, max_y) br_bbox = markers_found['BR']['bbox'] # νŒ¨ν„΄ μ˜μ—­ = 마컀 μ•ˆμͺ½ κ°€μž₯자리 λ°”λ‘œ λ‹€μŒ roi_x1 = tl_bbox[2] + 1 # TL 마컀 였λ₯Έμͺ½ 끝 + 1 roi_y1 = tl_bbox[3] + 1 # TL 마컀 μ•„λž˜μͺ½ 끝 + 1 roi_x2 = br_bbox[0] - 1 # BR 마컀 μ™Όμͺ½ 끝 - 1 roi_y2 = br_bbox[1] - 1 # BR 마컀 μœ„μͺ½ 끝 - 1 info_lines.append(f" μ „λž΅: TL+BR μ½”λ„ˆ 쌍") # ── 방법 2: 아무 λŒ€κ°μ„  마컀 쌍 ── elif len(markers_found) >= 2: all_bboxes = list(markers_found.values()) all_min_x = min(m['bbox'][0] for m in all_bboxes) all_min_y = min(m['bbox'][1] for m in all_bboxes) all_max_x = max(m['bbox'][2] for m in all_bboxes) all_max_y = max(m['bbox'][3] for m in all_bboxes) ms = MARKER_SIZE roi_x1 = all_min_x + ms roi_y1 = all_min_y + ms roi_x2 = all_max_x - ms roi_y2 = all_max_y - ms info_lines.append(f" μ „λž΅: 볡수 마컀 μ™Έκ³½μ„  기반") # ── 방법 3: μ˜€λ Œμ§€ ν…Œλ‘λ¦¬ 탐색 ── elif len(markers_found) == 0: border_result = find_marker_regions(cap, BORDER_COLOR, threshold=50) if border_result and border_result['pixel_count'] > 50: bx1, by1, bx2, by2 = border_result['bbox'] bw = BORDER_WIDTH roi_x1 = bx1 + bw roi_y1 = by1 + bw roi_x2 = bx2 - bw roi_y2 = by2 - bw info_lines.append(f" μ „λž΅: ν…Œλ‘λ¦¬ 색상 감지") else: return None, "마컀/ν…Œλ‘λ¦¬λ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.\n" + "\n".join(info_lines) else: # 마컀 1개만 발견 β†’ λΆ€μ •ν™• m = list(markers_found.values())[0] return None, f"마컀 1개만 감지 (μ΅œμ†Œ 2개 ν•„μš”)\n" + "\n".join(info_lines) # λ²”μœ„ ν΄λž¨ν•‘ roi_x1 = max(0, int(roi_x1)) roi_y1 = max(0, int(roi_y1)) roi_x2 = min(w, int(roi_x2)) roi_y2 = min(h, int(roi_y2)) roi_w = roi_x2 - roi_x1 roi_h = roi_y2 - roi_y1 if roi_w < 50 or roi_h < 50: return None, f"ROI 크기 λΆ€μ‘± ({roi_w}x{roi_h})\n" + "\n".join(info_lines) roi = cap[roi_y1:roi_y2, roi_x1:roi_x2] info_lines.append(f" βœ… ROI: ({roi_x1},{roi_y1})-({roi_x2},{roi_y2}) = {roi_w}x{roi_h}") return roi, "\n".join(info_lines) # ============================================================ # ColorChecker κΈ°μ€€κ°’ # ============================================================ COLORCHECKER = { "1-DarkSkin":(115,82,68), "2-LightSkin":(194,150,130), "3-BlueSky":(98,122,157), "4-Foliage":(87,108,67), "5-BlueFlower":(133,128,177), "6-BluishGreen":(103,189,170), "7-Orange":(214,126,44), "8-PurplishBlue":(80,91,166), "9-ModerateRed":(193,90,99), "10-Purple":(94,60,108), "11-YellowGreen":(157,188,64), "12-OrangeYellow":(224,163,46), "13-Blue":(56,61,150), "14-Green":(70,148,73), "15-Red":(175,54,60), "16-Yellow":(231,199,31), "17-Magenta":(187,86,149), "18-Cyan":(8,133,161), "19-White":(243,243,242), "20-Neutral8":(200,200,200), "21-Neutral6.5":(160,160,160), "22-Neutral5":(122,122,121), "23-Neutral3.5":(85,85,85), "24-Black":(52,52,52), } # ============================================================ # ν…ŒμŠ€νŠΈ νŒ¨ν„΄ 생성 (마컀 포함) # ============================================================ def generate_colorchecker(w=1200, h=800): img = Image.new('RGB', (w,h), (40,40,40)) draw = ImageDraw.Draw(img) colors = list(COLORCHECKER.items()) cols, rows = 6, 4 mg = 30 pw = (w - mg*(cols+1)) // cols ph = (h - mg*(rows+1) - 60) // rows draw.text((w//2-150, 10), "Reference ColorChecker 24", fill=(200,200,200)) for idx, (name,(r,g,b)) in enumerate(colors): row, col = idx//cols, idx%cols x = mg + col*(pw+mg) y = 50 + mg + row*(ph+mg) draw.rectangle([x,y,x+pw,y+ph], fill=(r,g,b)) br = 0.299*r+0.587*g+0.114*b tc = (0,0,0) if br>128 else (255,255,255) draw.text((x+5,y+5), name.split('-')[0], fill=tc) draw.text((x+5,y+ph-18), f"({r},{g},{b})", fill=tc) return np.array(img) def generate_grayscale(w=1200, h=400, steps=32): img = Image.new('RGB', (w,h), (0,0,0)) draw = ImageDraw.Draw(img) sw = w // steps draw.text((w//2-100, 5), "Grayscale Reference Ramp", fill=(200,200,200)) for i in range(steps): v = int(255*i/(steps-1)) x = i*sw draw.rectangle([x, 30, x+sw, h-30], fill=(v,v,v)) if i%4==0: tc = (255,255,255) if v<128 else (0,0,0) draw.text((x+2, h//2-5), str(v), fill=tc) return np.array(img) def generate_gamma_check(w=1200, h=600): img = Image.new('RGB', (w,h), (30,30,30)) draw = ImageDraw.Draw(img) draw.text((w//2-120, 5), "Gamma Verification Pattern", fill=(200,200,200)) steps = 11 pw = (w-40)//steps ph = (h-100)//2 for i in range(steps): pct = i/(steps-1) v = int(255*pct) x = 20+i*pw draw.rectangle([x,40,x+pw-4,40+ph-10], fill=(v,v,v)) ys = 40+ph cs = 2 for cy in range(ys, ys+ph-10, cs): for cx in range(x, x+pw-4, cs): is_w = ((cx-x)//cs + (cy-ys)//cs)%2 c = 255 if is_w else 0 draw.rectangle([cx,cy,cx+cs-1,cy+cs-1], fill=(c,c,c)) tc = (255,255,255) if v<128 else (0,0,0) draw.text((x+pw//2-10, 40+ph//2-5), f"{int(pct*100)}%", fill=tc) draw.text((20, h-25), "상단=μ†”λ¦¬λ“œ, ν•˜λ‹¨=μ²΄μ»€λ³΄λ“œ β†’ Ξ³2.2μ—μ„œ 밝기 동일", fill=(160,160,160)) return np.array(img) def generate_rgb_ramps(w=1200, h=500): img = Image.new('RGB', (w,h), (30,30,30)) draw = ImageDraw.Draw(img) draw.text((w//2-80, 5), "RGB Channel Ramps", fill=(200,200,200)) bh = (h-80)//3 for ci, (cn, ch) in enumerate([("Red",0),("Green",1),("Blue",2)]): y = 35+ci*(bh+10) draw.text((5,y+2), cn, fill=(200,200,200)) for x in range(w): v = int(255*x/(w-1)) px = [0,0,0]; px[ch] = v draw.line([(x,y+20),(x,y+20+bh-25)], fill=tuple(px)) return np.array(img) def generate_whitebalance(w=1200, h=600): img = Image.new('RGB', (w,h), (30,30,30)) draw = ImageDraw.Draw(img) draw.text((w//2-130, 5), "White Balance & Neutral Patches", fill=(200,200,200)) ww, wh = 500, 280 wx = (w-ww)//2 draw.rectangle([wx,40,wx+ww,40+wh], fill=(255,255,255)) draw.text((wx+10,50), "White (255,255,255)", fill=(0,0,0)) grays = [("N9.5",243),("N8",200),("N6.5",160),("N5",122),("N3.5",85),("N2",52)] gpw = (w-40)//len(grays) for i,(nm,v) in enumerate(grays): x = 20+i*gpw y = 40+wh+30 draw.rectangle([x,y,x+gpw-4,y+140], fill=(v,v,v)) tc = (255,255,255) if v<128 else (0,0,0) draw.text((x+5,y+5), f"{nm}({v})", fill=tc) return np.array(img) PATTERN_GENERATORS = { "ColorChecker 24색": generate_colorchecker, "κ·Έλ ˆμ΄μŠ€μΌ€μΌ 32단계": generate_grayscale, "RGB 채널 λž¨ν”„": generate_rgb_ramps, "감마 검증 νŒ¨ν„΄": generate_gamma_check, "ν™”μ΄νŠΈλ°ΈλŸ°μŠ€ 패치": generate_whitebalance, } def create_framed_pattern(pattern_type): """마컀 ν”„λ ˆμž„μ΄ ν¬ν•¨λœ κΈ°μ€€ νŒ¨ν„΄ 생성 β†’ 미리보기 + λ‹€μš΄λ‘œλ“œ + State""" gen = PATTERN_GENERATORS.get(pattern_type, generate_colorchecker) raw = gen() framed, inner_size = add_marker_frame(raw) # PNG μ €μž₯ pil = Image.fromarray(framed) path = tempfile.mktemp(suffix=".png") pil.save(path, "PNG", compress_level=0) return framed, path, raw, inner_size, f"βœ… {pattern_type} 생성 μ™„λ£Œ ({raw.shape[1]}x{raw.shape[0]})" # ============================================================ # 슀크린캑처 뢄석 (핡심) # ============================================================ def extract_checker_patches(img_array, cols=6, rows=4): """μ΄λ―Έμ§€μ—μ„œ ColorChecker νŒ¨μΉ˜λ³„ 평균 RGB μΆ”μΆœ""" h, w = img_array.shape[:2] mg = 30 pw = (w - mg*(cols+1)) // cols ph = (h - mg*(rows+1) - 60) // rows extracted = {} names = list(COLORCHECKER.keys()) for idx, name in enumerate(names): row, col = idx//cols, idx%cols x = mg + col*(pw+mg) y = 50 + mg + row*(ph+mg) # 패치 쀑앙 60% μƒ˜ν”Œλ§ mx, my = int(pw*0.2), int(ph*0.2) x1, y1 = min(x+mx, w-1), min(y+my, h-1) x2, y2 = min(x+pw-mx, w), min(y+ph-my, h) if x2>x1 and y2>y1: region = img_array[y1:y2, x1:x2, :3] extracted[name] = tuple(np.round(np.mean(region, axis=(0,1))).astype(int)) else: extracted[name] = (0,0,0) return extracted def run_full_analysis(ref_raw, ref_inner_size, captured_base64, pattern_type): """ 전체 뢄석 νŒŒμ΄ν”„λΌμΈ: 1. base64 캑처 λ””μ½”λ”© 2. 마컀 감지 β†’ ROI μΆ”μΆœ 3. κΈ°μ€€ 이미지 크기둜 λ¦¬μ‚¬μ΄μ¦ˆ 4. ν”½μ…€ 비ꡐ + Delta E 뢄석 """ if ref_raw is None: return None, None, "❌ κΈ°μ€€ 이미지가 μ—†μŠ΅λ‹ˆλ‹€. β‘  νƒ­μ—μ„œ λ¨Όμ € νŒ¨ν„΄μ„ μƒμ„±ν•˜μ„Έμš”.", None, None if captured_base64 is None or not captured_base64.strip(): return None, None, "❌ 캑처 데이터가 μ—†μŠ΅λ‹ˆλ‹€. πŸ“Έ λ²„νŠΌμ„ ν΄λ¦­ν•˜μ„Έμš”.", None, None # 1. base64 β†’ numpy try: if ',' in captured_base64: captured_base64 = captured_base64.split(',', 1)[1] img_bytes = base64.b64decode(captured_base64) cap_pil = Image.open(io.BytesIO(img_bytes)).convert('RGB') cap_array = np.array(cap_pil) except Exception as e: return None, None, f"❌ 캑처 λ””μ½”λ”© 였λ₯˜: {str(e)}", None, None cap_h, cap_w = cap_array.shape[:2] # 2. 마컀 감지 β†’ ROI μΆ”μΆœ roi, detect_info = detect_pattern_roi(cap_array) if roi is None: # 폴백: 전체 캑처λ₯Ό μ‚¬μš© (마컀 없이) roi = cap_array detect_info += "\n⚠️ 마컀 미감지 β†’ 전체 캑처λ₯Ό 뢄석에 μ‚¬μš©" # 3. κΈ°μ€€ 크기둜 λ¦¬μ‚¬μ΄μ¦ˆ ref_h, ref_w = ref_raw.shape[:2] roi_pil = Image.fromarray(roi).resize((ref_w, ref_h), Image.LANCZOS) roi_resized = np.array(roi_pil) # 4. 뢄석 μ‹€ν–‰ report_lines = [] report_lines.append("━" * 56) report_lines.append(" πŸ“Š 슀크린캑처 μžλ™ 뢄석 κ²°κ³Ό") report_lines.append(f" μΈ‘μ •μΌμ‹œ: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") report_lines.append("━" * 56) report_lines.append(f" 캑처 원본: {cap_w}x{cap_h}") report_lines.append(f" 감지 ROI: {roi.shape[1]}x{roi.shape[0]}") report_lines.append(f" κΈ°μ€€ 크기: {ref_w}x{ref_h}") report_lines.append(f" λ¦¬μ‚¬μ΄μ¦ˆ: {roi.shape[1]}x{roi.shape[0]} β†’ {ref_w}x{ref_h}") report_lines.append(f"\n [마컀 감지]\n {detect_info}") report_lines.append("") # ── ColorChecker 패치 뢄석 ── if "ColorChecker" in pattern_type: ref_patches = extract_checker_patches(ref_raw) cap_patches = extract_checker_patches(roi_resized) report_lines.append("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”") report_lines.append("β”‚ 패치 β”‚ κΈ°μ€€ RGB β”‚ 캑처 RGB β”‚ Ξ”E β”‚ νŒμ • β”‚") report_lines.append("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€") de_values = [] for name in COLORCHECKER: r_rgb = np.array(ref_patches.get(name, COLORCHECKER[name])) c_rgb = np.array(cap_patches.get(name, (0,0,0))) de = delta_e_2000(rgb_to_lab(r_rgb), rgb_to_lab(c_rgb)) rating, _ = delta_e_rating(de) de_values.append(de) sn = name[:14].ljust(14) rs = f"({r_rgb[0]:3d},{r_rgb[1]:3d},{r_rgb[2]:3d})" cs = f"({c_rgb[0]:3d},{c_rgb[1]:3d},{c_rgb[2]:3d})" report_lines.append(f"β”‚ {sn} β”‚ {rs:14s} β”‚ {cs:14s} β”‚{de:6.2f} β”‚ {rating.split()[0]:4s} β”‚") report_lines.append("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”˜") avg_de = np.mean(de_values) max_de = np.max(de_values) min_de = np.min(de_values) max_name = list(COLORCHECKER.keys())[np.argmax(de_values)] min_name = list(COLORCHECKER.keys())[np.argmin(de_values)] avg_rating, _ = delta_e_rating(avg_de) report_lines.append(f"\n [μ’…ν•© 톡계]") report_lines.append(f" 평균 Ξ”E2000: {avg_de:.4f} β”‚ νŒμ •: {avg_rating}") report_lines.append(f" μ΅œλŒ€ Ξ”E2000: {max_de:.4f} β”‚ {max_name}") report_lines.append(f" μ΅œμ†Œ Ξ”E2000: {min_de:.4f} β”‚ {min_name}") report_lines.append(f" Ξ”E < 1.0: {sum(1 for d in de_values if d<1)}/24") report_lines.append(f" Ξ”E < 2.0: {sum(1 for d in de_values if d<2)}/24") report_lines.append(f" Ξ”E < 3.0: {sum(1 for d in de_values if d<3)}/24") report_lines.append(f" Ξ”E β‰₯ 5.0: {sum(1 for d in de_values if d>=5)}/24") # ── 전체 ν”½μ…€ 뢄석 ── report_lines.append(f"\n [전체 ν”½μ…€ 뢄석]") scale = max(1, min(ref_h, ref_w) // 150) ref_ds = ref_raw[::scale, ::scale, :3] cap_ds = roi_resized[::scale, ::scale, :3] ds_h, ds_w = ref_ds.shape[:2] de_map = np.zeros((ds_h, ds_w)) rgb_diff = cap_ds.astype(float) - ref_ds.astype(float) for y in range(ds_h): for x in range(ds_w): r_lab = rgb_to_lab(ref_ds[y,x].astype(float)) c_lab = rgb_to_lab(cap_ds[y,x].astype(float)) de_map[y,x] = delta_e_2000(r_lab, c_lab) px_avg = np.mean(de_map) px_max = np.max(de_map) px_p95 = np.percentile(de_map, 95) report_lines.append(f" μƒ˜ν”Œ: {ds_w}x{ds_h} (1/{scale})") report_lines.append(f" ν”½μ…€ 평균 Ξ”E: {px_avg:.4f}") report_lines.append(f" ν”½μ…€ μ΅œλŒ€ Ξ”E: {px_max:.4f}") report_lines.append(f" 95% λ°±λΆ„μœ„: {px_p95:.4f}") # 채널 편차 dr = np.mean(rgb_diff[:,:,0]) dg = np.mean(rgb_diff[:,:,1]) db = np.mean(rgb_diff[:,:,2]) report_lines.append(f"\n [채널 편차] Ξ”R:{dr:+.2f} Ξ”G:{dg:+.2f} Ξ”B:{db:+.2f}") abs_d = [abs(dr), abs(dg), abs(db)] max_ch = ['Red','Green','Blue'][np.argmax(abs_d)] vals = [dr, dg, db] max_val = vals[np.argmax(abs_d)] if max(abs_d) < 1.0: bias = "βšͺ 편ν–₯ μ—†μŒ (κ· ν˜•)" else: direction = "κ³Όλ‹€" if max_val > 0 else "λΆ€μ‘±" bias = f"{'πŸ”΄πŸŸ’πŸ”΅'[np.argmax(abs_d)]} {max_ch} {direction} ({max_val:+.1f})" report_lines.append(f" μƒ‰νŽΈν–₯: {bias}") report_lines.append("\n" + "━" * 56) # ── 히트맡 생성 ── de_norm = np.clip(de_map / 10, 0, 1) heatmap = np.zeros((ds_h, ds_w, 3), dtype=np.uint8) for y in range(ds_h): for x in range(ds_w): v = de_norm[y,x] if v < 0.2: r,g,b = int(255*v/0.2), 255, 0 elif v < 0.5: t = (v-0.2)/0.3 r,g,b = 255, int(255*(1-t)), 0 else: t = min((v-0.5)/0.5, 1) r,g,b = 255, 0, 0 heatmap[y,x] = [r,g,b] heatmap_full = np.array(Image.fromarray(heatmap).resize((ref_w, ref_h), Image.NEAREST)) # ── 비ꡐ 이미지 ── comp_h = ref_h comp = np.zeros((comp_h, ref_w*2+10, 3), dtype=np.uint8) comp[:ref_h, :ref_w] = ref_raw[:,:,:3] comp[:ref_h, ref_w+10:] = roi_resized[:,:,:3] comp[:, ref_w:ref_w+10] = 60 return roi_resized, heatmap_full, "\n".join(report_lines), comp, detect_info def process_uploaded_capture(ref_raw, ref_inner_size, capture_image, pattern_type): """μˆ˜λ™ μ—…λ‘œλ“œλœ 캑처 이미지 뢄석""" if capture_image is None: return None, None, "이미지λ₯Ό μ—…λ‘œλ“œν•˜μ„Έμš”.", None, "" # numpy β†’ base64 pil = Image.fromarray(capture_image) buf = io.BytesIO() pil.save(buf, format='PNG') b64 = base64.b64encode(buf.getvalue()).decode() return run_full_analysis(ref_raw, ref_inner_size, f"data:image/png;base64,{b64}", pattern_type) # ============================================================ # ICC ν”„λ‘œνŒŒμΌ νŒŒμ„œ # ============================================================ def parse_icc_profile(icc_file): if icc_file is None: return "ICC νŒŒμΌμ„ μ—…λ‘œλ“œν•˜μ„Έμš”.", None, None try: with open(icc_file.name, 'rb') as f: data = f.read() except: return "파일 읽기 였λ₯˜", None, None if len(data) < 128: return "μœ νš¨ν•˜μ§€ μ•Šμ€ ICC 파일", None, None lines = [] lines.append("━"*56) lines.append(" ICC ν”„λ‘œνŒŒμΌ 뢄석 κ²°κ³Ό") lines.append("━"*56) profile_size = struct.unpack('>I', data[0:4])[0] ver_major, ver_minor = data[8], data[9]>>4 dev_class = data[12:16].decode('ascii', errors='replace').strip('\x00') color_space = data[16:20].decode('ascii', errors='replace').strip('\x00') pcs = data[20:24].decode('ascii', errors='replace').strip('\x00') platform = data[40:44].decode('ascii', errors='replace').strip('\x00') year = struct.unpack('>H', data[24:26])[0] month = struct.unpack('>H', data[26:28])[0] day = struct.unpack('>H', data[28:30])[0] class_names = {'scnr':'Scanner','mntr':'Monitor','prtr':'Printer','link':'DeviceLink','spac':'ColorSpace','abst':'Abstract','nmcl':'NamedColor'} plat_names = {'APPL':'Apple','MSFT':'Microsoft','SGI ':'SGI','SUNW':'Sun'} lines.append(f" 크기: {profile_size:,}B | ICC v{ver_major}.{ver_minor}") lines.append(f" λ””λ°”μ΄μŠ€: {class_names.get(dev_class,dev_class)} | 색곡간: {color_space} | PCS: {pcs}") lines.append(f" ν”Œλž«νΌ: {plat_names.get(platform,platform)} | 생성: {year}-{month:02d}-{day:02d}") # νƒœκ·Έ νŒŒμ‹± tag_count = struct.unpack('>I', data[128:132])[0] tags = {} for i in range(tag_count): off = 132+i*12 if off+12>len(data): break sig = data[off:off+4].decode('ascii',errors='replace') tags[sig] = (struct.unpack('>I',data[off+4:off+8])[0], struct.unpack('>I',data[off+8:off+12])[0]) # ν™”μ΄νŠΈν¬μΈνŠΈ primaries = {} if 'wtpt' in tags: o,s = tags['wtpt'] if o+20<=len(data): wx=struct.unpack('>i',data[o+8:o+12])[0]/65536 wy=struct.unpack('>i',data[o+12:o+16])[0]/65536 wz=struct.unpack('>i',data[o+16:o+20])[0]/65536 ss=wx+wy+wz if ss>0: cx,cy=wx/ss,wy/ss n=(cx-0.332)/(0.1858-cy) if (0.1858-cy)!=0 else 0 cct=449*n**3+3525*n**2+6823.3*n+5520.33 lines.append(f"\n [ν™”μ΄νŠΈν¬μΈνŠΈ] XYZ:({wx:.4f},{wy:.4f},{wz:.4f}) xy:({cx:.4f},{cy:.4f}) CCT:{cct:.0f}K") # 원색 for ch,tag in [('Red','rXYZ'),('Green','gXYZ'),('Blue','bXYZ')]: if tag in tags: o,s=tags[tag] if o+20<=len(data): px=struct.unpack('>i',data[o+8:o+12])[0]/65536 py=struct.unpack('>i',data[o+12:o+16])[0]/65536 pz=struct.unpack('>i',data[o+16:o+20])[0]/65536 ss=px+py+pz if ss>0: primaries[ch]=(px/ss,py/ss) if primaries: lines.append(f"\n [원색 μ’Œν‘œ CIE xy]") srgb_p = {'Red':(0.64,0.33),'Green':(0.30,0.60),'Blue':(0.15,0.06)} for ch in ['Red','Green','Blue']: if ch in primaries: px,py=primaries[ch]; sx,sy=srgb_p[ch] lines.append(f" {ch}: ({px:.4f},{py:.4f}) vs sRGB({sx},{sy})") def tri_area(p1,p2,p3): return 0.5*abs((p2[0]-p1[0])*(p3[1]-p1[1])-(p3[0]-p1[0])*(p2[1]-p1[1])) if all(c in primaries for c in ['Red','Green','Blue']): pa=tri_area(primaries['Red'],primaries['Green'],primaries['Blue']) sa=tri_area((0.64,0.33),(0.30,0.60),(0.15,0.06)) da=tri_area((0.680,0.320),(0.265,0.690),(0.150,0.060)) lines.append(f" 색역: sRGB {pa/sa*100:.1f}% | DCI-P3 {pa/da*100:.1f}%") # 감마/TRC gamma_data = {} for ch,tag in [('Red','rTRC'),('Green','gTRC'),('Blue','bTRC')]: if tag in tags: o,s=tags[tag] if o+12<=len(data): tt=data[o:o+4].decode('ascii',errors='replace') if tt=='curv': cnt=struct.unpack('>I',data[o+8:o+12])[0] if cnt==0: gamma_data[ch]={"type":"identity","gamma":1.0,"curve":None} elif cnt==1: gv=struct.unpack('>H',data[o+12:o+14])[0]/256 gamma_data[ch]={"type":"gamma","gamma":gv,"curve":None} else: curve=[] for j in range(min(cnt,4096)): if o+12+j*2+2<=len(data): curve.append(struct.unpack('>H',data[o+12+j*2:o+14+j*2])[0]/65535) eg=0 if len(curve)>10: mi=len(curve)//2; iv=mi/(len(curve)-1); ov=curve[mi] if iv>0 and ov>0: eg=np.log(ov)/np.log(iv) gamma_data[ch]={"type":"curve","gamma":eg,"curve":curve} elif tt=='para': ft=struct.unpack('>H',data[o+8:o+10])[0] if ft==0 and o+16<=len(data): gv=struct.unpack('>i',data[o+12:o+16])[0]/65536 gamma_data[ch]={"type":"para","gamma":gv,"curve":None} if gamma_data: lines.append(f"\n [감마 TRC]") for ch in ['Red','Green','Blue']: if ch in gamma_data: gd=gamma_data[ch] lines.append(f" {ch}: Ξ³={gd['gamma']:.4f} ({gd['type']})") gammas=[g['gamma'] for g in gamma_data.values() if g['gamma']>0] if gammas: lines.append(f" 평균: {np.mean(gammas):.4f} | μ±„λ„νŽΈμ°¨: {max(gammas)-min(gammas):.4f}") lines.append(f"\n [νƒœκ·Έ {tag_count}개]") for sig in sorted(tags.keys()): o,s=tags[sig] lines.append(f" {sig:6s} off:{o:6d} sz:{s:6d}") # desc if 'desc' in tags: o,s=tags['desc'] try: dt=data[o:o+4].decode('ascii',errors='replace') if dt=='mluc': rc=struct.unpack('>I',data[o+8:o+12])[0] if rc>0 and o+28<=len(data): so=struct.unpack('>I',data[o+20:o+24])[0] sl=struct.unpack('>I',data[o+24:o+28])[0] ao=o+so if ao+sl<=len(data): ds=data[ao:ao+sl].decode('utf-16-be',errors='replace').strip('\x00') lines.insert(4, f" ν”„λ‘œνŒŒμΌ: {ds}") elif dt=='desc': sl=struct.unpack('>I',data[o+8:o+12])[0] ds=data[o+12:o+12+sl-1].decode('ascii',errors='replace') lines.insert(4, f" ν”„λ‘œνŒŒμΌ: {ds}") except: pass lines.append("\n"+"━"*56) # TRC μ‹œκ°ν™” trc_img = None if gamma_data: trc_img = _draw_trc(gamma_data) gamut_img = None if primaries and all(c in primaries for c in ['Red','Green','Blue']): gamut_img = _draw_gamut(primaries) return "\n".join(lines), trc_img, gamut_img def _draw_trc(gamma_data, w=600, h=400): img = Image.new('RGB',(w,h),(25,25,30)); draw=ImageDraw.Draw(img) m=50; pw=w-2*m; ph=h-2*m for i in range(11): x=m+int(pw*i/10); y=m+int(ph*i/10) draw.line([(x,m),(x,m+ph)],fill=(50,50,55)) draw.line([(m,y),(m+pw,y)],fill=(50,50,55)) draw.text((w//2-50,5),"TRC / Gamma",fill=(200,200,200)) # sRGB ref pts=[(m+int(pw*i/99), m+ph-int(ph*(i/99)**2.2)) for i in range(100)] draw.line(pts,fill=(80,80,80),width=1) colors={'Red':(255,80,80),'Green':(80,255,80),'Blue':(80,80,255)} for ch in ['Red','Green','Blue']: if ch not in gamma_data: continue gd=gamma_data[ch]; c=colors[ch] if gd['curve'] and len(gd['curve'])>1: cv=gd['curve'] pts=[(m+int(pw*i/(len(cv)-1)), m+ph-int(ph*cv[i])) for i in range(len(cv))] elif gd['gamma']>0: pts=[(m+int(pw*i/99), m+ph-int(ph*(i/99)**gd['gamma'])) for i in range(100)] else: continue if len(pts)>1: draw.line(pts,fill=c,width=2) return np.array(img) def _draw_gamut(primaries, w=500, h=500): img = Image.new('RGB',(w,h),(20,20,25)); draw=ImageDraw.Draw(img) m=50; pw=w-2*m; ph=h-2*m def xy2px(x,y): return (m+int(pw*x/0.8), m+ph-int(ph*y/0.9)) for i in range(9): v=i*0.1; x=m+int(pw*v/0.8); y=m+ph-int(ph*v/0.9) draw.line([(x,m),(x,m+ph)],fill=(40,40,45)) draw.line([(m,y),(m+pw,y)],fill=(40,40,45)) draw.text((w//2-40,5),"CIE xy Gamut",fill=(200,200,200)) # sRGB sp={'R':(0.64,0.33),'G':(0.30,0.60),'B':(0.15,0.06)} spts=[xy2px(*sp['R']),xy2px(*sp['G']),xy2px(*sp['B']),xy2px(*sp['R'])] draw.line(spts,fill=(150,150,150),width=1) # Profile ppts=[xy2px(*primaries['Red']),xy2px(*primaries['Green']),xy2px(*primaries['Blue']),xy2px(*primaries['Red'])] draw.line(ppts,fill=(0,255,200),width=2) for ch,c in [('Red',(255,50,50)),('Green',(50,255,50)),('Blue',(50,50,255))]: if ch in primaries: px,py=xy2px(*primaries[ch]) draw.ellipse([px-4,py-4,px+4,py+4],fill=c) d65=xy2px(0.3127,0.329) draw.ellipse([d65[0]-3,d65[1]-3,d65[0]+3,d65[1]+3],fill=(255,255,255)) draw.text((d65[0]+6,d65[1]-5),"D65",fill=(200,200,200)) return np.array(img) # ============================================================ # 슀크린캑처 JavaScript # ============================================================ CAPTURE_JS = """ async (current_val) => { try { const stream = await navigator.mediaDevices.getDisplayMedia({ video: { displaySurface: 'monitor' }, preferCurrentTab: false }); const video = document.createElement('video'); video.srcObject = stream; video.muted = true; await video.play(); await new Promise(r => setTimeout(r, 300)); const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; canvas.getContext('2d').drawImage(video, 0, 0); stream.getTracks().forEach(t => t.stop()); video.remove(); const dataUrl = canvas.toDataURL('image/png'); return dataUrl; } catch(e) { console.error('Screen capture error:', e); return null; } } """ FULLSCREEN_JS = """ () => { const imgs = document.querySelectorAll('#pattern-preview img'); if (imgs.length > 0) { const img = imgs[imgs.length - 1]; if (img.requestFullscreen) img.requestFullscreen(); else if (img.webkitRequestFullscreen) img.webkitRequestFullscreen(); } return []; } """ # ============================================================ # Gradio UI # ============================================================ def build_app(): with gr.Blocks( title="λͺ¨λ‹ˆν„° μΊ˜λ¦¬λΈŒλ ˆμ΄μ…˜ v3", theme=gr.themes.Soft(), css=""" .result-box { font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.35; } .big-btn { min-height: 52px !important; font-size: 16px !important; } .capture-btn { min-height: 56px !important; font-size: 18px !important; background: #2196F3 !important; } """ ) as app: # ── 곡유 State ── ref_raw_state = gr.State(None) # 원본 κΈ°μ€€ 이미지 (마컀 μ—†λŠ”) ref_inner_size_state = gr.State(None) # 원본 크기 (w, h) pattern_type_state = gr.State("ColorChecker 24색") gr.Markdown("# πŸ–₯️ λͺ¨λ‹ˆν„° νŒ¨λ„ μΊ˜λ¦¬λΈŒλ ˆμ΄μ…˜ 계츑 μ‹œμŠ€ν…œ v3") gr.Markdown("**원클릭 μžλ™ 캑처** β”‚ 마컀 μžλ™ 감지 β”‚ ROI μΆ”μΆœ β”‚ Delta E 뢄석 β”‚ ICC ν”„λ‘œνŒŒμΌ") # ━━━━━ νƒ­ 1: κΈ°μ€€ νŒ¨ν„΄ 생성 ━━━━━ with gr.Tab("β‘  κΈ°μ€€ νŒ¨ν„΄ 생성"): gr.Markdown(""" ### πŸ“ κΈ°μ€€ ν…ŒμŠ€νŠΈ νŒ¨ν„΄ 생성 νŒ¨ν„΄ 선택 β†’ 생성 β†’ **전체화면** ν‘œμ‹œ β†’ **β‘‘νƒ­μ—μ„œ 캑처** (μžλ™ 연동) > πŸ’‘ 4색 μ½”λ„ˆ λ§ˆμ»€κ°€ μžλ™ μ‚½μž…λ˜μ–΄ 캑처 μ‹œ νŒ¨ν„΄ μ˜μ—­μ„ μžλ™ μΈμ‹ν•©λ‹ˆλ‹€ """) with gr.Row(): with gr.Column(scale=1): pattern_select = gr.Dropdown( choices=list(PATTERN_GENERATORS.keys()), value="ColorChecker 24색", label="νŒ¨ν„΄ μœ ν˜•" ) gen_btn = gr.Button("🎨 κΈ°μ€€ νŒ¨ν„΄ 생성", variant="primary", elem_classes=["big-btn"]) fullscreen_btn = gr.Button("πŸ”² 전체화면 미리보기", elem_classes=["big-btn"]) download_file = gr.File(label="πŸ“₯ PNG λ‹€μš΄λ‘œλ“œ") status_text = gr.Textbox(label="μƒνƒœ", interactive=False) with gr.Column(scale=2): pattern_preview = gr.Image(label="κΈ°μ€€ νŒ¨ν„΄ (마컀 ν”„λ ˆμž„ 포함)", type="numpy", elem_id="pattern-preview") def on_generate(ptype): framed, path, raw, inner_sz, msg = create_framed_pattern(ptype) return framed, path, raw, inner_sz, ptype, msg gen_btn.click( on_generate, inputs=[pattern_select], outputs=[pattern_preview, download_file, ref_raw_state, ref_inner_size_state, pattern_type_state, status_text] ) fullscreen_btn.click(fn=None, inputs=[], outputs=[], js=FULLSCREEN_JS) # ━━━━━ νƒ­ 2: μžλ™ 캑처 & 뢄석 ━━━━━ with gr.Tab("β‘‘ μžλ™ 캑처 & 뢄석"): gr.Markdown(""" ### πŸ“Έ 원클릭 슀크린캑처 β†’ μžλ™ 뢄석 **μ‚¬μš©λ²•:** β‘ νƒ­μ—μ„œ νŒ¨ν„΄ 생성 β†’ 전체화면 ν‘œμ‹œ β†’ μ•„λž˜ **πŸ“Έ ν™”λ©΄ 캑처** 클릭 > 마컀 ν”„λ ˆμž„μ„ μžλ™ κ°μ§€ν•˜μ—¬ νŒ¨ν„΄ μ˜μ—­λ§Œ μΆ”μΆœ β†’ 크기 보정 β†’ Delta E 뢄석 """) with gr.Row(): with gr.Column(scale=1): gr.Markdown("#### κΈ°μ€€ 이미지 (μžλ™ 연동)") ref_display = gr.Image(label="κΈ°μ€€ νŒ¨ν„΄", type="numpy", interactive=False, height=250) gr.Markdown("---") # 캑처 base64 μˆ˜μ‹ μš© (μˆ¨κΉ€) capture_base64 = gr.Textbox(visible=False, elem_id="capture-data") capture_btn = gr.Button("πŸ“Έ ν™”λ©΄ 캑처 (μžλ™)", variant="primary", elem_classes=["capture-btn"]) gr.Markdown("λ˜λŠ”") manual_upload = gr.Image(label="πŸ“ 캑처 이미지 μˆ˜λ™ μ—…λ‘œλ“œ", type="numpy", height=200) manual_btn = gr.Button("πŸ”¬ μˆ˜λ™ μ—…λ‘œλ“œ 뢄석", variant="secondary") detect_log = gr.Textbox(label="마컀 감지 둜그", lines=5, interactive=False) with gr.Column(scale=2): captured_display = gr.Image(label="κ°μ§€λœ νŒ¨ν„΄ μ˜μ—­ (ROI)", type="numpy") heatmap_display = gr.Image(label="πŸ—ΊοΈ Delta E 히트맡 (초둝=μ •ν™•, λΉ¨κ°•=λΆ€μ •ν™•)", type="numpy") comparison_display = gr.Image(label="κΈ°μ€€(쒌) vs 캑처(우)", type="numpy") analysis_report = gr.Textbox(label="πŸ“Š 뢄석 리포트", lines=38, elem_classes=["result-box"]) # νƒ­ μ „ν™˜ μ‹œ κΈ°μ€€ 이미지 μžλ™ ν‘œμ‹œ def refresh_ref(raw): return raw ref_raw_state.change(refresh_ref, inputs=[ref_raw_state], outputs=[ref_display]) # μžλ™ 캑처: JS β†’ base64 β†’ 뢄석 capture_btn.click( fn=None, inputs=[capture_base64], outputs=[capture_base64], js=CAPTURE_JS ) capture_base64.change( run_full_analysis, inputs=[ref_raw_state, ref_inner_size_state, capture_base64, pattern_type_state], outputs=[captured_display, heatmap_display, analysis_report, comparison_display, detect_log] ) # μˆ˜λ™ μ—…λ‘œλ“œ 뢄석 manual_btn.click( process_uploaded_capture, inputs=[ref_raw_state, ref_inner_size_state, manual_upload, pattern_type_state], outputs=[captured_display, heatmap_display, analysis_report, comparison_display, detect_log] ) # ━━━━━ νƒ­ 3: ICC ν”„λ‘œνŒŒμΌ ━━━━━ with gr.Tab("β‘’ ICC ν”„λ‘œνŒŒμΌ"): gr.Markdown(""" ### πŸ“„ ICC ν”„λ‘œνŒŒμΌ 뢄석 λͺ¨λ‹ˆν„° ICC/ICM 파일의 색역, 감마, ν™”μ΄νŠΈν¬μΈνŠΈλ₯Ό νŒŒμ‹±ν•©λ‹ˆλ‹€. **파일 μœ„μΉ˜:** Win: `C:\\Windows\\System32\\spool\\drivers\\color\\` β”‚ Mac: μ‹œμŠ€ν…œμ„€μ •β†’λ””μŠ€ν”Œλ ˆμ΄β†’μƒ‰μƒ """) with gr.Row(): with gr.Column(): icc_file = gr.File(label="ICC/ICM μ—…λ‘œλ“œ", file_types=[".icc",".icm"]) icc_btn = gr.Button("πŸ” 뢄석", variant="primary", elem_classes=["big-btn"]) with gr.Column(): trc_img = gr.Image(label="TRC 감마 곑선", type="numpy") gamut_img = gr.Image(label="CIE xy 색역", type="numpy") icc_report = gr.Textbox(label="ICC 리포트", lines=30, elem_classes=["result-box"]) icc_btn.click(parse_icc_profile, inputs=[icc_file], outputs=[icc_report, trc_img, gamut_img]) # ━━━━━ νƒ­ 4: κ°€μ΄λ“œ ━━━━━ with gr.Tab("ℹ️ κ°€μ΄λ“œ"): gr.Markdown(""" ### πŸ“– μž‘λ™ 원리 ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β‘  κΈ°μ€€ νŒ¨ν„΄ 생성 (RGBκ°’ ν™•μ •) + 마컀 ν”„λ ˆμž„ μžλ™ μ‚½μž… β”‚ β”‚ ↓ β”‚ β”‚ λͺ¨λ‹ˆν„°μ— 전체화면 ν‘œμ‹œ (OS 색상관리 ICC νŒŒμ΄ν”„λΌμΈ 톡과) β”‚ β”‚ ↓ β”‚ β”‚ β‘‘ [πŸ“Έ ν™”λ©΄ 캑처] 클릭 β†’ getDisplayMedia() μžλ™ 슀크린캑처 β”‚ β”‚ ↓ β”‚ β”‚ 마컀 μžλ™ 감지 β†’ νŒ¨ν„΄ ROI μΆ”μΆœ β†’ κΈ°μ€€ 크기둜 λ¦¬μ‚¬μ΄μ¦ˆ β”‚ β”‚ ↓ β”‚ β”‚ κΈ°μ€€ RGB vs 캑처 RGB β†’ CIEDE2000 β†’ 히트맡 + 리포트 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### 🎯 마컀 μ‹œμŠ€ν…œ 4색 μ½”λ„ˆ 마컀 + μ˜€λ Œμ§€ ν…Œλ‘λ¦¬λ‘œ 캑처 μ΄λ―Έμ§€μ—μ„œ νŒ¨ν„΄ μ˜μ—­μ„ μžλ™ μΈμ‹ν•©λ‹ˆλ‹€. - μ’Œμƒ: λ‘œμ¦ˆν•‘ν¬ (255,0,128) - μš°μƒ: μŠ€ν”„λ§κ·Έλ¦° (0,255,128) - μ’Œν•˜: λ°”μ΄μ˜¬λ › (128,0,255) - μš°ν•˜: 도저블루 (0,128,255) - ν…Œλ‘λ¦¬: μ˜€λ Œμ§€ (255,128,0) λΈŒλΌμš°μ € UI, μž‘μ—…ν‘œμ‹œμ€„ 등이 ν¬ν•¨λ˜μ–΄λ„ 마컀 기반으둜 νŒ¨ν„΄λ§Œ μΆ”μΆœν•©λ‹ˆλ‹€. ### ⚠️ μ£Όμ˜μ‚¬ν•­ - μŠ€ν¬λ¦°μΊ‘μ²˜λŠ” **OS 색상관리 ν›„** ν”„λ ˆμž„λ²„νΌλ₯Ό 캑처 (λͺ¨λ‹ˆν„° 물리좜λ ₯κ³ΌλŠ” 닀름) - ν•˜λ“œμ›¨μ–΄ 계츑기(i1Display, SpyderX)μ™€λŠ” μΈ‘μ • λŒ€μƒμ΄ 닀름 - ICC ν”„λ‘œνŒŒμΌμ΄ λΉ„ν™œμ„±ν™”λœ 경우 κΈ°μ€€=캑처 β†’ Ξ”Eβ‰ˆ0 (정상) - PNG 무손싀 ν˜•μ‹ μ‚¬μš© ν•„μˆ˜ (JPEG μ••μΆ• μ•„ν‹°νŒ©νŠΈ λ°©μ§€) ### πŸ“ νŒμ • κΈ°μ€€ | Ξ”E2000 | νŒμ • | 의미 | |--------|------|------| | < 1.0 | β—Ž μ™„λ²½ | μœ‘μ•ˆ ꡬ뢄 λΆˆκ°€ | | < 2.0 | β—‹ 우수 | κ·Όμ ‘ 비ꡐ μ‹œ ꡬ뢄 | | < 3.0 | β–³ μ–‘ν˜Έ | 주의 깊게 보면 ꡬ뢄 | | < 5.0 | β–½ 보톡 | λͺ…ν™•νžˆ ꡬ뢄 κ°€λŠ₯ | | β‰₯ 5.0 | βœ• λΆˆλŸ‰ | λšœλ ·ν•œ 색차 | """) gr.Markdown("---") gr.Markdown("*Monitor Panel Calibration v3 β”‚ Auto Capture + Marker Detection + ICC*") return app if __name__ == "__main__": app = build_app() app.launch()