AI 아이콘의 마젠타 배경을 지우면 테두리에 왜 분홍이 남나
외곽선 쪽으로 투영해 알파를 구한다
테두리 픽셀은 마젠타와 외곽선 색이 섞인 색이라, RGB 공간에서 둘을 잇는 직선의 몇 % 지점에 있는지가 곧 알파입니다.
아래 unkey는 그 직선 위 비율을 재고, 직선에서 벗어난 칠 면만 배경색과의 RGB 거리로 알파를 정합니다.
import numpy as npfrom PIL import Imagedef unkey(cell, line=(242, 240, 234)): # 격자 한 칸(float 배열)과 외곽선 색 key = np.median(np.r_[cell[0], cell[-1], cell[:, 0], cell[:, -1]], axis=0) d, v = cell - key, np.array(line) - key n = np.linalg.norm(d, axis=2) # 배경과의 RGB 거리 proj = (d @ v) / (v @ v) # 배경에서 외곽선 쪽으로 간 비율 off = np.linalg.norm(d - np.clip(proj, 0, 1)[..., None] * v, axis=2) > 24 low = np.where(key > 127, 255 - cell, cell).max(2) / 255 # 색역 하한 t = np.clip(np.maximum(np.where(off, n / 90 - 1, proj), low), 0, 1) fg = key + d / np.maximum(t, 1e-3)[..., None] # 배경 몫을 뺀 색 a = np.where((n < 24) | (t < 0.1), 0, t) return np.dstack([fg.clip(0, 255), a * 255]).astype(np.uint8)grid = np.asarray(Image.open("grid.png").convert("RGB"), float) # 1024px 2x2 격자icon = Image.fromarray(unkey(grid[:512, :512])) # 왼쪽 위 칸icon.resize((256, 256), Image.Resampling.BOX).save("icon.png")외곽선 색은 프롬프트에서 하나로 정해 line에 넣습니다.
Gemini API로 격자를 뽑고 칸을 모두 자르려면
Gemini API 무료 등급에는 이미지 모델이 없습니다. 가격표에서 이미지 모델의 무료 등급 칸은 모두 Not available이고, 결제를 연결하지 않은 키로 부르면 429 RESOURCE_EXHAUSTED가 돌아옵니다.
| 모델 | 1K 이미지 한 장 |
|---|---|
| gemini-3.1-flash-lite-image | $0.0336 |
| gemini-3.1-flash-image | $0.067 |
| gemini-3-pro-image | $0.134 |
프롬프트에는 배경과 외곽선 색을 숫자로 적고, 아이콘 안에 분홍·보라를 쓰지 말라고 적습니다. 외곽선이 숫자와 조금 다르게 나와도 됩니다. 구슬 외곽선이 (230,226,214)나 (225,225,225)로 나와도 line 기본값 그대로 틀린 픽셀이 없고, (250,246,236)처럼 더 밝게 나오면 어두운 바탕에서 152픽셀이 틀립니다.
A 2x2 grid of four game icons in one consistent style:thick cream #F2F0EA outlines, flat fills, readable at 40px.Solid #FF00FF magenta background in every cell, no text, no frames,one subject per cell, centered. No pink or purple inside the icons.1 (top-left): ... 2 (top-right): ... 3 (bottom-left): ... 4 (bottom-right): ...칸을 모두 자를 때는 위 코드의 마지막 세 줄 대신 아래를 붙여 python cut.py grid.png 2 2 orb,shield,bolt,empty로 돌립니다. 빈 칸은 건너뛰고, 칸마다 그림 둘레를 잘라 정사각형으로 맞춘 뒤 256px로 저장합니다.
import syspath, rows, cols, names = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), sys.argv[4].split(",")grid = np.asarray(Image.open(path).convert("RGB"), float)h, w = grid.shape[0] // rows, grid.shape[1] // colsfor i, name in enumerate(names): r, c = divmod(i, cols) icon = Image.fromarray(unkey(grid[r * h:(r + 1) * h, c * w:(c + 1) * w])) box = icon.getbbox() if not box: print("빈 칸:", name) continue icon = icon.crop(box) side = int(max(icon.size) * 1.08) # 둘레에 여백 조금 sq = Image.new("RGBA", (side, side)) sq.paste(icon, ((side - icon.width) // 2, (side - icon.height) // 2)) # BOX 는 늘릴 때 NEAREST 와 같아서, 칸이 256보다 작으면 BICUBIC 으로 늘린다 flt = Image.Resampling.BOX if side >= 256 else Image.Resampling.BICUBIC sq.resize((256, 256), flt).save(name + ".png")RGB 거리로는 섞인 비율을 못 구한다
테두리 픽셀 하나에서 아는 값은 R·G·B 셋인데 모르는 값은 알파와 원래 색의 R·G·B, 모두 넷이라 RGB 거리만으로는 알파가 정해지지 않습니다. 반투명 픽셀의 색은 알파 합성 식 그대로 알파만큼의 원래 색에 나머지만큼의 배경색을 더한 값이고, 더해진 배경색 쪽이 배경 몫입니다. 원래 색을 따로 알아야 풀리는 식이라, 외곽선 색을 알면 미지수가 알파 하나로 줄어듭니다.
구슬 위쪽 테두리를 키운 그림에서 네모 칸 세 픽셀의 실제 알파는 0.50·0.69·0.87입니다.
아래 그래프의 RGB 거리 규칙은 거리 90 밑을 투명, 180 위를 불투명으로 보고 그 사이를 고르게 나눕니다. 실제 알파는 마젠타부터 외곽선까지 고르게 오르는데, 이 규칙은 그 오르막을 좁은 구간에 몰아 읽습니다.
RGB 거리로 읽은 알파는 0.6 아래에서 작아 초록, 위에서 커서 분홍이 된다
작게 읽은 0.50 픽셀은 배경 몫을 너무 빼서 초록으로 넘치고, 크게 읽은 두 픽셀은 마젠타가 덜 빠져 분홍으로 남습니다.
투영과 RGB 거리 중 큰 값을 쓰면 0.69·0.87 픽셀에서 거리 쪽이 이겨 분홍이 남고, 투영만 쓰면 파랑 칠 면까지 섞인 색으로 읽어 반투명 청록이 됩니다. 이 코드가 직선 위 픽셀에만 투영을 쓰는 까닭입니다.
| 분홍 남은 픽셀 | 칠 면 | |
|---|---|---|
| RGB 거리 | 1,838 | 그대로 |
| RGB 거리 + 배경 빼기 | 995 | 그대로 |
| 투영·거리 큰 값 | 995 | 그대로 |
| 투영만 | 0 | 반투명 청록 |
| 이 코드 | 0 | 그대로 |
외곽선이 없거나 그림자·분홍 면이 든 아이콘이라면
- 배경 몫을 뺀 색이 0~255 안에 들려면 알파가 적어도
max(G, 255 - R, 255 - B) / 255여야 합니다. 코드의low가 이 최솟값인 색역 하한입니다. 칠 면이 마젠타에 닿는 경계에서 알파를 받치고, 빼면 G가 넘쳐 초록이 됩니다. - 외곽선 없는 파랑 원은 테두리 알파 오차가 평균 0.10 남고,
line에 칠 색을 넣으면 0.01이 됩니다. 칠 색이 여럿이면 프롬프트에서 외곽선을 그리게 합니다. - 반투명 검정 그림자는 직선 밖이라 RGB 거리로 읽혀 알파 0.35가 0.41이 되고, 흰 바탕에서 회색 (165,165,165)이 옅은 분홍 (165,150,165)으로 보입니다.
t =줄을 아래 두 줄로 바꿔 마젠타와 검정을 잇는 직선 위에서는 색역 하한만 쓰면 거의 사라집니다.
blk = np.linalg.norm(d + np.clip((d @ -key) / (key @ key), 0, 1)[..., None] * key, axis=2) < 24t = np.clip(np.maximum(np.where(off, np.where(blk, low, n / 90 - 1), proj), low), 0, 1)- 아이콘에 분홍·보라 면이 있으면 그 면을 배경과 섞인 색으로 읽어 반투명 연두로 바꿉니다. 초록
#00FF00배경으로 뽑으면 같은 코드로 틀린 픽셀이 없습니다. - 배경이 고르지 않으면
n < 24의 24를 키웁니다. 배경이 픽셀마다 6 안팎으로 흔들리면 24에서는 분홍 223픽셀이 남고, 32로 키우면 21픽셀로 줄어듭니다.
LANCZOS는 테두리 값을 넘치게 한다
줄일 때 LANCZOS를 쓰면 외곽선의 바깥쪽 줄이 원래보다 밝아지고, BOX를 쓰면 그대로입니다. 구슬을 256px로 줄이면 136픽셀이 흰색에 가깝게 밝아집니다.
LANCZOS가 값을 넘치게 하는 까닭이 궁금하다면
Pillow는 RGBA를 줄일 때 색에 알파를 곱한 값으로 바꿔 채널마다 따로 섞습니다. LANCZOS 가중치는 sinc 둘을 곱한 모양이라 중심 옆에 음수 구간이 있어, 경계 근처 값이 원래 범위를 넘습니다. 투명 쪽에 닿은 외곽선 줄에서는 알파가 1을 넘었다가 1로 잘리고 같이 커진 색은 남아서, 크림 (242,240,234)이 (255,255,251) 안팎으로 밝아집니다. BOX는 칸 안 픽셀을 같은 가중치로 평균만 내서 원래 범위를 넘지 않습니다.
같은 구슬에서 알파가 128 밑인 둘레 띠는 BOX의 세 배로 넓어지고, 그 안 124픽셀이 원본에 없던 색을 띱니다. 알파가 4 이하라 어두운 바탕에서는 거의 보이지 않습니다. 늘릴 때는 BOX가 NEAREST와 같아지니 BICUBIC으로 늘립니다.