Compare commits
133
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2434c4140d | ||
|
|
abb81f6bfd | ||
|
|
200e13a7ba | ||
|
|
7a86495d6a | ||
|
|
efba6dbe29 | ||
|
|
38c0b9948a | ||
|
|
6f7805c78d | ||
|
|
ac85d3a23c | ||
|
|
1f272668f1 | ||
|
|
22bfc8adfb | ||
|
|
17446d4053 | ||
|
|
5b1b063bee | ||
|
|
d5aa82f15e | ||
|
|
95570fede1 | ||
|
|
4d3e0ee04f | ||
|
|
d4330afd6f | ||
|
|
2f88885ccc | ||
|
|
07a4ade712 | ||
|
|
433a06716a | ||
|
|
875f0c5be5 | ||
|
|
209fe6a260 | ||
|
|
74271d0ad6 | ||
|
|
272d3900d0 | ||
|
|
6b67c75c7e | ||
|
|
f7fccb2ae9 | ||
|
|
c5f93374f4 | ||
|
|
351a0a274f | ||
|
|
b106a7976e | ||
|
|
05bae3ad90 | ||
|
|
eba47cae18 | ||
|
|
5f06661d9b | ||
|
|
73044aa575 | ||
|
|
4d5f371e99 | ||
|
|
56fcf290ba | ||
|
|
47b5e629f0 | ||
|
|
e68ecd01db | ||
|
|
990fff69c5 | ||
|
|
c5dc504b1f | ||
|
|
4713198454 | ||
|
|
a86f98ea46 | ||
|
|
b45baaa39e | ||
|
|
59f1f7e873 | ||
|
|
2e8b15f3a9 | ||
|
|
c809edbc8e | ||
|
|
453622d3f2 | ||
|
|
e555b9c804 | ||
|
|
974095dafa | ||
|
|
2d1f6a13e5 | ||
|
|
c287fe0fc7 | ||
|
|
dbbcb4d557 | ||
|
|
d442e213d7 | ||
|
|
f96aca73a1 | ||
|
|
4ae6404044 | ||
|
|
9665d1226a | ||
|
|
10accfce73 | ||
|
|
8a62a8766c | ||
|
|
32a270b287 | ||
|
|
93dbc93c72 | ||
|
|
707a62057a | ||
|
|
864f5cafb9 | ||
|
|
593ec6cc02 | ||
|
|
7ff01eafdb | ||
|
|
902170f747 | ||
|
|
c84dab57f0 | ||
|
|
adedcfd4f1 | ||
|
|
56f3c93216 | ||
|
|
b89bf91ccf | ||
|
|
04495f3426 | ||
|
|
b8f2e18fd3 | ||
|
|
5cf68ce634 | ||
|
|
c8969a05cb | ||
|
|
1b9cf3ea46 | ||
|
|
9fb90b00d7 | ||
|
|
35319544dd | ||
|
|
411e41e54f | ||
|
|
73b8d77980 | ||
|
|
e208883638 | ||
|
|
9d29df54a9 | ||
|
|
6e0e0f9cc7 | ||
|
|
881bf55089 | ||
|
|
fb20fcf2c6 | ||
|
|
56ad2c49cb | ||
|
|
af1c3c0510 | ||
|
|
e8e3632341 | ||
|
|
1fe47d9448 | ||
|
|
1fb47e1afe | ||
|
|
fb5839c8e7 | ||
|
|
e5411b42b4 | ||
|
|
81036ad095 | ||
|
|
ff7b75d219 | ||
|
|
b0c165499c | ||
|
|
3adc1c012a | ||
|
|
93ee28a3ca | ||
|
|
6922abe692 | ||
|
|
5229b4f284 | ||
|
|
d95c2fc8ff | ||
|
|
29388cf341 | ||
|
|
42f6d9c84c | ||
|
|
82c019ee49 | ||
|
|
46d5d65b3f | ||
|
|
93e7d60416 | ||
|
|
680c34242a | ||
|
|
4a9013f12f | ||
|
|
0d816514fa | ||
|
|
ddb42d08f4 | ||
|
|
7a6887349e | ||
|
|
c70d56d3cb | ||
|
|
f831f93361 | ||
|
|
0e1b02cda5 | ||
|
|
5e59d7c60b | ||
|
|
c38456c7b4 | ||
|
|
3843bea5f6 | ||
|
|
992439bc44 | ||
|
|
4105129a7d | ||
|
|
8529835712 | ||
|
|
e2c5313fdd | ||
|
|
c9d1eac96a | ||
|
|
0dceffb66f | ||
|
|
f787e874aa | ||
|
|
01ef0f2a1d | ||
|
|
d82e3f491a | ||
|
|
811f85bbbd | ||
|
|
80c0dcbb49 | ||
|
|
1b93beb86a | ||
|
|
84a492600f | ||
|
|
0f24212b87 | ||
|
|
6271fb3af0 | ||
|
|
7747494618 | ||
|
|
aadfbb4b08 | ||
|
|
e1258c2d3e | ||
|
|
c70d12b645 | ||
|
|
7fd81ffbea | ||
|
|
5e1623e303 |
No files matched your search
+37
-1
@@ -7,7 +7,6 @@
|
|||||||
# Precompiled Headers
|
# Precompiled Headers
|
||||||
*.gch
|
*.gch
|
||||||
*.pch
|
*.pch
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Compiled Dynamic libraries
|
# Compiled Dynamic libraries
|
||||||
*.so
|
*.so
|
||||||
@@ -75,3 +74,40 @@ libs/MVS/texture_output/texture_33.png
|
|||||||
libs/MVS/texture_output/texture_34.png
|
libs/MVS/texture_output/texture_34.png
|
||||||
libs/MVS/texture_output/texture_35.png
|
libs/MVS/texture_output/texture_35.png
|
||||||
texture_output/texture_0.png
|
texture_output/texture_0.png
|
||||||
|
color_change/__pycache__/calib_io.cpython-310.pyc
|
||||||
|
.gitignore
|
||||||
|
.gitignore
|
||||||
|
color_change/__pycache__/calibration.cpython-310.pyc
|
||||||
|
fill2sc/632723.jpg
|
||||||
|
fill2sc/632723.mtl
|
||||||
|
fill2sc/fill_preview.png
|
||||||
|
fill2sc/fill_report.txt
|
||||||
|
.gitignore
|
||||||
|
fill2sc/index.txt
|
||||||
|
fill2sc/data/20261102/20261102_glasses_vertices.txt
|
||||||
|
fill2sc/data/20261102/20261102_glasses.blend
|
||||||
|
fill2sc/data/20261102/20261102.mtl
|
||||||
|
fill2sc/data/20261102/20261102.zip
|
||||||
|
fill2sc/data/20261102/20261102Tex1.jpg
|
||||||
|
fill2sc/data/20261102/fill_20261102_preview.png
|
||||||
|
fill2sc/data/20261102/index.txt
|
||||||
|
fill2sc/data/631230/631230_glasses_vertices.txt
|
||||||
|
fill2sc/data/631230/631230_glasses.blend
|
||||||
|
fill2sc/data/631230/631230.jpg
|
||||||
|
fill2sc/data/631230/631230.mtl
|
||||||
|
fill2sc/data/631230/631230.zip
|
||||||
|
fill2sc/data/631230/fill_631230_preview.png
|
||||||
|
fill2sc/data/631230/index.txt
|
||||||
|
fill2sc/data/632723/632723.jpg
|
||||||
|
fill2sc/data/632723/632723.mtl
|
||||||
|
fill2sc/data/632723/fill_632723_preview.png
|
||||||
|
fill2sc/data/632723/fill_preview.png
|
||||||
|
fill2sc/data/632723/fill_report.txt
|
||||||
|
fill2sc/data/632723/index.txt
|
||||||
|
fill2sc/data/636394/636394_glasses_vertices.txt
|
||||||
|
fill2sc/data/636394/636394_glasses.blend
|
||||||
|
fill2sc/data/636394/636394.jpg
|
||||||
|
fill2sc/data/636394/636394.mtl
|
||||||
|
fill2sc/data/636394/636394.zip
|
||||||
|
fill2sc/data/636394/fill_636394_preview.png
|
||||||
|
fill2sc/data/636394/index.txt
|
||||||
+1
-1
@@ -52,7 +52,7 @@ ENDIF()
|
|||||||
PROJECT(OpenMVS)
|
PROJECT(OpenMVS)
|
||||||
|
|
||||||
SET(ENV_PYTHON_PATH "/home/algo/.conda/envs/py310_pyt210/lib/python3.10/site-packages")
|
SET(ENV_PYTHON_PATH "/home/algo/.conda/envs/py310_pyt210/lib/python3.10/site-packages")
|
||||||
SET(ENV_MVS_PATH "/home/algo/Documents/openMVS/openMVS/libs/MVS")
|
SET(ENV_MVS_PATH "/home/algo/Documents/openMVS/openMVS.ManualUV/libs/MVS")
|
||||||
|
|
||||||
CONFIGURE_FILE(
|
CONFIGURE_FILE(
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/build/Templates/ConfigEnv.h.in"
|
"${CMAKE_CURRENT_SOURCE_DIR}/build/Templates/ConfigEnv.h.in"
|
||||||
|
|||||||
@@ -94,6 +94,9 @@ unsigned nMaxThreads;
|
|||||||
int nMaxTextureSize;
|
int nMaxTextureSize;
|
||||||
String strExportType;
|
String strExportType;
|
||||||
String strConfigFileName;
|
String strConfigFileName;
|
||||||
|
bool bUseExistingUV;
|
||||||
|
String strUVMeshFileName;
|
||||||
|
|
||||||
boost::program_options::variables_map vm;
|
boost::program_options::variables_map vm;
|
||||||
} // namespace OPT
|
} // namespace OPT
|
||||||
|
|
||||||
@@ -163,6 +166,8 @@ 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")
|
("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")
|
("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")
|
("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
|
// hidden options, allowed both on command line and
|
||||||
@@ -1098,9 +1103,7 @@ int main(int argc, LPCTSTR* argv)
|
|||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
printf("Now\n");
|
printf("Now\n");
|
||||||
VERBOSE(MAKE_PATH_SAFE(OPT::strMeshFileName));
|
VERBOSE(MAKE_PATH_SAFE(OPT::strMeshFileName));
|
||||||
bool bVertexColorExists = false;
|
if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName))) {
|
||||||
bool *pBExistVertexColor = &bVertexColorExists;
|
|
||||||
if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName), pBExistVertexColor)) {
|
|
||||||
VERBOSE("error: cannot load mesh file");
|
VERBOSE("error: cannot load mesh file");
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
@@ -1136,12 +1139,12 @@ int main(int argc, LPCTSTR* argv)
|
|||||||
if (!scene.TextureMesh(OPT::nResolutionLevel, OPT::nMinResolution, OPT::minCommonCameras, OPT::fOutlierThreshold, OPT::fRatioDataSmoothness,
|
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::bGlobalSeamLeveling, OPT::bLocalSeamLeveling, OPT::nTextureSizeMultiple, OPT::nRectPackingHeuristic, Pixel8U(OPT::nColEmpty),
|
||||||
OPT::fSharpnessWeight, OPT::nIgnoreMaskLabel, OPT::nMaxTextureSize, views, baseFileName, OPT::bOriginFaceview,
|
OPT::fSharpnessWeight, OPT::nIgnoreMaskLabel, OPT::nMaxTextureSize, views, baseFileName, OPT::bOriginFaceview,
|
||||||
OPT::strInputFileName, OPT::strMeshFileName, *pBExistVertexColor))
|
OPT::strInputFileName, OPT::strMeshFileName, OPT::bUseExistingUV, OPT::strUVMeshFileName))
|
||||||
return EXIT_FAILURE;
|
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());
|
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
|
// save the final mesh
|
||||||
scene.mesh.Save(baseFileName+OPT::strExportType);
|
scene.mesh.Save(baseFileName+OPT::strExportType,cList<String>(),true,OPT::bUseExistingUV);
|
||||||
#if TD_VERBOSE != TD_VERBOSE_OFF
|
#if TD_VERBOSE != TD_VERBOSE_OFF
|
||||||
if (VERBOSITY_LEVEL > 2)
|
if (VERBOSITY_LEVEL > 2)
|
||||||
scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType);
|
scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType);
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,467 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#!/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()
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#!/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()
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,482 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
make_shell.py — 为每个眼镜实例的顶点连通区域生成"最小壳" (minimal tight shell)。
|
||||||
|
|
||||||
|
输入支持两种方式:
|
||||||
|
1. data.json (推荐, 支持多眼镜实例, 每个实例单独处理并各导出一个壳):
|
||||||
|
{"input": "xxx.obj",
|
||||||
|
"instances": [
|
||||||
|
{"id": 0, "type": "ring-pair", "score": 0.91, "count": 1234,
|
||||||
|
"indices": [12, 13, 14, ...]},
|
||||||
|
{"id": 1, "type": "ring-pair", "score": 0.87, "count": 1180,
|
||||||
|
"indices": [5012, 5013, ...]}
|
||||||
|
]}
|
||||||
|
"input" 相对 data.json 所在目录解析 (解析不到时按同名 obj 兜底); 也可用 --obj 显式指定 (优先级更高)。
|
||||||
|
各实例独立计算, 壳最后合并导出为单个 obj (默认 shell.obj)。
|
||||||
|
2. index.txt (兼容旧的单实例流程): 顶点索引, 每行一个, 输出单个 shell.obj。
|
||||||
|
|
||||||
|
不是 AABB, 也不是带 padding 的膨胀包裹, 而是 alpha shape (3D 凹包):
|
||||||
|
- 壳面由区域最外侧的原始采样点构成, 紧贴外侧轮廓 (含凹陷, 如眼镜框);
|
||||||
|
- alpha 自动取"使壳保持单一连通的最小值" (二分搜索), 即最小壳;
|
||||||
|
- 桥接只发生在顶点漏采的空洞处, 桥的粗细由最小 alpha 自然决定;
|
||||||
|
- 最后做可选的 Taubin 平滑 (不收缩), 兼顾"最小"与"光滑"。
|
||||||
|
|
||||||
|
流程 (每个实例独立执行一遍):
|
||||||
|
1. 读 obj (全局一次) 与实例顶点索引 (默认 0-based), 取区域顶点 +
|
||||||
|
"三顶点均在区域内"的三角形重心/边中点加密 -> 采样点;
|
||||||
|
2. 3D Delaunay 四面体化, 计算每个四面体的外接球半径;
|
||||||
|
3. 二分搜索最小 alpha: alpha 复合体 (外接球半径 <= alpha 的四面体之并)
|
||||||
|
满足 (a) 连通 (b) 覆盖全部采样点;
|
||||||
|
4. 若最小 alpha 下边界非流形/不水密, alpha 向上微调至水密;
|
||||||
|
5. 抽复合体的边界三角形 (外向绕序), 得到紧贴的壳;
|
||||||
|
6. 可选 Taubin 平滑 + 数值校验 (点偏移, 体积, 水密性);
|
||||||
|
7. 全部实例的壳合并 (同一坐标系直接拼接) 导出单个 obj。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python make_shell.py # 读同目录 data.json, 各实例合并导出 shell.obj
|
||||||
|
python make_shell.py --data other.json # 指定 data.json
|
||||||
|
python make_shell.py --index index.txt # 兼容旧单索引, 导出 shell.obj
|
||||||
|
python make_shell.py --alpha 3.0 # 手动指定 alpha (更大=更圆润)
|
||||||
|
python make_shell.py --smooth 0 # 关闭平滑, 严格贴点
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy.spatial import Delaunay, cKDTree
|
||||||
|
from scipy.sparse import coo_matrix
|
||||||
|
from scipy.sparse.csgraph import connected_components
|
||||||
|
from scipy import ndimage
|
||||||
|
from skimage import measure
|
||||||
|
import trimesh
|
||||||
|
from trimesh.smoothing import filter_taubin
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- IO
|
||||||
|
def load_obj(path):
|
||||||
|
verts, faces = [], []
|
||||||
|
with open(path) as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("v "):
|
||||||
|
verts.append([float(x) for x in line.split()[1:4]])
|
||||||
|
elif line.startswith("f "):
|
||||||
|
faces.append([int(t.split("/")[0]) - 1 for t in line.split()[1:4]])
|
||||||
|
return np.asarray(verts, np.float64), np.asarray(faces, np.int64)
|
||||||
|
|
||||||
|
|
||||||
|
def load_data(args):
|
||||||
|
"""解析输入, 返回 (instances, input_obj)。
|
||||||
|
|
||||||
|
instances: [(实例名, 顶点索引 ndarray), ...]
|
||||||
|
input_obj: data.json 声明的 obj 路径 (未解析相对路径, 可能为 None)
|
||||||
|
|
||||||
|
优先级: --index 单实例 txt > --data 多实例 json。
|
||||||
|
"""
|
||||||
|
if args.index:
|
||||||
|
idx = np.loadtxt(args.index, dtype=np.int64).ravel()
|
||||||
|
if args.one_based:
|
||||||
|
idx = idx - 1
|
||||||
|
return [("index", idx)], None
|
||||||
|
|
||||||
|
if not os.path.exists(args.data):
|
||||||
|
raise SystemExit(f"找不到 {args.data}, 请用 --data 指定 data.json, "
|
||||||
|
f"或用 --index 指定单实例索引文件")
|
||||||
|
with open(args.data) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
insts = []
|
||||||
|
for inst in data.get("instances", []):
|
||||||
|
name = str(inst.get("id", len(insts)))
|
||||||
|
idx = np.asarray(inst.get("indices", []), dtype=np.int64).ravel()
|
||||||
|
if args.one_based:
|
||||||
|
idx = idx - 1
|
||||||
|
cnt = inst.get("count")
|
||||||
|
if cnt is not None and cnt != len(idx):
|
||||||
|
print(f"[warn] 实例 {name}: count={cnt} 与 indices 数量 {len(idx)} 不一致")
|
||||||
|
if len(idx) == 0:
|
||||||
|
print(f"[warn] 实例 {name}: 索引为空, 跳过")
|
||||||
|
continue
|
||||||
|
insts.append((name, idx))
|
||||||
|
if not insts:
|
||||||
|
raise SystemExit(f"{args.data} 中没有可用实例 (instances 为空)")
|
||||||
|
return insts, data.get("input")
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_obj(args, input_obj):
|
||||||
|
"""确定 obj 路径: --obj 显式指定 > data.json 的 input 字段 > data 目录内唯一 obj。
|
||||||
|
|
||||||
|
input 字段可能是相对其他根目录的路径 (如 "datasets/xxx/xxx.obj"), 本地解析不到时
|
||||||
|
按 basename 在 data 目录内找同名 obj 兜底。
|
||||||
|
"""
|
||||||
|
if args.obj:
|
||||||
|
return args.obj
|
||||||
|
base = os.path.dirname(os.path.abspath(args.data))
|
||||||
|
if input_obj:
|
||||||
|
p = input_obj if os.path.isabs(input_obj) else os.path.join(base, input_obj)
|
||||||
|
if os.path.exists(p):
|
||||||
|
return p
|
||||||
|
name = os.path.basename(input_obj) # 按文件名在 data 目录内找同名 obj
|
||||||
|
cands = sorted(glob.glob(os.path.join(base, name)))
|
||||||
|
if len(cands) == 1:
|
||||||
|
print(f"[warn] data.json 的 input '{input_obj}' 本地不存在, "
|
||||||
|
f"使用同名 obj: {cands[0]}")
|
||||||
|
return cands[0]
|
||||||
|
print(f"[warn] data.json 的 input '{input_obj}' 本地不存在, 尝试目录内查找 obj")
|
||||||
|
cands = sorted(glob.glob(os.path.join(base, "*.obj")))
|
||||||
|
if len(cands) == 1:
|
||||||
|
return cands[0]
|
||||||
|
if not cands:
|
||||||
|
raise SystemExit(f"未找到 obj: 请用 --obj 指定, 或在 {base} 放置 .obj 文件")
|
||||||
|
raise SystemExit(f"{base} 下有多个 obj, 请用 --obj 指定其一: "
|
||||||
|
f"{', '.join(os.path.basename(c) for c in cands)}")
|
||||||
|
|
||||||
|
|
||||||
|
def densify_points(verts, faces, region_mask):
|
||||||
|
"""区域顶点 + 区域内部三角形的重心/边中点采样点。"""
|
||||||
|
pts = [verts[region_mask]]
|
||||||
|
inner = faces[np.all(region_mask[faces], axis=1)]
|
||||||
|
if len(inner):
|
||||||
|
tris = verts[inner] # (F,3,3)
|
||||||
|
for a, b, c in ((1/3, 1/3, 1/3),
|
||||||
|
(0.5, 0.5, 0.0), (0.5, 0.0, 0.5), (0.0, 0.5, 0.5)):
|
||||||
|
pts.append(a * tris[:, 0] + b * tris[:, 1] + c * tris[:, 2])
|
||||||
|
pts = np.concatenate(pts, axis=0)
|
||||||
|
return np.unique(pts, axis=0), inner # 去重, 避免退化四面体
|
||||||
|
|
||||||
|
|
||||||
|
def largest_cluster(pts, r):
|
||||||
|
"""半径 r 欧氏聚类, 返回最大簇的布尔掩码 (剔除分割噪声)。"""
|
||||||
|
tree = cKDTree(pts)
|
||||||
|
pairs = np.array(list(tree.query_pairs(r=r)), dtype=np.int64).reshape(-1, 2)
|
||||||
|
n = len(pts)
|
||||||
|
if len(pairs):
|
||||||
|
m = coo_matrix((np.ones(len(pairs)), (pairs[:, 0], pairs[:, 1])), shape=(n, n))
|
||||||
|
_, lab = connected_components(m, directed=False)
|
||||||
|
sizes = np.bincount(lab)
|
||||||
|
return lab == int(np.argmax(sizes)), int(sizes.max())
|
||||||
|
return np.ones(n, bool), n
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ Delaunay 预处理
|
||||||
|
class AlphaStructure:
|
||||||
|
"""Delaunay 四面体化 + 面邻接/外接球半径, 供 alpha 二分搜索复用。"""
|
||||||
|
|
||||||
|
def __init__(self, pts):
|
||||||
|
self.pts = pts
|
||||||
|
tri = Delaunay(pts)
|
||||||
|
self.tri = tri # find_simplex 用于凸包内部栅格化
|
||||||
|
tets = tri.simplices.copy()
|
||||||
|
a = pts[tets[:, 0]]
|
||||||
|
m = np.stack([pts[tets[:, 1]] - a, pts[tets[:, 2]] - a, pts[tets[:, 3]] - a], axis=1)
|
||||||
|
det = np.linalg.det(m)
|
||||||
|
scale = float(np.abs(det).max()) ** (1 / 3) + 1e-30
|
||||||
|
good = np.abs(det) > 1e-10 * scale # 剔除退化四面体
|
||||||
|
tets, det, a = tets[good], det[good], a[good]
|
||||||
|
tets[det < 0] = tets[det < 0][:, [0, 1, 3, 2]] # 统一正体积绕序
|
||||||
|
self.tets = tets
|
||||||
|
self.T = len(tets)
|
||||||
|
|
||||||
|
# 外接球半径: 解 2(b-a).x = |b|^2-|a|^2 线性方程组
|
||||||
|
e = [np.einsum("ij,ij->i", pts[tets[:, k]], pts[tets[:, k]]) for k in range(4)]
|
||||||
|
A = 2.0 * np.stack([pts[tets[:, 1]] - a, pts[tets[:, 2]] - a, pts[tets[:, 3]] - a], axis=1)
|
||||||
|
rhs = np.stack([e[1] - e[0], e[2] - e[0], e[3] - e[0]], axis=1)
|
||||||
|
center = np.linalg.solve(A, rhs)
|
||||||
|
self.circum = np.linalg.norm(center - a, axis=1)
|
||||||
|
|
||||||
|
# 每个四面体的 4 个面 (tet 优先排列: 线性位置 t*4+f), 外向绕序 + 面共享关系
|
||||||
|
faces = np.concatenate([tets[:, [1, 2, 3]], tets[:, [0, 3, 2]],
|
||||||
|
tets[:, [0, 1, 3]], tets[:, [0, 2, 1]]],
|
||||||
|
axis=1).reshape(self.T * 4, 3)
|
||||||
|
fcent = pts[faces].mean(axis=1)
|
||||||
|
tcent = np.repeat(pts[tets].mean(axis=1), 4, axis=0)
|
||||||
|
n = np.cross(pts[faces[:, 1]] - pts[faces[:, 0]], pts[faces[:, 2]] - pts[faces[:, 0]])
|
||||||
|
flip = np.einsum("ij,ij->i", n, fcent - tcent) < 0
|
||||||
|
faces[flip] = faces[flip][:, [0, 2, 1]] # 法向朝外
|
||||||
|
self.faces = faces
|
||||||
|
|
||||||
|
keys = np.sort(self.faces, axis=1)
|
||||||
|
_, inv = np.unique(keys, axis=0, return_inverse=True)
|
||||||
|
self.face_id = np.asarray(inv).reshape(-1)
|
||||||
|
cnt = np.bincount(self.face_id)
|
||||||
|
shared = np.flatnonzero(cnt == 2)
|
||||||
|
# 每个共享面 -> 一对四面体 (面在线性数组中的两个出现位置)
|
||||||
|
order = np.argsort(self.face_id, kind="stable")
|
||||||
|
inv_sorted = self.face_id[order]
|
||||||
|
starts = np.searchsorted(inv_sorted, shared, side="left")
|
||||||
|
self.pairs = np.stack([order[starts], order[starts + 1]], axis=1) // 4
|
||||||
|
self.n_pts = len(pts)
|
||||||
|
|
||||||
|
# ---- 给定 alpha 的复合体状态 --------------------------------------
|
||||||
|
def status(self, alpha):
|
||||||
|
keep = self.circum <= alpha
|
||||||
|
used = np.zeros(self.n_pts, bool)
|
||||||
|
used[self.tets[keep].ravel()] = True
|
||||||
|
ok_used = bool(used.all())
|
||||||
|
pe = keep[self.pairs[:, 0]] & keep[self.pairs[:, 1]]
|
||||||
|
g = coo_matrix((np.ones(int(pe.sum()), bool),
|
||||||
|
(self.pairs[pe, 0], self.pairs[pe, 1])), shape=(self.T, self.T))
|
||||||
|
nc, lab = connected_components(g, directed=False)
|
||||||
|
sizes = np.bincount(lab, weights=keep.astype(np.int64))
|
||||||
|
ok_conn = nc - int((~keep).sum()) == 1 and int(sizes.max()) == int(keep.sum())
|
||||||
|
return keep, ok_used, ok_conn
|
||||||
|
|
||||||
|
def boundary_faces(self, keep):
|
||||||
|
"""复合体的边界三角形 (恰属于一个保留四面体的面, 外向绕序)。"""
|
||||||
|
mask = np.repeat(keep, 4) # faces 按 [tet 的 4 面] 排列
|
||||||
|
fid = self.face_id[mask]
|
||||||
|
cnt = np.bincount(fid) # 该面在保留四面体中的出现次数
|
||||||
|
return self.faces[mask][cnt[fid] == 1]
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ 眼镜盒式外壳
|
||||||
|
def build_case(pts, tri, res, sigma, faces_target):
|
||||||
|
"""凸包(最外侧点连成的最小凸外壳) -> 内部栅格化 -> 高斯平滑 -> MC。
|
||||||
|
|
||||||
|
壳贴着最外侧点(误差 ~ 1 voxel), 边角以 sigma*voxel 尺度圆润, 均匀光滑, 水密。
|
||||||
|
"""
|
||||||
|
hull = trimesh.Trimesh(pts).convex_hull
|
||||||
|
lo, hi = pts.min(axis=0), pts.max(axis=0)
|
||||||
|
voxel = (hi - lo).max() / res
|
||||||
|
pad = 6 * sigma * voxel + 0.5
|
||||||
|
lo2 = lo - pad
|
||||||
|
dims = np.maximum(np.ceil((hi + pad - lo2) / voxel).astype(int) + 1, 2)
|
||||||
|
|
||||||
|
# 体素中心是否在凸包内 (Delaunay find_simplex, C 实现)
|
||||||
|
grids = np.meshgrid(*[lo2[i] + (np.arange(dims[i]) + 0.5) * voxel for i in range(3)], indexing="ij")
|
||||||
|
centers = np.stack([g.ravel() for g in grids], axis=1)
|
||||||
|
occ = (tri.find_simplex(centers) >= 0).reshape(dims) # 体素中心在凸包内
|
||||||
|
|
||||||
|
field = ndimage.gaussian_filter(occ.astype(np.float32), sigma=sigma)
|
||||||
|
v, f, _, _ = measure.marching_cubes(field, level=0.5)
|
||||||
|
v = (v + 0.5) * voxel + lo2 # 网格索引 -> 世界坐标
|
||||||
|
m = trimesh.Trimesh(vertices=v, faces=f[:, ::-1], process=True)
|
||||||
|
m = max(m.split(only_watertight=False), key=lambda x: x.volume)
|
||||||
|
filter_taubin(m, lamb=0.5, nu=0.5, iterations=5)
|
||||||
|
|
||||||
|
if faces_target and len(m.faces) > faces_target: # pymeshlab 简化 + 轻平滑
|
||||||
|
import pymeshlab as pml
|
||||||
|
ms = pml.MeshSet()
|
||||||
|
ms.add_mesh(pml.Mesh(vertex_matrix=m.vertices, face_matrix=m.faces.astype(np.int64)))
|
||||||
|
ms.meshing_decimation_quadric_edge_collapse(targetfacenum=int(faces_target),
|
||||||
|
preservenormal=True)
|
||||||
|
out = ms.current_mesh()
|
||||||
|
m = trimesh.Trimesh(vertices=np.array(out.vertex_matrix()),
|
||||||
|
faces=np.array(out.face_matrix()), process=False)
|
||||||
|
filter_taubin(m, lamb=0.5, nu=0.5, iterations=3)
|
||||||
|
return m, hull
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ 单实例流水线
|
||||||
|
def run_instance(verts, faces, idx, args):
|
||||||
|
"""处理一个眼镜实例 (一组顶点索引) 的完整流程, 返回该实例的壳 mesh (由调用方合并导出)。"""
|
||||||
|
assert idx.min() >= 0 and idx.max() < len(verts), \
|
||||||
|
"index 超出顶点范围, 请确认索引含义 (0/1-based, --one-based 可切换)"
|
||||||
|
region_mask = np.zeros(len(verts), bool)
|
||||||
|
region_mask[idx] = True # Blender 0-based 顶点索引
|
||||||
|
|
||||||
|
# ---- 噪声过滤: 只保留最大空间簇 (index 中混有 SAM3 多视角假阳性) ----
|
||||||
|
if args.filter == "main":
|
||||||
|
ridx = np.flatnonzero(region_mask)
|
||||||
|
mkeep, nkeep = largest_cluster(verts[ridx], args.cluster_r)
|
||||||
|
region_mask[ridx[~mkeep]] = False
|
||||||
|
print(f"[filter] 聚类(r={args.cluster_r}) 保留最大簇 {nkeep}/{len(ridx)} 顶点, "
|
||||||
|
f"剔除 {len(ridx) - nkeep} 个噪声点")
|
||||||
|
|
||||||
|
pts, inner = densify_points(verts, faces, region_mask)
|
||||||
|
print(f"[region] 顶点 {int(region_mask.sum())}, 内部面 {len(inner)}, "
|
||||||
|
f"采样点 {len(pts)}, bbox {np.round(pts.max(0) - pts.min(0), 3)}")
|
||||||
|
|
||||||
|
print("[delaunay] 四面体化中 ...")
|
||||||
|
st = AlphaStructure(pts)
|
||||||
|
print(f"[delaunay] 四面体 {st.T}, 采样密度决定的最小可行 alpha ~ "
|
||||||
|
f"{np.percentile(st.circum, 50):.3f}")
|
||||||
|
|
||||||
|
if args.mode == "case":
|
||||||
|
# ---- 眼镜盒模式: 最外侧点连成凸外壳 + 栅格化高斯隐式面 + MC -----
|
||||||
|
mesh, hull = build_case(pts, st.tri, args.res, args.sigma, args.case_faces)
|
||||||
|
kd = cKDTree(mesh.vertices)
|
||||||
|
dd, _ = kd.query(pts, k=1)
|
||||||
|
print(f"[case] 凸包体积 {hull.volume:.1f} -> 壳体积 {mesh.volume:.1f} "
|
||||||
|
f"({mesh.volume / hull.volume * 100:.1f}%)")
|
||||||
|
print(f"[shell] 顶点 {len(mesh.vertices)}, 面 {len(mesh.faces)}, "
|
||||||
|
f"水密 {mesh.is_watertight}, 欧拉数 {mesh.euler_number}, "
|
||||||
|
f"bbox {np.round(mesh.extents, 3)} (区域 bbox {np.round(pts.max(0) - pts.min(0), 3)})")
|
||||||
|
print(f"[fit] 采样点到壳最近距离: mean={dd.mean():.4f}, "
|
||||||
|
f"p99={np.percentile(dd, 99):.4f}, max={dd.max():.4f}")
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
# ---- 最小 alpha 二分搜索 ------------------------------------------
|
||||||
|
if args.alpha is not None:
|
||||||
|
alpha = args.alpha
|
||||||
|
keep, ok_used, ok_conn = st.status(alpha)
|
||||||
|
print(f"[alpha] 手动 alpha={alpha:.4f}, 连通={ok_conn}, 覆盖={ok_used}")
|
||||||
|
else:
|
||||||
|
cand = np.unique(st.circum)
|
||||||
|
lo, hi = 0, len(cand) - 1
|
||||||
|
keep, ok_used, ok_conn = st.status(cand[hi])
|
||||||
|
assert ok_conn and ok_used, "最大 alpha 仍不连通, 数据异常"
|
||||||
|
while lo < hi:
|
||||||
|
mid = (lo + hi) // 2
|
||||||
|
k, u, c = st.status(cand[mid])
|
||||||
|
if u and c:
|
||||||
|
hi, keep, ok_used, ok_conn = mid, k, u, c
|
||||||
|
else:
|
||||||
|
lo = mid + 1
|
||||||
|
alpha = float(cand[lo])
|
||||||
|
print(f"[alpha] 最小连通 alpha = {alpha:.4f}")
|
||||||
|
|
||||||
|
# ---- 边界抽取 + 掐点拓扑修复 (仅局部补四面体, 不牺牲最小性) ---------
|
||||||
|
def try_extract(keep):
|
||||||
|
"""抽边界; 若有非 2 重边(掐点), 把该边周围的四面体扇补进复合体再试。"""
|
||||||
|
keep = keep.copy()
|
||||||
|
for _ in range(12):
|
||||||
|
bf = st.boundary_faces(keep)
|
||||||
|
m = trimesh.Trimesh(vertices=pts, faces=bf, process=False)
|
||||||
|
uniq, cnt = np.unique(m.edges_sorted, axis=0, return_counts=True)
|
||||||
|
bad = uniq[cnt != 2]
|
||||||
|
if len(bad) == 0:
|
||||||
|
return keep, m, m.is_watertight
|
||||||
|
added = 0
|
||||||
|
for a, b in bad:
|
||||||
|
fan = (st.tets == a).any(axis=1) & (st.tets == b).any(axis=1) & ~keep
|
||||||
|
keep |= fan
|
||||||
|
added += int(fan.sum())
|
||||||
|
if added == 0: # 无可补的四面体, 修复失败
|
||||||
|
return keep, m, False
|
||||||
|
return keep, m, m.is_watertight
|
||||||
|
|
||||||
|
keep, mesh, ok = try_extract(keep)
|
||||||
|
if not ok:
|
||||||
|
for _ in range(6): # 兜底: 逐步放大 alpha 重试
|
||||||
|
alpha = alpha * 1.1
|
||||||
|
k1, _, _ = st.status(alpha)
|
||||||
|
keep, mesh, ok = try_extract(k1)
|
||||||
|
print(f"[alpha] 局部修复不足, alpha 上调 10% -> {alpha:.4f}")
|
||||||
|
if ok:
|
||||||
|
break
|
||||||
|
if not mesh.is_watertight:
|
||||||
|
print("[warn] 仍未水密, 直接导出当前结果")
|
||||||
|
|
||||||
|
# ---- 外壳模式: 搜最小 alpha 使边界为单一 genus-0 闭合外壳 ----------
|
||||||
|
if args.mode == "outer" and mesh.euler_number != 2:
|
||||||
|
def pred(a):
|
||||||
|
"""alpha=a 时边界(修复掐点后)是否已是单一 genus-0 外壳。"""
|
||||||
|
k, _, _ = st.status(a)
|
||||||
|
_, m, _ = try_extract(k)
|
||||||
|
parts = m.split(only_watertight=False)
|
||||||
|
outer = [p for p in parts if p.volume > 0]
|
||||||
|
if len(outer) != 1:
|
||||||
|
return False, None
|
||||||
|
m = outer[0]
|
||||||
|
if m.is_watertight and m.euler_number == 2:
|
||||||
|
return True, m
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
a_lo, a_hi, found = alpha, None, None # a_lo: 当前非外壳
|
||||||
|
a = alpha
|
||||||
|
for _ in range(20): # 指数上扫找可行 alpha
|
||||||
|
a = a * 1.3
|
||||||
|
okf, mf = pred(a)
|
||||||
|
if okf:
|
||||||
|
a_hi, found = a, mf
|
||||||
|
break
|
||||||
|
if a_hi is None:
|
||||||
|
print("[warn] 未找到 genus-0 外壳, 保持紧凑壳")
|
||||||
|
else:
|
||||||
|
for _ in range(8): # 二分细化到最小外壳 alpha
|
||||||
|
mid = (a_lo + a_hi) / 2
|
||||||
|
okf, mf = pred(mid)
|
||||||
|
if okf:
|
||||||
|
a_hi, found = mid, mf
|
||||||
|
else:
|
||||||
|
a_lo = mid
|
||||||
|
alpha, mesh = a_hi, found
|
||||||
|
print(f"[alpha] 外壳模式: 最小 genus-0 外壳 alpha = {alpha:.4f}")
|
||||||
|
vol0 = mesh.volume
|
||||||
|
|
||||||
|
# 去掉内腔壳 (负体积分量), 只留外壳
|
||||||
|
parts = mesh.split(only_watertight=False)
|
||||||
|
outer = [p for p in parts if p.volume > 0]
|
||||||
|
if len(outer) != len(parts):
|
||||||
|
print(f"[shell] 去除 {len(parts) - len(outer)} 个内腔面片")
|
||||||
|
mesh = max(outer, key=lambda m: m.volume) if len(outer) > 1 else outer[0]
|
||||||
|
|
||||||
|
# ---- Taubin 平滑 (不收缩) ------------------------------------------
|
||||||
|
if args.smooth > 0:
|
||||||
|
before = mesh.vertices.copy()
|
||||||
|
filter_taubin(mesh, lamb=0.5, nu=0.5, iterations=args.smooth)
|
||||||
|
drift = np.linalg.norm(mesh.vertices - before, axis=1)
|
||||||
|
print(f"[smooth] Taubin x{args.smooth}, 顶点漂移 mean={drift.mean():.4f} max={drift.max():.4f}")
|
||||||
|
|
||||||
|
# ---- 数值校验 ------------------------------------------------------
|
||||||
|
prox = trimesh.proximity.ProximityQuery(mesh)
|
||||||
|
d = np.abs(prox.signed_distance(pts))
|
||||||
|
print(f"[shell] 顶点 {len(mesh.vertices)}, 面 {len(mesh.faces)}, "
|
||||||
|
f"水密 {mesh.is_watertight}, 欧拉数 {mesh.euler_number}")
|
||||||
|
print(f"[shell] 体积 {mesh.volume:.1f} (平滑前 {vol0:.1f}), "
|
||||||
|
f"bbox {np.round(mesh.extents, 3)}")
|
||||||
|
print(f"[fit] 采样点到壳距离 |d|: mean={d.mean():.4f}, "
|
||||||
|
f"p99={np.percentile(d, 99):.4f}, max={d.max():.4f}")
|
||||||
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------ 主流程
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description="生成每个眼镜实例的最小壳 (alpha shape)")
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
ap.add_argument("--obj", default=None,
|
||||||
|
help="obj 路径; 缺省依次取 data.json 的 input 字段 / data 目录内唯一 obj")
|
||||||
|
ap.add_argument("--data", default=os.path.join(here, "data.json"),
|
||||||
|
help="多实例 data.json (instances[].indices), 每实例单独处理")
|
||||||
|
ap.add_argument("--index", default=None,
|
||||||
|
help="兼容旧的单实例 index.txt 顶点索引文件; 指定时优先于 --data")
|
||||||
|
ap.add_argument("--out", default=os.path.join(here, "shell.obj"),
|
||||||
|
help="输出路径, 所有实例的壳合并导出为这一个文件 (默认 shell.obj)")
|
||||||
|
ap.add_argument("--mode", choices=["case", "outer", "tight"], default="case",
|
||||||
|
help="case=眼镜盒式光滑凸外壳(最外侧连成一体), outer=跟随外形的 genus-0 外壳, "
|
||||||
|
"tight=贴表面的紧身壳 (默认 case)")
|
||||||
|
ap.add_argument("--res", type=int, default=128, help="case 模式体素分辨率 (默认 128)")
|
||||||
|
ap.add_argument("--sigma", type=float, default=2.5, help="case 模式高斯平滑 sigma, 体素单位 (默认 2.5)")
|
||||||
|
ap.add_argument("--case-faces", type=int, default=25000, help="case 模式简化目标面数, 0=不简化 (默认 25000)")
|
||||||
|
ap.add_argument("--alpha", type=float, default=None,
|
||||||
|
help="outer/tight 模式手动指定 alpha; 缺省自动搜索")
|
||||||
|
ap.add_argument("--smooth", type=int, default=10,
|
||||||
|
help="Taubin 平滑迭代数, 0=关闭取严格最小壳 (默认 10)")
|
||||||
|
ap.add_argument("--filter", choices=["main", "all"], default="main",
|
||||||
|
help="main=只保留最大空间簇(去 SAM3 噪声), all=用全部 index 点 (默认 main)")
|
||||||
|
ap.add_argument("--cluster-r", type=float, default=2.5,
|
||||||
|
help="噪声过滤的聚类半径, 世界单位 (默认 2.5)")
|
||||||
|
ap.add_argument("--one-based", action="store_true",
|
||||||
|
help="索引为 1-based (默认按 Blender 导出的 0-based 读取)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
instances, input_obj = load_data(args)
|
||||||
|
obj_path = resolve_obj(args, input_obj)
|
||||||
|
verts, faces = load_obj(obj_path)
|
||||||
|
print(f"[load] {obj_path}: 顶点 {len(verts)}, 面 {len(faces)}, "
|
||||||
|
f"待处理实例 {len(instances)} 个")
|
||||||
|
|
||||||
|
meshes, n_ok = [], 0
|
||||||
|
for name, idx in instances:
|
||||||
|
print(f"\n===== 实例 {name}: {len(idx)} 顶点 =====")
|
||||||
|
try:
|
||||||
|
meshes.append(run_instance(verts, faces, idx, args))
|
||||||
|
n_ok += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[error] 实例 {name} 处理失败, 跳过: {e}")
|
||||||
|
if not meshes:
|
||||||
|
raise SystemExit("[error] 所有实例处理失败, 未导出任何壳")
|
||||||
|
|
||||||
|
# ---- 合并所有实例的壳 (同一坐标系, 直接拼接), 导出单个 obj ----
|
||||||
|
combined = meshes[0] if len(meshes) == 1 else trimesh.util.concatenate(meshes)
|
||||||
|
combined.export(args.out)
|
||||||
|
print(f"\n[summary] 完成 {n_ok}/{len(instances)} 个实例, 合并导出: {args.out} "
|
||||||
|
f"(顶点 {len(combined.vertices)}, 面 {len(combined.faces)}, "
|
||||||
|
f"连通分量 {len(combined.split())})")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
File diff suppressed because it is too large.
Load diff
+789
-518
File diff suppressed because it is too large.
Load diff
@@ -579,16 +579,6 @@ inline TPoint3<TYPE> ComputeTriangleNormal(const TPoint3<TYPE>& v0, const TPoint
|
|||||||
} // ComputeTriangleNormal
|
} // ComputeTriangleNormal
|
||||||
/*----------------------------------------------------------------*/
|
/*----------------------------------------------------------------*/
|
||||||
|
|
||||||
template<typename ColorType>
|
|
||||||
ColorType ComputeTriangleColor(const ColorType& c0, const ColorType& c1, const ColorType& c2) {
|
|
||||||
// 手动计算每个通道的平均值
|
|
||||||
ColorType result;
|
|
||||||
result.r = static_cast<uint8_t>((static_cast<int>(c0.r) + c1.r + c2.r) / 3 + 0.5f); // 整数运算避免溢出
|
|
||||||
result.g = static_cast<uint8_t>((c0.g + c1.g + c2.g) / 3 + 0.5f);
|
|
||||||
result.b = static_cast<uint8_t>((c0.b + c1.b + c2.b) / 3 + 0.5f);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute the area of a triangle using Heron's formula
|
// compute the area of a triangle using Heron's formula
|
||||||
template <typename TYPE>
|
template <typename TYPE>
|
||||||
TYPE ComputeTriangleAreaSqLen(TYPE lena, TYPE lenb, TYPE lenc) {
|
TYPE ComputeTriangleAreaSqLen(TYPE lena, TYPE lenb, TYPE lenc) {
|
||||||
|
|||||||
+128
-1
@@ -48,6 +48,7 @@ ObjModel::MaterialLib::MaterialLib()
|
|||||||
|
|
||||||
bool ObjModel::MaterialLib::Save(const String& prefix, bool texLossless) const
|
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());
|
std::ofstream out((prefix+".mtl").c_str());
|
||||||
if (!out.good())
|
if (!out.good())
|
||||||
return false;
|
return false;
|
||||||
@@ -130,7 +131,7 @@ bool ObjModel::MaterialLib::Load(const String& fileName)
|
|||||||
|
|
||||||
// S T R U C T S ///////////////////////////////////////////////////
|
// S T R U C T S ///////////////////////////////////////////////////
|
||||||
|
|
||||||
bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless) const
|
bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless, bool bUseExistingUV) const
|
||||||
{
|
{
|
||||||
if (vertices.empty())
|
if (vertices.empty())
|
||||||
return false;
|
return false;
|
||||||
@@ -140,6 +141,9 @@ bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless
|
|||||||
if (!material_lib.Save(prefix, texLossless))
|
if (!material_lib.Save(prefix, texLossless))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (bUseExistingUV)
|
||||||
|
return true;
|
||||||
|
|
||||||
std::ofstream out((prefix + ".obj").c_str());
|
std::ofstream out((prefix + ".obj").c_str());
|
||||||
if (!out.good())
|
if (!out.good())
|
||||||
return false;
|
return false;
|
||||||
@@ -269,6 +273,129 @@ bool ObjModel::Load(const String& fileName)
|
|||||||
return !vertices.empty();
|
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)
|
ObjModel::Group& ObjModel::AddGroup(const String& material_name)
|
||||||
{
|
{
|
||||||
|
|||||||
+2
-1
@@ -93,9 +93,10 @@ public:
|
|||||||
ObjModel() {}
|
ObjModel() {}
|
||||||
|
|
||||||
// Saves the obj model to an .obj file, its material lib and the materials with the given file name
|
// 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) const;
|
bool Save(const String& fileName, unsigned precision=6, bool texLossless=false, bool bUseExistingUV=false) const;
|
||||||
// Loads the obj model from an .obj file, its material lib and the materials with the given file name
|
// 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 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
|
// Creates a new group with the given material name
|
||||||
Group& AddGroup(const String& material_name);
|
Group& AddGroup(const String& material_name);
|
||||||
|
|||||||
@@ -1,767 +0,0 @@
|
|||||||
#include "ColorComparisonFace.h"
|
|
||||||
#include "Mesh.h" // 在cpp中包含完整定义
|
|
||||||
#include <opencv2/opencv.hpp>
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
using namespace MVS;
|
|
||||||
|
|
||||||
// 辅助函数:检查目录是否存在
|
|
||||||
static bool directoryExists(const std::string& path) {
|
|
||||||
struct stat info;
|
|
||||||
if (stat(path.c_str(), &info) != 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (info.st_mode & S_IFDIR) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:创建目录
|
|
||||||
static bool createDirectory(const std::string& path) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
return _mkdir(path.c_str()) == 0;
|
|
||||||
#else
|
|
||||||
return mkdir(path.c_str(), 0755) == 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:递归创建目录
|
|
||||||
static bool createDirectories(const std::string& path) {
|
|
||||||
size_t pos = 0;
|
|
||||||
std::string dir;
|
|
||||||
|
|
||||||
while ((pos = path.find_first_of("/\\", pos + 1)) != std::string::npos) {
|
|
||||||
dir = path.substr(0, pos);
|
|
||||||
if (!directoryExists(dir)) {
|
|
||||||
if (!createDirectory(dir)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建最终目录
|
|
||||||
if (!directoryExists(path)) {
|
|
||||||
return createDirectory(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造函数
|
|
||||||
ColorComparisonFace::ColorComparisonFace(const std::string& dir) : outputDir(dir) {
|
|
||||||
// 确保输出目录以斜杠结尾
|
|
||||||
if (!outputDir.empty() && outputDir.back() != '/' && outputDir.back() != '\\') {
|
|
||||||
outputDir += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建输出目录
|
|
||||||
if (!createDirectories(outputDir)) {
|
|
||||||
printf("⚠️ 无法创建目录: %s\n", outputDir.c_str());
|
|
||||||
} else {
|
|
||||||
printf("✅ 输出目录: %s\n", outputDir.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ColorComparisonFace::addExactTriangleInfo(int faceId,
|
|
||||||
MeshColor gaussianColor,
|
|
||||||
MeshColor originalColor,
|
|
||||||
const cv::Mat& triangleRegionWithAlpha, // 带透明通道
|
|
||||||
const cv::Mat& visualization, // 可视化图像
|
|
||||||
float colorDistance,
|
|
||||||
float threshold,
|
|
||||||
const std::string& filename) {
|
|
||||||
// 存储信息
|
|
||||||
FaceColorInfo info;
|
|
||||||
info.faceId = faceId;
|
|
||||||
info.gaussianColor = gaussianColor;
|
|
||||||
info.originalColor = originalColor;
|
|
||||||
info.triangleRegion = triangleRegionWithAlpha.clone(); // 带透明通道
|
|
||||||
info.visualization = visualization.clone(); // 可视化图像
|
|
||||||
info.colorDistance = colorDistance;
|
|
||||||
info.threshold = threshold;
|
|
||||||
info.filename = filename;
|
|
||||||
|
|
||||||
faceViewColorMap[faceId][filename].push_back(info);
|
|
||||||
printf("addExactTriangleInfo faceId=%d\n", faceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取总face数
|
|
||||||
int ColorComparisonFace::getTotalFaces() const {
|
|
||||||
return faceViewColorMap.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取总记录数
|
|
||||||
int ColorComparisonFace::getTotalRecords() const {
|
|
||||||
int total = 0;
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
for (const auto& viewEntry : faceEntry.second) {
|
|
||||||
total += viewEntry.second.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取faceid列表
|
|
||||||
std::vector<int> ColorComparisonFace::getFaceIds() const {
|
|
||||||
std::vector<int> faceIds;
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
faceIds.push_back(faceEntry.first);
|
|
||||||
}
|
|
||||||
return faceIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建三角形区域对比图
|
|
||||||
void ColorComparisonFace::createBatchComparison(int maxBlocksPerRow, int maxFacesPerImage) {
|
|
||||||
if (faceViewColorMap.empty()) {
|
|
||||||
printf("⚠️ 没有颜色信息可生成\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("正在创建三角形区域对比图...\n");
|
|
||||||
printf("总face数: %zu\n", faceViewColorMap.size());
|
|
||||||
printf("总记录数: %d\n", getTotalRecords());
|
|
||||||
|
|
||||||
// 块参数
|
|
||||||
int triangleSize = 200; // 三角形区域显示大小
|
|
||||||
int colorBlockSize = 100; // 颜色块大小
|
|
||||||
int blockMargin = 20; // 块之间的边距
|
|
||||||
int infoHeight = 100; // 信息区域高度
|
|
||||||
|
|
||||||
// 限制处理的face数
|
|
||||||
int facesToProcess = std::min((int)faceViewColorMap.size(), maxFacesPerImage);
|
|
||||||
|
|
||||||
// 处理前N个face
|
|
||||||
int faceCount = 0;
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
if (faceCount >= facesToProcess) break;
|
|
||||||
|
|
||||||
int faceId = faceEntry.first;
|
|
||||||
const auto& viewMap = faceEntry.second;
|
|
||||||
|
|
||||||
printf("处理 face %d (%zu 个视图)...\n", faceId, viewMap.size());
|
|
||||||
|
|
||||||
// 计算需要的行数和列数
|
|
||||||
int numBlocks = 0;
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
numBlocks += viewEntry.second.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
int numCols = std::min(maxBlocksPerRow, numBlocks);
|
|
||||||
int numRows = (numBlocks + numCols - 1) / numCols;
|
|
||||||
|
|
||||||
// 计算每个块的宽度和高度
|
|
||||||
int blockWidth = colorBlockSize + triangleSize + blockMargin * 3;
|
|
||||||
int blockHeight = triangleSize + blockMargin + infoHeight;
|
|
||||||
|
|
||||||
// 图片总尺寸
|
|
||||||
int totalWidth = numCols * (blockWidth + blockMargin) + blockMargin;
|
|
||||||
int totalHeight = 60 + (numRows * (blockHeight + blockMargin)) + blockMargin;
|
|
||||||
|
|
||||||
// 创建大图
|
|
||||||
cv::Mat faceImage(totalHeight, totalWidth, CV_8UC3, cv::Scalar(245, 245, 245));
|
|
||||||
|
|
||||||
// 添加标题
|
|
||||||
std::string title = cv::format("Face %d - Triangle Region Comparison", faceId);
|
|
||||||
cv::putText(faceImage, title,
|
|
||||||
cv::Point(20, 30),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加统计信息
|
|
||||||
int totalRecords = 0;
|
|
||||||
int needFixCount = 0;
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
totalRecords += viewEntry.second.size();
|
|
||||||
for (const auto& info : viewEntry.second) {
|
|
||||||
if (info.colorDistance > info.threshold) needFixCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string stats = cv::format("Views: %zu, Records: %d, Need Fix: %d",
|
|
||||||
viewMap.size(), totalRecords, needFixCount);
|
|
||||||
cv::putText(faceImage, stats,
|
|
||||||
cv::Point(20, 55),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
int blockIndex = 0;
|
|
||||||
int currentRow = 0;
|
|
||||||
int currentCol = 0;
|
|
||||||
|
|
||||||
// 遍历这个face的所有视图
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
const std::string& viewName = viewEntry.first;
|
|
||||||
const std::vector<FaceColorInfo>& infos = viewEntry.second;
|
|
||||||
|
|
||||||
for (const auto& info : infos) {
|
|
||||||
// 计算当前块的位置
|
|
||||||
int blockX = blockMargin + currentCol * (blockWidth + blockMargin);
|
|
||||||
int blockY = 60 + blockMargin + currentRow * (blockHeight + blockMargin);
|
|
||||||
|
|
||||||
// 绘制块背景
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(blockX, blockY, blockWidth, blockHeight),
|
|
||||||
cv::Scalar(255, 255, 255), -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(blockX, blockY, blockWidth, blockHeight),
|
|
||||||
cv::Scalar(200, 200, 200), 2);
|
|
||||||
|
|
||||||
// 绘制高斯颜色块
|
|
||||||
int gaussianX = blockX + blockMargin;
|
|
||||||
int gaussianY = blockY + blockMargin;
|
|
||||||
|
|
||||||
cv::Scalar gaussianBGR(info.gaussianColor[2], info.gaussianColor[1], info.gaussianColor[0]);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(gaussianX, gaussianY, colorBlockSize, colorBlockSize),
|
|
||||||
gaussianBGR, -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(gaussianX, gaussianY, colorBlockSize, colorBlockSize),
|
|
||||||
cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加高斯标签
|
|
||||||
cv::putText(faceImage, "GAUSSIAN",
|
|
||||||
cv::Point(gaussianX + 10, gaussianY - 5),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 绘制三角形区域
|
|
||||||
int triangleX = blockX + blockMargin + colorBlockSize + blockMargin;
|
|
||||||
int triangleY = blockY + blockMargin;
|
|
||||||
|
|
||||||
if (!info.visualization.empty()) {
|
|
||||||
// 调整三角形区域大小
|
|
||||||
cv::Mat resizedTriangle;
|
|
||||||
cv::resize(info.visualization, resizedTriangle, cv::Size(triangleSize, triangleSize));
|
|
||||||
|
|
||||||
// 将三角形区域绘制到指定位置(已经是RGB顺序)
|
|
||||||
resizedTriangle.copyTo(faceImage(cv::Rect(triangleX, triangleY, triangleSize, triangleSize)));
|
|
||||||
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(triangleX, triangleY, triangleSize, triangleSize),
|
|
||||||
cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加三角形标签
|
|
||||||
cv::putText(faceImage, "TRIANGLE",
|
|
||||||
cv::Point(triangleX + 10, triangleY - 5),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);
|
|
||||||
} else {
|
|
||||||
// 如果没有三角形区域,绘制占位符
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(triangleX, triangleY, triangleSize, triangleSize),
|
|
||||||
cv::Scalar(200, 200, 200), -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(triangleX, triangleY, triangleSize, triangleSize),
|
|
||||||
cv::Scalar(150, 150, 150), 2);
|
|
||||||
|
|
||||||
cv::putText(faceImage, "NO TRIANGLE",
|
|
||||||
cv::Point(triangleX + triangleSize/2 - 40, triangleY + triangleSize/2),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(100, 100, 100), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制信息区域
|
|
||||||
int infoY = blockY + blockMargin + triangleSize;
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(blockX, infoY, blockWidth, infoHeight),
|
|
||||||
cv::Scalar(240, 240, 240), -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(blockX, infoY, blockWidth, infoHeight),
|
|
||||||
cv::Scalar(200, 200, 200), 1);
|
|
||||||
|
|
||||||
// 添加视图信息
|
|
||||||
std::string displayName = viewName;
|
|
||||||
if (displayName.length() > 20) {
|
|
||||||
displayName = displayName.substr(0, 18) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
cv::putText(faceImage, cv::format("View: %s", displayName.c_str()),
|
|
||||||
cv::Point(blockX + 10, infoY + 20),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::putText(faceImage, cv::format("Face: %d", info.faceId),
|
|
||||||
cv::Point(blockX + 10, infoY + 40),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加颜色值
|
|
||||||
cv::putText(faceImage,
|
|
||||||
cv::format("Gauss: (%d,%d,%d)",
|
|
||||||
info.gaussianColor[0], info.gaussianColor[1], info.gaussianColor[2]),
|
|
||||||
cv::Point(blockX + 10, infoY + 60),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::putText(faceImage,
|
|
||||||
cv::format("Orgin: (%d,%d,%d)",
|
|
||||||
info.originalColor[0], info.originalColor[1], info.originalColor[2]),
|
|
||||||
cv::Point(blockX + 140, infoY + 60),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加距离和阈值
|
|
||||||
cv::putText(faceImage,
|
|
||||||
cv::format("Distance: %.4f", info.colorDistance),
|
|
||||||
cv::Point(blockX + 10, infoY + 80),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::putText(faceImage,
|
|
||||||
cv::format("Threshold: %.4f", info.threshold),
|
|
||||||
cv::Point(blockX + 10, infoY + 95),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加判断结果
|
|
||||||
int resultX = blockX + blockWidth - 60;
|
|
||||||
int resultY = infoY + 25;
|
|
||||||
|
|
||||||
std::string resultText = (info.colorDistance > info.threshold) ? "FIX" : "OK";
|
|
||||||
cv::Scalar resultColor = (info.colorDistance > info.threshold) ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 180, 0);
|
|
||||||
|
|
||||||
cv::putText(faceImage, resultText,
|
|
||||||
cv::Point(resultX, resultY),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, resultColor, 1);
|
|
||||||
|
|
||||||
// 更新行和列索引
|
|
||||||
blockIndex++;
|
|
||||||
currentCol++;
|
|
||||||
if (currentCol >= numCols) {
|
|
||||||
currentCol = 0;
|
|
||||||
currentRow++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已经达到最大块数,跳出循环
|
|
||||||
if (blockIndex >= maxBlocksPerRow * numRows) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已经达到最大块数,跳出循环
|
|
||||||
if (blockIndex >= maxBlocksPerRow * numRows) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存图片
|
|
||||||
std::string outputPath = outputDir + cv::format("face_%d_triangle_comparison.png", faceId);
|
|
||||||
|
|
||||||
if (!cv::imwrite(outputPath, faceImage)) {
|
|
||||||
printf("❌❌ 无法保存face %d的三角形对比图: %s\n", faceId, outputPath.c_str());
|
|
||||||
} else {
|
|
||||||
printf("✅ face %d三角形对比图已保存: %s\n", faceId, outputPath.c_str());
|
|
||||||
printf(" 尺寸: %d x %d 像素, 视图数: %zu, 记录数: %d\n",
|
|
||||||
totalWidth, totalHeight, viewMap.size(), totalRecords);
|
|
||||||
}
|
|
||||||
|
|
||||||
faceCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存颜色信息到CSV文件
|
|
||||||
saveColorInfoToFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存颜色信息到CSV文件
|
|
||||||
void ColorComparisonFace::saveColorInfoToFile() {
|
|
||||||
// 为每个face保存单独的CSV
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
int faceId = faceEntry.first;
|
|
||||||
const auto& viewMap = faceEntry.second;
|
|
||||||
|
|
||||||
if (viewMap.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string filePath = outputDir + cv::format("face_%d_triangle_colors.csv", faceId);
|
|
||||||
FILE* fp = fopen(filePath.c_str(), "w");
|
|
||||||
|
|
||||||
if (!fp) {
|
|
||||||
printf("❌❌ 无法创建文件: %s\n", filePath.c_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入CSV标题
|
|
||||||
fprintf(fp, "FaceID,View,Gaussian_R,Gaussian_G,Gaussian_B,Distance,Threshold,NeedsFix\n");
|
|
||||||
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
const std::string& viewName = viewEntry.first;
|
|
||||||
const std::vector<FaceColorInfo>& infos = viewEntry.second;
|
|
||||||
|
|
||||||
for (int i = 0; i < (int)infos.size(); i++) {
|
|
||||||
const FaceColorInfo& info = infos[i];
|
|
||||||
bool needsFix = (info.colorDistance > info.threshold);
|
|
||||||
fprintf(fp, "%d,%s,%d,%d,%d,%.4f,%.4f,%s\n",
|
|
||||||
faceId,
|
|
||||||
viewName.c_str(),
|
|
||||||
info.gaussianColor[0], info.gaussianColor[1], info.gaussianColor[2],
|
|
||||||
info.colorDistance, info.threshold,
|
|
||||||
needsFix ? "YES" : "NO");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
printf("✅ face %d三角形颜色信息已保存到: %s\n", faceId, filePath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存汇总统计信息
|
|
||||||
std::string summaryPath = outputDir + "triangle_comparison_summary.csv";
|
|
||||||
FILE* summaryFp = fopen(summaryPath.c_str(), "w");
|
|
||||||
|
|
||||||
if (summaryFp) {
|
|
||||||
fprintf(summaryFp, "FaceID,Views,TotalRecords,NeedFixRecords,NeedFix%%,MinDistance,MaxDistance,AvgDistance\n");
|
|
||||||
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
int faceId = faceEntry.first;
|
|
||||||
const auto& viewMap = faceEntry.second;
|
|
||||||
|
|
||||||
int totalRecords = 0;
|
|
||||||
int needFixCount = 0;
|
|
||||||
float minDist = std::numeric_limits<float>::max();
|
|
||||||
float maxDist = 0.0f;
|
|
||||||
float sumDist = 0.0f;
|
|
||||||
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
const std::vector<FaceColorInfo>& infos = viewEntry.second;
|
|
||||||
totalRecords += infos.size();
|
|
||||||
|
|
||||||
for (const auto& info : infos) {
|
|
||||||
if (info.colorDistance > info.threshold) needFixCount++;
|
|
||||||
minDist = std::min(minDist, info.colorDistance);
|
|
||||||
maxDist = std::max(maxDist, info.colorDistance);
|
|
||||||
sumDist += info.colorDistance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float avgDist = (totalRecords > 0) ? (sumDist / totalRecords) : 0.0f;
|
|
||||||
float fixPercentage = (totalRecords > 0) ? (needFixCount * 100.0f / totalRecords) : 0.0f;
|
|
||||||
|
|
||||||
fprintf(summaryFp, "%d,%zu,%d,%d,%.2f%%,%.4f,%.4f,%.4f\n",
|
|
||||||
faceId,
|
|
||||||
viewMap.size(),
|
|
||||||
totalRecords,
|
|
||||||
needFixCount,
|
|
||||||
fixPercentage,
|
|
||||||
minDist,
|
|
||||||
maxDist,
|
|
||||||
avgDist);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(summaryFp);
|
|
||||||
printf("✅ 三角形汇总统计信息已保存到: %s\n", summaryPath.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在ColorComparisonFace.cpp中添加以下函数实现
|
|
||||||
|
|
||||||
// 添加连续区域信息
|
|
||||||
void ColorComparisonFace::addContinuousRegionInfo(int regionId,
|
|
||||||
const std::set<unsigned int>& faceIds, // 修改为 unsigned int
|
|
||||||
MeshColor regionGaussianColor) {
|
|
||||||
if (continuousRegions.find(regionId) == continuousRegions.end()) {
|
|
||||||
ContinuousRegionInfo regionInfo;
|
|
||||||
regionInfo.regionId = regionId;
|
|
||||||
regionInfo.faceIds = faceIds; // 这里类型匹配
|
|
||||||
regionInfo.regionGaussianColor = regionGaussianColor;
|
|
||||||
regionInfo.totalPixels = 0;
|
|
||||||
continuousRegions[regionId] = regionInfo;
|
|
||||||
|
|
||||||
printf("添加连续区域 %d: 包含 %zu 个面\n", regionId, faceIds.size());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加连续区域在特定视图中的信息
|
|
||||||
void ColorComparisonFace::addRegionViewInfo(int regionId,
|
|
||||||
const std::string& filename,
|
|
||||||
MeshColor viewColor,
|
|
||||||
const cv::Mat& regionImage,
|
|
||||||
const cv::Mat& visualization,
|
|
||||||
float colorDistance) {
|
|
||||||
if (continuousRegions.find(regionId) != continuousRegions.end()) {
|
|
||||||
ContinuousRegionInfo& regionInfo = continuousRegions[regionId];
|
|
||||||
regionInfo.viewColors[filename] = viewColor;
|
|
||||||
regionInfo.viewRegions[filename] = regionImage.clone();
|
|
||||||
regionInfo.viewVisualizations[filename] = visualization.clone();
|
|
||||||
regionInfo.viewDistances[filename] = colorDistance;
|
|
||||||
|
|
||||||
printf(" 视图 %s: 颜色(R=%d,G=%d,B=%d), 距离=%.4f\n",
|
|
||||||
filename.c_str(), viewColor.r, viewColor.g, viewColor.b, colorDistance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取连续区域数
|
|
||||||
int ColorComparisonFace::getTotalRegions() const {
|
|
||||||
return continuousRegions.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建连续区域跨视图比较图
|
|
||||||
void ColorComparisonFace::createContinuousRegionComparison(int maxBlocksPerRow, int maxRegionsPerImage) {
|
|
||||||
if (continuousRegions.empty()) {
|
|
||||||
printf("⚠️ 没有连续区域信息可生成\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("正在创建连续区域跨视图比较图...\n");
|
|
||||||
printf("总连续区域数: %zu\n", continuousRegions.size());
|
|
||||||
|
|
||||||
// 限制处理的区域数
|
|
||||||
int regionsToProcess = std::min((int)continuousRegions.size(), maxRegionsPerImage);
|
|
||||||
|
|
||||||
// 处理前N个区域
|
|
||||||
int regionCount = 0;
|
|
||||||
for (const auto& regionEntry : continuousRegions) {
|
|
||||||
if (regionCount >= regionsToProcess) break;
|
|
||||||
|
|
||||||
int regionId = regionEntry.first;
|
|
||||||
const ContinuousRegionInfo& regionInfo = regionEntry.second;
|
|
||||||
|
|
||||||
printf("处理连续区域 %d: 在 %zu 个视图中可见\n", regionId, regionInfo.viewColors.size());
|
|
||||||
|
|
||||||
if (regionInfo.viewColors.size() < 2) {
|
|
||||||
printf(" 区域 %d 在少于2个视图中可见,跳过跨视图比较\n", regionId);
|
|
||||||
regionCount++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 块参数
|
|
||||||
int regionSize = 200; // 区域显示大小
|
|
||||||
int colorBlockSize = 100; // 颜色块大小
|
|
||||||
int blockMargin = 20; // 块之间的边距
|
|
||||||
int infoHeight = 120; // 信息区域高度
|
|
||||||
|
|
||||||
// 计算需要的行数和列数
|
|
||||||
int numBlocks = regionInfo.viewColors.size();
|
|
||||||
int numCols = std::min(maxBlocksPerRow, numBlocks);
|
|
||||||
int numRows = (numBlocks + numCols - 1) / numCols;
|
|
||||||
|
|
||||||
// 计算每个块的宽度和高度
|
|
||||||
int blockWidth = colorBlockSize + regionSize + blockMargin * 3;
|
|
||||||
int blockHeight = regionSize + blockMargin + infoHeight;
|
|
||||||
|
|
||||||
// 图片总尺寸
|
|
||||||
int totalWidth = numCols * (blockWidth + blockMargin) + blockMargin;
|
|
||||||
int totalHeight = 100 + (numRows * (blockHeight + blockMargin)) + blockMargin;
|
|
||||||
|
|
||||||
// 创建大图
|
|
||||||
cv::Mat regionImage(totalHeight, totalWidth, CV_8UC3, cv::Scalar(240, 240, 240));
|
|
||||||
|
|
||||||
// 添加标题
|
|
||||||
std::string title = cv::format("Continuous Region %d - Cross-View Comparison", regionId);
|
|
||||||
cv::putText(regionImage, title,
|
|
||||||
cv::Point(20, 30),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加区域统计信息
|
|
||||||
std::string regionStats = cv::format("Contains %zu faces, Visible in %zu views",
|
|
||||||
regionInfo.faceIds.size(), regionInfo.viewColors.size());
|
|
||||||
cv::putText(regionImage, regionStats,
|
|
||||||
cv::Point(20, 60),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
std::string gaussStats = cv::format("Region Gaussian Color: R=%d, G=%d, B=%d",
|
|
||||||
regionInfo.regionGaussianColor.r,
|
|
||||||
regionInfo.regionGaussianColor.g,
|
|
||||||
regionInfo.regionGaussianColor.b);
|
|
||||||
cv::putText(regionImage, gaussStats,
|
|
||||||
cv::Point(20, 85),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(255, 0, 0), 1);
|
|
||||||
|
|
||||||
int blockIndex = 0;
|
|
||||||
int currentRow = 0;
|
|
||||||
int currentCol = 0;
|
|
||||||
|
|
||||||
// 计算统计数据
|
|
||||||
float sumDistances = 0.0f;
|
|
||||||
float maxDistance = 0.0f;
|
|
||||||
std::string worstView;
|
|
||||||
|
|
||||||
// 遍历这个区域的所有视图
|
|
||||||
for (const auto& viewEntry : regionInfo.viewColors) {
|
|
||||||
const std::string& viewName = viewEntry.first;
|
|
||||||
const MeshColor& viewColor = viewEntry.second;
|
|
||||||
float colorDistance = regionInfo.viewDistances.at(viewName);
|
|
||||||
|
|
||||||
sumDistances += colorDistance;
|
|
||||||
if (colorDistance > maxDistance) {
|
|
||||||
maxDistance = colorDistance;
|
|
||||||
worstView = viewName;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算当前块的位置
|
|
||||||
int blockX = blockMargin + currentCol * (blockWidth + blockMargin);
|
|
||||||
int blockY = 100 + blockMargin + currentRow * (blockHeight + blockMargin);
|
|
||||||
|
|
||||||
// 绘制块背景
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(blockX, blockY, blockWidth, blockHeight),
|
|
||||||
cv::Scalar(255, 255, 255), -1);
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(blockX, blockY, blockWidth, blockHeight),
|
|
||||||
cv::Scalar(200, 200, 200), 2);
|
|
||||||
|
|
||||||
// 绘制区域高斯颜色块
|
|
||||||
int gaussianX = blockX + blockMargin;
|
|
||||||
int gaussianY = blockY + blockMargin;
|
|
||||||
|
|
||||||
cv::Scalar gaussianBGR(regionInfo.regionGaussianColor[2],
|
|
||||||
regionInfo.regionGaussianColor[1],
|
|
||||||
regionInfo.regionGaussianColor[0]);
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(gaussianX, gaussianY, colorBlockSize, colorBlockSize),
|
|
||||||
gaussianBGR, -1);
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(gaussianX, gaussianY, colorBlockSize, colorBlockSize),
|
|
||||||
cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加高斯标签
|
|
||||||
cv::putText(regionImage, "REGION GAUSS",
|
|
||||||
cv::Point(gaussianX + 5, gaussianY - 5),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 绘制区域图像块
|
|
||||||
int regionX = blockX + blockMargin + colorBlockSize + blockMargin;
|
|
||||||
int regionY = blockY + blockMargin;
|
|
||||||
|
|
||||||
auto visIt = regionInfo.viewVisualizations.find(viewName);
|
|
||||||
if (visIt != regionInfo.viewVisualizations.end() && !visIt->second.empty()) {
|
|
||||||
cv::Mat resizedRegion;
|
|
||||||
cv::resize(visIt->second, resizedRegion, cv::Size(regionSize, regionSize));
|
|
||||||
|
|
||||||
// 将区域图像绘制到指定位置
|
|
||||||
resizedRegion.copyTo(regionImage(cv::Rect(regionX, regionY, regionSize, regionSize)));
|
|
||||||
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(regionX, regionY, regionSize, regionSize),
|
|
||||||
cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加视图标签
|
|
||||||
cv::putText(regionImage, "VIEW REGION",
|
|
||||||
cv::Point(regionX + 5, regionY - 5),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 绘制信息区域
|
|
||||||
int infoY = blockY + blockMargin + regionSize;
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(blockX, infoY, blockWidth, infoHeight),
|
|
||||||
cv::Scalar(240, 240, 240), -1);
|
|
||||||
cv::rectangle(regionImage,
|
|
||||||
cv::Rect(blockX, infoY, blockWidth, infoHeight),
|
|
||||||
cv::Scalar(200, 200, 200), 1);
|
|
||||||
|
|
||||||
// 添加视图信息
|
|
||||||
std::string displayName = viewName;
|
|
||||||
if (displayName.length() > 20) {
|
|
||||||
displayName = displayName.substr(0, 18) + "...";
|
|
||||||
}
|
|
||||||
|
|
||||||
cv::putText(regionImage, cv::format("View: %s", displayName.c_str()),
|
|
||||||
cv::Point(blockX + 10, infoY + 20),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::putText(regionImage, cv::format("Region: %d", regionId),
|
|
||||||
cv::Point(blockX + 10, infoY + 40),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加颜色值
|
|
||||||
cv::putText(regionImage,
|
|
||||||
cv::format("Region Gauss: (%d,%d,%d)",
|
|
||||||
regionInfo.regionGaussianColor[0],
|
|
||||||
regionInfo.regionGaussianColor[1],
|
|
||||||
regionInfo.regionGaussianColor[2]),
|
|
||||||
cv::Point(blockX + 10, infoY + 60),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::putText(regionImage,
|
|
||||||
cv::format("View Color: (%d,%d,%d)",
|
|
||||||
viewColor[0], viewColor[1], viewColor[2]),
|
|
||||||
cv::Point(blockX + 10, infoY + 80),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加距离和阈值
|
|
||||||
cv::putText(regionImage,
|
|
||||||
cv::format("Distance: %.4f", colorDistance),
|
|
||||||
cv::Point(blockX + 10, infoY + 100),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加判断结果
|
|
||||||
int resultX = blockX + blockWidth - 50;
|
|
||||||
int resultY = infoY + 30;
|
|
||||||
|
|
||||||
std::string resultText = (colorDistance > 0.31f) ? "FIX" : "OK";
|
|
||||||
cv::Scalar resultColor = (colorDistance > 0.31f) ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 180, 0);
|
|
||||||
|
|
||||||
cv::putText(regionImage, resultText,
|
|
||||||
cv::Point(resultX, resultY),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, resultColor, 1);
|
|
||||||
|
|
||||||
// 更新行和列索引
|
|
||||||
blockIndex++;
|
|
||||||
currentCol++;
|
|
||||||
if (currentCol >= numCols) {
|
|
||||||
currentCol = 0;
|
|
||||||
currentRow++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在底部添加统计信息
|
|
||||||
float avgDistance = sumDistances / regionInfo.viewColors.size();
|
|
||||||
std::string stats = cv::format("Avg Distance: %.4f, Max Distance: %.4f (in %s)",
|
|
||||||
avgDistance, maxDistance, worstView.c_str());
|
|
||||||
cv::putText(regionImage, stats,
|
|
||||||
cv::Point(20, totalHeight - 20),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 保存图片
|
|
||||||
std::string outputPath = outputDir + cv::format("continuous_region_%d_comparison.png", regionId);
|
|
||||||
|
|
||||||
if (!cv::imwrite(outputPath, regionImage)) {
|
|
||||||
printf("❌❌ 无法保存连续区域 %d 的对比图: %s\n", regionId, outputPath.c_str());
|
|
||||||
} else {
|
|
||||||
printf("✅ 连续区域 %d 对比图已保存: %s\n", regionId, outputPath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
regionCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存连续区域信息到CSV文件
|
|
||||||
std::string regionSummaryPath = outputDir + "continuous_regions_summary.csv";
|
|
||||||
FILE* regionFp = fopen(regionSummaryPath.c_str(), "w");
|
|
||||||
|
|
||||||
if (regionFp) {
|
|
||||||
fprintf(regionFp, "RegionID,FaceCount,Views,AvgDistance,MaxDistance,WorstView,NeedsFixCount,NeedsFix%%\n");
|
|
||||||
|
|
||||||
for (const auto& regionEntry : continuousRegions) {
|
|
||||||
int regionId = regionEntry.first;
|
|
||||||
const ContinuousRegionInfo& regionInfo = regionEntry.second;
|
|
||||||
|
|
||||||
float sumDist = 0.0f;
|
|
||||||
float maxDist = 0.0f;
|
|
||||||
int needsFixCount = 0;
|
|
||||||
|
|
||||||
for (const auto& distEntry : regionInfo.viewDistances) {
|
|
||||||
float dist = distEntry.second;
|
|
||||||
sumDist += dist;
|
|
||||||
if (dist > maxDist) maxDist = dist;
|
|
||||||
if (dist > 0.31f) needsFixCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
float avgDist = (regionInfo.viewDistances.size() > 0) ?
|
|
||||||
(sumDist / regionInfo.viewDistances.size()) : 0.0f;
|
|
||||||
float fixPercentage = (regionInfo.viewDistances.size() > 0) ?
|
|
||||||
(needsFixCount * 100.0f / regionInfo.viewDistances.size()) : 0.0f;
|
|
||||||
|
|
||||||
fprintf(regionFp, "%d,%zu,%zu,%.4f,%.4f,",
|
|
||||||
regionId, regionInfo.faceIds.size(), regionInfo.viewDistances.size(),
|
|
||||||
avgDist, maxDist);
|
|
||||||
|
|
||||||
// 找到最差的视图
|
|
||||||
std::string worstView = "";
|
|
||||||
for (const auto& distEntry : regionInfo.viewDistances) {
|
|
||||||
if (distEntry.second == maxDist) {
|
|
||||||
worstView = distEntry.first;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(regionFp, "%s,%d,%.2f%%\n",
|
|
||||||
worstView.c_str(), needsFixCount, fixPercentage);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(regionFp);
|
|
||||||
printf("✅ 连续区域汇总统计信息已保存到: %s\n", regionSummaryPath.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
#ifndef COLORCOMPARISONBFACE_H
|
|
||||||
#define COLORCOMPARISONBFACE_H
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
|
||||||
#include <set>
|
|
||||||
#include <opencv2/opencv.hpp>
|
|
||||||
|
|
||||||
// 不使用前向声明,直接定义简单的颜色结构
|
|
||||||
struct MeshColor {
|
|
||||||
unsigned char r, g, b;
|
|
||||||
|
|
||||||
MeshColor() : r(0), g(0), b(0) {}
|
|
||||||
MeshColor(unsigned char r, unsigned char g, unsigned char b)
|
|
||||||
: r(r), g(g), b(b) {}
|
|
||||||
|
|
||||||
unsigned char operator[](size_t idx) const {
|
|
||||||
if (idx == 0) return r;
|
|
||||||
if (idx == 1) return g;
|
|
||||||
if (idx == 2) return b;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
class ColorComparisonFace {
|
|
||||||
private:
|
|
||||||
// 单面信息结构
|
|
||||||
struct FaceColorInfo {
|
|
||||||
int faceId; // 面的ID
|
|
||||||
MeshColor gaussianColor; // 高斯颜色
|
|
||||||
MeshColor originalColor; // 原始颜色
|
|
||||||
cv::Mat triangleRegion; // 带透明通道的三角形区域
|
|
||||||
cv::Mat visualization; // 三角形可视化图像
|
|
||||||
float colorDistance; // 颜色距离
|
|
||||||
float threshold; // 阈值
|
|
||||||
std::string filename; // 文件名
|
|
||||||
};
|
|
||||||
|
|
||||||
// 连续区域信息结构
|
|
||||||
struct ContinuousRegionInfo {
|
|
||||||
int regionId; // 区域ID
|
|
||||||
std::set<unsigned int> faceIds; // 修改为 unsigned int
|
|
||||||
MeshColor regionGaussianColor; // 区域的整体高斯颜色
|
|
||||||
std::map<std::string, MeshColor> viewColors; // 每个视图的颜色
|
|
||||||
std::map<std::string, cv::Mat> viewRegions; // 每个视图的区域图像
|
|
||||||
std::map<std::string, cv::Mat> viewVisualizations; // 每个视图的可视化
|
|
||||||
std::map<std::string, float> viewDistances; // 每个视图的距离
|
|
||||||
int totalPixels; // 区域总像素数
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用嵌套map: faceid -> filename -> FaceColorInfo列表
|
|
||||||
std::map<int, std::map<std::string, std::vector<FaceColorInfo>>> faceViewColorMap;
|
|
||||||
|
|
||||||
// 连续区域映射: regionId -> ContinuousRegionInfo
|
|
||||||
std::map<int, ContinuousRegionInfo> continuousRegions;
|
|
||||||
|
|
||||||
std::string outputDir;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// 构造函数
|
|
||||||
ColorComparisonFace(const std::string& dir);
|
|
||||||
|
|
||||||
// 析构函数
|
|
||||||
virtual ~ColorComparisonFace() {}
|
|
||||||
|
|
||||||
// 添加精确三角形信息(替代原来的addColorInfo)
|
|
||||||
void addExactTriangleInfo(int faceId,
|
|
||||||
MeshColor gaussianColor,
|
|
||||||
MeshColor originalColor,
|
|
||||||
const cv::Mat& triangleRegionWithAlpha, // 带透明通道
|
|
||||||
const cv::Mat& visualization, // 可视化图像
|
|
||||||
float colorDistance,
|
|
||||||
float threshold,
|
|
||||||
const std::string& filename);
|
|
||||||
|
|
||||||
// 添加连续区域信息
|
|
||||||
void addContinuousRegionInfo(int regionId,
|
|
||||||
const std::set<unsigned int>& faceIds, // 修改为 unsigned int
|
|
||||||
MeshColor regionGaussianColor);
|
|
||||||
|
|
||||||
// 添加连续区域在特定视图中的信息
|
|
||||||
void addRegionViewInfo(int regionId,
|
|
||||||
const std::string& filename,
|
|
||||||
MeshColor viewColor,
|
|
||||||
const cv::Mat& regionImage,
|
|
||||||
const cv::Mat& visualization,
|
|
||||||
float colorDistance);
|
|
||||||
|
|
||||||
// 创建三角形区域对比图
|
|
||||||
void createBatchComparison(int maxBlocksPerRow = 6, int maxFacesPerImage = 20);
|
|
||||||
|
|
||||||
// 创建连续区域跨视图比较图
|
|
||||||
void createContinuousRegionComparison(int maxBlocksPerRow = 6, int maxRegionsPerImage = 20);
|
|
||||||
|
|
||||||
// 保存颜色信息到CSV文件
|
|
||||||
void saveColorInfoToFile();
|
|
||||||
|
|
||||||
// 获取总face数
|
|
||||||
int getTotalFaces() const;
|
|
||||||
|
|
||||||
// 获取总记录数
|
|
||||||
int getTotalRecords() const;
|
|
||||||
|
|
||||||
// 获取faceid列表
|
|
||||||
std::vector<int> getFaceIds() const;
|
|
||||||
|
|
||||||
// 获取连续区域数
|
|
||||||
int getTotalRegions() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COLORCOMPARISONBFACE_H
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
#include "ColorComparisonPixel.h"
|
|
||||||
#include "Mesh.h" // 在cpp中包含完整定义
|
|
||||||
#include <opencv2/opencv.hpp>
|
|
||||||
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <sys/stat.h>
|
|
||||||
#include <iomanip>
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
using namespace MVS;
|
|
||||||
|
|
||||||
// 辅助函数:检查目录是否存在
|
|
||||||
static bool directoryExists(const std::string& path) {
|
|
||||||
struct stat info;
|
|
||||||
if (stat(path.c_str(), &info) != 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (info.st_mode & S_IFDIR) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:创建目录
|
|
||||||
static bool createDirectory(const std::string& path) {
|
|
||||||
#ifdef _WIN32
|
|
||||||
return _mkdir(path.c_str()) == 0;
|
|
||||||
#else
|
|
||||||
return mkdir(path.c_str(), 0755) == 0;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:递归创建目录
|
|
||||||
static bool createDirectories(const std::string& path) {
|
|
||||||
size_t pos = 0;
|
|
||||||
std::string dir;
|
|
||||||
|
|
||||||
while ((pos = path.find_first_of("/\\", pos + 1)) != std::string::npos) {
|
|
||||||
dir = path.substr(0, pos);
|
|
||||||
if (!directoryExists(dir)) {
|
|
||||||
if (!createDirectory(dir)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建最终目录
|
|
||||||
if (!directoryExists(path)) {
|
|
||||||
return createDirectory(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造函数
|
|
||||||
ColorComparisonPixel::ColorComparisonPixel(const std::string& dir) : outputDir(dir) {
|
|
||||||
// 确保输出目录以斜杠结尾
|
|
||||||
if (!outputDir.empty() && outputDir.back() != '/' && outputDir.back() != '\\') {
|
|
||||||
outputDir += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建输出目录
|
|
||||||
if (!createDirectories(outputDir)) {
|
|
||||||
printf("⚠️ 无法创建目录: %s\n", outputDir.c_str());
|
|
||||||
} else {
|
|
||||||
printf("✅ 输出目录: %s\n", outputDir.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加颜色信息
|
|
||||||
void ColorComparisonPixel::addColorInfo(int faceId,
|
|
||||||
const MeshColor& gaussianColor,
|
|
||||||
const MeshColor& originalColor,
|
|
||||||
float distance, float threshold,
|
|
||||||
const std::string& filename) {
|
|
||||||
ColorInfo info = {faceId, gaussianColor, originalColor, distance, threshold, filename};
|
|
||||||
faceViewColorMap[faceId][filename].push_back(info);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取总face数
|
|
||||||
int ColorComparisonPixel::getTotalFaces() const {
|
|
||||||
return faceViewColorMap.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取总记录数
|
|
||||||
int ColorComparisonPixel::getTotalRecords() const {
|
|
||||||
int total = 0;
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
for (const auto& viewEntry : faceEntry.second) {
|
|
||||||
total += viewEntry.second.size();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取faceid列表
|
|
||||||
std::vector<int> ColorComparisonPixel::getFaceIds() const {
|
|
||||||
std::vector<int> faceIds;
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
faceIds.push_back(faceEntry.first);
|
|
||||||
}
|
|
||||||
return faceIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建按faceid分组的对比图
|
|
||||||
void ColorComparisonPixel::createBatchComparison(int maxCellsPerRow, int maxFacesPerImage) {
|
|
||||||
if (faceViewColorMap.empty()) {
|
|
||||||
printf("⚠️ 没有颜色信息可生成\n");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("正在创建按faceid分组的对比图...\n");
|
|
||||||
printf("总face数: %zu\n", faceViewColorMap.size());
|
|
||||||
printf("总记录数: %d\n", getTotalRecords());
|
|
||||||
|
|
||||||
// 单元格参数
|
|
||||||
int cellWidth = 220; // 每个单元格宽度
|
|
||||||
int cellHeight = 150; // 每个单元格高度
|
|
||||||
int cellMargin = 5; // 单元格边距
|
|
||||||
|
|
||||||
// 左侧视图名称区域宽度
|
|
||||||
int viewNameWidth = 200;
|
|
||||||
|
|
||||||
// 限制处理的face数
|
|
||||||
int facesToProcess = std::min((int)faceViewColorMap.size(), maxFacesPerImage);
|
|
||||||
|
|
||||||
// 处理前N个face
|
|
||||||
int faceCount = 0;
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
if (faceCount >= facesToProcess) break;
|
|
||||||
|
|
||||||
int faceId = faceEntry.first;
|
|
||||||
const auto& viewMap = faceEntry.second;
|
|
||||||
|
|
||||||
printf("处理 face %d (%zu 个视图)...\n", faceId, viewMap.size());
|
|
||||||
|
|
||||||
// 计算这个face需要的总行数
|
|
||||||
int totalRows = 0;
|
|
||||||
std::vector<std::pair<std::string, int>> viewRowsInfo; // 存储每个视图需要的行数
|
|
||||||
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
const std::string& viewName = viewEntry.first;
|
|
||||||
const std::vector<ColorInfo>& infos = viewEntry.second;
|
|
||||||
|
|
||||||
// 计算这个视图需要的行数
|
|
||||||
int rowsForThisView = (infos.size() + maxCellsPerRow - 1) / maxCellsPerRow;
|
|
||||||
viewRowsInfo.emplace_back(viewName, rowsForThisView);
|
|
||||||
totalRows += rowsForThisView;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalRows == 0) continue;
|
|
||||||
|
|
||||||
// 图片总尺寸
|
|
||||||
int totalWidth = viewNameWidth + (maxCellsPerRow * cellWidth);
|
|
||||||
int totalHeight = 60 + (totalRows * cellHeight); // 60像素用于标题
|
|
||||||
|
|
||||||
// 创建大图
|
|
||||||
cv::Mat faceImage(totalHeight, totalWidth, CV_8UC3, cv::Scalar(245, 245, 245));
|
|
||||||
|
|
||||||
// 添加标题
|
|
||||||
std::string title = cv::format("Face %d - Color Comparison", faceId);
|
|
||||||
cv::putText(faceImage, title,
|
|
||||||
cv::Point(20, 30),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.8, cv::Scalar(0, 0, 0), 2);
|
|
||||||
|
|
||||||
// 添加统计信息
|
|
||||||
int totalRecords = 0;
|
|
||||||
int needFixCount = 0;
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
totalRecords += viewEntry.second.size();
|
|
||||||
for (const auto& info : viewEntry.second) {
|
|
||||||
if (info.distance > info.threshold) needFixCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string stats = cv::format("Views: %zu, Records: %d, Need Fix: %d",
|
|
||||||
viewMap.size(), totalRecords, needFixCount);
|
|
||||||
cv::putText(faceImage, stats,
|
|
||||||
cv::Point(20, 55),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
int currentGlobalRow = 0; // 全局行索引
|
|
||||||
|
|
||||||
// 遍历这个face的所有视图
|
|
||||||
for (const auto& viewRowPair : viewRowsInfo) {
|
|
||||||
const std::string& viewName = viewRowPair.first;
|
|
||||||
int rowsForThisView = viewRowPair.second;
|
|
||||||
const std::vector<ColorInfo>& infos = viewMap.at(viewName);
|
|
||||||
|
|
||||||
// 为这个视图的每一行绘制
|
|
||||||
for (int viewRow = 0; viewRow < rowsForThisView; viewRow++) {
|
|
||||||
int globalRowY = 60 + (currentGlobalRow * cellHeight);
|
|
||||||
|
|
||||||
// 绘制视图名称区域(只在第一行显示)
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(0, globalRowY, viewNameWidth, cellHeight),
|
|
||||||
cv::Scalar(255, 255, 255), -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(0, globalRowY, viewNameWidth, cellHeight),
|
|
||||||
cv::Scalar(200, 200, 200), 1);
|
|
||||||
|
|
||||||
if (viewRow == 0) {
|
|
||||||
// 第一行:显示完整视图信息
|
|
||||||
cv::putText(faceImage, viewName,
|
|
||||||
cv::Point(10, globalRowY + 25),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
std::string countText = cv::format("Records: %zu", infos.size());
|
|
||||||
cv::putText(faceImage, countText,
|
|
||||||
cv::Point(10, globalRowY + 50),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(100, 100, 100), 1);
|
|
||||||
|
|
||||||
// 计算这个视图中需要修复的记录数
|
|
||||||
int viewNeedFixCount = 0;
|
|
||||||
for (const auto& info : infos) {
|
|
||||||
if (info.distance > info.threshold) viewNeedFixCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewNeedFixCount > 0) {
|
|
||||||
std::string fixText = cv::format("Fix: %d", viewNeedFixCount);
|
|
||||||
cv::Scalar fixColor = cv::Scalar(0, 0, 255);
|
|
||||||
cv::putText(faceImage, fixText,
|
|
||||||
cv::Point(10, globalRowY + 70),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, fixColor, 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 后续行:显示视图名称缩写和行号
|
|
||||||
std::string continuationText = cv::format("%s (cont.)", viewName.substr(0, 8).c_str());
|
|
||||||
cv::putText(faceImage, continuationText,
|
|
||||||
cv::Point(10, globalRowY + 25),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(100, 100, 100), 1);
|
|
||||||
|
|
||||||
std::string rowText = cv::format("Row %d/%d", viewRow + 1, rowsForThisView);
|
|
||||||
cv::putText(faceImage, rowText,
|
|
||||||
cv::Point(10, globalRowY + 50),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(100, 100, 100), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算这一行要显示的记录
|
|
||||||
int startIndex = viewRow * maxCellsPerRow;
|
|
||||||
int endIndex = std::min(startIndex + maxCellsPerRow, (int)infos.size());
|
|
||||||
|
|
||||||
// 绘制这一行的单元格
|
|
||||||
for (int col = 0; col < (endIndex - startIndex); col++) {
|
|
||||||
int infoIndex = startIndex + col;
|
|
||||||
const ColorInfo& info = infos[infoIndex];
|
|
||||||
|
|
||||||
// 计算单元格位置
|
|
||||||
int cellX = viewNameWidth + (col * cellWidth);
|
|
||||||
int cellY = globalRowY;
|
|
||||||
|
|
||||||
// 绘制单个单元格
|
|
||||||
int innerX = cellX + cellMargin;
|
|
||||||
int innerY = cellY + cellMargin;
|
|
||||||
int innerWidth = cellWidth - 2 * cellMargin;
|
|
||||||
int innerHeight = cellHeight - 2 * cellMargin;
|
|
||||||
|
|
||||||
// 创建单元格背景
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(cellX, cellY, cellWidth, cellHeight),
|
|
||||||
cv::Scalar(255, 255, 255), -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(cellX, cellY, cellWidth, cellHeight),
|
|
||||||
cv::Scalar(200, 200, 200), 1);
|
|
||||||
|
|
||||||
// 颜色块参数
|
|
||||||
int colorBlockSize = 35;
|
|
||||||
int colorBlockY = innerY + 10;
|
|
||||||
|
|
||||||
// 绘制高斯颜色块
|
|
||||||
cv::Scalar gaussianBGR(info.gaussianColor[2], info.gaussianColor[1], info.gaussianColor[0]);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Point(innerX + 5, colorBlockY),
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize, colorBlockY + colorBlockSize),
|
|
||||||
gaussianBGR, -1);
|
|
||||||
|
|
||||||
// 绘制原始颜色块
|
|
||||||
cv::Scalar originalBGR(info.originalColor[2], info.originalColor[1], info.originalColor[0]);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35, colorBlockY),
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35 + colorBlockSize, colorBlockY + colorBlockSize),
|
|
||||||
originalBGR, -1);
|
|
||||||
|
|
||||||
// 添加边框
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Point(innerX + 5, colorBlockY),
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize, colorBlockY + colorBlockSize),
|
|
||||||
cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35, colorBlockY),
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35 + colorBlockSize, colorBlockY + colorBlockSize),
|
|
||||||
cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加标签
|
|
||||||
int labelY = colorBlockY - 5;
|
|
||||||
cv::putText(faceImage, "G",
|
|
||||||
cv::Point(innerX + 5 + 10, labelY),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
cv::putText(faceImage, "O",
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35 + 10, labelY),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加颜色值(简化显示)
|
|
||||||
std::string gValue = cv::format("%d,%d,%d",
|
|
||||||
info.gaussianColor[0], info.gaussianColor[1], info.gaussianColor[2]);
|
|
||||||
cv::putText(faceImage, gValue,
|
|
||||||
cv::Point(innerX + 5, innerY + 60),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.3, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
std::string oValue = cv::format("%d,%d,%d",
|
|
||||||
info.originalColor[0], info.originalColor[1], info.originalColor[2]);
|
|
||||||
cv::putText(faceImage, oValue,
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35, innerY + 60),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.3, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加距离
|
|
||||||
std::string distText = cv::format("D:%.3f", info.distance);
|
|
||||||
cv::putText(faceImage, distText,
|
|
||||||
cv::Point(innerX + 5, innerY + 75),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加阈值
|
|
||||||
std::string threshText = cv::format("T:%.3f", info.threshold);
|
|
||||||
cv::putText(faceImage, threshText,
|
|
||||||
cv::Point(innerX + 5 + colorBlockSize + 35, innerY + 75),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.35, cv::Scalar(0, 0, 0), 1);
|
|
||||||
|
|
||||||
// 添加判断结果
|
|
||||||
std::string resultText = (info.distance > info.threshold) ? "FIX" : "OK";
|
|
||||||
cv::Scalar resultColor = (info.distance > info.threshold) ? cv::Scalar(0, 0, 255) : cv::Scalar(0, 180, 0);
|
|
||||||
|
|
||||||
cv::putText(faceImage, resultText,
|
|
||||||
cv::Point(innerX + 5, innerY + 90),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.4, resultColor, 1);
|
|
||||||
|
|
||||||
// 添加记录序号
|
|
||||||
std::string seqText = cv::format("#%d", infoIndex + 1);
|
|
||||||
cv::putText(faceImage, seqText,
|
|
||||||
cv::Point(innerX + 5, innerY + 110),
|
|
||||||
cv::FONT_HERSHEY_SIMPLEX, 0.3, cv::Scalar(100, 100, 100), 1);
|
|
||||||
|
|
||||||
// 添加判断标记
|
|
||||||
if (info.distance > info.threshold) {
|
|
||||||
cv::circle(faceImage,
|
|
||||||
cv::Point(innerX + innerWidth - 10, innerY + 10),
|
|
||||||
5, cv::Scalar(0, 0, 255), -1);
|
|
||||||
} else {
|
|
||||||
cv::circle(faceImage,
|
|
||||||
cv::Point(innerX + innerWidth - 10, innerY + 10),
|
|
||||||
5, cv::Scalar(0, 180, 0), -1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果这一行没有填满,留空
|
|
||||||
for (int col = (endIndex - startIndex); col < maxCellsPerRow; col++) {
|
|
||||||
int cellX = viewNameWidth + (col * cellWidth);
|
|
||||||
int cellY = globalRowY;
|
|
||||||
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(cellX, cellY, cellWidth, cellHeight),
|
|
||||||
cv::Scalar(245, 245, 245), -1);
|
|
||||||
cv::rectangle(faceImage,
|
|
||||||
cv::Rect(cellX, cellY, cellWidth, cellHeight),
|
|
||||||
cv::Scalar(220, 220, 220), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGlobalRow++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在每个视图之后添加一个浅色分隔线
|
|
||||||
if (currentGlobalRow < totalRows) {
|
|
||||||
int separatorY = 60 + (currentGlobalRow * cellHeight);
|
|
||||||
cv::line(faceImage,
|
|
||||||
cv::Point(0, separatorY - 1),
|
|
||||||
cv::Point(totalWidth, separatorY - 1),
|
|
||||||
cv::Scalar(220, 220, 220), 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加网格线
|
|
||||||
for (int row = 0; row <= totalRows; row++) {
|
|
||||||
int y = 60 + (row * cellHeight);
|
|
||||||
cv::line(faceImage,
|
|
||||||
cv::Point(0, y),
|
|
||||||
cv::Point(totalWidth, y),
|
|
||||||
cv::Scalar(200, 200, 200), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加视图名称区域分隔线
|
|
||||||
cv::line(faceImage,
|
|
||||||
cv::Point(viewNameWidth, 60),
|
|
||||||
cv::Point(viewNameWidth, totalHeight),
|
|
||||||
cv::Scalar(150, 150, 150), 2);
|
|
||||||
|
|
||||||
// 保存图片
|
|
||||||
std::string outputPath = outputDir + cv::format("face_%d_comparison.png", faceId);
|
|
||||||
|
|
||||||
if (!cv::imwrite(outputPath, faceImage)) {
|
|
||||||
printf("❌ 无法保存face %d的对比图: %s\n", faceId, outputPath.c_str());
|
|
||||||
} else {
|
|
||||||
printf("✅ face %d对比图已保存: %s\n", faceId, outputPath.c_str());
|
|
||||||
printf(" 尺寸: %d x %d 像素, 视图数: %zu, 记录数: %d\n",
|
|
||||||
totalWidth, totalHeight, viewMap.size(), totalRecords);
|
|
||||||
}
|
|
||||||
|
|
||||||
faceCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存颜色信息到CSV文件
|
|
||||||
saveColorInfoToFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存颜色信息到CSV文件
|
|
||||||
void ColorComparisonPixel::saveColorInfoToFile() {
|
|
||||||
// 为每个face保存单独的CSV
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
int faceId = faceEntry.first;
|
|
||||||
const auto& viewMap = faceEntry.second;
|
|
||||||
|
|
||||||
if (viewMap.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string filePath = outputDir + cv::format("face_%d_colors.csv", faceId);
|
|
||||||
FILE* fp = fopen(filePath.c_str(), "w");
|
|
||||||
|
|
||||||
if (!fp) {
|
|
||||||
printf("❌ 无法创建文件: %s\n", filePath.c_str());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 写入CSV标题
|
|
||||||
fprintf(fp, "FaceID,View,RecordNo,Gaussian_R,Gaussian_G,Gaussian_B,Original_R,Original_G,Original_B,Distance,Threshold,NeedsFix\n");
|
|
||||||
|
|
||||||
int recordNo = 1;
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
const std::string& viewName = viewEntry.first;
|
|
||||||
const std::vector<ColorInfo>& infos = viewEntry.second;
|
|
||||||
|
|
||||||
for (int i = 0; i < (int)infos.size(); i++) {
|
|
||||||
const ColorInfo& info = infos[i];
|
|
||||||
bool needsFix = (info.distance > info.threshold);
|
|
||||||
fprintf(fp, "%d,%s,%d,%d,%d,%d,%d,%d,%d,%.4f,%.4f,%s\n",
|
|
||||||
faceId,
|
|
||||||
viewName.c_str(),
|
|
||||||
i + 1,
|
|
||||||
info.gaussianColor[0], info.gaussianColor[1], info.gaussianColor[2],
|
|
||||||
info.originalColor[0], info.originalColor[1], info.originalColor[2],
|
|
||||||
info.distance, info.threshold,
|
|
||||||
needsFix ? "YES" : "NO");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(fp);
|
|
||||||
printf("✅ face %d颜色信息已保存到: %s\n", faceId, filePath.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存汇总统计信息
|
|
||||||
std::string summaryPath = outputDir + "face_comparison_summary.csv";
|
|
||||||
FILE* summaryFp = fopen(summaryPath.c_str(), "w");
|
|
||||||
|
|
||||||
if (summaryFp) {
|
|
||||||
fprintf(summaryFp, "FaceID,Views,TotalRecords,NeedFixRecords,NeedFix%%,MinDistance,MaxDistance,AvgDistance\n");
|
|
||||||
|
|
||||||
for (const auto& faceEntry : faceViewColorMap) {
|
|
||||||
int faceId = faceEntry.first;
|
|
||||||
const auto& viewMap = faceEntry.second;
|
|
||||||
|
|
||||||
int totalRecords = 0;
|
|
||||||
int needFixCount = 0;
|
|
||||||
float minDist = std::numeric_limits<float>::max();
|
|
||||||
float maxDist = 0.0f;
|
|
||||||
float sumDist = 0.0f;
|
|
||||||
|
|
||||||
for (const auto& viewEntry : viewMap) {
|
|
||||||
const std::vector<ColorInfo>& infos = viewEntry.second;
|
|
||||||
totalRecords += infos.size();
|
|
||||||
|
|
||||||
for (const auto& info : infos) {
|
|
||||||
if (info.distance > info.threshold) needFixCount++;
|
|
||||||
minDist = std::min(minDist, info.distance);
|
|
||||||
maxDist = std::max(maxDist, info.distance);
|
|
||||||
sumDist += info.distance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float avgDist = (totalRecords > 0) ? (sumDist / totalRecords) : 0.0f;
|
|
||||||
float fixPercentage = (totalRecords > 0) ? (needFixCount * 100.0f / totalRecords) : 0.0f;
|
|
||||||
|
|
||||||
fprintf(summaryFp, "%d,%zu,%d,%d,%.2f%%,%.4f,%.4f,%.4f\n",
|
|
||||||
faceId,
|
|
||||||
viewMap.size(),
|
|
||||||
totalRecords,
|
|
||||||
needFixCount,
|
|
||||||
fixPercentage,
|
|
||||||
minDist,
|
|
||||||
maxDist,
|
|
||||||
avgDist);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(summaryFp);
|
|
||||||
printf("✅ 汇总统计信息已保存到: %s\n", summaryPath.c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
#ifndef COLORCOMPARISONPIXEL_H
|
|
||||||
#define COLORCOMPARISONPIXEL_H
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <map>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include "ColorComparisonFace.h"
|
|
||||||
|
|
||||||
/*
|
|
||||||
// 不使用前向声明,直接定义简单的颜色结构
|
|
||||||
struct MeshColor {
|
|
||||||
unsigned char r, g, b;
|
|
||||||
|
|
||||||
MeshColor() : r(0), g(0), b(0) {}
|
|
||||||
MeshColor(unsigned char r, unsigned char g, unsigned char b)
|
|
||||||
: r(r), g(g), b(b) {}
|
|
||||||
|
|
||||||
unsigned char operator[](size_t idx) const {
|
|
||||||
if (idx == 0) return r;
|
|
||||||
if (idx == 1) return g;
|
|
||||||
if (idx == 2) return b;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
*/
|
|
||||||
|
|
||||||
class ColorComparisonPixel {
|
|
||||||
private:
|
|
||||||
struct ColorInfo {
|
|
||||||
int faceId;
|
|
||||||
MeshColor gaussianColor;
|
|
||||||
MeshColor originalColor;
|
|
||||||
float distance;
|
|
||||||
float threshold;
|
|
||||||
std::string filename;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 使用嵌套map: faceid -> filename -> ColorInfo列表
|
|
||||||
std::map<int, std::map<std::string, std::vector<ColorInfo>>> faceViewColorMap;
|
|
||||||
std::string outputDir;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// 构造函数
|
|
||||||
ColorComparisonPixel(const std::string& dir);
|
|
||||||
|
|
||||||
// 析构函数
|
|
||||||
virtual ~ColorComparisonPixel() {}
|
|
||||||
|
|
||||||
// 添加颜色信息
|
|
||||||
void addColorInfo(int faceId,
|
|
||||||
const MeshColor& gaussianColor,
|
|
||||||
const MeshColor& originalColor,
|
|
||||||
float distance, float threshold,
|
|
||||||
const std::string& filename);
|
|
||||||
|
|
||||||
// 创建按faceid分组的对比图
|
|
||||||
void createBatchComparison(int maxCellsPerRow = 10, int maxFacesPerImage = 50);
|
|
||||||
|
|
||||||
// 保存颜色信息到CSV文件
|
|
||||||
void saveColorInfoToFile();
|
|
||||||
|
|
||||||
// 获取总face数
|
|
||||||
int getTotalFaces() const;
|
|
||||||
|
|
||||||
// 获取总记录数
|
|
||||||
int getTotalRecords() const;
|
|
||||||
|
|
||||||
// 获取faceid列表
|
|
||||||
std::vector<int> getFaceIds() const;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif // COLORCOMPARISONPIXEL_H
|
|
||||||
+68
-47
@@ -350,16 +350,6 @@ void Mesh::ComputeNormalFaces()
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// compute color for all faces
|
|
||||||
void Mesh::ComputeColorFaces()
|
|
||||||
{
|
|
||||||
faceColors.resize(faces.size());
|
|
||||||
|
|
||||||
FOREACH(idxFace, faces)
|
|
||||||
faceColors[idxFace] = FaceColor(faces[idxFace]);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute normal for all vertices
|
// compute normal for all vertices
|
||||||
#if 1
|
#if 1
|
||||||
// computes the vertex normal as the area weighted face normals average
|
// computes the vertex normal as the area weighted face normals average
|
||||||
@@ -1106,20 +1096,18 @@ namespace BasicPLY {
|
|||||||
struct Vertex {
|
struct Vertex {
|
||||||
Mesh::Vertex v;
|
Mesh::Vertex v;
|
||||||
Mesh::Normal n;
|
Mesh::Normal n;
|
||||||
Mesh::Color color;
|
|
||||||
static void InitLoadProps(PLY& ply, int elem_count,
|
static void InitLoadProps(PLY& ply, int elem_count,
|
||||||
Mesh::VertexArr& vertices, Mesh::ColorArr& vertexColors)
|
Mesh::VertexArr& vertices, Mesh::NormalArr& vertexNormals)
|
||||||
{
|
{
|
||||||
PLY::PlyElement* elm = ply.find_element(elem_names[0]);
|
PLY::PlyElement* elm = ply.find_element(elem_names[0]);
|
||||||
const size_t nMaxProps(SizeOfArray(props));
|
const size_t nMaxProps(SizeOfArray(props));
|
||||||
for (size_t p=0; p<nMaxProps; ++p) {
|
for (size_t p=0; p<nMaxProps; ++p) {
|
||||||
// DEBUG_EXTRA("InitLoadProps name=%s\n", props[p].name.c_str());
|
|
||||||
if (ply.find_property(elm, props[p].name.c_str()) < 0)
|
if (ply.find_property(elm, props[p].name.c_str()) < 0)
|
||||||
continue;
|
continue;
|
||||||
ply.setup_property(props[p]);
|
ply.setup_property(props[p]);
|
||||||
switch (p) {
|
switch (p) {
|
||||||
case 0: vertices.resize((IDX)elem_count); break;
|
case 0: vertices.resize((IDX)elem_count); break;
|
||||||
case 3: vertexColors.resize((IDX)elem_count); break;
|
case 3: vertexNormals.resize((IDX)elem_count); break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1137,15 +1125,6 @@ namespace BasicPLY {
|
|||||||
}
|
}
|
||||||
static const PLY::PlyProperty props[9];
|
static const PLY::PlyProperty props[9];
|
||||||
};
|
};
|
||||||
const PLY::PlyProperty Vertex::props[] = {
|
|
||||||
{"x", PLY::Float32, PLY::Float32, offsetof(Vertex,v.x), 0, 0, 0, 0},
|
|
||||||
{"y", PLY::Float32, PLY::Float32, offsetof(Vertex,v.y), 0, 0, 0, 0},
|
|
||||||
{"z", PLY::Float32, PLY::Float32, offsetof(Vertex,v.z), 0, 0, 0, 0},
|
|
||||||
{"red", PLY::Int8, PLY::Int8, offsetof(Vertex,color.r), 0, 0, 0, 0},
|
|
||||||
{"green", PLY::Int8, PLY::Int8, offsetof(Vertex,color.g), 0, 0, 0, 0},
|
|
||||||
{"blue", PLY::Int8, PLY::Int8, offsetof(Vertex,color.b), 0, 0, 0, 0}
|
|
||||||
};
|
|
||||||
/*
|
|
||||||
const PLY::PlyProperty Vertex::props[] = {
|
const PLY::PlyProperty Vertex::props[] = {
|
||||||
{"x", PLY::Float32, PLY::Float32, offsetof(Vertex,v.x), 0, 0, 0, 0},
|
{"x", PLY::Float32, PLY::Float32, offsetof(Vertex,v.x), 0, 0, 0, 0},
|
||||||
{"y", PLY::Float32, PLY::Float32, offsetof(Vertex,v.y), 0, 0, 0, 0},
|
{"y", PLY::Float32, PLY::Float32, offsetof(Vertex,v.y), 0, 0, 0, 0},
|
||||||
@@ -1154,7 +1133,6 @@ namespace BasicPLY {
|
|||||||
{"ny", PLY::Float32, PLY::Float32, offsetof(Vertex,n.y), 0, 0, 0, 0},
|
{"ny", PLY::Float32, PLY::Float32, offsetof(Vertex,n.y), 0, 0, 0, 0},
|
||||||
{"nz", PLY::Float32, PLY::Float32, offsetof(Vertex,n.z), 0, 0, 0, 0}
|
{"nz", PLY::Float32, PLY::Float32, offsetof(Vertex,n.z), 0, 0, 0, 0}
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
// list of property information for a face
|
// list of property information for a face
|
||||||
struct Face {
|
struct Face {
|
||||||
struct FaceIndices {
|
struct FaceIndices {
|
||||||
@@ -1213,25 +1191,61 @@ namespace BasicPLY {
|
|||||||
} // namespace MeshInternal
|
} // namespace MeshInternal
|
||||||
|
|
||||||
// import the mesh from the given file
|
// import the mesh from the given file
|
||||||
bool Mesh::Load(const String& fileName, bool *pBExistVertexColor)
|
bool Mesh::Load(const String& fileName, bool bLoadUV)
|
||||||
{
|
{
|
||||||
TD_TIMER_STARTD();
|
TD_TIMER_STARTD();
|
||||||
const String ext(Util::getFileExt(fileName).ToLower());
|
const String ext(Util::getFileExt(fileName).ToLower());
|
||||||
bool ret;
|
bool ret;
|
||||||
if (ext == _T(".obj"))
|
if (ext == _T(".obj"))
|
||||||
ret = LoadOBJ(fileName);
|
ret = LoadOBJ(fileName, bLoadUV);
|
||||||
else
|
else if (ext == _T(".gltf") || ext == _T(".glb"))
|
||||||
if (ext == _T(".gltf") || ext == _T(".glb"))
|
|
||||||
ret = LoadGLTF(fileName, ext == _T(".glb"));
|
ret = LoadGLTF(fileName, ext == _T(".glb"));
|
||||||
else
|
else
|
||||||
ret = LoadPLY(fileName, pBExistVertexColor);
|
ret = LoadPLY(fileName);
|
||||||
if (!ret)
|
if (!ret)
|
||||||
return false;
|
return false;
|
||||||
DEBUG_EXTRA("Mesh loaded: %u vertices, %u faces (%s)", vertices.size(), faces.size(), TD_TIMER_GET_FMT().c_str());
|
DEBUG_EXTRA("Mesh loaded: %u vertices, %u faces (%s)", vertices.size(), faces.size(), TD_TIMER_GET_FMT().c_str());
|
||||||
return true;
|
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
|
// import the mesh as a PLY file
|
||||||
bool Mesh::LoadPLY(const String& fileName, bool *pBExistVertexColor)
|
bool Mesh::LoadPLY(const String& fileName)
|
||||||
{
|
{
|
||||||
ASSERT(!fileName.empty());
|
ASSERT(!fileName.empty());
|
||||||
Release();
|
Release();
|
||||||
@@ -1266,23 +1280,16 @@ bool Mesh::LoadPLY(const String& fileName, bool *pBExistVertexColor)
|
|||||||
LPCSTR elem_name = ply.setup_element_read(i, &elem_count);
|
LPCSTR elem_name = ply.setup_element_read(i, &elem_count);
|
||||||
if (PLY::equal_strings(BasicPLY::elem_names[0], elem_name)) {
|
if (PLY::equal_strings(BasicPLY::elem_names[0], elem_name)) {
|
||||||
ASSERT(vertices.size() == (VIndex)elem_count);
|
ASSERT(vertices.size() == (VIndex)elem_count);
|
||||||
// BasicPLY::Vertex::InitLoadProps(ply, elem_count, vertices, vertexNormals);
|
BasicPLY::Vertex::InitLoadProps(ply, elem_count, vertices, vertexNormals);
|
||||||
BasicPLY::Vertex::InitLoadProps(ply, elem_count, vertices, vertexColors);
|
if (vertexNormals.empty()) {
|
||||||
if (vertexColors.empty()) {
|
|
||||||
// printf("vertexColors.empty, elem_count=%u\n", elem_count);
|
|
||||||
*pBExistVertexColor = false;
|
|
||||||
for (Vertex& vert: vertices)
|
for (Vertex& vert: vertices)
|
||||||
ply.get_element(&vert);
|
ply.get_element(&vert);
|
||||||
} else {
|
} else {
|
||||||
// printf("Not vertexColors.empty, elem_count=%u\n", elem_count);
|
|
||||||
*pBExistVertexColor = true;
|
|
||||||
BasicPLY::Vertex vertex;
|
BasicPLY::Vertex vertex;
|
||||||
for (int v=0; v<elem_count; ++v) {
|
for (int v=0; v<elem_count; ++v) {
|
||||||
ply.get_element(&vertex);
|
ply.get_element(&vertex);
|
||||||
vertices[v] = vertex.v;
|
vertices[v] = vertex.v;
|
||||||
vertexColors[v] = vertex.color;
|
vertexNormals[v] = vertex.n;
|
||||||
// if (v<10)
|
|
||||||
// printf("vertexColors v=%u, rgb=(%u, %u, %u)\n", v, vertex.color.r, vertex.color.g, vertex.color.b);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
@@ -1331,17 +1338,29 @@ bool Mesh::LoadPLY(const String& fileName, bool *pBExistVertexColor)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// import the mesh as a OBJ file
|
// import the mesh as a OBJ file
|
||||||
bool Mesh::LoadOBJ(const String& fileName)
|
bool Mesh::LoadOBJ(const String& fileName, bool bLoadUV)
|
||||||
{
|
{
|
||||||
ASSERT(!fileName.empty());
|
ASSERT(!fileName.empty());
|
||||||
Release();
|
Release();
|
||||||
|
|
||||||
// open and parse OBJ file
|
// open and parse OBJ file
|
||||||
ObjModel model;
|
ObjModel model;
|
||||||
if (!model.Load(fileName)) {
|
if (bLoadUV)
|
||||||
DEBUG_EXTRA("error: invalid OBJ file");
|
{
|
||||||
|
if (!model.LoadUV(fileName, faceTexcoords, true)) {
|
||||||
|
DEBUG_EXTRA("error: LoadUV invalid OBJ file %s", fileName.c_str());
|
||||||
return false;
|
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.get_vertices().empty() || model.get_groups().empty()) {
|
if (model.get_vertices().empty() || model.get_groups().empty()) {
|
||||||
DEBUG_EXTRA("error: invalid mesh file");
|
DEBUG_EXTRA("error: invalid mesh file");
|
||||||
@@ -1369,11 +1388,13 @@ bool Mesh::LoadOBJ(const String& fileName)
|
|||||||
for (const ObjModel::Face& f: group.faces) {
|
for (const ObjModel::Face& f: group.faces) {
|
||||||
ASSERT(f.vertices[0] != NO_ID);
|
ASSERT(f.vertices[0] != NO_ID);
|
||||||
faces.emplace_back(f.vertices[0], f.vertices[1], f.vertices[2]);
|
faces.emplace_back(f.vertices[0], f.vertices[1], f.vertices[2]);
|
||||||
|
/*
|
||||||
if (f.texcoords[0] != NO_ID) {
|
if (f.texcoords[0] != NO_ID) {
|
||||||
for (int i=0; i<3; ++i)
|
for (int i=0; i<3; ++i)
|
||||||
faceTexcoords.emplace_back(model.get_texcoords()[f.texcoords[i]]);
|
faceTexcoords.emplace_back(model.get_texcoords()[f.texcoords[i]]);
|
||||||
faceTexindices.emplace_back((TexIndex)groupIdx);
|
faceTexindices.emplace_back((TexIndex)groupIdx);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if (f.normals[0] != NO_ID) {
|
if (f.normals[0] != NO_ID) {
|
||||||
Normal& n = faceNormals.emplace_back(Normal::ZERO);
|
Normal& n = faceNormals.emplace_back(Normal::ZERO);
|
||||||
for (int i=0; i<3; ++i)
|
for (int i=0; i<3; ++i)
|
||||||
@@ -1474,13 +1495,13 @@ bool Mesh::LoadGLTF(const String& fileName, bool bBinary)
|
|||||||
/*----------------------------------------------------------------*/
|
/*----------------------------------------------------------------*/
|
||||||
|
|
||||||
// export the mesh to the given file
|
// export the mesh to the given file
|
||||||
bool Mesh::Save(const String& fileName, const cList<String>& comments, bool bBinary) const
|
bool Mesh::Save(const String& fileName, const cList<String>& comments, bool bBinary, bool bUseExistingUV) const
|
||||||
{
|
{
|
||||||
TD_TIMER_STARTD();
|
TD_TIMER_STARTD();
|
||||||
const String ext(Util::getFileExt(fileName).ToLower());
|
const String ext(Util::getFileExt(fileName).ToLower());
|
||||||
bool ret;
|
bool ret;
|
||||||
if (ext == _T(".obj"))
|
if (ext == _T(".obj"))
|
||||||
ret = SaveOBJ(fileName);
|
ret = SaveOBJ(fileName, bUseExistingUV);
|
||||||
else
|
else
|
||||||
if (ext == _T(".gltf") || ext == _T(".glb"))
|
if (ext == _T(".gltf") || ext == _T(".glb"))
|
||||||
ret = SaveGLTF(fileName, ext == _T(".glb"));
|
ret = SaveGLTF(fileName, ext == _T(".glb"));
|
||||||
@@ -1567,7 +1588,7 @@ bool Mesh::SavePLY(const String& fileName, const cList<String>& comments, bool b
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// export the mesh as a OBJ file
|
// export the mesh as a OBJ file
|
||||||
bool Mesh::SaveOBJ(const String& fileName) const
|
bool Mesh::SaveOBJ(const String& fileName, bool bUseExistingUV) const
|
||||||
{
|
{
|
||||||
ASSERT(!fileName.empty());
|
ASSERT(!fileName.empty());
|
||||||
Util::ensureFolder(fileName);
|
Util::ensureFolder(fileName);
|
||||||
@@ -1626,7 +1647,7 @@ bool Mesh::SaveOBJ(const String& fileName) const
|
|||||||
pMaterial->diffuse_map = texturesDiffuse[idxTexture];
|
pMaterial->diffuse_map = texturesDiffuse[idxTexture];
|
||||||
}
|
}
|
||||||
|
|
||||||
return model.Save(fileName, 6U, true);
|
return model.Save(fileName, 6U, true, bUseExistingUV);
|
||||||
}
|
}
|
||||||
// export the mesh as a GLTF file
|
// export the mesh as a GLTF file
|
||||||
template <typename T>
|
template <typename T>
|
||||||
|
|||||||
+7
-17
@@ -61,9 +61,6 @@ public:
|
|||||||
typedef SEACAVE::cList<Vertex,const Vertex&,0,8192,VIndex> VertexArr;
|
typedef SEACAVE::cList<Vertex,const Vertex&,0,8192,VIndex> VertexArr;
|
||||||
typedef SEACAVE::cList<Face,const Face&,0,8192,FIndex> FaceArr;
|
typedef SEACAVE::cList<Face,const Face&,0,8192,FIndex> FaceArr;
|
||||||
|
|
||||||
typedef Color8U Color;
|
|
||||||
typedef SEACAVE::cList<Color,const Color&,0,8192,VIndex> ColorArr;
|
|
||||||
|
|
||||||
typedef SEACAVE::cList<VIndex,VIndex,0,8,VIndex> VertexIdxArr;
|
typedef SEACAVE::cList<VIndex,VIndex,0,8,VIndex> VertexIdxArr;
|
||||||
typedef SEACAVE::cList<FIndex,FIndex,0,8,FIndex> FaceIdxArr;
|
typedef SEACAVE::cList<FIndex,FIndex,0,8,FIndex> FaceIdxArr;
|
||||||
typedef SEACAVE::cList<VertexIdxArr,const VertexIdxArr&,2,8192,VIndex> VertexVerticesArr;
|
typedef SEACAVE::cList<VertexIdxArr,const VertexIdxArr&,2,8192,VIndex> VertexVerticesArr;
|
||||||
@@ -157,9 +154,6 @@ public:
|
|||||||
VertexFacesArr vertexFaces; // for each vertex, the ordered list of faces containing it (optional)
|
VertexFacesArr vertexFaces; // for each vertex, the ordered list of faces containing it (optional)
|
||||||
BoolArr vertexBoundary; // for each vertex, stores if it is at the boundary or not (optional)
|
BoolArr vertexBoundary; // for each vertex, stores if it is at the boundary or not (optional)
|
||||||
|
|
||||||
ColorArr vertexColors; // for each vertex, the normal to the surface in that point (optional)
|
|
||||||
ColorArr faceColors; // for each face, the color to it
|
|
||||||
|
|
||||||
NormalArr faceNormals; // for each face, the normal to it (optional)
|
NormalArr faceNormals; // for each face, the normal to it (optional)
|
||||||
FaceFacesArr faceFaces; // for each face, the list of adjacent faces, NO_ID for border edges (optional)
|
FaceFacesArr faceFaces; // for each face, the list of adjacent faces, NO_ID for border edges (optional)
|
||||||
TexCoordArr faceTexcoords; // for each face, the texture-coordinates corresponding to its vertices, 3x num faces OR for each vertex (optional)
|
TexCoordArr faceTexcoords; // for each face, the texture-coordinates corresponding to its vertices, 3x num faces OR for each vertex (optional)
|
||||||
@@ -200,8 +194,6 @@ public:
|
|||||||
void ComputeNormalFaces();
|
void ComputeNormalFaces();
|
||||||
void ComputeNormalVertices();
|
void ComputeNormalVertices();
|
||||||
|
|
||||||
void ComputeColorFaces();
|
|
||||||
|
|
||||||
void SmoothNormalFaces(float fMaxGradient=25.f, float fOriginalWeight=0.5f, unsigned nIterations=3);
|
void SmoothNormalFaces(float fMaxGradient=25.f, float fOriginalWeight=0.5f, unsigned nIterations=3);
|
||||||
|
|
||||||
void GetEdgeFaces(VIndex, VIndex, FaceIdxArr&) const;
|
void GetEdgeFaces(VIndex, VIndex, FaceIdxArr&) const;
|
||||||
@@ -246,10 +238,6 @@ public:
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline Color FaceColor(const Face& f) const {
|
|
||||||
return ComputeTriangleColor(vertexColors[f[0]], vertexColors[f[1]], vertexColors[f[2]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Planef EstimateGroundPlane(const ImageArr& images, float sampleMesh=0, float planeThreshold=0, const String& fileExportPlane="") const;
|
Planef EstimateGroundPlane(const ImageArr& images, float sampleMesh=0, float planeThreshold=0, const String& fileExportPlane="") const;
|
||||||
|
|
||||||
Vertex ComputeCentroid(FIndex) const;
|
Vertex ComputeCentroid(FIndex) const;
|
||||||
@@ -274,8 +262,8 @@ public:
|
|||||||
bool TransferTexture(Mesh& mesh, const FaceIdxArr& faceSubsetIndices={}, unsigned borderSize=3, unsigned textureSize=4096);
|
bool TransferTexture(Mesh& mesh, const FaceIdxArr& faceSubsetIndices={}, unsigned borderSize=3, unsigned textureSize=4096);
|
||||||
|
|
||||||
// file IO
|
// file IO
|
||||||
bool Load(const String& fileName, bool *pBExistVertexColor=nullptr);
|
bool Load(const String& fileName, bool bLoadUV=false);
|
||||||
bool Save(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true) const;
|
bool Save(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true, bool bUseExistingUV=false) const;
|
||||||
bool Save(const FacesChunkArr&, 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);
|
static bool Save(const VertexArr& vertices, const String& fileName, bool bBinary=true);
|
||||||
|
|
||||||
@@ -283,13 +271,15 @@ 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(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]; }
|
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:
|
protected:
|
||||||
bool LoadPLY(const String& fileName, bool *pBExistVertexColor);
|
bool LoadPLY(const String& fileName);
|
||||||
bool LoadOBJ(const String& fileName);
|
bool LoadOBJ(const String& fileName, bool bLoadUV);
|
||||||
bool LoadGLTF(const String& fileName, bool bBinary=true);
|
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 SavePLY(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true, bool bTexLossless=true) const;
|
||||||
bool SaveOBJ(const String& fileName) const;
|
bool SaveOBJ(const String& fileName, bool bUseExistingUV) const;
|
||||||
bool SaveGLTF(const String& fileName, bool bBinary=true) const;
|
bool SaveGLTF(const String& fileName, bool bBinary=true) const;
|
||||||
|
|
||||||
#ifdef _USE_CUDA
|
#ifdef _USE_CUDA
|
||||||
|
|||||||
+16
-5
@@ -66,9 +66,6 @@ public:
|
|||||||
std::map<std::string, std::unordered_set<int>> edge_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_map;
|
||||||
std::unordered_set<int> face_visible_relative;
|
std::unordered_set<int> face_visible_relative;
|
||||||
std::unordered_set<int> face_test;
|
|
||||||
|
|
||||||
std::string base_path;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
inline Scene(unsigned _nMaxThreads=0)
|
inline Scene(unsigned _nMaxThreads=0)
|
||||||
@@ -167,13 +164,27 @@ public:
|
|||||||
std::map<std::string, std::unordered_set<int>>& edge_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_map,
|
||||||
std::string& basePath);
|
std::string& basePath);
|
||||||
bool LoadTestFacesData(std::unordered_set<int>& face_test, 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
|
// Mesh texturing
|
||||||
bool TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned minCommonCameras=0, float fOutlierThreshold=0.f, float fRatioDataSmoothness=0.3f,
|
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),
|
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,
|
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 bExistVertexColor = false);
|
const std::string& inputFileName = "", const std::string& meshFileName = "", bool bUseExistingUV = false, const std::string& strUVMeshFileName = "");
|
||||||
|
|
||||||
std::string runPython(const std::string& command);
|
std::string runPython(const std::string& command);
|
||||||
bool is_face_visible(const std::string& image_name, int face_index);
|
bool is_face_visible(const std::string& image_name, int face_index);
|
||||||
|
|||||||
+14366
-2616
File diff suppressed because it is too large.
Load diff
Executable
+120
@@ -0,0 +1,120 @@
|
|||||||
|
"""
|
||||||
|
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()
|
||||||
@@ -4,8 +4,7 @@ import os
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy.spatial.transform import Rotation
|
from scipy.spatial.transform import Rotation
|
||||||
import sys
|
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 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
|
# from get_pose_matrix import get_w2c
|
||||||
import argparse
|
import argparse
|
||||||
@@ -34,7 +33,6 @@ from typing import Dict, List, Set
|
|||||||
import struct
|
import struct
|
||||||
import math
|
import math
|
||||||
# import os
|
# import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
CameraModel = collections.namedtuple(
|
CameraModel = collections.namedtuple(
|
||||||
"CameraModel", ["model_id", "model_name", "num_params"]
|
"CameraModel", ["model_id", "model_name", "num_params"]
|
||||||
@@ -2177,8 +2175,6 @@ class ModelProcessor:
|
|||||||
base_path: 基础文件路径
|
base_path: 基础文件路径
|
||||||
"""
|
"""
|
||||||
|
|
||||||
os.makedirs(base_path, exist_ok = True)
|
|
||||||
|
|
||||||
print(f"save_occlusion_data {base_path}, {len(result1)}, {len(result2)}, {len(result3)}")
|
print(f"save_occlusion_data {base_path}, {len(result1)}, {len(result2)}, {len(result3)}")
|
||||||
|
|
||||||
# 处理返回的可见面字典 - 转换为图像名到面编号集合的映射
|
# 处理返回的可见面字典 - 转换为图像名到面编号集合的映射
|
||||||
@@ -2201,12 +2197,9 @@ class ModelProcessor:
|
|||||||
for image_name, face_list in result3.items():
|
for image_name, face_list in result3.items():
|
||||||
delete_edge_faces_map[image_name] = set(face_list)
|
delete_edge_faces_map[image_name] = set(face_list)
|
||||||
|
|
||||||
|
|
||||||
# 保存 visible_faces_map
|
# 保存 visible_faces_map
|
||||||
try:
|
try:
|
||||||
file_name = "_visible_faces_map.txt"
|
with open(base_path + "/_visible_faces_map.txt", "w", encoding='utf-8') as map_file:
|
||||||
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():
|
for image_name, face_set in visible_faces_map.items():
|
||||||
# 写入图像名称和所有面ID,用空格分隔
|
# 写入图像名称和所有面ID,用空格分隔
|
||||||
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
|
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
|
||||||
@@ -2216,9 +2209,7 @@ class ModelProcessor:
|
|||||||
|
|
||||||
# 保存 face_visible_relative
|
# 保存 face_visible_relative
|
||||||
try:
|
try:
|
||||||
file_name = "_face_visible_relative.txt"
|
with open(base_path + "/_face_visible_relative.txt", "w", encoding='utf-8') as relative_file:
|
||||||
file_path = Path(base_path) / file_name
|
|
||||||
with open(file_path, "w", encoding='utf-8') as relative_file:
|
|
||||||
for face in face_visible_relative:
|
for face in face_visible_relative:
|
||||||
relative_file.write(str(face) + "\n")
|
relative_file.write(str(face) + "\n")
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
@@ -2226,9 +2217,7 @@ class ModelProcessor:
|
|||||||
|
|
||||||
# 保存 edge_faces_map
|
# 保存 edge_faces_map
|
||||||
try:
|
try:
|
||||||
file_name = "_edge_faces_map.txt"
|
with open(base_path + "/_edge_faces_map.txt", "w", encoding='utf-8') as map_file2:
|
||||||
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():
|
for image_name, face_set in edge_faces_map.items():
|
||||||
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
|
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
|
||||||
map_file2.write(line)
|
map_file2.write(line)
|
||||||
@@ -2237,9 +2226,7 @@ class ModelProcessor:
|
|||||||
|
|
||||||
# 保存 delete_edge_faces_map
|
# 保存 delete_edge_faces_map
|
||||||
try:
|
try:
|
||||||
file_name = "_delete_edge_faces_map.txt"
|
with open(base_path + "/_delete_edge_faces_map.txt", "w", encoding='utf-8') as map_file3:
|
||||||
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():
|
for image_name, face_set in delete_edge_faces_map.items():
|
||||||
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
|
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
|
||||||
map_file3.write(line)
|
map_file3.write(line)
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import bpy
|
|
||||||
import os
|
|
||||||
|
|
||||||
mesh = bpy.context.active_object.data
|
|
||||||
|
|
||||||
selected_faces = [polygon.index for polygon in mesh.polygons if polygon.select]
|
|
||||||
|
|
||||||
output_lines = ["详细信息:"]
|
|
||||||
|
|
||||||
for face_idx in selected_faces[:10]: # 只显示前10个
|
|
||||||
face = mesh.polygons[face_idx]
|
|
||||||
output_lines.append(f" 面 {face_idx}: 顶点数={len(face.vertices)}, ={face.area:.4f}")
|
|
||||||
|
|
||||||
text_name = f"selected_faces_report"
|
|
||||||
|
|
||||||
# 删除已存在的同名文本数据块
|
|
||||||
if text_name in bpy.data.texts:
|
|
||||||
bpy.data.texts.remove(bpy.data.texts[text_name])
|
|
||||||
|
|
||||||
text_block = bpy.data.texts.new(text_name)
|
|
||||||
for line in output_lines:
|
|
||||||
text_block.write(line + "\n")
|
|
||||||
|
|
||||||
save_dir = "/home/algo/Documents/openMVS/data/446442/out.446442"
|
|
||||||
os.makedirs(save_dir, exist_ok=True)
|
|
||||||
|
|
||||||
file_path = os.path.join(save_dir, "_test_faces.txt")
|
|
||||||
|
|
||||||
with open(file_path, 'w') as f:
|
|
||||||
for face_index in selected_faces:
|
|
||||||
f.write(f"{face_index}\n")
|
|
||||||
|
|
||||||
print(f"面片索引已保存到: {file_path}")
|
|
||||||
Reference in new issue
Block a user