一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。
二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。
三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
127 lines
4.5 KiB
Python
127 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""把同一透明角色层精确放到轨道顶部和底部,生成一致的首尾帧。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
def parse_size(raw: str) -> tuple[int, int]:
|
|
try:
|
|
width, height = raw.lower().split("x", 1)
|
|
return int(width), int(height)
|
|
except (ValueError, AttributeError) as error:
|
|
raise ValueError("--size 必须是 WIDTHxHEIGHT") from error
|
|
|
|
|
|
def build(args: argparse.Namespace) -> int:
|
|
source = Path(args.source).expanduser().resolve()
|
|
first_output = Path(args.first_output).expanduser().resolve()
|
|
last_output = Path(args.last_output).expanduser().resolve()
|
|
width, height = parse_size(args.size)
|
|
|
|
image = Image.open(source).convert("RGBA")
|
|
bbox = image.getbbox()
|
|
if not bbox:
|
|
raise ValueError("输入没有可见像素")
|
|
subject = image.crop(bbox)
|
|
|
|
target_height = round(height * args.subject_height)
|
|
scale = target_height / subject.height
|
|
target_width = round(subject.width * scale)
|
|
if target_width > width * args.max_subject_width:
|
|
scale = (width * args.max_subject_width) / subject.width
|
|
target_width = round(subject.width * scale)
|
|
target_height = round(subject.height * scale)
|
|
subject = subject.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
|
|
|
pole_x = round(width * args.pole_x)
|
|
anchor_x = round(subject.width * args.subject_anchor_x)
|
|
subject_x = pole_x - anchor_x
|
|
start_y = round(height * args.top_margin)
|
|
end_y = height - round(height * args.bottom_margin) - subject.height
|
|
if end_y <= start_y:
|
|
raise ValueError("角色过高,没有足够的上下移动空间")
|
|
|
|
def compose(y: int) -> Image.Image:
|
|
canvas = Image.new("RGB", (width, height), args.key_color)
|
|
draw = ImageDraw.Draw(canvas)
|
|
outline = max(2, round(width * args.pole_outline_ratio))
|
|
pole_width = max(outline + 2, round(width * args.pole_width_ratio))
|
|
draw.rectangle(
|
|
(
|
|
pole_x - pole_width // 2 - outline,
|
|
0,
|
|
pole_x + pole_width // 2 + outline,
|
|
height,
|
|
),
|
|
fill=args.pole_outline,
|
|
)
|
|
draw.rectangle(
|
|
(
|
|
pole_x - pole_width // 2,
|
|
0,
|
|
pole_x + pole_width // 2,
|
|
height,
|
|
),
|
|
fill=args.pole_fill,
|
|
)
|
|
canvas.paste(subject, (subject_x, y), subject)
|
|
return canvas
|
|
|
|
first_output.parent.mkdir(parents=True, exist_ok=True)
|
|
last_output.parent.mkdir(parents=True, exist_ok=True)
|
|
compose(start_y).save(first_output)
|
|
compose(end_y).save(last_output)
|
|
print(
|
|
{
|
|
"source": str(source),
|
|
"subjectSize": [subject.width, subject.height],
|
|
"canvas": [width, height],
|
|
"poleX": pole_x,
|
|
"subjectX": subject_x,
|
|
"startY": start_y,
|
|
"endY": end_y,
|
|
"first": str(first_output),
|
|
"last": str(last_output),
|
|
}
|
|
)
|
|
return 0
|
|
|
|
|
|
def parser() -> argparse.ArgumentParser:
|
|
result = argparse.ArgumentParser(
|
|
description="用同一透明主体生成沿竖直轨道移动的首帧和尾帧"
|
|
)
|
|
result.add_argument("source", help="已抠图的 RGBA PNG")
|
|
result.add_argument("--first-output", required=True)
|
|
result.add_argument("--last-output", required=True)
|
|
result.add_argument("--size", default="864x1536")
|
|
result.add_argument("--key-color", default="#00FF00")
|
|
result.add_argument("--pole-x", type=float, default=0.5)
|
|
result.add_argument("--pole-fill", default="#B8B8B2")
|
|
result.add_argument("--pole-outline", default="#181818")
|
|
result.add_argument("--pole-width-ratio", type=float, default=0.012)
|
|
result.add_argument("--pole-outline-ratio", type=float, default=0.004)
|
|
result.add_argument("--subject-height", type=float, default=0.36)
|
|
result.add_argument("--max-subject-width", type=float, default=0.84)
|
|
result.add_argument(
|
|
"--subject-anchor-x",
|
|
type=float,
|
|
default=0.535,
|
|
help="透明主体中应与轨道重合的横向比例",
|
|
)
|
|
result.add_argument("--top-margin", type=float, default=0.045)
|
|
result.add_argument("--bottom-margin", type=float, default=0.045)
|
|
return result
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(build(parser().parse_args()))
|
|
except (FileNotFoundError, ValueError) as error:
|
|
raise SystemExit(f"错误:{error}") from error
|