587 lines
25 KiB
Python
587 lines
25 KiB
Python
import logging
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import pandas as pd
|
|
|
|
from . import config
|
|
from .fonts import get_cached_font
|
|
from .layout import OptimizedEfficientWordCloud
|
|
from .mask import analyze_mask, apply_safe_padding, calculate_dynamic_dimensions, prepare_mask
|
|
from .render import apply_dot_matrix, compute_fill_ratio_fast
|
|
from .weights import calculate_font_by_area_model, extract_weights_from_df, get_stroke_complexity_batch
|
|
|
|
log = logging.getLogger("core.pipeline")
|
|
|
|
|
|
def run_generation_pass(names, frequencies_data, name_weights_map, mask_hd, real_hd_w, real_hd_h):
|
|
log.info("[run_generation_pass] 开始")
|
|
log.info(" 输入: %d 词 | HD尺寸: %dx%d", len(names), real_hd_w, real_hd_h)
|
|
|
|
w_small = max(1, int(real_hd_w * config.WORK_SCALE))
|
|
h_small = max(1, int(real_hd_h * config.WORK_SCALE))
|
|
img_small = Image.fromarray(mask_hd).resize((w_small, h_small), Image.NEAREST)
|
|
mask_small = np.array(img_small)
|
|
apply_safe_padding(mask_small)
|
|
|
|
log.info(" 运算网格: %dx%d (WORK_SCALE=%.4f)", w_small, h_small, config.WORK_SCALE)
|
|
log.info(" mask_small 统计: 总像素=%d, 空闲像素=%d, 空闲率=%.4f",
|
|
mask_small.size, int(np.sum(mask_small == 0)),
|
|
int(np.sum(mask_small == 0)) / mask_small.size if mask_small.size else 0)
|
|
|
|
if config.SAVE_DEBUG_IMAGES:
|
|
debug_dir = Path(config.DEBUG_OUTPUT_DIR)
|
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
Image.fromarray(mask_hd).save(str(debug_dir / "mask_hd.png"))
|
|
Image.fromarray(mask_small).save(str(debug_dir / "mask_small.png"))
|
|
|
|
print(f"最终输出: {real_hd_w}x{real_hd_h} | 运算网格: {w_small}x{h_small}")
|
|
|
|
current_min_font = max(config.MIN_FONT_FLOOR, int(config.MIN_READABLE_HEIGHT_PX * config.WORK_SCALE))
|
|
total_target = len(names) * config.N_REPETITIONS
|
|
current_packing_eff = config.PACKING_EFFICIENCY
|
|
grow_step = config.GROW_FONT_STEP if config.GROW_FONT_ON_LOW_FILL else 1.0
|
|
final_wc = None
|
|
final_scale = 1.0
|
|
base_min_font = current_min_font
|
|
base_max_font = current_min_font + 1
|
|
|
|
def compute_font_bounds(packing_eff):
|
|
min_font, max_font = calculate_font_by_area_model(
|
|
mask_small, names, name_weights_map, config.TARGET_FILL_RATIO, config.SIZE_RATIO, packing_eff, config.N_REPETITIONS
|
|
)
|
|
min_font = max(current_min_font, min_font)
|
|
|
|
if config.USER_MIN_FONT_SIZE is not None:
|
|
user_min = int(config.USER_MIN_FONT_SIZE)
|
|
if user_min < config.MIN_FONT_FLOOR:
|
|
config._warn(f"USER_MIN_FONT_SIZE={config.USER_MIN_FONT_SIZE} 过小,提升到 {config.MIN_FONT_FLOOR}")
|
|
user_min = config.MIN_FONT_FLOOR
|
|
min_font = user_min
|
|
|
|
if config.USER_MAX_FONT_SIZE is not None:
|
|
user_max = int(config.USER_MAX_FONT_SIZE)
|
|
if user_max < config.MIN_FONT_FLOOR:
|
|
config._warn(f"USER_MAX_FONT_SIZE={config.USER_MAX_FONT_SIZE} 过小,提升到 {config.MIN_FONT_FLOOR}")
|
|
user_max = config.MIN_FONT_FLOOR
|
|
max_font = user_max
|
|
|
|
if max_font <= min_font:
|
|
config._warn(f"字号区间无效: min={min_font}, max={max_font},自动修正 max=min+1")
|
|
max_font = min_font + 1
|
|
|
|
return min_font, max_font
|
|
|
|
def try_place(min_font, max_font, large_ratio=config.LARGE_FONT_LIMIT_RATIO, size_scale=1.0):
|
|
min_font = max(config.MIN_FONT_FLOOR, int(min_font))
|
|
max_font = max(min_font + 1, int(max_font))
|
|
wc = OptimizedEfficientWordCloud(
|
|
width=w_small,
|
|
height=h_small,
|
|
mask=mask_small,
|
|
font_path=config.WC_FONT_PATH,
|
|
max_words=total_target,
|
|
min_font_size=min_font,
|
|
max_font_size=max_font,
|
|
background_color=config.get_output_background(),
|
|
use_spiral_search=True,
|
|
large_font_ratio=large_ratio,
|
|
size_scale=size_scale,
|
|
)
|
|
if config.ENABLE_STRATIFIED_SAMPLING:
|
|
wc.grid.reorder_stratified(config.STRATIFIED_BANDS)
|
|
wc.generate_from_frequencies(frequencies_data)
|
|
return wc, len(wc.layout_)
|
|
|
|
print(f"--- 5. 启动生成 (目标: {total_target} 词) ---")
|
|
log.info("--- 5. 启动生成 ---")
|
|
log.info(" 目标词数: %d (names=%d * N_REPETITIONS=%d)", total_target, len(names), config.N_REPETITIONS)
|
|
log.info(" 当前最小字号: %d, 效率: %.2f", current_min_font, current_packing_eff)
|
|
for attempt in range(1, config.MAX_ATTEMPTS + 1):
|
|
base_min_font, base_max_font = compute_font_bounds(current_packing_eff)
|
|
print(f"尝试 #{attempt}: 基准字号 [{base_min_font}, {base_max_font}], 效率: {current_packing_eff:.2f}")
|
|
log.info("[尝试 #%d] 字号区间: [%d, %d], 效率: %.2f, 大字率: %.2f",
|
|
attempt, base_min_font, base_max_font, current_packing_eff, config.LARGE_FONT_LIMIT_RATIO)
|
|
|
|
best_wc = None
|
|
best_count = 0
|
|
best_scale = config.FONT_SCALE_MIN
|
|
best_success_wc = None
|
|
best_success_scale = None
|
|
current_large_ratio = config.LARGE_FONT_LIMIT_RATIO
|
|
low_scale = max(config.SCALE_FLOOR, config.FONT_SCALE_MIN)
|
|
high_scale = max(low_scale + 0.01, config.FONT_SCALE_MAX)
|
|
|
|
for _ in range(max(1, config.SCALE_SEARCH_ROUNDS)):
|
|
mid_scale = ((low_scale + high_scale) / 2) * grow_step
|
|
wc, placed_count = try_place(base_min_font, base_max_font, current_large_ratio, mid_scale)
|
|
print(f" 尺度 {mid_scale:.3f} (字号 {base_min_font}-{base_max_font}) -> 成功: {placed_count}/{total_target}")
|
|
log.info(" 尺度 %.3f -> 放置 %d/%d", mid_scale, placed_count, total_target)
|
|
|
|
if placed_count > best_count:
|
|
best_wc = wc
|
|
best_count = placed_count
|
|
best_scale = mid_scale
|
|
|
|
if config.REQUIRE_ALL_WORDS:
|
|
if placed_count >= total_target:
|
|
best_success_wc = wc
|
|
best_success_scale = mid_scale
|
|
low_scale = max(low_scale, mid_scale / max(grow_step, 1e-6))
|
|
else:
|
|
high_scale = min(high_scale, mid_scale / max(grow_step, 1e-6))
|
|
else:
|
|
if placed_count >= best_count:
|
|
low_scale = max(low_scale, mid_scale / max(grow_step, 1e-6))
|
|
else:
|
|
high_scale = min(high_scale, mid_scale / max(grow_step, 1e-6))
|
|
|
|
if abs(high_scale - low_scale) < 0.02:
|
|
break
|
|
|
|
if best_success_wc is not None:
|
|
final_wc = best_success_wc
|
|
final_scale = best_success_scale if best_success_scale is not None else best_scale
|
|
break
|
|
|
|
if best_wc is not None:
|
|
shrink_min = current_min_font
|
|
for _ in range(config.AUTO_SHRINK_ROUNDS):
|
|
shrink_min = max(config.MIN_FONT_FLOOR, int(shrink_min * 0.8))
|
|
if shrink_min >= base_min_font:
|
|
continue
|
|
|
|
print(f" [降级:缩小字号] {shrink_min}...")
|
|
wc, placed_count = try_place(shrink_min, base_max_font, current_large_ratio, best_scale)
|
|
if placed_count > best_count:
|
|
best_wc = wc
|
|
best_count = placed_count
|
|
best_scale = best_scale
|
|
if config.REQUIRE_ALL_WORDS and placed_count >= total_target:
|
|
final_wc = wc
|
|
final_scale = best_scale
|
|
break
|
|
if final_wc is not None:
|
|
break
|
|
|
|
if config.ENABLE_SMART_LARGE_FONT_REDUCTION:
|
|
print(" [降级:牺牲大字] 仍然放不下,尝试减少大字数量...")
|
|
strict_large_ratio = 0.05
|
|
retry_min = shrink_min if 'shrink_min' in locals() else current_min_font
|
|
wc, placed_count = try_place(retry_min, base_max_font, strict_large_ratio, best_scale)
|
|
print(f" [严格模式] 大字率 {strict_large_ratio} -> 成功: {placed_count}/{total_target}")
|
|
|
|
if placed_count > best_count:
|
|
best_wc = wc
|
|
best_count = placed_count
|
|
if config.REQUIRE_ALL_WORDS and placed_count >= total_target:
|
|
final_wc = wc
|
|
final_scale = best_scale
|
|
break
|
|
|
|
if attempt == config.MAX_ATTEMPTS:
|
|
final_wc = best_wc
|
|
final_scale = best_scale
|
|
break
|
|
|
|
shrink_ratio = (best_count / total_target) if best_count else 0.5
|
|
current_packing_eff *= min(0.95, max(0.5, shrink_ratio))
|
|
|
|
if final_wc is None:
|
|
return {
|
|
"wc": None,
|
|
"fill_ratio": 0.0,
|
|
"occ_fast": None,
|
|
"w_small": w_small,
|
|
"h_small": h_small,
|
|
"base_min_font": base_min_font,
|
|
"base_max_font": base_max_font,
|
|
"mask_small": mask_small,
|
|
"size_scale": final_scale,
|
|
}
|
|
|
|
fill_ratio, occ_fast = compute_fill_ratio_fast(final_wc.layout_, mask_small, config.WC_FONT_PATH)
|
|
print(f"填充率: {fill_ratio:.3f}")
|
|
log.info("[填充率] 初始填充率: %.4f (最低要求: %.4f)", fill_ratio, config.MIN_ACCEPT_FILL_RATIO)
|
|
log.info(" layout_ 词数: %d", len(final_wc.layout_))
|
|
if config.SAVE_DEBUG_IMAGES and occ_fast is not None:
|
|
debug_dir = Path(config.DEBUG_OUTPUT_DIR)
|
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
Image.fromarray((occ_fast * 255).astype(np.uint8)).save(str(debug_dir / "occ_fast.png"))
|
|
|
|
if fill_ratio < config.MIN_ACCEPT_FILL_RATIO:
|
|
print(f"[填充率不足] {fill_ratio:.3f} < {config.MIN_ACCEPT_FILL_RATIO:.2f},启动二分放大字号重试...")
|
|
low_scale = max(final_scale, 1.0)
|
|
high_scale = max(low_scale, config.FILL_RETRY_MAX_SCALE)
|
|
retry_round = 0
|
|
best_wc = final_wc
|
|
best_fill = fill_ratio
|
|
|
|
while retry_round < config.FILL_RETRY_MAX_ROUNDS:
|
|
mid_scale = (low_scale + high_scale) / 2
|
|
retry_large_ratio = 1.0 if config.FILL_RETRY_RELAX_LARGE_CAP else config.LARGE_FONT_LIMIT_RATIO
|
|
|
|
wc, _placed_count = try_place(base_min_font, base_max_font, retry_large_ratio, mid_scale)
|
|
new_fill, occ_fast = compute_fill_ratio_fast(wc.layout_, mask_small, config.WC_FONT_PATH)
|
|
print(f" [二分重试#{retry_round + 1}] scale={mid_scale:.3f} 填充率={new_fill:.3f}")
|
|
|
|
if new_fill > best_fill:
|
|
best_fill = new_fill
|
|
best_wc = wc
|
|
|
|
if new_fill >= config.MIN_ACCEPT_FILL_RATIO:
|
|
final_wc = wc
|
|
final_scale = mid_scale
|
|
fill_ratio = new_fill
|
|
if config.SAVE_DEBUG_IMAGES and occ_fast is not None:
|
|
debug_dir = Path(config.DEBUG_OUTPUT_DIR)
|
|
debug_dir.mkdir(parents=True, exist_ok=True)
|
|
Image.fromarray((occ_fast * 255).astype(np.uint8)).save(
|
|
str(debug_dir / f"occ_fast_retry_{retry_round + 1}.png")
|
|
)
|
|
break
|
|
|
|
if new_fill > fill_ratio:
|
|
low_scale = mid_scale
|
|
else:
|
|
high_scale = mid_scale
|
|
|
|
retry_round += 1
|
|
|
|
if fill_ratio < config.MIN_ACCEPT_FILL_RATIO:
|
|
final_wc = best_wc
|
|
fill_ratio = best_fill
|
|
|
|
print(f"最终填充率: {fill_ratio:.3f}")
|
|
return {
|
|
"wc": final_wc,
|
|
"fill_ratio": fill_ratio,
|
|
"occ_fast": occ_fast,
|
|
"w_small": w_small,
|
|
"h_small": h_small,
|
|
"base_min_font": base_min_font,
|
|
"base_max_font": base_max_font,
|
|
"mask_small": mask_small,
|
|
"size_scale": final_scale,
|
|
}
|
|
|
|
|
|
def main():
|
|
t_start = time.time()
|
|
print("--- 1. 读取数据 ---")
|
|
log.info("=" * 60)
|
|
log.info("[Pipeline] main() 开始")
|
|
log.info(" EXCEL_PATH = %s", config.EXCEL_PATH)
|
|
log.info(" DATA_COL = %d", config.DATA_COL_INDEX)
|
|
log.info(" MODE = %s", config.MODE)
|
|
log.info(" FILL_ON = %s", config.FILL_ON)
|
|
log.info(" WORK_SCALE = %.4f", config.WORK_SCALE)
|
|
log.info(" SEED = %s", config.SEED)
|
|
|
|
names = []
|
|
df = None
|
|
if os.path.exists(config.EXCEL_PATH):
|
|
try:
|
|
df = pd.read_excel(config.EXCEL_PATH)
|
|
raw_names = df.iloc[:, config.DATA_COL_INDEX].dropna().astype(str)
|
|
if config.REMOVE_DUPLICATES:
|
|
names = raw_names.unique().tolist()
|
|
print(f"模式: 去重 | 数量: {len(names)}")
|
|
else:
|
|
names = raw_names.tolist()
|
|
print(f"模式: 保留重复 | 数量: {len(names)}")
|
|
except Exception as e:
|
|
print(f"读取 Excel 失败: {e}")
|
|
log.error("读取 Excel 失败: %s", e)
|
|
sys.exit(1)
|
|
else:
|
|
count = 12000
|
|
print(f"未找到Excel,使用测试数据: {count}条")
|
|
log.info("未找到 Excel,使用测试数据: %d 条", count)
|
|
names = [f"测试_{i % 100}" for i in range(count)]
|
|
|
|
input_count = len(names)
|
|
log.info("[阶段1] 读取完成: input_count=%d, 去重=%s", input_count, config.REMOVE_DUPLICATES)
|
|
if names:
|
|
sample = names[:min(10, len(names))]
|
|
log.info(" 前10个名字: %s", sample)
|
|
|
|
print("--- 2. 智能画幅计算 ---")
|
|
log.info("--- 阶段2: 智能画幅计算 ---")
|
|
avg_len = sum(len(n) for n in names) / len(names) if names else 3
|
|
log.info(" 平均名字长度: %.2f 字符", avg_len)
|
|
log.info(" BASE_HD: %dx%d", config.BASE_HD_WIDTH, config.BASE_HD_HEIGHT)
|
|
|
|
probe_mask_hd, (probe_w, probe_h), _ = prepare_mask(config.BASE_HD_WIDTH, config.BASE_HD_HEIGHT)
|
|
probe_stats = analyze_mask(probe_mask_hd)
|
|
log.info(" Probe mask: %dx%d, free_ratio=%.4f, bbox_fill_ratio=%.4f",
|
|
probe_w, probe_h, probe_stats['free_ratio'], probe_stats['bbox_fill_ratio'])
|
|
if probe_stats.get('bbox'):
|
|
log.info(" Probe bbox: %s", probe_stats['bbox'])
|
|
|
|
print(f"[Mask Probe] 可填充比例={probe_stats['free_ratio']:.3f}")
|
|
hd_w, hd_h = calculate_dynamic_dimensions(probe_w, probe_h, len(names), avg_len, probe_stats)
|
|
log.info(" 动态画幅计算结果: %dx%d", hd_w, hd_h)
|
|
|
|
print("--- 3. 生成掩膜 (High Quality & Edge Fix) ---")
|
|
log.info("--- 阶段3: 生成掩膜 ---")
|
|
mask_hd, (real_hd_w, real_hd_h), _ = prepare_mask(hd_w, hd_h)
|
|
mask_stats = analyze_mask(mask_hd)
|
|
log.info(" mask_hd: %dx%d", real_hd_w, real_hd_h)
|
|
log.info(" free_area=%d, free_ratio=%.6f", mask_stats['free_area'], mask_stats['free_ratio'])
|
|
log.info(" bbox_fill_ratio=%.6f", mask_stats['bbox_fill_ratio'])
|
|
if mask_stats.get('bbox'):
|
|
log.info(" bbox=%s", mask_stats['bbox'])
|
|
print(f"[Mask Final] 可填充比例={mask_stats['free_ratio']:.3f}")
|
|
|
|
print("--- 4. 计算权重 ---")
|
|
log.info("--- 阶段4: 计算权重 ---")
|
|
t_weights = time.time()
|
|
if config.ENABLE_STROKE_WEIGHTS:
|
|
stroke_weights_map = get_stroke_complexity_batch(names, config.WC_FONT_PATH)
|
|
log.info(" 笔画权重计算完成: %d 个词, 耗时=%.3fs", len(stroke_weights_map), time.time() - t_weights)
|
|
else:
|
|
stroke_weights_map = {}
|
|
print("笔画权重已关闭")
|
|
log.info(" 笔画权重已关闭")
|
|
|
|
# 打印权重分布统计
|
|
if stroke_weights_map:
|
|
w_vals = list(stroke_weights_map.values())
|
|
log.info(" 笔画权重分布: min=%.1f, max=%.1f, avg=%.1f, median=%.1f",
|
|
min(w_vals), max(w_vals), sum(w_vals)/len(w_vals),
|
|
sorted(w_vals)[len(w_vals)//2])
|
|
sample_items = list(stroke_weights_map.items())[:5]
|
|
log.info(" 笔画权重样本: %s", sample_items)
|
|
|
|
excel_weights_map = extract_weights_from_df(df, names) if df is not None else {}
|
|
if excel_weights_map:
|
|
print(f"Excel 权重生效: {len(excel_weights_map)} 个词")
|
|
log.info(" Excel 权重生效: %d 个词", len(excel_weights_map))
|
|
ew_vals = list(excel_weights_map.values())
|
|
log.info(" Excel 权重分布: min=%.1f, max=%.1f, avg=%.1f",
|
|
min(ew_vals), max(ew_vals), sum(ew_vals)/len(ew_vals))
|
|
elif config.WEIGHT_COL_NAME is not None or config.WEIGHT_COL_INDEX is not None:
|
|
fallback = "笔画权重" if config.ENABLE_STROKE_WEIGHTS else "均等权重"
|
|
print(f"Excel 权重不可用,已回退{fallback}")
|
|
log.info(" Excel 权重不可用,已回退%s", fallback)
|
|
|
|
name_weights_map = dict(stroke_weights_map)
|
|
name_weights_map.update(excel_weights_map)
|
|
frequencies_data = name_weights_map if config.REMOVE_DUPLICATES else [(name, name_weights_map.get(name, 10)) for name in names]
|
|
|
|
canvas_retry_round = 0
|
|
generation_result = None
|
|
t_gen = time.time()
|
|
while True:
|
|
log.info("[画布] 第%d轮生成 pass, 当前画布: %dx%d", canvas_retry_round + 1, real_hd_w, real_hd_h)
|
|
generation_result = run_generation_pass(
|
|
names,
|
|
frequencies_data,
|
|
name_weights_map,
|
|
mask_hd,
|
|
real_hd_w,
|
|
real_hd_h,
|
|
)
|
|
|
|
if generation_result["wc"] is None:
|
|
if canvas_retry_round >= config.CANVAS_RETRY_MAX_ROUNDS:
|
|
print("生成失败:未找到合适布局")
|
|
log.error("生成失败:未找到合适布局 (已重试 %d 轮)", canvas_retry_round)
|
|
sys.exit(1)
|
|
log.warning(" 本轮生成失败 (wc=None), 将重试")
|
|
elif generation_result["fill_ratio"] >= config.MIN_ACCEPT_FILL_RATIO or canvas_retry_round >= config.CANVAS_RETRY_MAX_ROUNDS:
|
|
log.info(" 生成成功! fill_ratio=%.4f (要求>=%.4f), 重试轮次=%d",
|
|
generation_result['fill_ratio'], config.MIN_ACCEPT_FILL_RATIO, canvas_retry_round)
|
|
break
|
|
|
|
canvas_retry_round += 1
|
|
next_w = int(real_hd_w * config.CANVAS_RETRY_GROWTH)
|
|
next_h = int(real_hd_h * config.CANVAS_RETRY_GROWTH)
|
|
print(f"[画布重试#{canvas_retry_round}] {real_hd_w}x{real_hd_h} -> {next_w}x{next_h}")
|
|
log.info("[画布重试#%d] %dx%d -> %dx%d (growth=%.2f)",
|
|
canvas_retry_round, real_hd_w, real_hd_h, next_w, next_h, config.CANVAS_RETRY_GROWTH)
|
|
mask_hd, (real_hd_w, real_hd_h), _ = prepare_mask(next_w, next_h)
|
|
mask_stats = analyze_mask(mask_hd)
|
|
|
|
final_wc = generation_result["wc"]
|
|
fill_ratio = generation_result["fill_ratio"]
|
|
w_small = generation_result["w_small"]
|
|
h_small = generation_result["h_small"]
|
|
log.info("[阶段5完成] 生成耗时=%.2fs, fill_ratio=%.4f, size_scale=%.4f",
|
|
time.time() - t_gen, fill_ratio, generation_result["size_scale"])
|
|
|
|
print("--- 6. 高清渲染 ---")
|
|
log.info("--- 阶段6: 高清渲染 ---")
|
|
t_render = time.time()
|
|
hd_layout = []
|
|
for text, size, (y, x), orient, color in final_wc.layout_:
|
|
hd_size = int(size / config.WORK_SCALE)
|
|
hd_y = int(y / config.WORK_SCALE)
|
|
hd_x = int(x / config.WORK_SCALE)
|
|
hd_layout.append((text, hd_size, (hd_y, hd_x), orient, color))
|
|
|
|
log.info(" HD layout 词数: %d", len(hd_layout))
|
|
log.info(" HD 画布: %dx%d", real_hd_w, real_hd_h)
|
|
if hd_layout:
|
|
sample = hd_layout[:3]
|
|
for s in sample:
|
|
log.info(" 样本: text='%s', size=%d, pos=(%d,%d), orient=%s, color=%s",
|
|
s[0], s[1], s[2][1], s[2][0], s[3], s[4])
|
|
|
|
final_wc.layout_ = hd_layout
|
|
final_wc.width = real_hd_w
|
|
final_wc.height = real_hd_h
|
|
|
|
base_img = final_wc.to_image().convert("RGB")
|
|
if config.ENABLE_DOT_MATRIX:
|
|
base_img = apply_dot_matrix(base_img, mask_hd)
|
|
|
|
base_img.save(config.OUTPUT_PNG)
|
|
print(f"已保存: {config.OUTPUT_PNG}")
|
|
log.info(" PNG 已保存: %s (%.2f MB)", config.OUTPUT_PNG,
|
|
Path(config.OUTPUT_PNG).stat().st_size / 1024 / 1024 if Path(config.OUTPUT_PNG).exists() else 0)
|
|
final_wc.to_svg(config.OUTPUT_SVG)
|
|
print(f"已保存: {config.OUTPUT_SVG}")
|
|
log.info(" SVG 已保存: %s (%.2f MB)", config.OUTPUT_SVG,
|
|
Path(config.OUTPUT_SVG).stat().st_size / 1024 / 1024 if Path(config.OUTPUT_SVG).exists() else 0)
|
|
|
|
# 描边版 SVG(激光雕刻用)
|
|
stroke_svg = str(Path(config.OUTPUT_SVG).with_name(
|
|
Path(config.OUTPUT_SVG).stem + "_stroke" + Path(config.OUTPUT_SVG).suffix
|
|
))
|
|
final_wc.to_svg_stroke(stroke_svg)
|
|
print(f"已保存: {stroke_svg}")
|
|
log.info(" SVG(stroke) 已保存: %s (%.2f MB)", stroke_svg,
|
|
Path(stroke_svg).stat().st_size / 1024 / 1024 if Path(stroke_svg).exists() else 0)
|
|
log.info(" 渲染耗时: %.2fs", time.time() - t_render)
|
|
|
|
log.info("--- 阶段7: 写入数据库 ---")
|
|
t_db = time.time()
|
|
try:
|
|
conn = sqlite3.connect(config.DB_PATH)
|
|
cursor = conn.cursor()
|
|
cursor.execute("DROP TABLE IF EXISTS word_locations")
|
|
cursor.execute("""
|
|
CREATE TABLE word_locations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT,
|
|
x INTEGER,
|
|
y INTEGER,
|
|
font_size INTEGER,
|
|
color TEXT,
|
|
orientation TEXT,
|
|
box_x INTEGER,
|
|
box_y INTEGER,
|
|
box_width INTEGER,
|
|
box_height INTEGER
|
|
)
|
|
""")
|
|
bbox_canvas = Image.new("L", (1, 1), 0)
|
|
bbox_draw = ImageDraw.Draw(bbox_canvas)
|
|
db_data = []
|
|
for name, font_size, (y, x), orient, color in final_wc.layout_:
|
|
font = get_cached_font(config.WC_FONT_PATH, max(1, int(font_size)))
|
|
orientation = "vertical" if orient else "horizontal"
|
|
if orient:
|
|
font = ImageFont.TransposedFont(font, orientation=orient)
|
|
bbox = bbox_draw.textbbox((x, y), name, font=font)
|
|
db_data.append(
|
|
(
|
|
name,
|
|
x,
|
|
y,
|
|
font_size,
|
|
color,
|
|
orientation,
|
|
bbox[0],
|
|
bbox[1],
|
|
bbox[2] - bbox[0],
|
|
bbox[3] - bbox[1],
|
|
)
|
|
)
|
|
cursor.executemany(
|
|
"""
|
|
INSERT INTO word_locations
|
|
(name, x, y, font_size, color, orientation, box_x, box_y, box_width, box_height)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
""",
|
|
db_data,
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
log.info(" DB 写入完成: %s, %d 行, 耗时=%.3fs", config.DB_PATH, len(db_data), time.time() - t_db)
|
|
except sqlite3.Error as e:
|
|
print(f"DB Error: {e}")
|
|
log.error(" DB 写入失败: %s", e)
|
|
sys.exit(1)
|
|
|
|
elapsed = time.time() - t_start
|
|
placed_count = len(final_wc.layout_)
|
|
metrics = {
|
|
"seed": config.SEED,
|
|
"layout_order_mode": config.LAYOUT_ORDER_MODE,
|
|
"layout_seed": config.LAYOUT_SEED,
|
|
"input_count": input_count,
|
|
"placed_count": placed_count,
|
|
"fill_ratio": fill_ratio,
|
|
"elapsed_seconds": round(elapsed, 4),
|
|
"font_info": {
|
|
"layout_font_path": config.WC_FONT_PATH,
|
|
"mask_font_path": config.MASK_FONT_PATH,
|
|
"palette": list(config.get_output_palette()),
|
|
"background": config.get_output_background(),
|
|
},
|
|
"mask_info": {
|
|
"free_ratio": round(mask_stats["free_ratio"], 6),
|
|
"bbox_fill_ratio": round(mask_stats["bbox_fill_ratio"], 6),
|
|
"canvas_retry_rounds": canvas_retry_round,
|
|
},
|
|
"canvas_info": {
|
|
"hd_width": real_hd_w,
|
|
"hd_height": real_hd_h,
|
|
"work_width": w_small,
|
|
"work_height": h_small,
|
|
"work_scale": config.WORK_SCALE,
|
|
},
|
|
"output_paths": {
|
|
"png": config.OUTPUT_PNG,
|
|
"svg": config.OUTPUT_SVG,
|
|
"db": config.DB_PATH,
|
|
"metrics": config.METRICS_FILE,
|
|
"debug_dir": config.DEBUG_OUTPUT_DIR,
|
|
},
|
|
"config_snapshot": {
|
|
"mode": config.MODE,
|
|
"excel_path": config.EXCEL_PATH,
|
|
"mask_image_path": config.MASK_IMAGE_PATH,
|
|
"output_dir": config.OUTPUT_DIR,
|
|
"output_prefix": config.OUTPUT_PREFIX,
|
|
"min_font_size": config.MIN_FONT_SIZE,
|
|
"max_attempts": config.MAX_ATTEMPTS,
|
|
"fill_on": config.FILL_ON,
|
|
"min_accept_fill_ratio": config.MIN_ACCEPT_FILL_RATIO,
|
|
"require_all_words": config.REQUIRE_ALL_WORDS,
|
|
"layout_order_mode": config.LAYOUT_ORDER_MODE,
|
|
"layout_seed": config.LAYOUT_SEED,
|
|
}
|
|
}
|
|
config.write_metrics(metrics)
|
|
|
|
print(f"\n✅ 完成! 总耗时: {elapsed:.2f}s")
|
|
log.info("=" * 60)
|
|
log.info("[Pipeline] 全流程完成!")
|
|
log.info(" 总耗时: %.2fs", elapsed)
|
|
log.info(" 输入: %d 词 -> 放置: %d 词", input_count, placed_count)
|
|
log.info(" 填充率: %.4f", fill_ratio)
|
|
log.info(" 画布: %dx%d (运算: %dx%d)", real_hd_w, real_hd_h, w_small, h_small)
|
|
log.info(" 输出: PNG=%s", config.OUTPUT_PNG)
|
|
log.info(" 输出: SVG=%s", config.OUTPUT_SVG)
|
|
log.info(" 输出: DB=%s", config.DB_PATH)
|
|
log.info("=" * 60)
|