Compare commits

..
86 Commits
Author SHA1 Message Date
hesuicong 7ffaf9cef2 关闭虚拟面增长优化 2026-08-24 15:09:48 +08:00
hesuicong ccd621f0b1 解决大模型贴图卡住的问题 2026-08-24 11:35:50 +08:00
hesuicong 9008e27d93 还原 2026-08-22 14:43:26 +08:00
hesuicong 2fefb4738c 解决一个崩溃的Bug 2026-08-21 14:48:20 +08:00
hesuicong 3dff609cb8 高光抑制 2026-08-19 15:59:23 +08:00
hesuicong 033dca0dd9 面选择调整 2026-08-19 14:45:05 +08:00
hesuicong 03eee01348 rect添加限制 2026-08-14 15:36:48 +08:00
hesuicong c49c3df5ad 色差问题优化 2026-08-14 14:56:42 +08:00
hesuicong 8577d13579 取消一些注释 2026-08-06 14:27:22 +08:00
hesuicong 982106fec2 解决构建虚拟面邻居图时的一个崩溃Bug 2026-08-06 14:25:15 +08:00
hesuicong 617f8ea2b5 优化虚拟面生成,增强法线和几何约束 2026-08-05 17:41:18 +08:00
hesuicong 715bf75c74 平坦区域虚拟面增长优化 2026-07-29 11:15:08 +08:00
hesuicong 068a06820d 区域生长策略 2026-07-28 17:59:30 +08:00
hesuicong debbe84898 临时代码 2026-07-28 16:41:08 +08:00
hesuicong 9dd09bfc11 代码优化 2026-07-27 14:41:36 +08:00
hesuicong f38ada2abd 更新 2026-07-21 16:17:06 +08:00
hesuicong 72dae852bb 简化相机选择逻辑 2026-07-08 17:36:52 +08:00
hesuicong 97b429ec98 解决色块的问题 2026-07-07 15:23:11 +08:00
hesuicong 4724b2e754 优化接口调用 2026-07-03 17:01:57 +08:00
hesuicong 4cc5558629 优化 2026-07-02 16:01:09 +08:00
hesuicong 0d3dd2909a 去掉一些注释 2026-07-02 15:24:10 +08:00
hesuicong d157312fba 多视图混合采样替换patch 2026-07-02 14:41:00 +08:00
hesuicong b28b78ee93 参数优化 2026-06-29 16:07:31 +08:00
hesuicong b9d12648a1 参数进一步优化 2026-06-29 11:51:52 +08:00
hesuicong 78aaaefb1a 兼容性更大 2026-06-26 09:39:23 +08:00
hesuicong 8ff037ff5e 参数优化 2026-06-25 18:17:47 +08:00
hesuicong 9696034738 参数优化 2026-06-25 16:54:03 +08:00
hesuicong 7f967e5434 参数优化 2026-06-25 10:45:15 +08:00
hesuicong eda58b268f 参数优化 2026-06-24 14:47:15 +08:00
hesuicong e21b2f572b 优化参数 2026-06-23 17:05:09 +08:00
hesuicong 54c7ea1240 评分系统优化 2026-06-23 16:47:53 +08:00
hesuicong b47e0ab82f 添加评分系统来处理视图选择 2026-06-23 15:53:04 +08:00
hesuicong 9c3e97f447 单视图投射进一步优化 2026-06-22 14:54:09 +08:00
hesuicong c927ce7db7 小臂问题优化 2026-06-16 10:38:05 +08:00
hesuicong 65d8242c9c 小臂测试优化 2026-06-15 18:02:34 +08:00
hesuicong 383bd34431 进一步调优版本 2026-06-13 16:25:06 +08:00
hesuicong ec1a89b078 两轮填数据方式 2026-06-12 17:31:09 +08:00
hesuicong b469eb83ff 供面片决策的筛选视图决策宽松,每一个面片一旦优势命中后,剔除所有视图 2026-06-11 17:56:40 +08:00
hesuicong 4048de2dc4 优化筛选逻辑 2026-06-11 16:13:33 +08:00
hesuicong 9ab39219b1 最佳视图选择逻辑优化 2026-06-10 18:34:36 +08:00
hesuicong 68cb2ea1fc 优化bug 2026-06-06 17:47:04 +08:00
hesuicong 3159aa8b5e 一个失误 2026-06-05 14:35:24 +08:00
hesuicong ef9de71d44 单视图优化 2026-06-05 14:13:53 +08:00
hesuicong 1421d21597 进一步优化 2026-05-30 15:18:09 +08:00
hesuicong 3693365bdf 算法优化 2026-05-29 18:05:56 +08:00
hesuicong 03929e5b95 视图排序优化 2026-05-28 18:13:42 +08:00
hesuicong 1086eb09c5 优化染色版本 2026-05-28 10:54:42 +08:00
hesuicong 7d0487ea30 针对小黄点过多和手指染色问题的参数优化 2026-05-25 14:40:16 +08:00
hesuicong cbe065a6a1 参数进一步优化 2026-05-22 17:42:12 +08:00
hesuicong 488f5a3f93 颜色溢出进一步优化 2026-05-21 14:57:14 +08:00
hesuicong e18f91e894 颜色溢出优化 2026-05-20 17:50:50 +08:00
hesuicong eafae796c2 删除保留的函数,简化 2026-04-16 09:16:33 +08:00
hesuicong 35ac5deff9 参数优化 2026-04-14 14:47:15 +08:00
hesuicong f9e7f3f18b 解决线上问题 2026-04-11 17:59:47 +08:00
hesuicong d035a20e91 参数优化 2026-04-03 10:24:05 +08:00
hesuicong d11d956b58 参数优化 2026-04-01 16:39:20 +08:00
hesuicong 743835d1b0 参数优化 2026-03-27 17:41:10 +08:00
hesuicong 4968fcc954 优化参数 2026-03-27 11:40:58 +08:00
hesuicong dbe42abf1f 优化脸色发青问题 2026-03-26 17:28:05 +08:00
hesuicong 5685a46331 处理脸色发青问题 2026-03-24 16:47:59 +08:00
hesuicong bbddc87ccc 优化过滤算法 2026-03-23 11:05:51 +08:00
hesuicong 634590e6d4 处理脸部青黄问题 2026-03-21 15:58:56 +08:00
hesuicong 955419e110 参数优化 2026-03-19 16:43:04 +08:00
hesuicong 1483610ed5 优化参数 2026-03-16 09:39:00 +08:00
hesuicong 6bd6b27f17 鼻子黑 2026-03-11 18:16:15 +08:00
hesuicong 37ebcdf0fd 鼻子黑的问题优化 2026-03-11 18:08:17 +08:00
hesuicong ef09d2521a 白痕优化 2026-03-06 17:05:01 +08:00
hesuicong 60f024abaf 优化性能 2026-03-03 16:02:18 +08:00
hesuicong 469f3df88d 优化性能 2026-03-03 16:01:18 +08:00
hesuicong bc461bd595 添加调试单视图信息 2026-02-24 17:43:34 +08:00
hesuicong 5e4872f41d 去小黄点bug修复 2026-02-09 10:16:06 +08:00
hesuicong f123063221 染色处理 2026-01-23 17:17:35 +08:00
hesuicong be5adbf434 优化明暗问题 2026-01-23 16:48:33 +08:00
hesuicong 94b3aee50a Merge branch 'master' of https://git.suwa3d.com/hesuicong/openMVS 2026-01-23 16:46:28 +08:00
hesuicong 53111c6275 染色问题处理 2026-01-23 16:45:56 +08:00
kehan 86627b5d24 将python缓存从库中忽略 2026-01-22 14:53:25 +08:00
kehan 7f14d73b13 修改了路径符合线上代码格式 2026-01-22 14:51:53 +08:00
hesuicong 3e6410b290 针对线上做进一步优化 2026-01-20 17:43:46 +08:00
hesuicong 9e3c78c92e 处理色彩斑驳的问题 2026-01-20 10:06:34 +08:00
hesuicong cb545049c2 修正路径问题 2026-01-19 09:49:32 +08:00
hesuicong b95b9ca27a 优化 2026-01-16 15:09:48 +08:00
hesuicong bccd31fea6 优化明暗问题 2026-01-13 14:09:48 +08:00
hesuicong f045aa534e 处理贴图问题 2026-01-12 17:01:19 +08:00
hesuicong 338243fa14 明亮问题处理 2026-01-12 16:01:03 +08:00
hesuicong 9c1b1c3944 处理过亮过暗的问题 2026-01-09 16:06:44 +08:00
hesuicong 7baf59740f ignore 2025-12-31 09:56:42 +08:00
15 changed files with 4636 additions and 15889 deletions

No files matched your search

+1 -4
View File
@@ -7,6 +7,7 @@
# Precompiled Headers
*.gch
*.pch
*.pyc
# Compiled Dynamic libraries
*.so
@@ -74,7 +75,3 @@ libs/MVS/texture_output/texture_33.png
libs/MVS/texture_output/texture_34.png
libs/MVS/texture_output/texture_35.png
texture_output/texture_0.png
color_change/__pycache__/calib_io.cpython-310.pyc
.gitignore
.gitignore
color_change/__pycache__/calibration.cpython-310.pyc
+1 -1
View File
@@ -52,7 +52,7 @@ ENDIF()
PROJECT(OpenMVS)
SET(ENV_PYTHON_PATH "/home/algo/.conda/envs/py310_pyt210/lib/python3.10/site-packages")
SET(ENV_MVS_PATH "/home/algo/Documents/openMVS/openMVS.ManualUV/libs/MVS")
SET(ENV_MVS_PATH "/home/algo/Documents/openMVS/openMVS/libs/MVS")
CONFIGURE_FILE(
"${CMAKE_CURRENT_SOURCE_DIR}/build/Templates/ConfigEnv.h.in"
+2 -7
View File
@@ -94,9 +94,6 @@ unsigned nMaxThreads;
int nMaxTextureSize;
String strExportType;
String strConfigFileName;
bool bUseExistingUV;
String strUVMeshFileName;
boost::program_options::variables_map vm;
} // namespace OPT
@@ -166,8 +163,6 @@ bool Application::Initialize(size_t argc, LPCTSTR* argv)
("image-folder", boost::program_options::value<std::string>(&OPT::strImageFolder)->default_value(COLMAP_IMAGES_FOLDER), "folder to the undistorted images")
("origin-faceview", boost::program_options::value(&OPT::bOriginFaceview)->default_value(false), "use origin faceview selection")
("id", boost::program_options::value(&OPT::nID)->default_value(0), "id")
("use-existing-uv", boost::program_options::value(&OPT::bUseExistingUV)->default_value(false), "use existing UV coordinates from the input mesh")
("uv-mesh-file", boost::program_options::value<std::string>(&OPT::strUVMeshFileName), "mesh file with pre-computed UV coordinates")
;
// hidden options, allowed both on command line and
@@ -1139,12 +1134,12 @@ int main(int argc, LPCTSTR* argv)
if (!scene.TextureMesh(OPT::nResolutionLevel, OPT::nMinResolution, OPT::minCommonCameras, OPT::fOutlierThreshold, OPT::fRatioDataSmoothness,
OPT::bGlobalSeamLeveling, OPT::bLocalSeamLeveling, OPT::nTextureSizeMultiple, OPT::nRectPackingHeuristic, Pixel8U(OPT::nColEmpty),
OPT::fSharpnessWeight, OPT::nIgnoreMaskLabel, OPT::nMaxTextureSize, views, baseFileName, OPT::bOriginFaceview,
OPT::strInputFileName, OPT::strMeshFileName, OPT::bUseExistingUV, OPT::strUVMeshFileName))
OPT::strInputFileName, OPT::strMeshFileName))
return EXIT_FAILURE;
VERBOSE("Mesh texturing completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str());
// save the final mesh
scene.mesh.Save(baseFileName+OPT::strExportType,cList<String>(),true,OPT::bUseExistingUV);
scene.mesh.Save(baseFileName+OPT::strExportType);
#if TD_VERBOSE != TD_VERBOSE_OFF
if (VERBOSITY_LEVEL > 2)
scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType);
-265
View File
@@ -1,265 +0,0 @@
from __future__ import annotations
import json
import os
from collections.abc import Callable, Mapping
from dataclasses import dataclass
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import TypeAlias
import numpy as np
import pycolmap
from numpy.typing import NDArray
from PIL import Image
@dataclass(frozen=True, slots=True)
class CalibrationView:
image_id: int
name: str
rgb_path: Path
mask_path: Path
camera: pycolmap.Camera
cam_from_world: "pycolmap.Rigid3d"
@dataclass(frozen=True, slots=True)
class CalibrationDataset:
reconstruction: pycolmap.Reconstruction
views: tuple[CalibrationView, ...]
@dataclass(frozen=True, slots=True)
class CalibrationResult:
corrected_images: Mapping[int, NDArray[np.uint8]]
report: Mapping[str, object]
@dataclass(frozen=True, slots=True)
class CalibrationOutputs:
image_paths: tuple[Path, ...]
report_path: Path
Calibrator: TypeAlias = Callable[[CalibrationDataset], CalibrationResult]
def _relative_image_path(name: str) -> Path:
posix_path = PurePosixPath(name)
windows_path = PureWindowsPath(name)
if (
not posix_path.parts
or posix_path.is_absolute()
or ".." in posix_path.parts
or windows_path.is_absolute()
or windows_path.drive
or ".." in windows_path.parts
):
raise ValueError(f"unsafe COLMAP image name: {name}")
return Path(*posix_path.parts)
def _validate_output_target(
output_root: Path,
resolved_output_root: Path,
relative: Path,
seen_targets: set[str],
seen_ancestors: set[str],
) -> Path:
target = output_root / relative
resolved_target = target.resolve()
if not resolved_target.is_relative_to(resolved_output_root):
raise ValueError(f"output path escapes output_dir: {target}")
ancestor = target.parent
while True:
if (ancestor.exists() or ancestor.is_symlink()) and not ancestor.is_dir():
raise NotADirectoryError(f"output ancestor is not a directory: {ancestor}")
if ancestor == output_root:
break
ancestor = ancestor.parent
target_key = os.path.normcase(str(resolved_target))
if target_key in seen_targets:
raise ValueError(f"duplicate output path: {target}")
if target_key in seen_ancestors or any(
os.path.normcase(str(parent)) in seen_targets
for parent in resolved_target.parents
):
raise ValueError(f"conflicting output path: {target}")
seen_targets.add(target_key)
seen_ancestors.update(
os.path.normcase(str(parent)) for parent in resolved_target.parents
)
if target.exists() or target.is_symlink():
raise FileExistsError(f"output already exists: {target}")
return target
def load_dataset(
model_dir: str | os.PathLike[str],
rgb_dir: str | os.PathLike[str],
mask_dir: str | os.PathLike[str],
) -> CalibrationDataset:
# ✅ pycolmap 4.x: 直接用 Reconstruction 加载模型目录
reconstruction = pycolmap.Reconstruction(str(Path(model_dir)))
# 验证加载成功
if reconstruction.num_images() == 0:
raise RuntimeError(
f"Failed to load COLMAP model from {model_dir}. "
f"Expected cameras.bin/images.bin/points3D.bin or .txt files."
)
print(f"Loaded COLMAP model: {reconstruction.num_images()} images, "
f"{reconstruction.num_cameras()} cameras, "
f"{reconstruction.num_points3D()} 3D points")
rgb_root = Path(rgb_dir)
mask_root = Path(mask_dir)
seen_names: set[str] = set()
views: list[CalibrationView] = []
# ✅ pycolmap 4.x: images 是一个 dict-like 对象
for image_id, image in reconstruction.images.items():
if not image.has_pose:
raise ValueError(f"COLMAP image has no pose: {image.name}")
relative = _relative_image_path(image.name)
normalized_name = os.path.normcase(str(relative))
if normalized_name in seen_names:
raise ValueError(f"duplicate COLMAP image name: {image.name}")
seen_names.add(normalized_name)
rgb_path = rgb_root / relative
mask_path = mask_root / relative.with_suffix(".png")
if not rgb_path.is_file():
raise FileNotFoundError(f"RGB image not found: {rgb_path}")
if not mask_path.is_file():
raise FileNotFoundError(f"mask image not found: {mask_path}")
camera = image.camera
if camera is None:
raise ValueError(f"COLMAP image has no camera: {image.name}")
with Image.open(rgb_path) as rgb_image:
rgb_size = rgb_image.size
with Image.open(mask_path) as mask_image:
mask_size = mask_image.size
camera_size = (camera.width, camera.height)
if rgb_size != camera_size or mask_size != camera_size:
raise ValueError(
f"size mismatch for {image.name}: camera={camera_size}, "
f"rgb={rgb_size}, mask={mask_size}"
)
views.append(
CalibrationView(
image_id=image_id,
name=image.name,
rgb_path=rgb_path,
mask_path=mask_path,
camera=camera,
# ✅ pycolmap 4.x: cam_from_world 是属性,不是方法
cam_from_world=image.cam_from_world,
)
)
return CalibrationDataset(reconstruction=reconstruction, views=tuple(views))
def read_rgb(view: CalibrationView) -> NDArray[np.uint8]:
with Image.open(view.rgb_path) as image:
return np.array(image.convert("RGB"), dtype=np.uint8, copy=True)
def read_mask(view: CalibrationView) -> NDArray[np.bool_]:
with Image.open(view.mask_path) as image:
return np.array(image.convert("L"), dtype=np.uint8, copy=True) != 0
def run_calibration(
model_dir: str | os.PathLike[str],
rgb_dir: str | os.PathLike[str],
mask_dir: str | os.PathLike[str],
output_dir: str | os.PathLike[str],
*,
calibrator: Calibrator,
) -> CalibrationOutputs:
dataset = load_dataset(model_dir, rgb_dir, mask_dir)
output_root = Path(output_dir)
resolved_output_root = output_root.resolve()
if resolved_output_root == Path(rgb_dir).resolve():
raise ValueError("output_dir must differ from rgb_dir")
result = calibrator(dataset)
if not isinstance(result, CalibrationResult):
raise ValueError("calibrator must return CalibrationResult")
if not isinstance(result.corrected_images, Mapping):
raise ValueError("calibrator corrected_images must be a mapping")
try:
report_json = json.dumps(
result.report,
ensure_ascii=False,
indent=2,
sort_keys=True,
allow_nan=False,
)
except (TypeError, ValueError) as error:
raise ValueError("calibration report is not valid JSON") from error
expected_ids = {view.image_id for view in dataset.views}
returned_ids = set(result.corrected_images)
if returned_ids != expected_ids:
missing = sorted(expected_ids - returned_ids)
extra = sorted(returned_ids - expected_ids, key=repr)
raise ValueError(
f"calibrator returned wrong image ids: missing={missing} extra={extra}"
)
Image.init()
output_formats = Image.registered_extensions()
seen_targets: set[str] = set()
seen_ancestors: set[str] = set()
outputs: list[tuple[Path, NDArray[np.uint8]]] = []
for view in dataset.views:
rgb = result.corrected_images[view.image_id]
expected_shape = (view.camera.height, view.camera.width, 3)
if (
not isinstance(rgb, np.ndarray)
or rgb.dtype != np.uint8
or rgb.shape != expected_shape
):
raise ValueError(
f"invalid output for image {view.name}: expected shape "
f"{expected_shape} and dtype uint8, got shape "
f"{getattr(rgb, 'shape', None)} and dtype "
f"{getattr(rgb, 'dtype', None)}"
)
target = _validate_output_target(
output_root,
resolved_output_root,
_relative_image_path(view.name),
seen_targets,
seen_ancestors,
)
output_format = output_formats.get(target.suffix.lower())
if output_format not in Image.SAVE:
raise ValueError(f"unsupported output image extension: {target.suffix}")
outputs.append((target, rgb))
report_path = _validate_output_target(
output_root,
resolved_output_root,
Path("calibration_report.json"),
seen_targets,
seen_ancestors,
)
for target, rgb in outputs:
target.parent.mkdir(parents=True, exist_ok=True)
Image.fromarray(rgb).save(target)
report_path.parent.mkdir(parents=True, exist_ok=True)
report_path.write_text(f"{report_json}\n", encoding="utf-8")
return CalibrationOutputs(
image_paths=tuple(target for target, _ in outputs),
report_path=report_path,
)
-467
View File
@@ -1,467 +0,0 @@
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from itertools import combinations
import numpy as np
from numpy.typing import NDArray
from calib_io import CalibrationDataset, CalibrationResult, read_mask, read_rgb
_PATCH_RADIUS = 2
@dataclass(frozen=True, slots=True)
class CalibrationOptions:
mask_erosion_radius: int = 2
min_shared_tracks: int = 3
valid_color_min: int = 1
valid_color_max: int = 254
huber_k: float = 1.345
max_iterations: int = 20
tolerance: float = 1e-6
def _validate_options(options: CalibrationOptions) -> None:
if options.mask_erosion_radius < 0:
raise ValueError("mask_erosion_radius must be non-negative")
if options.min_shared_tracks < 1:
raise ValueError("min_shared_tracks must be positive")
if not 1 <= options.valid_color_min < options.valid_color_max <= 255:
raise ValueError("valid color range must satisfy 1 <= min < max <= 255")
if not np.isfinite(options.huber_k) or options.huber_k <= 0:
raise ValueError("huber_k must be finite and positive")
if options.max_iterations < 1:
raise ValueError("max_iterations must be positive")
if not np.isfinite(options.tolerance) or options.tolerance <= 0:
raise ValueError("tolerance must be finite and positive")
def _erode_mask(mask: NDArray[np.bool_], radius: int) -> NDArray[np.bool_]:
source = np.asarray(mask, dtype=np.bool_)
if radius == 0:
return source.copy()
result = np.zeros_like(source)
size = 2 * radius + 1
if source.shape[0] < size or source.shape[1] < size:
return result
windows = np.lib.stride_tricks.sliding_window_view(source, (size, size))
result[radius:-radius, radius:-radius] = windows.all(axis=(-2, -1))
return result
def _sample_tracks(
dataset: CalibrationDataset,
options: CalibrationOptions,
) -> tuple[dict[int, tuple[tuple[int, NDArray[np.float64]], ...]], int, int]:
observations: dict[int, list[tuple[int, NDArray[np.float64]]]] = defaultdict(
list
)
candidate_observations = 0
valid_observations = 0
duplicate_tracks: set[int] = set()
for view in dataset.views:
rgb = read_rgb(view)
eroded_mask = _erode_mask(
read_mask(view),
options.mask_erosion_radius,
)
# ✅ pycolmap 4.x: 从 images 字典取
image = dataset.reconstruction.images[view.image_id]
seen_point3d_ids: set[int] = set()
for point2D_idx in range(image.num_points2D()):
point = image.point2D(point2D_idx)
if not point.has_point3D():
continue
candidate_observations += 1
point3d_id = int(point.point3D_id)
if point3d_id in seen_point3d_ids:
duplicate_tracks.add(point3d_id)
else:
seen_point3d_ids.add(point3d_id)
x, y = np.asarray(point.xy, dtype=np.float64)
if not np.isfinite(x) or not np.isfinite(y):
continue
center_x = int(np.floor(x + 0.5))
center_y = int(np.floor(y + 0.5))
if not (
_PATCH_RADIUS <= center_x < rgb.shape[1] - _PATCH_RADIUS
and _PATCH_RADIUS <= center_y < rgb.shape[0] - _PATCH_RADIUS
and eroded_mask[center_y, center_x]
):
continue
median = np.median(
rgb[
center_y - _PATCH_RADIUS : center_y + _PATCH_RADIUS + 1,
center_x - _PATCH_RADIUS : center_x + _PATCH_RADIUS + 1,
].reshape(-1, 3),
axis=0,
)
if np.any(median < options.valid_color_min) or np.any(
median > options.valid_color_max
):
continue
observations[point3d_id].append(
(view.image_id, median.astype(np.float64, copy=False))
)
valid_observations += 1
del rgb, eroded_mask, seen_point3d_ids
sampled: dict[int, tuple[tuple[int, NDArray[np.float64]], ...]] = {}
for point3d_id in sorted(observations):
if point3d_id in duplicate_tracks:
continue
track = sorted(observations[point3d_id], key=lambda item: item[0])
image_ids = [image_id for image_id, _ in track]
if len(track) < 2 or len(set(image_ids)) != len(image_ids):
continue
sampled[point3d_id] = tuple(track)
return sampled, candidate_observations, valid_observations
def _build_constraints(
tracks: dict[int, tuple[tuple[int, NDArray[np.float64]], ...]],
min_shared_tracks: int,
) -> tuple[
dict[tuple[int, int], int],
NDArray[np.int64],
NDArray[np.int64],
NDArray[np.float64],
]:
edge_counts: dict[tuple[int, int], int] = defaultdict(int)
for point3d_id in sorted(tracks):
for (image_i, _), (image_j, _) in combinations(tracks[point3d_id], 2):
edge_counts[(image_i, image_j)] += 1
edges = {
edge: edge_counts[edge]
for edge in sorted(edge_counts)
if edge_counts[edge] >= min_shared_tracks
}
del edge_counts
constraint_count = sum(edges.values())
left = np.empty(constraint_count, dtype=np.int64)
right = np.empty(constraint_count, dtype=np.int64)
deltas = np.empty((constraint_count, 3), dtype=np.float64)
index = 0
for point3d_id in sorted(tracks):
for (image_i, color_i), (image_j, color_j) in combinations(
tracks[point3d_id], 2
):
if (image_i, image_j) not in edges:
continue
left[index] = image_i
right[index] = image_j
deltas[index] = np.log(color_j / 255.0) - np.log(color_i / 255.0)
index += 1
return edges, left, right, deltas
def _connected_components(
image_ids: tuple[int, ...],
edges: dict[tuple[int, int], int],
) -> tuple[tuple[int, ...], ...]:
neighbors = {image_id: set() for image_id in image_ids}
for image_i, image_j in edges:
neighbors[image_i].add(image_j)
neighbors[image_j].add(image_i)
components: list[tuple[int, ...]] = []
remaining = set(image_ids)
while remaining:
pending = [min(remaining)]
component: set[int] = set()
while pending:
image_id = pending.pop()
if image_id in component:
continue
component.add(image_id)
pending.extend(sorted(neighbors[image_id] - component, reverse=True))
remaining -= component
components.append(tuple(sorted(component)))
return tuple(sorted(components, key=lambda component: component[0]))
_ComponentSystem = tuple[
tuple[int, ...],
NDArray[np.int64],
NDArray[np.int64],
NDArray[np.int64],
NDArray[np.int64],
]
def _prepare_component_systems(
image_ids: tuple[int, ...],
components: tuple[tuple[int, ...], ...],
left_ids: NDArray[np.int64],
right_ids: NDArray[np.int64],
) -> tuple[
NDArray[np.int64],
NDArray[np.int64],
tuple[_ComponentSystem, ...],
]:
id_to_index = {image_id: index for index, image_id in enumerate(image_ids)}
left_indices = np.fromiter(
(id_to_index[int(value)] for value in left_ids),
dtype=np.int64,
count=len(left_ids),
)
right_indices = np.fromiter(
(id_to_index[int(value)] for value in right_ids),
dtype=np.int64,
count=len(right_ids),
)
component_by_index = np.empty(len(image_ids), dtype=np.int64)
component_indices: list[NDArray[np.int64]] = []
for component_index, component in enumerate(components):
indices = np.fromiter(
(id_to_index[image_id] for image_id in component),
dtype=np.int64,
count=len(component),
)
component_indices.append(indices)
component_by_index[indices] = component_index
if len(components) == 1:
rows_by_component = (np.arange(len(left_indices), dtype=np.int64),)
else:
constraint_components = component_by_index[left_indices]
order = np.argsort(constraint_components, kind="stable")
offsets = np.concatenate(
(
np.zeros(1, dtype=np.int64),
np.cumsum(
np.bincount(
constraint_components,
minlength=len(components),
)
),
)
)
rows_by_component = tuple(
order[offsets[index] : offsets[index + 1]]
for index in range(len(components))
)
systems: list[_ComponentSystem] = []
global_to_local = np.empty(len(image_ids), dtype=np.int64)
for component_index, component in enumerate(components):
indices = component_indices[component_index]
global_to_local[indices] = np.arange(len(indices), dtype=np.int64)
rows = rows_by_component[component_index]
systems.append(
(
component,
indices,
rows,
global_to_local[left_indices[rows]].copy(),
global_to_local[right_indices[rows]].copy(),
)
)
return left_indices, right_indices, tuple(systems)
def _solve_weighted_channel(
image_count: int,
systems: tuple[_ComponentSystem, ...],
deltas: NDArray[np.float64],
weights: NDArray[np.float64],
) -> NDArray[np.float64]:
solved = np.zeros(image_count, dtype=np.float64)
for component, indices, rows, local_left, local_right in systems:
if len(component) == 1:
continue
row_weights = weights[rows]
row_deltas = deltas[rows]
size = len(component)
laplacian = np.zeros((size, size), dtype=np.float64)
rhs = np.zeros(size, dtype=np.float64)
np.add.at(laplacian, (local_left, local_left), row_weights)
np.add.at(laplacian, (local_right, local_right), row_weights)
np.add.at(laplacian, (local_left, local_right), -row_weights)
np.add.at(laplacian, (local_right, local_left), -row_weights)
np.add.at(rhs, local_left, row_weights * row_deltas)
np.add.at(rhs, local_right, -row_weights * row_deltas)
component_solution = np.zeros(size, dtype=np.float64)
try:
component_solution[1:] = np.linalg.solve(
laplacian[1:, 1:], rhs[1:]
)
except np.linalg.LinAlgError as error:
raise ValueError(
f"gain solve failed for component {list(component)}"
) from error
component_solution -= component_solution.mean()
solved[indices] = component_solution
return solved
def _solve_log_gains(
image_ids: tuple[int, ...],
components: tuple[tuple[int, ...], ...],
left_ids: NDArray[np.int64],
right_ids: NDArray[np.int64],
delta_log_rgb: NDArray[np.float64],
options: CalibrationOptions,
) -> tuple[NDArray[np.float64], list[int], list[bool], list[float]]:
log_gains = np.zeros((len(image_ids), 3), dtype=np.float64)
if len(left_ids) == 0:
return log_gains, [0, 0, 0], [True, True, True], [0.0, 0.0, 0.0]
left_indices, right_indices, systems = _prepare_component_systems(
image_ids,
components,
left_ids,
right_ids,
)
iterations_rgb: list[int] = []
converged_rgb: list[bool] = []
residual_rgb: list[float] = []
for channel in range(3):
weights = np.ones(len(left_indices), dtype=np.float64)
previous: NDArray[np.float64] | None = None
converged = False
solution = np.zeros(len(image_ids), dtype=np.float64)
iterations = 0
for iterations in range(1, options.max_iterations + 1):
solution = _solve_weighted_channel(
len(image_ids),
systems,
delta_log_rgb[:, channel],
weights,
)
if previous is not None and np.max(np.abs(solution - previous)) <= options.tolerance:
converged = True
break
residual = (
solution[left_indices]
- solution[right_indices]
- delta_log_rgb[:, channel]
)
scale = max(1.4826 * float(np.median(np.abs(residual))), 1e-6)
threshold = options.huber_k * scale
absolute_residual = np.abs(residual)
weights = np.where(
absolute_residual <= threshold,
1.0,
threshold / np.maximum(absolute_residual, np.finfo(np.float64).tiny),
)
previous = solution
final_residual = (
solution[left_indices]
- solution[right_indices]
- delta_log_rgb[:, channel]
)
log_gains[:, channel] = solution
iterations_rgb.append(iterations)
converged_rgb.append(converged)
residual_rgb.append(float(np.median(np.abs(final_residual))))
if not np.all(np.isfinite(log_gains)):
raise ValueError("gain solve produced non-finite values")
return log_gains, iterations_rgb, converged_rgb, residual_rgb
def calibrate(
dataset: CalibrationDataset,
*,
options: CalibrationOptions = CalibrationOptions(),
) -> CalibrationResult:
_validate_options(options)
tracks, candidate_observations, valid_observations = _sample_tracks(
dataset, options
)
edges, left_ids, right_ids, delta_log_rgb = _build_constraints(
tracks, options.min_shared_tracks
)
constraint_count = len(left_ids)
sampled_track_count = len(tracks)
del tracks
image_ids = tuple(sorted(view.image_id for view in dataset.views))
components = _connected_components(image_ids, edges)
log_gains, iterations, converged, residuals = _solve_log_gains(
image_ids,
components,
left_ids,
right_ids,
delta_log_rgb,
options,
)
del left_ids, right_ids, delta_log_rgb
gains = np.exp(log_gains)
if not np.all(np.isfinite(gains)):
raise ValueError("gain solve produced non-finite values")
id_to_index = {image_id: index for index, image_id in enumerate(image_ids)}
corrected_images: dict[int, NDArray[np.uint8]] = {}
for view in dataset.views:
corrected = read_rgb(view).astype(np.float32)
corrected *= gains[id_to_index[view.image_id]].astype(np.float32)
np.rint(corrected, out=corrected)
np.clip(corrected, 0, 255, out=corrected)
corrected_images[view.image_id] = corrected.astype(np.uint8)
del corrected
component_by_image = {
image_id: component_index
for component_index, component in enumerate(components)
for image_id in component
}
report = {
"schema_version": 1,
"settings": {
"patch_size": 2 * _PATCH_RADIUS + 1,
"mask_erosion_radius": options.mask_erosion_radius,
"min_shared_tracks": options.min_shared_tracks,
"valid_color_range": [
options.valid_color_min,
options.valid_color_max,
],
"huber_k": options.huber_k,
"max_iterations": options.max_iterations,
"tolerance": options.tolerance,
},
"summary": {
"images": len(dataset.views),
"total_tracks": dataset.reconstruction.num_points3D(),
"candidate_observations": candidate_observations,
"valid_observations": valid_observations,
"sampled_tracks": sampled_track_count,
"retained_edges": len(edges),
"constraints": constraint_count,
"connected_components": len(components),
},
"images": [
{
"image_id": view.image_id,
"name": view.name,
"component": component_by_image[view.image_id],
"log_gain_rgb": log_gains[
id_to_index[view.image_id]
].tolist(),
"gain_rgb": gains[id_to_index[view.image_id]].tolist(),
}
for view in dataset.views
],
"edges": [
{
"image_id_a": image_i,
"image_id_b": image_j,
"shared_tracks": shared_tracks,
}
for (image_i, image_j), shared_tracks in edges.items()
],
"solver": {
"iterations_rgb": iterations,
"converged_rgb": converged,
"median_abs_residual_rgb": residuals,
},
}
return CalibrationResult(corrected_images=corrected_images, report=report)
-154
View File
@@ -1,154 +0,0 @@
#!/usr/bin/env python3
"""把 COLMAP txt 格式转成 bin 格式(用 pycolmap 4.x 写入)"""
import sys
import struct
import numpy as np
from pathlib import Path
def write_cameras_bin(cameras, output_path):
"""写 cameras.bin"""
with open(output_path, 'wb') as f:
f.write(struct.pack('Q', len(cameras))) # uint64 num_cameras
for cam_id in sorted(cameras.keys()):
cam = cameras[cam_id]
f.write(struct.pack('I', cam_id)) # uint32 camera_id
# model 映射:SIMPLE_PINHOLE=0, PINHOLE=1, SIMPLE_RADIAL=2, etc.
model_map = {
'SIMPLE_PINHOLE': 0,
'PINHOLE': 1,
'SIMPLE_RADIAL': 2,
'RADIAL': 3,
'OPENCV': 4,
'OPENCV_FISHEYE': 5,
'FULL_OPENCV': 6,
'FOV': 7,
'THIN_PRISM_FISHEYE': 8,
}
model_id = model_map.get(cam['model'], 0)
f.write(struct.pack('I', model_id)) # uint32 model_id
f.write(struct.pack('Q', cam['width'])) # uint64 width
f.write(struct.pack('Q', cam['height'])) # uint64 height
# params
for p in cam['params']:
f.write(struct.pack('d', float(p)))
def write_images_bin(images, output_path):
"""写 images.bin"""
with open(output_path, 'wb') as f:
f.write(struct.pack('Q', len(images))) # uint64 num_images
for img_id in sorted(images.keys()):
img = images[img_id]
f.write(struct.pack('I', img_id)) # uint32 image_id
# qvec (4 doubles)
for q in img['q']:
f.write(struct.pack('d', float(q)))
# tvec (3 doubles)
for t in img['t']:
f.write(struct.pack('d', float(t)))
f.write(struct.pack('I', img['cam_id'])) # uint32 camera_id
# name (null-terminated string)
name_bytes = img['name'].encode('utf-8')
f.write(name_bytes)
f.write(b'\x00')
# num_points2D (uint64)
f.write(struct.pack('Q', 0)) # 简化:0 个 2D 点
def write_points3D_bin(points3D, output_path):
"""写 points3D.bin"""
with open(output_path, 'wb') as f:
f.write(struct.pack('Q', len(points3D))) # uint64 num_points3D
for pt_id in sorted(points3D.keys()):
pt = points3D[pt_id]
f.write(struct.pack('Q', pt_id)) # uint64 point3D_id
# xyz (3 doubles)
for v in pt['xyz']:
f.write(struct.pack('d', float(v)))
# rgb (3 uint8)
f.write(struct.pack('B', 0))
f.write(struct.pack('B', 0))
f.write(struct.pack('B', 0))
# error (double)
f.write(struct.pack('d', 0.0))
# track_length (uint64)
f.write(struct.pack('Q', 0)) # 简化:空 track
def main():
input_dir = Path(sys.argv[1])
output_dir = Path(sys.argv[2])
output_dir.mkdir(parents=True, exist_ok=True)
print(f"Reading txt from {input_dir}")
# 解析 cameras.txt
cameras = {}
with open(input_dir / "cameras.txt") as f:
for line in f:
if line.startswith('#'):
continue
parts = line.strip().split()
if len(parts) < 5:
continue
cam_id = int(parts[0])
cameras[cam_id] = {
'model': parts[1],
'width': int(parts[2]),
'height': int(parts[3]),
'params': [float(x) for x in parts[4:]],
}
# 解析 images.txt
images = {}
with open(input_dir / "images.txt") as f:
lines = f.readlines()
i = 0
while i < len(lines):
line = lines[i].strip()
i += 1
if not line or line.startswith('#'):
continue
parts = line.split()
if len(parts) < 10:
continue
img_id = int(parts[0])
q = [float(x) for x in parts[1:5]]
t = [float(x) for x in parts[5:8]]
cam_id = int(parts[8])
name = parts[9]
images[img_id] = {'name': name, 'cam_id': cam_id, 'q': q, 't': t}
# 跳过 2D 点行
if i < len(lines):
i += 1
# 解析 points3D.txt
points3D = {}
with open(input_dir / "points3D.txt") as f:
for line in f:
if line.startswith('#'):
continue
parts = line.strip().split()
if len(parts) < 5:
continue
pt_id = int(parts[0])
xyz = [float(x) for x in parts[1:4]]
points3D[pt_id] = {'xyz': xyz}
print(f"Found: {len(cameras)} cameras, {len(images)} images, {len(points3D)} points")
# 写 bin
write_cameras_bin(cameras, output_dir / "cameras.bin")
write_images_bin(images, output_dir / "images.bin")
write_points3D_bin(points3D, output_dir / "points3D.bin")
print(f"Written bin files to {output_dir}")
print(f"Files: cameras.bin, images.bin, points3D.bin")
if __name__ == '__main__':
if len(sys.argv) != 3:
print("Usage: python convert_txt_to_bin.py <input_txt_dir> <output_bin_dir>")
sys.exit(1)
main()
-118
View File
@@ -1,118 +0,0 @@
#!/usr/bin/env python3
"""光度标定入口脚本
用法:
python run_calibration.py \
--model_dir /path/to/colmap_model \
--rgb_dir /path/to/rgb \
--mask_dir /path/to/masks \
--output_dir /path/to/output
"""
import argparse
import sys
from pathlib import Path
from calib_io import run_calibration
from calibration import calibrate, CalibrationOptions
def parse_args():
parser = argparse.ArgumentParser(
description="Photometric calibration using COLMAP reconstruction",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"--model_dir", "-m",
type=str, required=True,
help="COLMAP sparse reconstruction directory (contains cameras.bin/images.bin/points3D.bin)",
)
parser.add_argument(
"--rgb_dir", "-r",
type=str, required=True,
help="Directory containing input RGB images",
)
parser.add_argument(
"--mask_dir", "-mk",
type=str, required=True,
help="Directory containing foreground masks (.png, same names as RGB)",
)
parser.add_argument(
"--output_dir", "-o",
type=str, required=True,
help="Output directory for corrected images and report",
)
# ---- 可选参数 ----
parser.add_argument("--mask_erosion_radius", type=int, default=2)
parser.add_argument("--min_shared_tracks", type=int, default=3)
parser.add_argument("--valid_color_min", type=int, default=1)
parser.add_argument("--valid_color_max", type=int, default=254)
parser.add_argument("--huber_k", type=float, default=1.345)
parser.add_argument("--max_iterations", type=int, default=20)
parser.add_argument("--tolerance", type=float, default=1e-6)
return parser.parse_args()
def main():
args = parse_args()
# 路径校验
model_dir = Path(args.model_dir)
rgb_dir = Path(args.rgb_dir)
mask_dir = Path(args.mask_dir)
output_dir = Path(args.output_dir)
if not model_dir.exists():
print(f"[ERROR] model_dir does not exist: {model_dir}", file=sys.stderr)
sys.exit(1)
if not rgb_dir.exists():
print(f"[ERROR] rgb_dir does not exist: {rgb_dir}", file=sys.stderr)
sys.exit(1)
if not mask_dir.exists():
print(f"[ERROR] mask_dir does not exist: {mask_dir}", file=sys.stderr)
sys.exit(1)
# 参数
options = CalibrationOptions(
mask_erosion_radius=args.mask_erosion_radius,
min_shared_tracks=args.min_shared_tracks,
valid_color_min=args.valid_color_min,
valid_color_max=args.valid_color_max,
huber_k=args.huber_k,
max_iterations=args.max_iterations,
tolerance=args.tolerance,
)
print("=" * 50)
print("Photometric Calibration")
print("=" * 50)
print(f" model_dir : {model_dir}")
print(f" rgb_dir : {rgb_dir}")
print(f" mask_dir : {mask_dir}")
print(f" output_dir : {output_dir}")
print(f" min_shared_tracks: {options.min_shared_tracks}")
print(f" mask_erosion_radius: {options.mask_erosion_radius}")
print("-" * 50)
# 执行标定
print("Starting calibration...")
print(f"model_dir={model_dir}")
outputs = run_calibration(
model_dir=str(model_dir),
rgb_dir=str(rgb_dir),
mask_dir=str(mask_dir),
output_dir=str(output_dir),
calibrator=lambda dataset: calibrate(dataset, options=options),
)
print("=" * 50)
print(f"[DONE] Corrected images: {len(outputs.image_paths)}")
print(f" Output dir : {output_dir}")
print(f" Report : {outputs.report_path}")
print("=" * 50)
if __name__ == "__main__":
main()
+1 -128
View File
@@ -48,7 +48,6 @@ ObjModel::MaterialLib::MaterialLib()
bool ObjModel::MaterialLib::Save(const String& prefix, bool texLossless) const
{
DEBUG_EXTRA("MaterialLib::Save %s", prefix.c_str());
std::ofstream out((prefix+".mtl").c_str());
if (!out.good())
return false;
@@ -131,7 +130,7 @@ bool ObjModel::MaterialLib::Load(const String& fileName)
// S T R U C T S ///////////////////////////////////////////////////
bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless, bool bUseExistingUV) const
bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless) const
{
if (vertices.empty())
return false;
@@ -141,9 +140,6 @@ bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless
if (!material_lib.Save(prefix, texLossless))
return false;
if (bUseExistingUV)
return true;
std::ofstream out((prefix + ".obj").c_str());
if (!out.good())
return false;
@@ -273,129 +269,6 @@ bool ObjModel::Load(const String& fileName)
return !vertices.empty();
}
bool ObjModel::LoadUV(const String& fileName, SEACAVE::cList<TexCoord,const TexCoord&,0,8192,uint32_t> &faceTexcoords, bool bLoadUV)
{
DEBUG_EXTRA("LoadUV bLoadUV=%b", bLoadUV);
ASSERT(vertices.empty() && groups.empty() && material_lib.materials.empty());
std::ifstream fin(fileName.c_str());
String line, keyword;
std::istringstream in;
while (fin.good()) {
std::getline(fin, line);
if (line.empty() || line[0u] == '#')
continue;
in.str(line);
in >> keyword;
if (keyword == "v") {
Vertex v;
in >> v[0] >> v[1] >> v[2];
if (in.fail())
{
DEBUG_EXTRA("1");
return false;
}
vertices.push_back(v);
// DEBUG_EXTRA("10, %d", vertices.size());
} else if (keyword == "vt") {
TexCoord vt;
in >> vt[0] >> vt[1];
if (in.fail())
{
DEBUG_EXTRA("2");
return false;
}
texcoords.push_back(vt);
} else if (keyword == "vn") {
Normal vn;
in >> vn[0] >> vn[1] >> vn[2];
if (in.fail())
{
DEBUG_EXTRA("3");
return false;
}
normals.push_back(vn);
} else if (keyword == "f") {
Face f;
memset(&f, 0xFF, sizeof(Face));
for (size_t k = 0; k < 3; ++k) {
in >> keyword;
// 更健壮的解析方法,处理各种索引格式
std::vector<String> parts;
size_t start = 0, end = 0;
// 按'/'分割字符串
while ((end = keyword.find('/', start)) != String::npos) {
parts.push_back(keyword.substr(start, end - start));
start = end + 1;
}
parts.push_back(keyword.substr(start));
// 根据分割后的部分数量处理不同情况
if (parts.size() >= 1 && !parts[0].empty()) {
f.vertices[k] = std::stoi(parts[0]) - OBJ_INDEX_OFFSET;
}
if (parts.size() >= 2 && !parts[1].empty()) {
f.texcoords[k] = std::stoi(parts[1]) - OBJ_INDEX_OFFSET;
}
if (parts.size() >= 3 && !parts[2].empty()) {
f.normals[k] = std::stoi(parts[2]) - OBJ_INDEX_OFFSET;
}
}
if (in.fail())
{
DEBUG_EXTRA("4");
return false;
}
if (bLoadUV)
{
for (int i = 0; i < 3; ++i) {
if (f.texcoords[i] != NO_ID && f.texcoords[i] < texcoords.size()) {
faceTexcoords.push_back(texcoords[f.texcoords[i]]);
} else {
// 索引无效(例如面定义中未提供vt索引或索引越界),使用默认值
faceTexcoords.push_back(Point2f(0.0f, 0.0f)); // 默认UV
DEBUG_EXTRA("Invalid texcoords %d", f.texcoords[i])
}
}
// DEBUG_EXTRA("faceTexcoords push_back [(%f,%f),(%f,%f),(%f,%f)]", texcoords[f.texcoords[0]].x, texcoords[f.texcoords[0]].y,
// texcoords[f.texcoords[1]].x, texcoords[f.texcoords[1]].y, texcoords[f.texcoords[2]].x, texcoords[f.texcoords[2]].y);
}
if (groups.empty())
AddGroup("");
groups.back().faces.push_back(f);
// } else if (keyword == "mtllib") {
// in >> keyword;
// if (!material_lib.Load(keyword))
// DEBUG_EXTRA("3");
// return false;
} else if (keyword == "usemtl") {
Group group;
in >> group.material_name;
if (in.fail())
{
DEBUG_EXTRA("5");
return false;
}
groups.push_back(group);
}
in.clear();
}
DEBUG_EXTRA("6, vertices.size=%d, faceTexcoords.size=%d", vertices.size(), faceTexcoords.size());
return !vertices.empty();
}
ObjModel::Group& ObjModel::AddGroup(const String& material_name)
{
+1 -2
View File
@@ -93,10 +93,9 @@ public:
ObjModel() {}
// Saves the obj model to an .obj file, its material lib and the materials with the given file name
bool Save(const String& fileName, unsigned precision=6, bool texLossless=false, bool bUseExistingUV=false) const;
bool Save(const String& fileName, unsigned precision=6, bool texLossless=false) const;
// Loads the obj model from an .obj file, its material lib and the materials with the given file name
bool Load(const String& fileName);
bool LoadUV(const String& fileName, SEACAVE::cList<TexCoord,const TexCoord&,0,8192,uint32_t> &faceTexcoords, bool bLoadUV = false);
// Creates a new group with the given material name
Group& AddGroup(const String& material_name);
+12 -62
View File
@@ -1191,14 +1191,15 @@ namespace BasicPLY {
} // namespace MeshInternal
// import the mesh from the given file
bool Mesh::Load(const String& fileName, bool bLoadUV)
bool Mesh::Load(const String& fileName)
{
TD_TIMER_STARTD();
const String ext(Util::getFileExt(fileName).ToLower());
bool ret;
if (ext == _T(".obj"))
ret = LoadOBJ(fileName, bLoadUV);
else if (ext == _T(".gltf") || ext == _T(".glb"))
ret = LoadOBJ(fileName);
else
if (ext == _T(".gltf") || ext == _T(".glb"))
ret = LoadGLTF(fileName, ext == _T(".glb"));
else
ret = LoadPLY(fileName);
@@ -1207,43 +1208,6 @@ bool Mesh::Load(const String& fileName, bool bLoadUV)
DEBUG_EXTRA("Mesh loaded: %u vertices, %u faces (%s)", vertices.size(), faces.size(), TD_TIMER_GET_FMT().c_str());
return true;
}
void Mesh::CheckUVValid()
{
for (int_t idxFace = 0; idxFace < (int_t)faces.size(); ++idxFace)
{
FOREACH(idxFace, faces)
{
const FIndex faceID = (FIndex)idxFace;
const TexCoord* uv = &faceTexcoords[faceID * 3];
const Point2f& a = uv[0];
const Point2f& b = uv[1];
const Point2f& c = uv[2];
// DEBUG_EXTRA("a=(%f,%f),b=(%f,%f),c=(%f,%f)", a.x, a.y, b.x, b.y, c.x, c.y);
// 计算边向量
Point2f v0 = b - a;
Point2f v1 = c - a;
float denom = (v0.x * v0.x + v0.y * v0.y) * (v1.x * v1.x + v1.y * v1.y) -
std::pow(v0.x * v1.x + v0.y * v1.y, 2);
// 处理退化三角形情况(面积接近0)
const float epsilon = 1e-10f;
if (std::abs(denom) < epsilon)
{
// DEBUG_EXTRA("PointInTriangle - Degenerate triangle, denom=%.10f", denom);
}
else
{
// DEBUG_EXTRA("PointInTriangle Yes idxFace=%d", idxFace);
}
}
}
}
// import the mesh as a PLY file
bool Mesh::LoadPLY(const String& fileName)
{
@@ -1338,28 +1302,16 @@ bool Mesh::LoadPLY(const String& fileName)
return true;
}
// import the mesh as a OBJ file
bool Mesh::LoadOBJ(const String& fileName, bool bLoadUV)
bool Mesh::LoadOBJ(const String& fileName)
{
ASSERT(!fileName.empty());
Release();
// open and parse OBJ file
ObjModel model;
if (bLoadUV)
{
if (!model.LoadUV(fileName, faceTexcoords, true)) {
DEBUG_EXTRA("error: LoadUV invalid OBJ file %s", fileName.c_str());
return false;
}
}
else
{
if (!model.LoadUV(fileName, faceTexcoords)) {
// if (!model.Load(fileName)) {
DEBUG_EXTRA("error: Load invalid OBJ file %s", fileName.c_str());
return false;
}
if (!model.Load(fileName)) {
DEBUG_EXTRA("error: invalid OBJ file");
return false;
}
if (model.get_vertices().empty() || model.get_groups().empty()) {
@@ -1388,13 +1340,11 @@ bool Mesh::LoadOBJ(const String& fileName, bool bLoadUV)
for (const ObjModel::Face& f: group.faces) {
ASSERT(f.vertices[0] != NO_ID);
faces.emplace_back(f.vertices[0], f.vertices[1], f.vertices[2]);
/*
if (f.texcoords[0] != NO_ID) {
for (int i=0; i<3; ++i)
faceTexcoords.emplace_back(model.get_texcoords()[f.texcoords[i]]);
faceTexindices.emplace_back((TexIndex)groupIdx);
}
*/
if (f.normals[0] != NO_ID) {
Normal& n = faceNormals.emplace_back(Normal::ZERO);
for (int i=0; i<3; ++i)
@@ -1495,13 +1445,13 @@ bool Mesh::LoadGLTF(const String& fileName, bool bBinary)
/*----------------------------------------------------------------*/
// export the mesh to the given file
bool Mesh::Save(const String& fileName, const cList<String>& comments, bool bBinary, bool bUseExistingUV) const
bool Mesh::Save(const String& fileName, const cList<String>& comments, bool bBinary) const
{
TD_TIMER_STARTD();
const String ext(Util::getFileExt(fileName).ToLower());
bool ret;
if (ext == _T(".obj"))
ret = SaveOBJ(fileName, bUseExistingUV);
ret = SaveOBJ(fileName);
else
if (ext == _T(".gltf") || ext == _T(".glb"))
ret = SaveGLTF(fileName, ext == _T(".glb"));
@@ -1588,7 +1538,7 @@ bool Mesh::SavePLY(const String& fileName, const cList<String>& comments, bool b
return true;
}
// export the mesh as a OBJ file
bool Mesh::SaveOBJ(const String& fileName, bool bUseExistingUV) const
bool Mesh::SaveOBJ(const String& fileName) const
{
ASSERT(!fileName.empty());
Util::ensureFolder(fileName);
@@ -1647,7 +1597,7 @@ bool Mesh::SaveOBJ(const String& fileName, bool bUseExistingUV) const
pMaterial->diffuse_map = texturesDiffuse[idxTexture];
}
return model.Save(fileName, 6U, true, bUseExistingUV);
return model.Save(fileName, 6U, true);
}
// export the mesh as a GLTF file
template <typename T>
+4 -6
View File
@@ -262,8 +262,8 @@ public:
bool TransferTexture(Mesh& mesh, const FaceIdxArr& faceSubsetIndices={}, unsigned borderSize=3, unsigned textureSize=4096);
// file IO
bool Load(const String& fileName, bool bLoadUV=false);
bool Save(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true, bool bUseExistingUV=false) const;
bool Load(const String& fileName);
bool Save(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true) const;
bool Save(const FacesChunkArr&, const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true) const;
static bool Save(const VertexArr& vertices, const String& fileName, bool bBinary=true);
@@ -271,15 +271,13 @@ public:
static inline VIndex GetVertex(const Face& f, VIndex v) { const uint32_t idx(FindVertex(f, v)); ASSERT(idx != NO_ID); return f[idx]; }
static inline VIndex& GetVertex(Face& f, VIndex v) { const uint32_t idx(FindVertex(f, v)); ASSERT(idx != NO_ID); return f[idx]; }
void CheckUVValid();
protected:
bool LoadPLY(const String& fileName);
bool LoadOBJ(const String& fileName, bool bLoadUV);
bool LoadOBJ(const String& fileName);
bool LoadGLTF(const String& fileName, bool bBinary=true);
bool SavePLY(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true, bool bTexLossless=true) const;
bool SaveOBJ(const String& fileName, bool bUseExistingUV) const;
bool SaveOBJ(const String& fileName) const;
bool SaveGLTF(const String& fileName, bool bBinary=true) const;
#ifdef _USE_CUDA
+8 -17
View File
@@ -65,6 +65,8 @@ public:
std::map<std::string, std::unordered_set<int>> visible_faces_map;
std::map<std::string, std::unordered_set<int>> edge_faces_map;
std::map<std::string, std::unordered_set<int>> delete_edge_faces_map;
std::map<std::string, std::unordered_set<int>> delete_edge_faces_map2;
std::map<std::string, std::unordered_set<int>> face_normal_visible_map;
std::unordered_set<int> face_visible_relative;
public:
@@ -163,34 +165,23 @@ public:
std::unordered_set<int>& face_visible_relative,
std::map<std::string, std::unordered_set<int>>& edge_faces_map,
std::map<std::string, std::unordered_set<int>>& delete_edge_faces_map,
std::map<std::string, std::unordered_set<int>>& delete_edge_faces_map2,
std::map<std::string, std::unordered_set<int>>& face_normal_visible_map,
std::string& basePath);
bool LoadVisibleFacesData() {
std::map<std::string, std::unordered_set<int>> visible_faces_map;
std::unordered_set<int> face_visible_relative;
std::map<std::string, std::unordered_set<int>> edge_faces_map;
std::map<std::string, std::unordered_set<int>> delete_edge_faces_map;
std::string basePath = GetDefaultBasePath(); // 你需要实现这个函数
return LoadVisibleFacesData(visible_faces_map, face_visible_relative,
edge_faces_map, delete_edge_faces_map, basePath);
}
std::string GetDefaultBasePath() {
// 根据你的需求实现这个函数
// 例如:返回一个默认的路径,或者从配置中读取
return "default_path";
}
// Mesh texturing
bool TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned minCommonCameras=0, float fOutlierThreshold=0.f, float fRatioDataSmoothness=0.3f,
bool bGlobalSeamLeveling=true, bool bLocalSeamLeveling=true, unsigned nTextureSizeMultiple=0, unsigned nRectPackingHeuristic=3, Pixel8U colEmpty=Pixel8U(255,127,39),
float fSharpnessWeight=0.5f, int ignoreMaskLabel=-1, int maxTextureSize=0, const IIndexArr& views=IIndexArr(), const SEACAVE::String& basename = "", bool bOriginFaceview = false,
const std::string& inputFileName = "", const std::string& meshFileName = "", bool bUseExistingUV = false, const std::string& strUVMeshFileName = "");
const std::string& inputFileName = "", const std::string& meshFileName = "");
std::string runPython(const std::string& command);
bool is_face_visible(const std::string& image_name, int face_index);
bool is_face_visible_relative(int face_index);
bool is_face_edge(const std::string& image_name, int face_index);
bool is_face_delete_edge(const std::string& image_name, int face_index);
bool is_face_delete_edge2(const std::string& image_name, int face_index);
bool is_face_normal_visible_map(const std::string& image_name, int face_index);
void SegmentMeshBasedOnCurvature(Mesh::FaceIdxArr& regionMap, float curvatureThreshold);
+4467 -14379
View File
File diff suppressed because it is too large. Load diff
-120
View File
@@ -1,120 +0,0 @@
"""
calibrate.py —— 在跑 OpenMVS 之前,对输入照片做白平衡 + 曝光统一。
这是消除"源视图色温差异"最彻底、最安全的办法:在像素进入纹理管线之前就对齐。
用法: python calibrate.py <image_dir> <out_dir>
做了两件事(都可选、都保守,不会破坏细节):
1) Gray-World 白平衡:把每张图的通道均值拉向全局中值(去色温差)
2) 全局曝光归一化:把所有图的亮度中位数对齐到全局中位数(去曝光差)
注意:OpenMVS 的 EstimateGlobalPhotometricCorrection 强依赖稠密点云,
如果没有点云它会被跳过(你之前日志里 "Photometric: no point cloud, skip")。
所以在这里预处理是最稳的兜底。
"""
import os, sys, glob
import numpy as np
import cv2
def gray_world(img_bgr, ref_bgr=None):
"""灰度世界白平衡。
若给定 ref_bgr(全局参考图的 BGR 均值),则把本图通道均值拉向 ref;
否则用本图三通道的均值作为中性灰参考(单图自校正)。
不做任何“保守衰减”——必须完全对齐,否则等于没做。
"""
b, g, r = [img_bgr[:, :, c].astype(np.float32) for c in range(3)]
mb, mg, mr = b.mean(), g.mean(), r.mean()
if ref_bgr is not None:
rb, rg, rr = ref_bgr
else:
# 单图自校正:以三通道共同均值作为中性灰目标
m = (mb + mg + mr) / 3.0
rb, rg, rr = m, m, m
kb = rb / mb if mb > 1e-6 else 1.0
kg = rg / mg if mg > 1e-6 else 1.0
kr = rr / mr if mr > 1e-6 else 1.0
# 限制增益上限,防止极端单图把噪声放大
def clamp_gain(k):
return min(max(k, 0.3), 3.0)
kb, kg, kr = clamp_gain(kb), clamp_gain(kg), clamp_gain(kr)
out = np.stack([
np.clip(b * kb, 0, 255),
np.clip(g * kg, 0, 255),
np.clip(r * kr, 0, 255),
], axis=2).astype(np.uint8)
return out
def normalize_exposure(img_bgr, target_median):
"""曝光归一化:按亮度中位数线性缩放,对齐到全局目标中位数。
只做温和限制(上限 2.0),保证不同视图曝光真正一致。"""
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY).astype(np.float32)
med = np.median(gray)
if med < 1.0:
return img_bgr
scale = target_median / med
scale = min(scale, 2.0) # 防止极暗图放大噪声,但允许充分提亮
out = np.clip(img_bgr.astype(np.float32) * scale, 0, 255).astype(np.uint8)
return out
def main():
if len(sys.argv) < 3:
print("usage: python calibrate.py <image_dir> <out_dir>")
sys.exit(1)
in_dir, out_dir = sys.argv[1], sys.argv[2]
os.makedirs(out_dir, exist_ok=True)
paths = sorted(glob.glob(os.path.join(in_dir, "*.*")))
paths = [p for p in paths if p.lower().endswith((".jpg", ".jpeg", ".png", ".tif", ".bmp"))]
if not paths:
print("no images found in", in_dir)
sys.exit(1)
# ---- 第一遍:统计所有图,确定全局参考(目标白平衡 + 目标亮度)----
means = [] # (mb, mg, mr)
meds = []
for p in paths:
img = cv2.imread(p)
if img is None:
continue
b, g, r = img[:, :, 0].astype(np.float32), img[:, :, 1].astype(np.float32), img[:, :, 2].astype(np.float32)
means.append((b.mean(), g.mean(), r.mean()))
gray = 0.299 * r + 0.587 * g + 0.114 * b
meds.append(np.median(gray))
means = np.array(means)
meds = np.array(meds)
# 全局参考 = 所有图通道均值的均值(= 数据集整体真实色彩),亮度取中位数
ref_b = means[:, 0].mean()
ref_g = means[:, 1].mean()
ref_r = means[:, 2].mean()
ref_bgr = (ref_b, ref_g, ref_r)
target_median = float(np.median(meds))
print(f"[calibrate] {len(paths)} images")
print(f"[calibrate] global reference white-balance (B,G,R) = ({ref_b:.1f}, {ref_g:.1f}, {ref_r:.1f})")
print(f"[calibrate] global reference luminance median = {target_median:.1f}")
# ---- 第二遍:所有图统一校正到同一参考 ----
for i, p in enumerate(paths):
img = cv2.imread(p)
if img is None:
continue
# 1) 白平衡:把本图拉向全局参考 ref_bgr
out = gray_world(img, ref_bgr=ref_bgr)
# 2) 曝光归一化:亮度中位数对齐到全局目标
out = normalize_exposure(out, target_median)
name = os.path.basename(p)
cv2.imwrite(os.path.join(out_dir, name), out)
print(f"[calibrate] done -> {out_dir}")
if __name__ == "__main__":
main()
+139 -159
View File
@@ -4,7 +4,8 @@ import os
import numpy as np
from scipy.spatial.transform import Rotation
import sys
sys.path.append("/home/algo/Documents/openMVS/openMVS/libs/MVS/utils")
# sys.path.append("/home/algo/Documents/openMVS/openMVS/libs/MVS/utils")
sys.path.append("/root/code/openMVS/libs/MVS/utils")
from colmap_loader import read_cameras_text, read_images_text, read_int_text, write_int_text, read_indices_from_file
# from get_pose_matrix import get_w2c
import argparse
@@ -33,6 +34,7 @@ from typing import Dict, List, Set
import struct
import math
# import os
from pathlib import Path
CameraModel = collections.namedtuple(
"CameraModel", ["model_id", "model_name", "num_params"]
@@ -1350,14 +1352,90 @@ class ModelProcessor:
face_visible = v0_visible | v1_visible | v2_visible
# ============ 新增:法线夹角过滤 ============
# 1. 获取面片法线(向量化计算,性能更好)
if not hasattr(self, 'face_normals_tensor'):
# 从mesh获取顶点和面片索引
vertices = np.asarray(self.mesh.vertices) # 形状: (V, 3)
triangles = np.asarray(self.mesh.triangles) # 形状: (F, 3)
# 向量化计算法线
# 获取所有三角形的顶点
v0 = vertices[triangles[:, 0]] # 形状: (F, 3)
v1 = vertices[triangles[:, 1]] # 形状: (F, 3)
v2 = vertices[triangles[:, 2]] # 形状: (F, 3)
# 计算边向量
edge1 = v1 - v0
edge2 = v2 - v0
# 向量化叉积
# np.cross支持批量计算
normals = np.cross(edge1, edge2)
# 计算法线长度
norms = np.linalg.norm(normals, axis=1, keepdims=True)
# 避免除零
norms[norms < 1e-6] = 1.0
# 归一化
normals = normals / norms
# 将退化三角形的法线设为默认值
mask = norms[:, 0] < 1e-6
if np.any(mask):
normals[mask] = np.array([0.0, 0.0, 1.0])
# 转换为GPU张量
self.face_normals_tensor = torch.tensor(normals, device=self.device, dtype=torch.float32)
# 2. 计算摄像机方向(从面片中心指向摄像机)
# 获取面片顶点坐标
v0_pos = self.vertices_tensor[v0_indices]
v1_pos = self.vertices_tensor[v1_indices]
v2_pos = self.vertices_tensor[v2_indices]
# 计算面片中心
face_centers = (v0_pos + v1_pos + v2_pos) / 3.0
# 计算摄像机方向向量
camera_pos = torch.tensor(eye, device=self.device, dtype=torch.float32)
if len(camera_pos.shape) == 1:
camera_pos = camera_pos.unsqueeze(0) # 从(3)变为(1,3)
# 方向:从面片中心指向摄像机
view_dirs = camera_pos - face_centers
view_dirs = view_dirs / torch.norm(view_dirs, dim=1, keepdim=True)
# 3. 计算法线与摄像机方向的夹角
face_normals = self.face_normals_tensor
# 归一化法线
face_normals = face_normals / torch.norm(face_normals, dim=1, keepdim=True)
# 计算点积(余弦值)
cos_angles = torch.sum(face_normals * view_dirs, dim=1)
# 计算角度(弧度转角度)
angles_deg = torch.acos(torch.clamp(cos_angles, -1.0, 1.0)) * 180.0 / torch.pi
# 4. 创建法线可见性掩码(夹角 ≤ 45度)
normal_visible_mask = angles_deg <= 40.0
# 5. 结合遮挡可见性和法线可见性
face_normal_visible = face_visible & normal_visible_mask
# ============ 法线夹角过滤结束 ============
# 使用与CPU版本相同的后续处理
shrunk_visibility = self._shrink_face_visibility(face_visible.cpu().numpy(), 6)
expanded_visibility = self._expand_face_visibility(face_visible.cpu().numpy(), 30)
shrunk_visibility2 = self._shrink_face_visibility(face_visible.cpu().numpy(), 50)
expanded_visibility = self._expand_face_visibility(face_visible.cpu().numpy(), 0)
shrunk_visibility2 = self._shrink_face_visibility(face_visible.cpu().numpy(), 0)
shrunk_visibility3 = self._shrink_face_visibility(face_visible.cpu().numpy(), 100)
expanded_edge = expanded_visibility & ~shrunk_visibility2
delete_edge = face_visible.cpu().numpy() & ~shrunk_visibility
delete_edge2 = face_visible.cpu().numpy() & ~shrunk_visibility3
return shrunk_visibility, expanded_edge, delete_edge
return shrunk_visibility, expanded_edge, delete_edge, delete_edge2, face_normal_visible
"""
def _gen_depth_image_gpu(self, cam_data, render):
@@ -1388,6 +1466,8 @@ class ModelProcessor:
visible_faces_dict = {}
edge_faces_dict = {}
delete_edge_faces_dict = {}
delete_edge_faces_dict2 = {}
face_normal_visible_dict = {}
total_start = time.time()
@@ -1413,13 +1493,15 @@ class ModelProcessor:
# continue
start_time = time.time()
face_visibility, face_edge, face_delete_edge = self._flag_model_gpu(camera_data)
face_visibility, face_edge, face_delete_edge, face_delete_edge2, face_normal_visible = self._flag_model_gpu(camera_data)
processing_time = time.time() - start_time
visible_faces = np.where(face_visibility)[0].tolist()
visible_faces_dict[img_name] = visible_faces
edge_faces_dict[img_name] = np.where(face_edge)[0].tolist()
delete_edge_faces_dict[img_name] = np.where(face_delete_edge)[0].tolist()
delete_edge_faces_dict2[img_name] = np.where(face_delete_edge2)[0].tolist()
face_normal_visible_dict[img_name] = np.where(face_normal_visible.cpu().numpy())[0].tolist()
print(f"图像 {img_name} 处理完成,耗时: {processing_time:.2f}秒,可见面数量{len(visible_faces)}")
@@ -1427,12 +1509,14 @@ class ModelProcessor:
print(f"所有图像处理完成,总耗时: {total_time:.2f}秒")
print(f"平均每张图像耗时: {total_time/len(images):.2f}秒")
self.save_occlusion_data(visible_faces_dict, edge_faces_dict, delete_edge_faces_dict, self.asset_dir)
self.save_occlusion_data(visible_faces_dict, edge_faces_dict, delete_edge_faces_dict, delete_edge_faces_dict2, face_normal_visible_dict, self.asset_dir)
return {
"result1": visible_faces_dict,
"result2": edge_faces_dict,
"result3": delete_edge_faces_dict
"result3": delete_edge_faces_dict,
"result4": delete_edge_faces_dict2,
"result5": face_normal_visible_dict
}
#"""
@@ -1926,64 +2010,6 @@ class ModelProcessor:
depth = render.render_to_depth_image(z_in_view_space=True)
return np.asarray(depth)
def sort_vertices(vertices_original):
return sorted(
(v for v in vertices_original),
key=lambda v: (v.co.x, v.co.y, v.co.z)
)
def _flag_model(self, camera_data, face_points):
"""标记可见顶点"""
vertex_visible = []
vertex_occlusion = []
depth_images = []
render = o3d.visualization.rendering.OffscreenRenderer(camera_data['width'], camera_data['height'])
material = o3d.visualization.rendering.MaterialRecord()
render.scene.add_geometry("mesh", self.mesh, material)
# 生成深度图
depth_image = self._gen_depth_image(camera_data, render)
# 计算可见性
R = self.qvec2rotmat(camera_data['qvec']).T
eye = -R @ camera_data['tvec']
# eye = camera_data['tvec']
# final_visible_list, final_occlusion_list, final_vertex_difference_list = self._compute_vertex_in_frustum(
final_visible_list, final_occlusion_list = self._compute_vertex_in_frustum(
camera_data['fx'], camera_data['fy'],
camera_data['cx'], camera_data['cy'],
R, eye,
camera_data['height'], camera_data['width'],
depth_image,camera_data['qvec'], camera_data['tvec']
)
print("_flag_model", len(final_occlusion_list), len(self.mesh.vertices), len(self.mesh.vertex_colors))
# 获取三角形面片数组
triangles = np.asarray(self.mesh.triangles)
face_visible_bitmap = np.zeros(len(triangles), dtype=bool)
# 遍历所有面片
for face_idx, face in enumerate(triangles):
v0, v1, v2 = face
face_visible_bitmap[face_idx] = any([ # any all
final_visible_list[v0],
final_visible_list[v1],
final_visible_list[v2]
])
shrunk_visibility = self._shrink_face_visibility(face_visible_bitmap, 6) # 6 10
expanded_visibility = self._expand_face_visibility(face_visible_bitmap, 30)
shrunk_visibility2 = self._shrink_face_visibility(face_visible_bitmap, 50)
expanded_edge = expanded_visibility & ~shrunk_visibility2
delete_edge = face_visible_bitmap & ~shrunk_visibility
return shrunk_visibility, expanded_edge, delete_edge
def _flag_contour(self, camera_data, face_points):
"""标记可见顶点"""
vertex_visible = []
@@ -2073,97 +2099,11 @@ class ModelProcessor:
face_visible_bitmap = np.ones(len(triangles), dtype=bool) # 临时填充
return face_visible_bitmap, face_edge
"""
def _mask_face_occlusion(self):
# 读取相机数据
cameras = read_cameras_text(os.path.join(self.pose_path, "cameras.txt"))
images = read_images_text(os.path.join(self.pose_path, "images.txt"))
# cameras = read_cameras_text(os.path.join(self.pose_path, "backup_cameras.txt"))
# images = read_images_text(os.path.join(self.pose_path, "backup_images.txt"))
face_points_sorted_path = os.path.join(self.pose_path, "face_points_sorted.txt")
print("face_points_sorted_path=", face_points_sorted_path)
#face_points = read_int_text(face_points_sorted_path)
face_points = read_indices_from_file(face_points_sorted_path)
# face_points = {}
camera_data = {}
for img in images.values():
if self.mask_image == img.name[:-4]:
camera = cameras[img.camera_id]
camera_data = {
"qvec": img.qvec,
"tvec": img.tvec,
"fx": camera.params[0],
"fy": camera.params[1],
"cx": camera.params[2],
"cy": camera.params[3],
"width": camera.width,
"height": camera.height,
"name": img.name[:-4]
}
# print(face_points)
self._flag_model(camera_data, face_points)
"""
def _mask_occlusion(self):
# 读取相机数据
cameras = read_cameras_text(os.path.join(self.pose_path, "cameras.txt"))
images = read_images_text(os.path.join(self.pose_path, "images.txt"))
camera_data = {}
countour_faces_dict = {}
visible_faces_dict = {}
edge_faces_dict = {}
delete_edge_faces_dict = {}
total_start = time.time()
n = 0
for img in images.values():
camera = cameras[img.camera_id]
camera_data = {
"qvec": img.qvec,
"tvec": img.tvec,
"fx": camera.params[0],
"fy": camera.params[1],
"cx": camera.params[2],
"cy": camera.params[3],
"width": camera.width,
"height": camera.height,
"name": img.name[:-4]
}
img_name = img.name[:-4]
# if (img_name!="73_8" and img_name!="52_8" and img_name!="62_8"):
# if (img_name!="52_8" and img_name!="62_8"):
# if (img_name!="52_8"):
# continue
start_time = time.time()
face_visibility, face_edge, face_delete_edge = self._flag_model(camera_data, None)
processing_time = time.time() - start_time
visible_faces = np.where(face_visibility)[0].tolist()
visible_faces_dict[img.name[:-4]] = visible_faces
edge_faces_dict[img.name[:-4]] = np.where(face_edge)[0].tolist()
delete_edge_faces_dict[img.name[:-4]] = np.where(face_delete_edge)[0].tolist()
n += 1
print(f"图像={img_name},耗时={processing_time:.2f}秒,可见面数={len(visible_faces)}")
total_time = time.time() - total_start
print(f"所有图像处理完成,总耗时: {total_time:.2f}秒")
print(f"平均每张图像耗时: {total_time/len(images):.2f}秒")
self.save_occlusion_data(visible_faces_dict, edge_faces_dict, delete_edge_faces_dict, self.asset_dir)
return {"result1": visible_faces_dict, "result2": edge_faces_dict, "result3": delete_edge_faces_dict}
def save_occlusion_data(self, result1: Dict[str, List[int]],
result2: Dict[str, List[int]],
result3: Dict[str, List[int]],
result3: Dict[str, List[int]],
result4: Dict[str, List[int]],
result5: Dict[str, List[int]],
base_path: str) -> None:
"""
保存遮挡数据到文件
@@ -2174,6 +2114,8 @@ class ModelProcessor:
result3: 删除边面字典,包含图像名称和对应的删除边面列表
base_path: 基础文件路径
"""
os.makedirs(base_path, exist_ok = True)
print(f"save_occlusion_data {base_path}, {len(result1)}, {len(result2)}, {len(result3)}")
@@ -2196,10 +2138,20 @@ class ModelProcessor:
delete_edge_faces_map: Dict[str, Set[int]] = {}
for image_name, face_list in result3.items():
delete_edge_faces_map[image_name] = set(face_list)
delete_edge_faces_map2: Dict[str, Set[int]] = {}
for image_name, face_list in result4.items():
delete_edge_faces_map2[image_name] = set(face_list)
face_normal_visible_map: Dict[str, Set[int]] = {}
for image_name, face_list in result5.items():
face_normal_visible_map[image_name] = set(face_list)
# 保存 visible_faces_map
try:
with open(base_path + "/_visible_faces_map.txt", "w", encoding='utf-8') as map_file:
file_name = "_visible_faces_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file:
for image_name, face_set in visible_faces_map.items():
# 写入图像名称和所有面ID,用空格分隔
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
@@ -2209,7 +2161,9 @@ class ModelProcessor:
# 保存 face_visible_relative
try:
with open(base_path + "/_face_visible_relative.txt", "w", encoding='utf-8') as relative_file:
file_name = "_face_visible_relative.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as relative_file:
for face in face_visible_relative:
relative_file.write(str(face) + "\n")
except IOError as e:
@@ -2217,7 +2171,9 @@ class ModelProcessor:
# 保存 edge_faces_map
try:
with open(base_path + "/_edge_faces_map.txt", "w", encoding='utf-8') as map_file2:
file_name = "_edge_faces_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file2:
for image_name, face_set in edge_faces_map.items():
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
map_file2.write(line)
@@ -2226,13 +2182,37 @@ class ModelProcessor:
# 保存 delete_edge_faces_map
try:
with open(base_path + "/_delete_edge_faces_map.txt", "w", encoding='utf-8') as map_file3:
file_name = "_delete_edge_faces_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file3:
for image_name, face_set in delete_edge_faces_map.items():
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
map_file3.write(line)
except IOError as e:
print(f"Error writing delete_edge_faces_map file: {e}")
# 保存 delete_edge_faces_map2
try:
file_name = "_delete_edge_faces_map2.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file4:
for image_name, face_set in delete_edge_faces_map2.items():
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
map_file4.write(line)
except IOError as e:
print(f"Error writing delete_edge_faces_map2 file: {e}")
# 保存 face_normal_visible_map
try:
file_name = "_face_normal_visible_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file5:
for image_name, face_set in face_normal_visible_map.items():
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
map_file5.write(line)
except IOError as e:
print(f"Error writing _face_normal_visible_map file: {e}")
def process(self):
print("process")