163 lines
7.1 KiB
Python
163 lines
7.1 KiB
Python
|
|
from collections import deque
|
|||
|
|
from pathlib import Path
|
|||
|
|
|
|||
|
|
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
|||
|
|
|
|||
|
|
|
|||
|
|
OUT = Path(r"C:\Users\tangl\Desktop\Git\agent-foundation\output\jump_rope_video")
|
|||
|
|
OUT.mkdir(parents=True, exist_ok=True)
|
|||
|
|
|
|||
|
|
W, H = 1920, 1080
|
|||
|
|
FONT_PATH = r"C:\Windows\Fonts\msyh.ttc"
|
|||
|
|
FONT_BOLD_PATH = r"C:\Windows\Fonts\msyhbd.ttc"
|
|||
|
|
|
|||
|
|
|
|||
|
|
def font(size: int, bold: bool = False):
|
|||
|
|
return ImageFont.truetype(FONT_BOLD_PATH if bold else FONT_PATH, size)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def cover(img: Image.Image, size=(W, H)) -> Image.Image:
|
|||
|
|
img = img.convert("RGB")
|
|||
|
|
ratio = max(size[0] / img.width, size[1] / img.height)
|
|||
|
|
resized = img.resize((round(img.width * ratio), round(img.height * ratio)), Image.Resampling.LANCZOS)
|
|||
|
|
x = (resized.width - size[0]) // 2
|
|||
|
|
y = (resized.height - size[1]) // 2
|
|||
|
|
return resized.crop((x, y, x + size[0], y + size[1]))
|
|||
|
|
|
|||
|
|
|
|||
|
|
def fit(img: Image.Image, size, bg=(0, 0, 0)) -> Image.Image:
|
|||
|
|
img = img.convert("RGBA")
|
|||
|
|
ratio = min(size[0] / img.width, size[1] / img.height)
|
|||
|
|
resized = img.resize((round(img.width * ratio), round(img.height * ratio)), Image.Resampling.LANCZOS)
|
|||
|
|
canvas = Image.new("RGBA", size, bg + (255,))
|
|||
|
|
canvas.alpha_composite(resized, ((size[0] - resized.width) // 2, (size[1] - resized.height) // 2))
|
|||
|
|
return canvas
|
|||
|
|
|
|||
|
|
|
|||
|
|
def vignette(img: Image.Image, strength=0.42) -> Image.Image:
|
|||
|
|
img = img.convert("RGB")
|
|||
|
|
mask = Image.new("L", (W, H), 255)
|
|||
|
|
px = mask.load()
|
|||
|
|
cx, cy = W / 2, H / 2
|
|||
|
|
maxd = (cx * cx + cy * cy) ** 0.5
|
|||
|
|
for y in range(H):
|
|||
|
|
for x in range(W):
|
|||
|
|
d = (((x - cx) ** 2 + (y - cy) ** 2) ** 0.5) / maxd
|
|||
|
|
px[x, y] = int(255 * max(1 - strength * d * d, 0.45))
|
|||
|
|
black = Image.new("RGB", (W, H), "black")
|
|||
|
|
return Image.composite(img, black, mask)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def add_caption(img: Image.Image, headline: str, subline: str, align="left") -> Image.Image:
|
|||
|
|
img = img.convert("RGBA")
|
|||
|
|
overlay = Image.new("RGBA", (W, H), (0, 0, 0, 0))
|
|||
|
|
d = ImageDraw.Draw(overlay)
|
|||
|
|
if align == "left":
|
|||
|
|
x, anchor = 118, "la"
|
|||
|
|
else:
|
|||
|
|
x, anchor = W - 118, "ra"
|
|||
|
|
y = 735
|
|||
|
|
d.rounded_rectangle((72, 690, W - 72, 970), radius=28, fill=(0, 13, 30, 92))
|
|||
|
|
d.text((x, y), headline, font=font(64, True), fill=(232, 241, 62, 255), anchor=anchor)
|
|||
|
|
d.text((x, y + 88), subline, font=font(36), fill=(242, 247, 250, 245), anchor=anchor)
|
|||
|
|
d.line((x if align == "left" else x - 480, y - 30, x + 480 if align == "left" else x, y - 30), fill=(232, 241, 62, 220), width=4)
|
|||
|
|
return Image.alpha_composite(img, overlay).convert("RGB")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def remove_connected_background(img: Image.Image) -> Image.Image:
|
|||
|
|
rgba = img.convert("RGBA")
|
|||
|
|
rgb = rgba.convert("RGB")
|
|||
|
|
pix = rgb.load()
|
|||
|
|
alpha = Image.new("L", rgb.size, 255)
|
|||
|
|
apix = alpha.load()
|
|||
|
|
seen = bytearray(rgb.width * rgb.height)
|
|||
|
|
q = deque()
|
|||
|
|
|
|||
|
|
def bg_like(x, y):
|
|||
|
|
r, g, b = pix[x, y]
|
|||
|
|
return min(r, g, b) > 150 and max(r, g, b) - min(r, g, b) < 34
|
|||
|
|
|
|||
|
|
for x in range(rgb.width):
|
|||
|
|
q.append((x, 0)); q.append((x, rgb.height - 1))
|
|||
|
|
for y in range(rgb.height):
|
|||
|
|
q.append((0, y)); q.append((rgb.width - 1, y))
|
|||
|
|
while q:
|
|||
|
|
x, y = q.popleft()
|
|||
|
|
idx = y * rgb.width + x
|
|||
|
|
if seen[idx]:
|
|||
|
|
continue
|
|||
|
|
seen[idx] = 1
|
|||
|
|
if not bg_like(x, y):
|
|||
|
|
continue
|
|||
|
|
apix[x, y] = 0
|
|||
|
|
if x: q.append((x - 1, y))
|
|||
|
|
if x + 1 < rgb.width: q.append((x + 1, y))
|
|||
|
|
if y: q.append((x, y - 1))
|
|||
|
|
if y + 1 < rgb.height: q.append((x, y + 1))
|
|||
|
|
alpha = alpha.filter(ImageFilter.GaussianBlur(1.2))
|
|||
|
|
rgba.putalpha(alpha)
|
|||
|
|
bbox = alpha.getbbox()
|
|||
|
|
return rgba.crop(bbox) if bbox else rgba
|
|||
|
|
|
|||
|
|
|
|||
|
|
landing = Image.open(r"C:\Users\tangl\.codex\generated_images\019f88f8-7dd3-7c53-8d9a-b3544a768a7e\exec-872cf44f-8972-429c-92a9-9a1946daa114.png")
|
|||
|
|
bend = Image.open(r"C:\Users\tangl\.codex\generated_images\019f88f8-7dd3-7c53-8d9a-b3544a768a7e\exec-38051cbd-14da-49fc-a139-d5699968df0a.png")
|
|||
|
|
side = Image.open(r"C:\Users\tangl\xwechat_files\wxid_tjrokx4gscnl21_4d57\temp\RWTemp\2026-07\afdb9db85f27cdd1cc9425d3ab16ec83.jpg")
|
|||
|
|
front_l = Image.open(r"C:\Users\tangl\xwechat_files\wxid_tjrokx4gscnl21_4d57\temp\RWTemp\2026-07\a9fb3a735b799f5909ae993223228a9b.jpg")
|
|||
|
|
front_r = Image.open(r"C:\Users\tangl\xwechat_files\wxid_tjrokx4gscnl21_4d57\temp\RWTemp\2026-07\b0479771abcc3bc2b77bb4953e6d6b7e.jpg")
|
|||
|
|
sole = Image.open(r"C:\Users\tangl\xwechat_files\wxid_tjrokx4gscnl21_4d57\temp\RWTemp\2026-07\b2717ca770f0c7cfa0110af900a8a371.jpg")
|
|||
|
|
hold_ref = Image.open(r"C:\Users\tangl\AppData\Local\Temp\codex-clipboard-bde03ba2-b96f-4712-99a2-c9807418603d.png")
|
|||
|
|
end_ref = Image.open(r"C:\Users\tangl\AppData\Local\Temp\codex-clipboard-6858372d-d6d7-47b9-ac51-73f718e298eb.png")
|
|||
|
|
|
|||
|
|
frame1 = add_caption(vignette(cover(landing)), "轻盈落地 快速回弹", "前掌触地,蓄力即发")
|
|||
|
|
frame1.save(OUT / "01_landing.jpg", quality=95)
|
|||
|
|
|
|||
|
|
frame2 = add_caption(vignette(cover(bend)), "混合飞织 超强透气", "超薄前掌,轻盈如风", "right")
|
|||
|
|
frame2.save(OUT / "02_flex.jpg", quality=95)
|
|||
|
|
|
|||
|
|
# Dual-shoe product composition that reads as a synchronized bounce close-up.
|
|||
|
|
bg = Image.new("RGB", (W, H), (10, 44, 69))
|
|||
|
|
d = ImageDraw.Draw(bg)
|
|||
|
|
for y in range(H):
|
|||
|
|
t = y / H
|
|||
|
|
color = (int(16 - 8 * t), int(75 - 25 * t), int(106 - 34 * t))
|
|||
|
|
d.line((0, y, W, y), fill=color)
|
|||
|
|
for x in (310, 790, 1270, 1750):
|
|||
|
|
d.line((x, 0, x - 420, H), fill=(89, 142, 166), width=6)
|
|||
|
|
left_shoe = fit(front_l, (820, 650), (232, 233, 229)).convert("RGB")
|
|||
|
|
right_shoe = fit(front_r, (820, 650), (232, 233, 229)).convert("RGB")
|
|||
|
|
bg.paste(left_shoe, (105, 150))
|
|||
|
|
bg.paste(right_shoe, (995, 150))
|
|||
|
|
frame3 = add_caption(vignette(bg, 0.32), "弹跳力 MAX", "Y型支撑结构|减震灵活")
|
|||
|
|
frame3.save(OUT / "03_bounce.jpg", quality=95)
|
|||
|
|
|
|||
|
|
frame4 = add_caption(vignette(cover(side)), "为速度而生", "跳绳竞速鞋|轻、弹、稳", "right")
|
|||
|
|
frame4.save(OUT / "04_side.jpg", quality=95)
|
|||
|
|
|
|||
|
|
frame5 = add_caption(vignette(cover(sole)), "全掌抓地", "紫色橡胶外底|快速启停")
|
|||
|
|
frame5.save(OUT / "05_outsole.jpg", quality=95)
|
|||
|
|
|
|||
|
|
# Recreate the hand-held hero composition as a clean floating product panel over the supplied athlete backdrop.
|
|||
|
|
hold_bg = ImageEnhance.Brightness(cover(hold_ref)).enhance(0.72).filter(ImageFilter.GaussianBlur(4))
|
|||
|
|
hero = hold_bg.convert("RGBA")
|
|||
|
|
panel_size = (1240, 620)
|
|||
|
|
panel = fit(side, panel_size, (238, 238, 235))
|
|||
|
|
mask = Image.new("L", panel_size, 0)
|
|||
|
|
ImageDraw.Draw(mask).rounded_rectangle((0, 0, panel_size[0] - 1, panel_size[1] - 1), radius=34, fill=238)
|
|||
|
|
panel.putalpha(mask)
|
|||
|
|
shadow_mask = mask.filter(ImageFilter.GaussianBlur(24))
|
|||
|
|
shadow = Image.new("RGBA", panel_size, (0, 0, 0, 0))
|
|||
|
|
shadow.putalpha(shadow_mask.point(lambda a: int(a * 0.42)))
|
|||
|
|
pos = ((W - panel_size[0]) // 2, 205)
|
|||
|
|
hero.alpha_composite(shadow, (pos[0] + 16, pos[1] + 25))
|
|||
|
|
hero.alpha_composite(panel, pos)
|
|||
|
|
draw = ImageDraw.Draw(hero)
|
|||
|
|
draw.text((W // 2, 880), "专业跳绳竞速鞋", font=font(52, True), fill="white", anchor="mm")
|
|||
|
|
draw.text((W // 2, 952), "LIGHT · REBOUND · STABILITY", font=font(25), fill=(225, 234, 238), anchor="mm")
|
|||
|
|
hero.convert("RGB").save(OUT / "06_hero.jpg", quality=95)
|
|||
|
|
|
|||
|
|
frame7 = fit(end_ref, (W, H), (0, 0, 0)).convert("RGB")
|
|||
|
|
frame7.save(OUT / "07_end.jpg", quality=98)
|
|||
|
|
|
|||
|
|
print(OUT)
|