pintubug
This commit is contained in:
1 parent
5746fa10db
commit
1a7ba9afe2
3 files changed
+171
-21
No files matched your search
@@ -72,6 +72,10 @@ cd /home/zhangw/scan-calibrator
|
|||||||
运行`install.sh`会自动补齐。ImageMagick安装在RK3588宿主机,而不是smart-cam容器内,
|
运行`install.sh`会自动补齐。ImageMagick安装在RK3588宿主机,而不是smart-cam容器内,
|
||||||
因为验证拼图程序运行在宿主机。
|
因为验证拼图程序运行在宿主机。
|
||||||
|
|
||||||
|
验证拼图不会再把全部4K小图同时交给ImageMagick。程序会逐张缩图、逐行合并,最后只合并
|
||||||
|
已经缩小的行图;如果仍检测到缓存或可用内存不足,会自动缩小单格尺寸后重试两次。因此大型
|
||||||
|
扫描网格不需要现场修改ImageMagick策略,也不会靠放宽内存上限增加盒子OOM风险。
|
||||||
|
|
||||||
常用输入:
|
常用输入:
|
||||||
|
|
||||||
- `1`:确认、执行或选择野外部署;
|
- `1`:确认、执行或选择野外部署;
|
||||||
@@ -355,6 +359,10 @@ docker exec smart-cam curl -fsS -X POST \
|
|||||||
|
|
||||||
拼图完成后,终端返回大图的绝对存储路径,然后结束对话。回复`3`则不拼图并结束。
|
拼图完成后,终端返回大图的绝对存储路径,然后结束对话。回复`3`则不拼图并结束。
|
||||||
|
|
||||||
|
大型网格采用分阶段低内存拼图。若终端提示缓存不足,程序会自行降低验证图尺寸并重新拼图,
|
||||||
|
部署人员不需要重新扫图或手工重复第⑧步;只有源图缺失、磁盘空间耗尽或图片损坏时才会停止并
|
||||||
|
给出明确错误。
|
||||||
|
|
||||||
拼图结果统一保存在:
|
拼图结果统一保存在:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
+14
-1
@@ -23,7 +23,7 @@ scan-panorama/
|
|||||||
## 依赖
|
## 依赖
|
||||||
|
|
||||||
- Python 3
|
- Python 3
|
||||||
- ImageMagick的`identify`和`montage`
|
- ImageMagick的`identify`、`convert`和`montage`
|
||||||
|
|
||||||
当前RK3588设备已经安装这些依赖,可用下面的命令检查:
|
当前RK3588设备已经安装这些依赖,可用下面的命令检查:
|
||||||
|
|
||||||
@@ -31,6 +31,7 @@ scan-panorama/
|
|||||||
python3 --version
|
python3 --version
|
||||||
identify -version
|
identify -version
|
||||||
montage -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
@@ -11,12 +11,19 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
PROGRAM_DIR = Path(__file__).resolve().parent
|
PROGRAM_DIR = Path(__file__).resolve().parent
|
||||||
DEFAULT_SCAN_ROOT = Path("/userdata/data/scan")
|
DEFAULT_SCAN_ROOT = Path("/userdata/data/scan")
|
||||||
RESULT_DIR = PROGRAM_DIR / "result"
|
RESULT_DIR = PROGRAM_DIR / "result"
|
||||||
SAFE_IMAGE_DIMENSION = 16000
|
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":
|
def fail(message: str) -> "NoReturn":
|
||||||
@@ -105,6 +112,102 @@ def identify_dimensions(images: list[Path]) -> tuple[int, int]:
|
|||||||
return dimensions.pop()
|
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(
|
def build_mosaic(
|
||||||
scan_dir: Path,
|
scan_dir: Path,
|
||||||
tile_width: int,
|
tile_width: int,
|
||||||
@@ -134,28 +237,50 @@ def build_mosaic(
|
|||||||
fail(f"网格{cols}列×{rows}行过大,无法在{SAFE_IMAGE_DIMENSION}px尺寸限制内生成")
|
fail(f"网格{cols}列×{rows}行过大,无法在{SAFE_IMAGE_DIMENSION}px尺寸限制内生成")
|
||||||
tile_height = max(1, round(source_height * tile_width / source_width))
|
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)
|
RESULT_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
output = RESULT_DIR / f"{scan_id}_{mount}_scan_grid_panorama.jpg"
|
output = RESULT_DIR / f"{scan_id}_{mount}_scan_grid_panorama.jpg"
|
||||||
temporary = RESULT_DIR / f".{output.name}.{os.getpid()}.tmp.jpg"
|
temporary = RESULT_DIR / f".{output.name}.{os.getpid()}.tmp.jpg"
|
||||||
command = [
|
attempt_widths: list[int] = []
|
||||||
montage,
|
for scale in (1.0, 0.75, 0.5):
|
||||||
*map(str, images),
|
width = max(64, int(tile_width * scale))
|
||||||
"-thumbnail", f"{tile_width}x{tile_height}!",
|
if width not in attempt_widths:
|
||||||
"-tile", f"{cols}x{rows}",
|
attempt_widths.append(width)
|
||||||
"-geometry", "+0+0",
|
|
||||||
"-background", "black",
|
final_tile_width = tile_width
|
||||||
"-quality", str(quality),
|
last_error = ""
|
||||||
str(temporary),
|
|
||||||
]
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(command, text=True, capture_output=True)
|
for attempt, candidate_width in enumerate(attempt_widths, start=1):
|
||||||
if result.returncode != 0:
|
candidate_height = max(
|
||||||
fail(f"拼图失败:{result.stderr.strip()}")
|
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:
|
if not temporary.is_file() or temporary.stat().st_size == 0:
|
||||||
fail("拼图命令未生成有效结果")
|
fail("拼图程序未生成有效结果")
|
||||||
temporary.replace(output)
|
temporary.replace(output)
|
||||||
finally:
|
finally:
|
||||||
temporary.unlink(missing_ok=True)
|
temporary.unlink(missing_ok=True)
|
||||||
@@ -167,9 +292,13 @@ def build_mosaic(
|
|||||||
f",实测pan方向:{pan_direction}(列={'反向' if reverse_cols else '正向'})"
|
f",实测pan方向:{pan_direction}(列={'反向' if reverse_cols else '正向'})"
|
||||||
)
|
)
|
||||||
print(f"小图:{source_width}×{source_height}")
|
print(f"小图:{source_width}×{source_height}")
|
||||||
if tile_width != requested_tile_width:
|
if final_tile_width != requested_tile_width:
|
||||||
print(f"尺寸保护:单格宽度由{requested_tile_width}自动缩小到{tile_width},避免超过ImageMagick限制")
|
print(
|
||||||
print(f"大图:{cols * tile_width}×{rows * tile_height}")
|
f"尺寸保护:单格宽度由{requested_tile_width}自动缩小到"
|
||||||
|
f"{final_tile_width},避免尺寸或缓存超限"
|
||||||
|
)
|
||||||
|
print(f"拼图方式:分阶段低内存处理(逐张缩图 → 逐行合并 → 合并全部行)")
|
||||||
|
print(f"大图:{cols * final_tile_width}×{rows * tile_height}")
|
||||||
print(f"结果:{output}")
|
print(f"结果:{output}")
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user