This commit is contained in:
Ankle committed 2026-09-24 14:38:42 +08:00
1 parent 5746fa10db
commit 1a7ba9afe2
3 files changed
+171 -21

No files matched your search

+14 -1
View File
@@ -23,7 +23,7 @@ scan-panorama/
## 依赖
- Python 3
- ImageMagick的`identify`和`montage`
- ImageMagick的`identify`、`convert`和`montage`
当前RK3588设备已经安装这些依赖,可用下面的命令检查:
@@ -31,6 +31,7 @@ scan-panorama/
python3 --version
identify -version
montage -version
convert -version
```
## 最简单的使用方法
@@ -131,6 +132,18 @@ find /userdata/data/scan -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort
任一检查失败时不会生成拼图,并会在终端中说明具体问题。
## 大型网格和内存保护
程序不会一次性打开全部4K原图,而是按以下顺序处理:
1. 每次只读取一张原图并生成缩略图;
2. 每一行的小缩略图合并完成后立即删除该行临时缩略图;
3. 最后把已经缩小的行图纵向合并为验证大图。
若ImageMagick仍报告`cache resources exhausted`、内存分配失败或临时缓存空间不足,程序会
自动把单格宽度降到原计划的75%、50%并重新尝试。临时目录无论成功失败都会删除,不需要提高
ImageMagick的系统缓存上限,也不会影响原始扫描图片。
## 如何查看结果
桌面环境下可以直接打开:
+149 -20
View File
@@ -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