feat: add montage subcommand for multi-PNG grid stitching (Pillow)
This commit is contained in:
@@ -10,6 +10,7 @@ dependencies = [
|
|||||||
"pandas>=2.0",
|
"pandas>=2.0",
|
||||||
"numpy>=1.24",
|
"numpy>=1.24",
|
||||||
"matplotlib>=3.7",
|
"matplotlib>=3.7",
|
||||||
|
"Pillow>=10.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""montage_pngs 单元测试。"""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def _make_test_png(path, w=200, h=100, color="red"):
|
||||||
|
"""创建测试用 PNG。"""
|
||||||
|
img = Image.new("RGB", (w, h), color)
|
||||||
|
img.save(path)
|
||||||
|
return str(path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_montage_basic():
|
||||||
|
"""4 张图拼成 2x2 网格。"""
|
||||||
|
from ts_anomaly_td.visualize import montage_pngs
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
paths = []
|
||||||
|
for i, color in enumerate(["red", "green", "blue", "yellow"]):
|
||||||
|
p = Path(tmpdir) / f"img_{i}.png"
|
||||||
|
_make_test_png(p, color=color)
|
||||||
|
paths.append(str(p))
|
||||||
|
|
||||||
|
out = Path(tmpdir) / "montage.png"
|
||||||
|
result = montage_pngs(paths, str(out), cols=2, title="Test")
|
||||||
|
assert Path(result).exists()
|
||||||
|
assert Path(result).stat().st_size > 1000
|
||||||
|
|
||||||
|
|
||||||
|
def test_montage_uneven():
|
||||||
|
"""3 张图拼成 2 列(第二行只 1 张)。"""
|
||||||
|
from ts_anomaly_td.visualize import montage_pngs
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
paths = []
|
||||||
|
for i in range(3):
|
||||||
|
p = Path(tmpdir) / f"img_{i}.png"
|
||||||
|
_make_test_png(p)
|
||||||
|
paths.append(str(p))
|
||||||
|
|
||||||
|
out = Path(tmpdir) / "montage_uneven.png"
|
||||||
|
result = montage_pngs(paths, str(out), cols=2)
|
||||||
|
assert Path(result).exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_montage_empty():
|
||||||
|
"""空列表应抛 ValueError。"""
|
||||||
|
from ts_anomaly_td.visualize import montage_pngs
|
||||||
|
|
||||||
|
try:
|
||||||
|
montage_pngs([], "/dev/null")
|
||||||
|
assert False, "应抛 ValueError"
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
@@ -218,6 +218,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
p_ra.add_argument("--value-col", default="value", help="数值列名 (default: value)")
|
p_ra.add_argument("--value-col", default="value", help="数值列名 (default: value)")
|
||||||
p_ra.add_argument("--title", default="", help="图表标题")
|
p_ra.add_argument("--title", default="", help="图表标题")
|
||||||
|
|
||||||
|
# montage
|
||||||
|
p_mt = sub.add_parser("montage", help="多 PNG 拼图")
|
||||||
|
p_mt.add_argument("--inputs", nargs="+", required=True, help="输入 PNG 文件列表")
|
||||||
|
p_mt.add_argument("--output", required=True, help="拼图输出路径")
|
||||||
|
p_mt.add_argument("--cols", type=int, default=2, help="列数 (default: 2)")
|
||||||
|
p_mt.add_argument("--title", default="", help="总标题")
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
@@ -241,6 +248,16 @@ def cmd_render_algo(args):
|
|||||||
render_algo(ts, vals, labels, windows, args.algo, args.output, title=title)
|
render_algo(ts, vals, labels, windows, args.algo, args.output, title=title)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_montage(args):
|
||||||
|
"""montage 子命令:多 PNG 拼图。"""
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||||
|
|
||||||
|
from ts_anomaly_td.visualize import montage_pngs
|
||||||
|
|
||||||
|
montage_pngs(args.inputs, args.output, cols=args.cols, title=args.title)
|
||||||
|
logger.info("montage complete: %s", args.output)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = build_parser()
|
parser = build_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -253,6 +270,7 @@ def main():
|
|||||||
"visualize": cmd_visualize,
|
"visualize": cmd_visualize,
|
||||||
"watch": cmd_watch,
|
"watch": cmd_watch,
|
||||||
"render-algo": cmd_render_algo,
|
"render-algo": cmd_render_algo,
|
||||||
|
"montage": cmd_montage,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn = dispatch.get(args.command)
|
fn = dispatch.get(args.command)
|
||||||
|
|||||||
Reference in New Issue
Block a user