
ãAIã§RPGéçºãAIçæããRPGãã¯ãŒã«çšã®æ©è¡CGãæå®ãµã€ãºã«è£æ£å€æããïŒç¬¬1話ïŒ
ãŸãã¯ChatGPTã§ç»åçæ
RPGãã¯ãŒã«MZçšã®æ©è¡ã°ã©ãã£ãã¯ãChatGPTã«äœã£ãŠããããŸãããããã³ããã¯ã
RPGãã¯ãŒã«MZã§äœ¿ãããã®ãã£ã©ã¯ã¿ãŒã®æ©è¡ãã¿ãŒã³ã®ç»åãçæããŠãã ããã åºæ¬ãšãªãïŒãã¿ãŒã³ã¯48x48ãããã§ã
äžA äžB äžC
å·ŠA å·ŠB å·ŠC
å³A å³B å³C
äžA äžB äžC
ãšããåãã®ç»åã3x4ã«äžŠã¹ãã®ã§ã144x192ãããã«ãªããŸããBã¯æ°ãä»ããããããŒãºã§ãAã¯å³è¶³ãåã«åºããç¶æ ãCã¯å·Šè¶³ãåã«åºããç¶æ ã«ãªããŸããAãšCãåãæ¿ããŠè¡šç€ºããããšã§æ©ããŠããããã«èŠãããã®ã§ãã
çæããç»åã¯ãæ±æ¹Projectã®åéºé倢ã®ãã£ã©ã¯ã¿ãŒãïŒé 身ã§è¡šçŸããŠãã ãããæã«ã¯ç¹ã«äœãæãããªãã§ãã ããã
ãšããæãã§ããçæãããç»åã¯ä»¥äžã®ãã®ã«ãªããŸããã

ããããªããªãããæãã§ããã»ã»ã»ãšæã£ãã®ã§ãããåºåãããç»åã®è§£å床ã1086x1448ã§ããã144x192ãšæç€ºããŠããã®ã§ãããããã«ã¯åŸã£ãŠãããªãããã§ããã
ãšããããçž®å°ããŠã¿ã
ãšãããããPhotoshopã§çž®å°ããã®ã§ããã以äžã®ããã«ãã±ãŠããŸããŸããã

ãããªã£ãŠããŸãçç±ãšããŠã¯ã

æ¡å€§ããŠèŠãŠã¿ããšãïŒãããã®ãã¯ã»ã«ãµã€ãºãåäžã§ãªãããŸãã°ãªããå¢çã«äžèŽããŠããªãã®ãåå ã§ãã
Pixel Snapperã䜿ã£ãŠã¿ã
ããã§ãPixel Snapperã䜿ã£ãŠã¿ãããšã«ããŸããã
ããããåºåããããµã€ãºã¯ã206x270ã§ã

RPGãã¯ãŒã«ã§èŠæ±ããããµã€ãºãšã¯äžèŽããªãã®ã§ãããã§ã¯äœ¿ããŸããã
ãã£ãããŒå©ããŠïŒ
ããã§ãCodex GPT-6.1 Solã«ã
ããã¯ã»ã«ãµã€ãºãäžåäžããã°ãªãããããºã¬ãŠããããäžéè²ãæ··ããããã¢ã³ããšã€ãªã¢ã¹ãå ¥ãããªã©ã®åé¡ç¹ã解決ãããã€æå®ã®è§£å床ãžçž®å°ãããçž®å°ã«ãã£ãŠå€±ãããå€åšã®èŒªéç·ã远å ããã
ãšããæ©èœãæã£ãPythonã³ãŒããäœã£ãŠããããŸãããã³ãŒãã¯æåŸã«è²ŒããŸãã
çµæã

ãã®ããã«ãæå®ãã144x192ãµã€ãºã§ããã¯ã»ã«ããã±ãªãç»åçž®å°ãåºæ¥ãŸããã
æä¿®æ£ãã
ãã ããŸãããã§ãã¡ãã£ãšããããæ±ãã®ã§ãããããšãã£ã¿ã§å°ãèªåã§ä¿®æ£ããŠã¿ãŸããã

ããŒãããã£ã±ããããæã€ã®ã£ãŠè·äººæãèŠããŸãããçŽ äººã®åã§ã¯ãªããªãããæãã«ä¿®æ£ããã®ãé£ããã§ãã
Pythonã³ãŒã
ä»åãã£ãããŒã«äœã£ãŠããã£ãPythonã³ãŒãã以äžã«è²Œã£ãŠãããŸãã
"""ããã絵颚ç»åãåºå®ã°ãªãããšå
±éãã¬ãããžè£æ£ããã"""
from __future__ import annotations
import argparse
from collections import deque
from pathlib import Path
import numpy as np
from PIL import Image, ImageOps
def background_color(rgba: np.ndarray) -> np.ndarray:
"""ç»åã®åé
ã®äžå€®å€ããèæ¯è²ãæšå®ããã"""
h, w = rgba.shape[:2]
corners = rgba[[0, 0, h - 1, h - 1], [0, w - 1, 0, w - 1], :3]
return np.median(corners, axis=0)
def remove_background(rgba: np.ndarray, tolerance: int) -> np.ndarray:
"""åé
ã®èæ¯è²ã«è¿ããå€åšã«ã€ãªããé åã®ã¿éæã«ããã"""
result = rgba.copy()
h, w = result.shape[:2]
background = background_color(result)
eligible = (np.max(np.abs(result[..., :3].astype(float) - background), axis=2)
<= tolerance) | (result[..., 3] == 0)
seen = np.zeros((h, w), dtype=bool)
queue = deque()
for y, x in [(0, x) for x in range(w)] + [(h - 1, x) for x in range(w)] + [(y, 0) for y in range(h)] + [(y, w - 1) for y in range(h)]:
if eligible[y, x] and not seen[y, x]:
seen[y, x] = True
queue.append((y, x))
while queue:
y, x = queue.popleft()
for yy, xx in ((y - 1, x), (y + 1, x), (y, x - 1), (y, x + 1)):
if 0 <= yy < h and 0 <= xx < w and eligible[yy, xx] and not seen[yy, xx]:
seen[yy, xx] = True
queue.append((yy, xx))
result[seen] = 0
return result
def add_outline(rgba: np.ndarray) -> np.ndarray:
"""å€åšã«ã€ãªããéæé åã«äžäžå·Šå³ã§æ¥ããäžéæç»çŽ ãé»ãããã"""
result = rgba.copy()
transparent = rgba[..., 3] == 0
h, w = transparent.shape
exterior = np.zeros((h, w), dtype=bool)
queue = deque()
for y, x in [(0, x) for x in range(w)] + [(h - 1, x) for x in range(w)] + [(y, 0) for y in range(h)] + [(y, w - 1) for y in range(h)]:
if transparent[y, x] and not exterior[y, x]:
exterior[y, x] = True
queue.append((y, x))
while queue:
y, x = queue.popleft()
for yy, xx in ((y - 1, x), (y + 1, x), (y, x - 1), (y, x + 1)):
if 0 <= yy < h and 0 <= xx < w and transparent[yy, xx] and not exterior[yy, xx]:
exterior[yy, xx] = True
queue.append((yy, xx))
adjacent = np.zeros((h, w), dtype=bool)
adjacent[1:] |= exterior[:-1]
adjacent[:-1] |= exterior[1:]
adjacent[:, 1:] |= exterior[:, :-1]
adjacent[:, :-1] |= exterior[:, 1:]
result[adjacent & ~transparent, :3] = 0
return result
def boundaries(profile: np.ndarray, length: int, count: int, snap: float) -> np.ndarray:
"""çééã°ãªããã®è¿åã§åŒ·ãè²å¢çãžåºç»å¢çãç§»åããã"""
step = length / count
edges = [0]
for i in range(1, count):
ideal = i * step
lo = max(edges[-1] + 1, int(np.ceil(ideal - step * snap)))
hi = min(length - (count - i), int(np.floor(ideal + step * snap)))
candidates = np.arange(lo, hi + 1)
if candidates.size:
scores = profile[candidates - 1] - 0.15 * np.abs(candidates - ideal) / step
edges.append(int(candidates[np.argmax(scores)]))
else:
edges.append(int(round(ideal)))
return np.array(edges + [length])
def convert(image: Image.Image, size=(144, 192), sheet=(3, 4), colors=32,
background="auto", tolerance=24, snap=0.25, alpha_threshold=128,
outline=False, frame_size=None) -> Image.Image:
"""ãã¬ãŒã ãåå¥ã«è£æ£ããäºå€ã¢ã«ãã¡ã®RGBAç»åãè¿ãã"""
columns, rows = sheet
if frame_size is not None:
if min(frame_size) <= 0:
raise ValueError("ãã¬ãŒã ã®å¹
ã»é«ãã¯æ£ã®æŽæ°ã«ããŠãã ãã")
size = (frame_size[0] * columns, frame_size[1] * rows)
width, height = size
if min(width, height, columns, rows) <= 0 or width % columns or height % rows:
raise ValueError("åºåãµã€ãºã¯ã·ãŒãã®åæ°ã»è¡æ°ã§å²ãåããæ£ã®æŽæ°ã«ããŠãã ãã")
if not 2 <= colors <= 256 or not 0 <= snap <= 0.45:
raise ValueError("è²æ°ã¯2ïœ256ãè£æ£åŒ·åºŠã¯0ïœ0.45ã«ããŠãã ãã")
if not 0 <= tolerance <= 255 or not 1 <= alpha_threshold <= 255:
raise ValueError("èæ¯èš±å®¹å·®ã¯0ïœ255ãã¢ã«ãã¡éŸå€ã¯1ïœ255ã«ããŠãã ãã")
if background not in ("auto", "keep"):
raise ValueError("èæ¯ã¢ãŒãã¯autoãŸãã¯keepã«ããŠãã ãã")
rgba = np.array(ImageOps.exif_transpose(image).convert("RGBA"))
ih, iw = rgba.shape[:2]
if iw < width or ih < height:
raise ValueError("å
¥åç»åã¯åºåãµã€ãºä»¥äžã®è§£å床ãå¿
èŠã§ã")
if background == "auto":
rgba = remove_background(rgba, tolerance)
opaque = rgba[..., 3] >= alpha_threshold
output = np.zeros((height, width, 4), dtype=np.uint8)
if not opaque.any():
return Image.fromarray(output)
# éæé åãé€ããè²ããå
šãã¬ãŒã å
±éã®ãã¬ãããäœãã
samples = rgba[..., :3][opaque]
training = Image.fromarray(samples.reshape(1, -1, 3))
palette_image = training.quantize(colors=colors, method=Image.Quantize.MEDIANCUT,
dither=Image.Dither.NONE)
mapped = Image.fromarray(rgba[..., :3]).quantize(palette=palette_image, dither=Image.Dither.NONE)
labels = np.array(mapped).astype(np.int16)
labels[~opaque] = 256
palette = np.array(palette_image.getpalette(), dtype=np.uint8).reshape(-1, 3)
fw, fh = width // columns, height // rows
for row in range(rows):
for col in range(columns):
x0, x1 = round(col * iw / columns), round((col + 1) * iw / columns)
y0, y1 = round(row * ih / rows), round((row + 1) * ih / rows)
frame = labels[y0:y1, x0:x1]
xp = np.mean(frame[:, 1:] != frame[:, :-1], axis=0)
yp = np.mean(frame[1:, :] != frame[:-1, :], axis=1)
xs = boundaries(xp, frame.shape[1], fw, snap)
ys = boundaries(yp, frame.shape[0], fh, snap)
for y in range(fh):
for x in range(fw):
block = frame[ys[y]:ys[y + 1], xs[x]:xs[x + 1]]
# äžå€®ãéãæ±ããå¢çã®æ··è²ã代衚è²ãžæã¡èŸŒãŸãªãã
by, bx = np.indices(block.shape)
weights = 1 + 2 * (1 - np.abs((bx + .5) / block.shape[1] * 2 - 1)) * (1 - np.abs((by + .5) / block.shape[0] * 2 - 1))
label = int(np.argmax(np.bincount(block.ravel(), weights=weights.ravel(), minlength=257)))
if label != 256:
output[row * fh + y, col * fw + x] = (*palette[label], 255)
if outline:
# åãã¬ãŒã å
ã§å€å®ããé£ã®ãã¬ãŒã ã®éæç»çŽ ãåç
§ããªãã
region = output[row * fh:(row + 1) * fh, col * fw:(col + 1) * fw]
region[:] = add_outline(region)
return Image.fromarray(output, "RGBA")
def indexed_png(image: Image.Image) -> Image.Image:
"""éæè²ã確ä¿ããæå€§256è²ã®ãã¬ããç»åãžå€æããã"""
rgba = np.array(image.convert("RGBA"))
opaque = rgba[..., 3] != 0
offset = int(not opaque.all())
budget = 256 - offset
indices = np.zeros(opaque.shape, dtype=np.uint8)
palette = [0] * 768
if opaque.any():
samples = rgba[..., :3][opaque]
unique, inverse = np.unique(samples, axis=0, return_inverse=True)
if len(unique) <= budget:
indices[opaque] = inverse + offset
palette[offset * 3:(offset + len(unique)) * 3] = unique.ravel().tolist()
else:
# éæè²ãå«ããŠ256è²ãè¶
ããå Žåã ã远å ã§æžè²ããã
quantized = Image.fromarray(samples.reshape(1, -1, 3)).quantize(
colors=budget, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE)
indices[opaque] = np.array(quantized).ravel().astype(np.int16) + offset
palette[offset * 3:] = quantized.getpalette()[:budget * 3]
result = Image.fromarray(indices).convert("P")
result.putpalette(palette)
if offset:
result.info["transparency"] = 0
return result
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--width", type=int, help="ã·ãŒãå
šäœã®åºåå¹
ïŒæ¢å®144ïŒ")
parser.add_argument("--height", type=int, help="ã·ãŒãå
šäœã®åºåé«ãïŒæ¢å®192ïŒ")
parser.add_argument("--columns", type=int, default=3, help="ã·ãŒãã®åæ°ïŒæ¢å®3ïŒ")
parser.add_argument("--rows", type=int, default=4, help="ã·ãŒãã®è¡æ°ïŒæ¢å®4ïŒ")
parser.add_argument("--frame-width", type=int, help="åºåãã¬ãŒã 1æã®å¹
")
parser.add_argument("--frame-height", type=int, help="åºåãã¬ãŒã 1æã®é«ã")
parser.add_argument("--alpha-map", action=argparse.BooleanOptionalAction, default=True,
help="ã¢ã«ãã¡ä»ã32bit PNGã§ä¿åããïŒæ¢å®æå¹ïŒã--no-alpha-mapã§24bit RGB")
parser.add_argument("--png8", action="store_true", help="ã¢ã«ãã¡èšå®ãåæ ãã8bitãã¬ããPNGã§ä¿åãã")
parser.add_argument("--colors", type=int, default=32)
parser.add_argument("--background", choices=("auto", "keep"), default="auto")
parser.add_argument("--tolerance", type=int, default=24)
parser.add_argument("--snap", type=float, default=.25)
parser.add_argument("--alpha-threshold", type=int, default=128)
parser.add_argument("--outline", action="store_true", help="å€åšã®éæé åã«æ¥ããå
åŽ1ç»çŽ ãé»ã茪éç·ã«ãã")
parser.add_argument("--preview", type=Path, help="æè¿åã§4忡倧ãã確èªç»åã®ä¿åå
")
args = parser.parse_args()
paths = [path.resolve() for path in (args.input, args.output, args.preview) if path is not None]
if len(paths) != len(set(paths)):
parser.error("å
¥åã»åºåã»ãã¬ãã¥ãŒã«ã¯å¥ã
ã®ãã¹ãæå®ããŠãã ãã")
frame_size = None
if args.frame_width is not None or args.frame_height is not None:
if args.frame_width is None or args.frame_height is None:
parser.error("--frame-widthãš--frame-heightã¯äž¡æ¹æå®ããŠãã ãã")
if args.width is not None or args.height is not None:
parser.error("ãã¬ãŒã ãµã€ãºãš--width/--heightã¯åæã«æå®ã§ããŸãã")
frame_size = (args.frame_width, args.frame_height)
try:
with Image.open(args.input) as source:
restore_background = not args.alpha_map and args.background == "keep" and args.outline
fill_color = None
if restore_background:
# 茪éå€å®çšã«èæ¯ãäžæçã«éæåããä¿ååã«å
ã®èæ¯è²ãžæ»ãã
fill_color = background_color(np.array(ImageOps.exif_transpose(source).convert("RGBA")))
result = convert(source, (args.width if args.width is not None else 144,
args.height if args.height is not None else 192), (args.columns, args.rows),
args.colors, "auto" if restore_background else args.background,
args.tolerance, args.snap, args.alpha_threshold,
outline=args.outline, frame_size=frame_size)
if fill_color is not None:
pixels = np.array(result)
pixels[pixels[..., 3] == 0, :3] = np.rint(fill_color).astype(np.uint8)
result = Image.fromarray(pixels, "RGBA")
args.output.parent.mkdir(parents=True, exist_ok=True)
saved = result if args.alpha_map else result.convert("RGB")
if args.png8:
saved = indexed_png(saved)
saved.save(args.output, format="PNG", **({"bits": 8} if args.png8 else {}))
if args.preview:
args.preview.parent.mkdir(parents=True, exist_ok=True)
saved.resize((result.width * 4, result.height * 4), Image.Resampling.NEAREST).save(args.preview, format="PNG")
except (OSError, ValueError) as exc:
parser.exit(1, f"ãšã©ãŒ: {exc}\n")
print(f"ä¿å: {args.output} ({result.width}x{result.height}, {args.columns}åÃ{args.rows}è¡, "
f"ãã¬ãŒã {result.width // args.columns}x{result.height // args.rows}, {saved.mode})")
if __name__ == "__main__":
main()