| | """ |
| | π₯οΈ λͺ¨λν° ν¨λ μΊλ¦¬λΈλ μ΄μ
κ³μΈ‘ μμ€ν
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 |
| | BORDER_WIDTH = 8 |
| | 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 |
| | |
| | |
| | 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] |
| | |
| | |
| | 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']}") |
| | |
| | |
| | if 'TL' in markers_found and 'BR' in markers_found: |
| | tl_bbox = markers_found['TL']['bbox'] |
| | br_bbox = markers_found['BR']['bbox'] |
| | |
| | |
| | roi_x1 = tl_bbox[2] + 1 |
| | roi_y1 = tl_bbox[3] + 1 |
| | roi_x2 = br_bbox[0] - 1 |
| | roi_y2 = br_bbox[1] - 1 |
| | |
| | info_lines.append(f" μ λ΅: TL+BR μ½λ μ") |
| | |
| | |
| | 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" μ λ΅: 볡μ λ§μ»€ μΈκ³½μ κΈ°λ°") |
| | |
| | |
| | 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: |
| | |
| | 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 = { |
| | "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) |
| | |
| | |
| | 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) |
| | |
| | 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 |
| | |
| | |
| | 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] |
| | |
| | |
| | roi, detect_info = detect_pattern_roi(cap_array) |
| | |
| | if roi is None: |
| | |
| | roi = cap_array |
| | detect_info += "\nβ οΈ λ§μ»€ λ―Έκ°μ§ β μ 체 μΊ‘μ²λ₯Ό λΆμμ μ¬μ©" |
| | |
| | |
| | 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) |
| | |
| | |
| | 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("") |
| | |
| | |
| | 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, "" |
| | |
| | |
| | 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) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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}%") |
| | |
| | |
| | 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}") |
| | |
| | |
| | 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_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)) |
| | |
| | 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)) |
| | |
| | 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) |
| | |
| | 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) |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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 []; |
| | } |
| | """ |
| |
|
| |
|
| | |
| | |
| | |
| |
|
| | 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: |
| | |
| | |
| | ref_raw_state = gr.State(None) |
| | ref_inner_size_state = gr.State(None) |
| | pattern_type_state = gr.State("ColorChecker 24μ") |
| | |
| | gr.Markdown("# π₯οΈ λͺ¨λν° ν¨λ μΊλ¦¬λΈλ μ΄μ
κ³μΈ‘ μμ€ν
v3") |
| | gr.Markdown("**μν΄λ¦ μλ μΊ‘μ²** β λ§μ»€ μλ κ°μ§ β ROI μΆμΆ β Delta E λΆμ β ICC νλ‘νμΌ") |
| | |
| | |
| | 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) |
| | |
| | |
| | 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("---") |
| | |
| | |
| | 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]) |
| | |
| | |
| | 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] |
| | ) |
| | |
| | |
| | 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]) |
| | |
| | |
| | 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() |