calib / app.py
mayafree's picture
Update app.py
d796076 verified
"""
πŸ–₯️ λͺ¨λ‹ˆν„° νŒ¨λ„ μΊ˜λ¦¬λΈŒλ ˆμ΄μ…˜ 계츑 μ‹œμŠ€ν…œ 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()