pintubug
This commit is contained in:
1 parent
5746fa10db
commit
1a7ba9afe2
3 files changed
+171
-21
No files matched your search
+149
-20
@@ -11,12 +11,19 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
PROGRAM_DIR = Path(__file__).resolve().parent
|
||||
DEFAULT_SCAN_ROOT = Path("/userdata/data/scan")
|
||||
RESULT_DIR = PROGRAM_DIR / "result"
|
||||
SAFE_IMAGE_DIMENSION = 16000
|
||||
RESOURCE_ERROR_MARKERS = (
|
||||
"cache resources exhausted",
|
||||
"memory allocation failed",
|
||||
"unable to extend cache",
|
||||
"no space left on device",
|
||||
)
|
||||
|
||||
|
||||
def fail(message: str) -> "NoReturn":
|
||||
@@ -105,6 +112,102 @@ def identify_dimensions(images: list[Path]) -> tuple[int, int]:
|
||||
return dimensions.pop()
|
||||
|
||||
|
||||
def run_imagemagick(command: list[str], temporary_dir: Path) -> None:
|
||||
env = os.environ.copy()
|
||||
env["MAGICK_TEMPORARY_PATH"] = str(temporary_dir)
|
||||
result = subprocess.run(command, text=True, capture_output=True, env=env)
|
||||
if result.returncode == 0:
|
||||
return
|
||||
detail = (result.stderr or result.stdout).strip()
|
||||
raise RuntimeError(detail or f"ImageMagick命令返回{result.returncode}")
|
||||
|
||||
|
||||
def is_resource_error(message: str) -> bool:
|
||||
lowered = message.lower()
|
||||
return any(marker in lowered for marker in RESOURCE_ERROR_MARKERS)
|
||||
|
||||
|
||||
def build_staged_mosaic(
|
||||
images: list[Path],
|
||||
rows: int,
|
||||
cols: int,
|
||||
tile_width: int,
|
||||
tile_height: int,
|
||||
quality: int,
|
||||
output: Path,
|
||||
) -> None:
|
||||
"""Resize one source at a time, join rows, then join the finished rows.
|
||||
|
||||
A single ImageMagick montage invocation keeps every 4K source image open.
|
||||
Large scan grids therefore exceed the deliberately conservative cache
|
||||
policy on edge devices. Staging bounds the working set to one source
|
||||
image while resizing and one already-small row while joining.
|
||||
"""
|
||||
convert = shutil.which("convert")
|
||||
montage = shutil.which("montage")
|
||||
if not convert or not montage:
|
||||
fail("未找到ImageMagick convert/montage,请先安装imagemagick")
|
||||
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix=f".{output.stem}.stitch.", dir=RESULT_DIR
|
||||
) as temporary_name:
|
||||
temporary_dir = Path(temporary_name)
|
||||
row_outputs: list[Path] = []
|
||||
for row in range(rows):
|
||||
tile_outputs: list[Path] = []
|
||||
for col in range(cols):
|
||||
source = images[row * cols + col]
|
||||
tile = temporary_dir / f"tile_{row:04d}_{col:04d}.jpg"
|
||||
run_imagemagick(
|
||||
[
|
||||
convert,
|
||||
str(source),
|
||||
"-thumbnail", f"{tile_width}x{tile_height}!",
|
||||
"-strip",
|
||||
"-quality", str(quality),
|
||||
str(tile),
|
||||
],
|
||||
temporary_dir,
|
||||
)
|
||||
tile_outputs.append(tile)
|
||||
|
||||
row_output = temporary_dir / f"row_{row:04d}.jpg"
|
||||
run_imagemagick(
|
||||
[
|
||||
montage,
|
||||
*map(str, tile_outputs),
|
||||
"-tile", f"{cols}x1",
|
||||
"-geometry", "+0+0",
|
||||
"-background", "black",
|
||||
"-quality", str(quality),
|
||||
str(row_output),
|
||||
],
|
||||
temporary_dir,
|
||||
)
|
||||
if not row_output.is_file() or row_output.stat().st_size == 0:
|
||||
raise RuntimeError(f"第{row + 1}行没有生成有效结果")
|
||||
row_outputs.append(row_output)
|
||||
for tile in tile_outputs:
|
||||
tile.unlink(missing_ok=True)
|
||||
|
||||
temporary_output = temporary_dir / "panorama.jpg"
|
||||
run_imagemagick(
|
||||
[
|
||||
montage,
|
||||
*map(str, row_outputs),
|
||||
"-tile", f"1x{rows}",
|
||||
"-geometry", "+0+0",
|
||||
"-background", "black",
|
||||
"-quality", str(quality),
|
||||
str(temporary_output),
|
||||
],
|
||||
temporary_dir,
|
||||
)
|
||||
if not temporary_output.is_file() or temporary_output.stat().st_size == 0:
|
||||
raise RuntimeError("分阶段拼图没有生成有效结果")
|
||||
shutil.copyfile(temporary_output, output)
|
||||
|
||||
|
||||
def build_mosaic(
|
||||
scan_dir: Path,
|
||||
tile_width: int,
|
||||
@@ -134,28 +237,50 @@ def build_mosaic(
|
||||
fail(f"网格{cols}列×{rows}行过大,无法在{SAFE_IMAGE_DIMENSION}px尺寸限制内生成")
|
||||
tile_height = max(1, round(source_height * tile_width / source_width))
|
||||
|
||||
montage = shutil.which("montage")
|
||||
if not montage:
|
||||
fail("未找到ImageMagick montage,请先安装imagemagick")
|
||||
RESULT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
output = RESULT_DIR / f"{scan_id}_{mount}_scan_grid_panorama.jpg"
|
||||
temporary = RESULT_DIR / f".{output.name}.{os.getpid()}.tmp.jpg"
|
||||
command = [
|
||||
montage,
|
||||
*map(str, images),
|
||||
"-thumbnail", f"{tile_width}x{tile_height}!",
|
||||
"-tile", f"{cols}x{rows}",
|
||||
"-geometry", "+0+0",
|
||||
"-background", "black",
|
||||
"-quality", str(quality),
|
||||
str(temporary),
|
||||
]
|
||||
attempt_widths: list[int] = []
|
||||
for scale in (1.0, 0.75, 0.5):
|
||||
width = max(64, int(tile_width * scale))
|
||||
if width not in attempt_widths:
|
||||
attempt_widths.append(width)
|
||||
|
||||
final_tile_width = tile_width
|
||||
last_error = ""
|
||||
try:
|
||||
result = subprocess.run(command, text=True, capture_output=True)
|
||||
if result.returncode != 0:
|
||||
fail(f"拼图失败:{result.stderr.strip()}")
|
||||
for attempt, candidate_width in enumerate(attempt_widths, start=1):
|
||||
candidate_height = max(
|
||||
1, round(source_height * candidate_width / source_width)
|
||||
)
|
||||
temporary.unlink(missing_ok=True)
|
||||
try:
|
||||
build_staged_mosaic(
|
||||
images,
|
||||
rows,
|
||||
cols,
|
||||
candidate_width,
|
||||
candidate_height,
|
||||
quality,
|
||||
temporary,
|
||||
)
|
||||
final_tile_width = candidate_width
|
||||
tile_height = candidate_height
|
||||
break
|
||||
except RuntimeError as exc:
|
||||
last_error = str(exc)
|
||||
if not is_resource_error(last_error) or attempt == len(attempt_widths):
|
||||
fail(f"拼图失败:{last_error}")
|
||||
next_width = attempt_widths[attempt]
|
||||
print(
|
||||
"提示:盒子可用内存或ImageMagick缓存不足,"
|
||||
f"程序正在把单格宽度从{candidate_width}缩小到{next_width}后自动重试……",
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
fail(f"拼图失败:{last_error}")
|
||||
if not temporary.is_file() or temporary.stat().st_size == 0:
|
||||
fail("拼图命令未生成有效结果")
|
||||
fail("拼图程序未生成有效结果")
|
||||
temporary.replace(output)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
@@ -167,9 +292,13 @@ def build_mosaic(
|
||||
f",实测pan方向:{pan_direction}(列={'反向' if reverse_cols else '正向'})"
|
||||
)
|
||||
print(f"小图:{source_width}×{source_height}")
|
||||
if tile_width != requested_tile_width:
|
||||
print(f"尺寸保护:单格宽度由{requested_tile_width}自动缩小到{tile_width},避免超过ImageMagick限制")
|
||||
print(f"大图:{cols * tile_width}×{rows * tile_height}")
|
||||
if final_tile_width != requested_tile_width:
|
||||
print(
|
||||
f"尺寸保护:单格宽度由{requested_tile_width}自动缩小到"
|
||||
f"{final_tile_width},避免尺寸或缓存超限"
|
||||
)
|
||||
print(f"拼图方式:分阶段低内存处理(逐张缩图 → 逐行合并 → 合并全部行)")
|
||||
print(f"大图:{cols * final_tile_width}×{rows * tile_height}")
|
||||
print(f"结果:{output}")
|
||||
return output
|
||||
|
||||
|
||||
Reference in new issue
Block a user