Compare commits

..
Author SHA1 Message Date
hesuicong 7a86495d6a 边做圆角 2026-11-10 17:33:46 +08:00
hesuicong efba6dbe29 填不空隙,平滑处理 2026-11-10 15:53:08 +08:00
hesuicong 38c0b9948a 优化眼镜填空 2026-11-09 11:42:05 +08:00
hesuicong 6f7805c78d 上传代码 2026-11-09 10:05:17 +08:00
hesuicong ac85d3a23c 平滑处理 2026-11-06 17:03:58 +08:00
hesuicong 1f272668f1 整理代码和平滑处理 2026-11-06 16:47:58 +08:00
hesuicong 22bfc8adfb 整理代码 2026-11-06 15:59:59 +08:00
hesuicong 17446d4053 添加眼镜优化代码 2026-11-06 14:57:38 +08:00
hesuicong 5b1b063bee 删除代码 2026-11-06 14:55:15 +08:00
hesuicong d5aa82f15e 眼镜生成代码添加 2026-11-06 14:53:51 +08:00
hesuicong 95570fede1 暗部提亮 2026-11-05 17:08:50 +08:00
hesuicong 4d3e0ee04f 暗部提亮 2026-11-04 17:31:23 +08:00
hesuicong d4330afd6f 暗部提亮测试代码 2026-10-31 15:56:07 +08:00
hesuicong 2f88885ccc 解决色差问题 2026-10-23 14:56:18 +08:00
hesuicong 07a4ade712 模糊和清晰平衡 2026-10-22 10:34:49 +08:00
hesuicong 433a06716a 进一步清晰化 2026-10-21 17:02:00 +08:00
hesuicong 875f0c5be5 进一步整理finalPatch逻辑 2026-10-21 16:45:39 +08:00
hesuicong 209fe6a260 finalPatch逻辑整理 2026-10-21 15:03:39 +08:00
hesuicong 74271d0ad6 填满小黄点 2026-10-21 11:13:44 +08:00
hesuicong 272d3900d0 清晰化 2026-10-20 11:24:50 +08:00
hesuicong 6b67c75c7e 解决缝隙问题和填色问题 2026-10-19 18:25:04 +08:00
hesuicong f7fccb2ae9 参数和逻辑微调 2026-10-19 11:19:42 +08:00
hesuicong c5f93374f4 解决缝隙问题 2026-10-17 18:01:01 +08:00
hesuicong 351a0a274f 解决矩形杂色和色差问题 2026-10-16 18:13:21 +08:00
hesuicong b106a7976e 解决掉矩形杂色问题,但是过渡有色差 2026-10-16 11:04:26 +08:00
hesuicong 05bae3ad90 注释打印图片 2026-10-15 11:09:38 +08:00
hesuicong eba47cae18 优化UV包围盒问题 2026-10-13 16:07:50 +08:00
hesuicong 5f06661d9b 去掉空白间隙的问题 2026-10-13 14:17:40 +08:00
hesuicong 73044aa575 调试高低频分离 2026-10-13 11:22:57 +08:00
hesuicong 4d5f371e99 拆解高低频,并对低频融合,高频单视图 2026-10-13 09:43:04 +08:00
hesuicong 56fcf290ba 提高对有些数据兼容问题(607055出现贴错图区域) 2026-10-12 15:34:57 +08:00
hesuicong 47b5e629f0 恢复 affine校正 2026-09-28 18:47:37 +08:00
hesuicong e68ecd01db 色差进一步优化 2026-09-27 15:39:32 +08:00
hesuicong 990fff69c5 目前效果比较好的 2026-09-27 11:45:49 +08:00
hesuicong c5dc504b1f 进一步优化 2026-09-24 11:04:31 +08:00
hesuicong 4713198454 进一步优化 2026-09-22 19:30:24 +08:00
hesuicong a86f98ea46 进一步优化 2026-09-22 18:57:54 +08:00
hesuicong b45baaa39e 修复空白的小黄线 2026-09-21 16:02:47 +08:00
hesuicong 59f1f7e873 修改成最大的前2个数字 2026-09-19 17:54:53 +08:00
hesuicong 2e8b15f3a9 排除最上一排的算法 2026-09-19 17:40:22 +08:00
hesuicong c809edbc8e 皮肤基本没有染色 2026-09-19 11:04:54 +08:00
hesuicong 453622d3f2 从视图选择和光栅化进一步优化 2026-09-17 11:47:14 +08:00
hesuicong e555b9c804 最新优化结果 2026-09-15 18:29:24 +08:00
hesuicong 974095dafa 进一步优化 2026-09-12 17:45:35 +08:00
hesuicong 2d1f6a13e5 进一步优化 2026-09-12 09:51:31 +08:00
hesuicong c287fe0fc7 颜色约束生效 2026-09-11 18:36:49 +08:00
hesuicong dbbcb4d557 微调色差 2026-09-11 10:15:06 +08:00
hesuicong d442e213d7 成功用OpenMVS的几何信息编译通过 2026-09-10 16:30:04 +08:00
hesuicong f96aca73a1 模拟OpenMVS几何信息 2026-09-09 17:32:11 +08:00
hesuicong 4ae6404044 保留全局颜色优化 2026-09-08 11:00:27 +08:00
hesuicong 9665d1226a 优化 2026-09-05 17:48:40 +08:00
hesuicong 10accfce73 进一步优化 2026-09-04 15:59:52 +08:00
hesuicong 8a62a8766c 原始图色彩均衡处理 2026-09-04 15:03:44 +08:00
hesuicong 32a270b287 有优化 2026-08-30 15:52:00 +08:00
hesuicong 93dbc93c72 接缝处理 2026-08-30 14:54:56 +08:00
hesuicong 707a62057a 进一步优化 2026-08-30 11:01:47 +08:00
hesuicong 864f5cafb9 加View Smoothing 2026-08-29 17:43:50 +08:00
hesuicong 593ec6cc02 完成框架 2026-08-29 15:00:40 +08:00
hesuicong 7ff01eafdb 进一步优化 2026-08-28 18:02:52 +08:00
hesuicong 902170f747 色差有优化,没达标 2026-08-27 11:38:44 +08:00
hesuicong c84dab57f0 gitignore 2026-08-22 16:18:16 +08:00
hesuicong adedcfd4f1 源图均衡 2026-08-22 16:17:44 +08:00
hesuicong 56f3c93216 接缝优化 2026-08-18 11:02:48 +08:00
hesuicong b89bf91ccf 全局优化 2026-08-13 17:42:49 +08:00
hesuicong 04495f3426 回退 2026-08-13 14:57:49 +08:00
hesuicong b8f2e18fd3 色差处理 2026-08-13 14:26:07 +08:00
hesuicong 5cf68ce634 接缝处理 2026-08-12 10:51:54 +08:00
hesuicong c8969a05cb 取消Gutter 2026-08-11 10:02:06 +08:00
hesuicong 1b9cf3ea46 集成Gutter 边缘缓冲 2026-08-11 09:53:14 +08:00
hesuicong 9fb90b00d7 小问题处理 2026-08-10 15:52:24 +08:00
hesuicong 35319544dd 防止高质量视图被低质量邻居覆盖,从而减少局部染色 2026-08-10 11:27:19 +08:00
hesuicong 411e41e54f * 提高纹素密度
* 写入 Atlas 时加优先级(Texel 级竞争)
消除接缝模糊、局部染色
每个 texel 只相信“最正面”的那个视图。
2026-08-08 15:09:30 +08:00
hesuicong 73b8d77980 添加视图综合评分函数 2026-08-07 17:35:30 +08:00
hesuicong e208883638 合并小三角形 2026-08-04 16:52:45 +08:00
hesuicong 9d29df54a9 中间版本 2026-07-31 14:48:54 +08:00
hesuicong 6e0e0f9cc7 解决运行问题 2026-07-31 11:01:33 +08:00
hesuicong 881bf55089 中间代码 2026-07-30 14:50:36 +08:00
hesuicong fb20fcf2c6 彻底抛弃虚拟面 2026-07-23 10:40:44 +08:00
hesuicong 56ad2c49cb 代码整理 2026-07-23 09:43:07 +08:00
hesuicong af1c3c0510 清晰化 2026-07-22 17:38:02 +08:00
hesuicong e8e3632341 清晰和空白的中间版本 2026-07-22 16:55:29 +08:00
hesuicong 1fe47d9448 修正一些问题 2026-07-22 14:04:25 +08:00
hesuicong 1fb47e1afe 更新 2026-07-21 16:17:49 +08:00
hesuicong fb5839c8e7 清晰化 2026-07-21 14:03:44 +08:00
hesuicong e5411b42b4 进一步清晰化 2026-07-20 15:48:54 +08:00
hesuicong 81036ad095 清晰化版本 2026-07-20 14:52:50 +08:00
hesuicong ff7b75d219 正向光栅化 2026-07-20 11:32:25 +08:00
hesuicong b0c165499c 中间版本保存 2026-07-20 09:36:13 +08:00
hesuicong 3adc1c012a 中间流程 2026-07-17 17:18:40 +08:00
hesuicong 93ee28a3ca 模糊版本清晰化 2026-07-17 15:10:10 +08:00
hesuicong 6922abe692 添加清晰化功能 2026-07-16 16:56:51 +08:00
hesuicong 5229b4f284 单像素清晰化保存 2026-07-14 17:26:35 +08:00
hesuicong d95c2fc8ff 单像素采样清晰化中间保存 2026-07-10 16:53:22 +08:00
hesuicong 29388cf341 通过编译 2026-05-14 16:51:45 +08:00
hesuicong 42f6d9c84c 编译不通过 2026-05-14 09:06:37 +08:00
hesuicong 82c019ee49 编译通过,可以生成纹理,但是还是模糊 2026-05-13 14:07:15 +08:00
hesuicong 46d5d65b3f 可以生成纹理,不过还是不清晰 2026-05-12 15:21:41 +08:00
hesuicong 93e7d60416 可以通过编译和运行 2026-05-12 15:04:31 +08:00
hesuicong 680c34242a 编译通不过,先保存 2026-05-11 16:32:36 +08:00
hesuicong 4a9013f12f 编译通过,但是空白 2026-05-06 10:40:46 +08:00
hesuicong 0d816514fa 进一步优化2026050501 2026-05-05 14:35:33 +08:00
hesuicong ddb42d08f4 进一步优化 2026-05-05 09:32:15 +08:00
hesuicong 7a6887349e 进一步优化 2026-05-04 17:23:00 +08:00
hesuicong c70d56d3cb 中间版本,通过编译,但是贴图效果不好 2026-05-04 11:09:30 +08:00
hesuicong f831f93361 代码整理 2026-04-30 16:17:04 +08:00
hesuicong 0e1b02cda5 还是有色块的中间版本 2026-04-30 14:45:38 +08:00
hesuicong 5e59d7c60b 解决白平衡问题 2026-04-29 14:35:19 +08:00
hesuicong c38456c7b4 通过编译,但是有白平衡问题 2026-04-29 09:49:19 +08:00
hesuicong 3843bea5f6 贴图清晰化 2026-04-28 15:23:14 +08:00
hesuicong 992439bc44 填色完成 2026-04-28 11:10:59 +08:00
hesuicong 4105129a7d 修正纹理有杂点问题 2026-04-27 16:32:57 +08:00
hesuicong 8529835712 通过编译 2026-04-27 11:36:00 +08:00
hesuicong e2c5313fdd 中间版本取消锐化和填洞操作 2026-04-25 17:55:53 +08:00
hesuicong c9d1eac96a 处理编译问题 2026-04-23 16:28:21 +08:00
hesuicong 0dceffb66f 中间未通过编译版本 2026-04-22 17:04:49 +08:00
hesuicong f787e874aa 回退到上个版本 2026-04-17 10:30:21 +08:00
hesuicong 01ef0f2a1d 中间版本的坐标修正 2026-04-16 15:27:49 +08:00
hesuicong d82e3f491a 处理坐标问题 2026-04-15 16:48:49 +08:00
hesuicong 811f85bbbd 中间版本 2026-04-15 14:19:31 +08:00
hesuicong 80c0dcbb49 中间版本 2026-04-14 16:27:27 +08:00
hesuicong 1b93beb86a 中间版本 2026-04-14 15:47:22 +08:00
hesuicong 84a492600f 中间版本 2026-04-14 14:33:13 +08:00
hesuicong 0f24212b87 测试中间版本 2026-04-13 18:07:28 +08:00
hesuicong 6271fb3af0 优化 2026-04-09 15:37:36 +08:00
hesuicong 7747494618 优化 2026-04-04 15:57:41 +08:00
hesuicong aadfbb4b08 优化性能 2026-04-03 17:52:41 +08:00
hesuicong e1258c2d3e 颜色处理 2026-04-01 10:31:19 +08:00
hesuicong c70d12b645 UV分割空白的地方填充 2026-03-31 16:02:08 +08:00
hesuicong 7fd81ffbea 手动UV分支 2025-12-31 09:51:38 +08:00
hesuicong 5e1623e303 上传ManualUV分支 2025-12-31 09:41:07 +08:00
25 changed files with 21408 additions and 5072 deletions

No files matched your search

+10 -1
View File
@@ -7,7 +7,6 @@
# Precompiled Headers
*.gch
*.pch
*.pyc
# Compiled Dynamic libraries
*.so
@@ -75,3 +74,13 @@ libs/MVS/texture_output/texture_33.png
libs/MVS/texture_output/texture_34.png
libs/MVS/texture_output/texture_35.png
texture_output/texture_0.png
color_change/__pycache__/calib_io.cpython-310.pyc
.gitignore
.gitignore
color_change/__pycache__/calibration.cpython-310.pyc
fill2sc/632723.jpg
fill2sc/632723.mtl
fill2sc/fill_preview.png
fill2sc/fill_report.txt
.gitignore
fill2sc/index.txt
+1 -1
View File
@@ -52,7 +52,7 @@ ENDIF()
PROJECT(OpenMVS)
SET(ENV_PYTHON_PATH "/home/algo/.conda/envs/py310_pyt210/lib/python3.10/site-packages")
SET(ENV_MVS_PATH "/home/algo/Documents/openMVS/openMVS/libs/MVS")
SET(ENV_MVS_PATH "/home/algo/Documents/openMVS/openMVS.ManualUV/libs/MVS")
CONFIGURE_FILE(
"${CMAKE_CURRENT_SOURCE_DIR}/build/Templates/ConfigEnv.h.in"
+8 -5
View File
@@ -94,6 +94,9 @@ unsigned nMaxThreads;
int nMaxTextureSize;
String strExportType;
String strConfigFileName;
bool bUseExistingUV;
String strUVMeshFileName;
boost::program_options::variables_map vm;
} // 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")
("origin-faceview", boost::program_options::value(&OPT::bOriginFaceview)->default_value(false), "use origin faceview selection")
("id", boost::program_options::value(&OPT::nID)->default_value(0), "id")
("use-existing-uv", boost::program_options::value(&OPT::bUseExistingUV)->default_value(false), "use existing UV coordinates from the input mesh")
("uv-mesh-file", boost::program_options::value<std::string>(&OPT::strUVMeshFileName), "mesh file with pre-computed UV coordinates")
;
// hidden options, allowed both on command line and
@@ -1098,9 +1103,7 @@ int main(int argc, LPCTSTR* argv)
return EXIT_FAILURE;
printf("Now\n");
VERBOSE(MAKE_PATH_SAFE(OPT::strMeshFileName));
bool bVertexColorExists = false;
bool *pBExistVertexColor = &bVertexColorExists;
if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName), pBExistVertexColor)) {
if (!OPT::strMeshFileName.empty() && !scene.mesh.Load(MAKE_PATH_SAFE(OPT::strMeshFileName))) {
VERBOSE("error: cannot load mesh file");
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,
OPT::bGlobalSeamLeveling, OPT::bLocalSeamLeveling, OPT::nTextureSizeMultiple, OPT::nRectPackingHeuristic, Pixel8U(OPT::nColEmpty),
OPT::fSharpnessWeight, OPT::nIgnoreMaskLabel, OPT::nMaxTextureSize, views, baseFileName, OPT::bOriginFaceview,
OPT::strInputFileName, OPT::strMeshFileName, *pBExistVertexColor))
OPT::strInputFileName, OPT::strMeshFileName, OPT::bUseExistingUV, OPT::strUVMeshFileName))
return EXIT_FAILURE;
VERBOSE("Mesh texturing completed: %u vertices, %u faces (%s)", scene.mesh.vertices.GetSize(), scene.mesh.faces.GetSize(), TD_TIMER_GET_FMT().c_str());
// save the final mesh
scene.mesh.Save(baseFileName+OPT::strExportType);
scene.mesh.Save(baseFileName+OPT::strExportType,cList<String>(),true,OPT::bUseExistingUV);
#if TD_VERBOSE != TD_VERBOSE_OFF
if (VERBOSITY_LEVEL > 2)
scene.ExportCamerasMLP(baseFileName+_T(".mlp"), baseFileName+OPT::strExportType);
+265
View File
@@ -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,
)
+467
View File
@@ -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)
+154
View File
@@ -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()
+118
View File
@@ -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
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
-10
View File
@@ -579,16 +579,6 @@ inline TPoint3<TYPE> ComputeTriangleNormal(const TPoint3<TYPE>& v0, const TPoint
} // 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
template <typename TYPE>
TYPE ComputeTriangleAreaSqLen(TYPE lena, TYPE lenb, TYPE lenc) {
+128 -1
View File
@@ -48,6 +48,7 @@ ObjModel::MaterialLib::MaterialLib()
bool ObjModel::MaterialLib::Save(const String& prefix, bool texLossless) const
{
DEBUG_EXTRA("MaterialLib::Save %s", prefix.c_str());
std::ofstream out((prefix+".mtl").c_str());
if (!out.good())
return false;
@@ -130,7 +131,7 @@ bool ObjModel::MaterialLib::Load(const String& fileName)
// S T R U C T S ///////////////////////////////////////////////////
bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless) const
bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless, bool bUseExistingUV) const
{
if (vertices.empty())
return false;
@@ -140,6 +141,9 @@ bool ObjModel::Save(const String& fileName, unsigned precision, bool texLossless
if (!material_lib.Save(prefix, texLossless))
return false;
if (bUseExistingUV)
return true;
std::ofstream out((prefix + ".obj").c_str());
if (!out.good())
return false;
@@ -269,6 +273,129 @@ bool ObjModel::Load(const String& fileName)
return !vertices.empty();
}
bool ObjModel::LoadUV(const String& fileName, SEACAVE::cList<TexCoord,const TexCoord&,0,8192,uint32_t> &faceTexcoords, bool bLoadUV)
{
DEBUG_EXTRA("LoadUV bLoadUV=%b", bLoadUV);
ASSERT(vertices.empty() && groups.empty() && material_lib.materials.empty());
std::ifstream fin(fileName.c_str());
String line, keyword;
std::istringstream in;
while (fin.good()) {
std::getline(fin, line);
if (line.empty() || line[0u] == '#')
continue;
in.str(line);
in >> keyword;
if (keyword == "v") {
Vertex v;
in >> v[0] >> v[1] >> v[2];
if (in.fail())
{
DEBUG_EXTRA("1");
return false;
}
vertices.push_back(v);
// DEBUG_EXTRA("10, %d", vertices.size());
} else if (keyword == "vt") {
TexCoord vt;
in >> vt[0] >> vt[1];
if (in.fail())
{
DEBUG_EXTRA("2");
return false;
}
texcoords.push_back(vt);
} else if (keyword == "vn") {
Normal vn;
in >> vn[0] >> vn[1] >> vn[2];
if (in.fail())
{
DEBUG_EXTRA("3");
return false;
}
normals.push_back(vn);
} else if (keyword == "f") {
Face f;
memset(&f, 0xFF, sizeof(Face));
for (size_t k = 0; k < 3; ++k) {
in >> keyword;
// 更健壮的解析方法,处理各种索引格式
std::vector<String> parts;
size_t start = 0, end = 0;
// 按'/'分割字符串
while ((end = keyword.find('/', start)) != String::npos) {
parts.push_back(keyword.substr(start, end - start));
start = end + 1;
}
parts.push_back(keyword.substr(start));
// 根据分割后的部分数量处理不同情况
if (parts.size() >= 1 && !parts[0].empty()) {
f.vertices[k] = std::stoi(parts[0]) - OBJ_INDEX_OFFSET;
}
if (parts.size() >= 2 && !parts[1].empty()) {
f.texcoords[k] = std::stoi(parts[1]) - OBJ_INDEX_OFFSET;
}
if (parts.size() >= 3 && !parts[2].empty()) {
f.normals[k] = std::stoi(parts[2]) - OBJ_INDEX_OFFSET;
}
}
if (in.fail())
{
DEBUG_EXTRA("4");
return false;
}
if (bLoadUV)
{
for (int i = 0; i < 3; ++i) {
if (f.texcoords[i] != NO_ID && f.texcoords[i] < texcoords.size()) {
faceTexcoords.push_back(texcoords[f.texcoords[i]]);
} else {
// 索引无效(例如面定义中未提供vt索引或索引越界),使用默认值
faceTexcoords.push_back(Point2f(0.0f, 0.0f)); // 默认UV
DEBUG_EXTRA("Invalid texcoords %d", f.texcoords[i])
}
}
// DEBUG_EXTRA("faceTexcoords push_back [(%f,%f),(%f,%f),(%f,%f)]", texcoords[f.texcoords[0]].x, texcoords[f.texcoords[0]].y,
// texcoords[f.texcoords[1]].x, texcoords[f.texcoords[1]].y, texcoords[f.texcoords[2]].x, texcoords[f.texcoords[2]].y);
}
if (groups.empty())
AddGroup("");
groups.back().faces.push_back(f);
// } else if (keyword == "mtllib") {
// in >> keyword;
// if (!material_lib.Load(keyword))
// DEBUG_EXTRA("3");
// return false;
} else if (keyword == "usemtl") {
Group group;
in >> group.material_name;
if (in.fail())
{
DEBUG_EXTRA("5");
return false;
}
groups.push_back(group);
}
in.clear();
}
DEBUG_EXTRA("6, vertices.size=%d, faceTexcoords.size=%d", vertices.size(), faceTexcoords.size());
return !vertices.empty();
}
ObjModel::Group& ObjModel::AddGroup(const String& material_name)
{
+2 -1
View File
@@ -93,9 +93,10 @@ public:
ObjModel() {}
// Saves the obj model to an .obj file, its material lib and the materials with the given file name
bool Save(const String& fileName, unsigned precision=6, bool texLossless=false) 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
bool Load(const String& fileName);
bool LoadUV(const String& fileName, SEACAVE::cList<TexCoord,const TexCoord&,0,8192,uint32_t> &faceTexcoords, bool bLoadUV = false);
// Creates a new group with the given material name
Group& AddGroup(const String& material_name);
-767
View File
@@ -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());
}
}
-112
View File
@@ -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
-505
View File
@@ -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());
}
}
-74
View File
@@ -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
+69 -48
View File
@@ -350,16 +350,6 @@ void Mesh::ComputeNormalFaces()
#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
#if 1
// computes the vertex normal as the area weighted face normals average
@@ -1106,20 +1096,18 @@ namespace BasicPLY {
struct Vertex {
Mesh::Vertex v;
Mesh::Normal n;
Mesh::Color color;
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]);
const size_t nMaxProps(SizeOfArray(props));
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)
continue;
ply.setup_property(props[p]);
switch (p) {
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];
};
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[] = {
{"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},
@@ -1154,7 +1133,6 @@ namespace BasicPLY {
{"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}
};
*/
// list of property information for a face
struct Face {
struct FaceIndices {
@@ -1213,25 +1191,61 @@ namespace BasicPLY {
} // namespace MeshInternal
// 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();
const String ext(Util::getFileExt(fileName).ToLower());
bool ret;
if (ext == _T(".obj"))
ret = LoadOBJ(fileName);
else
if (ext == _T(".gltf") || ext == _T(".glb"))
ret = LoadOBJ(fileName, bLoadUV);
else if (ext == _T(".gltf") || ext == _T(".glb"))
ret = LoadGLTF(fileName, ext == _T(".glb"));
else
ret = LoadPLY(fileName, pBExistVertexColor);
ret = LoadPLY(fileName);
if (!ret)
return false;
DEBUG_EXTRA("Mesh loaded: %u vertices, %u faces (%s)", vertices.size(), faces.size(), TD_TIMER_GET_FMT().c_str());
return true;
}
void Mesh::CheckUVValid()
{
for (int_t idxFace = 0; idxFace < (int_t)faces.size(); ++idxFace)
{
FOREACH(idxFace, faces)
{
const FIndex faceID = (FIndex)idxFace;
const TexCoord* uv = &faceTexcoords[faceID * 3];
const Point2f& a = uv[0];
const Point2f& b = uv[1];
const Point2f& c = uv[2];
// DEBUG_EXTRA("a=(%f,%f),b=(%f,%f),c=(%f,%f)", a.x, a.y, b.x, b.y, c.x, c.y);
// 计算边向量
Point2f v0 = b - a;
Point2f v1 = c - a;
float denom = (v0.x * v0.x + v0.y * v0.y) * (v1.x * v1.x + v1.y * v1.y) -
std::pow(v0.x * v1.x + v0.y * v1.y, 2);
// 处理退化三角形情况(面积接近0)
const float epsilon = 1e-10f;
if (std::abs(denom) < epsilon)
{
// DEBUG_EXTRA("PointInTriangle - Degenerate triangle, denom=%.10f", denom);
}
else
{
// DEBUG_EXTRA("PointInTriangle Yes idxFace=%d", idxFace);
}
}
}
}
// import the mesh as a PLY file
bool Mesh::LoadPLY(const String& fileName, bool *pBExistVertexColor)
bool Mesh::LoadPLY(const String& fileName)
{
ASSERT(!fileName.empty());
Release();
@@ -1266,23 +1280,16 @@ bool Mesh::LoadPLY(const String& fileName, bool *pBExistVertexColor)
LPCSTR elem_name = ply.setup_element_read(i, &elem_count);
if (PLY::equal_strings(BasicPLY::elem_names[0], elem_name)) {
ASSERT(vertices.size() == (VIndex)elem_count);
// BasicPLY::Vertex::InitLoadProps(ply, elem_count, vertices, vertexNormals);
BasicPLY::Vertex::InitLoadProps(ply, elem_count, vertices, vertexColors);
if (vertexColors.empty()) {
// printf("vertexColors.empty, elem_count=%u\n", elem_count);
*pBExistVertexColor = false;
BasicPLY::Vertex::InitLoadProps(ply, elem_count, vertices, vertexNormals);
if (vertexNormals.empty()) {
for (Vertex& vert: vertices)
ply.get_element(&vert);
} else {
// printf("Not vertexColors.empty, elem_count=%u\n", elem_count);
*pBExistVertexColor = true;
BasicPLY::Vertex vertex;
for (int v=0; v<elem_count; ++v) {
ply.get_element(&vertex);
vertices[v] = vertex.v;
vertexColors[v] = vertex.color;
// if (v<10)
// printf("vertexColors v=%u, rgb=(%u, %u, %u)\n", v, vertex.color.r, vertex.color.g, vertex.color.b);
vertexNormals[v] = vertex.n;
}
}
} else
@@ -1331,16 +1338,28 @@ bool Mesh::LoadPLY(const String& fileName, bool *pBExistVertexColor)
return true;
}
// import the mesh as a OBJ file
bool Mesh::LoadOBJ(const String& fileName)
bool Mesh::LoadOBJ(const String& fileName, bool bLoadUV)
{
ASSERT(!fileName.empty());
Release();
// open and parse OBJ file
ObjModel model;
if (!model.Load(fileName)) {
DEBUG_EXTRA("error: invalid OBJ file");
return false;
if (bLoadUV)
{
if (!model.LoadUV(fileName, faceTexcoords, true)) {
DEBUG_EXTRA("error: LoadUV invalid OBJ file %s", fileName.c_str());
return false;
}
}
else
{
if (!model.LoadUV(fileName, faceTexcoords)) {
// if (!model.Load(fileName)) {
DEBUG_EXTRA("error: Load invalid OBJ file %s", fileName.c_str());
return false;
}
}
if (model.get_vertices().empty() || model.get_groups().empty()) {
@@ -1369,11 +1388,13 @@ bool Mesh::LoadOBJ(const String& fileName)
for (const ObjModel::Face& f: group.faces) {
ASSERT(f.vertices[0] != NO_ID);
faces.emplace_back(f.vertices[0], f.vertices[1], f.vertices[2]);
/*
if (f.texcoords[0] != NO_ID) {
for (int i=0; i<3; ++i)
faceTexcoords.emplace_back(model.get_texcoords()[f.texcoords[i]]);
faceTexindices.emplace_back((TexIndex)groupIdx);
}
*/
if (f.normals[0] != NO_ID) {
Normal& n = faceNormals.emplace_back(Normal::ZERO);
for (int i=0; i<3; ++i)
@@ -1474,13 +1495,13 @@ bool Mesh::LoadGLTF(const String& fileName, bool bBinary)
/*----------------------------------------------------------------*/
// export the mesh to the given file
bool Mesh::Save(const String& fileName, const cList<String>& comments, bool bBinary) const
bool Mesh::Save(const String& fileName, const cList<String>& comments, bool bBinary, bool bUseExistingUV) const
{
TD_TIMER_STARTD();
const String ext(Util::getFileExt(fileName).ToLower());
bool ret;
if (ext == _T(".obj"))
ret = SaveOBJ(fileName);
ret = SaveOBJ(fileName, bUseExistingUV);
else
if (ext == _T(".gltf") || 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;
}
// 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());
Util::ensureFolder(fileName);
@@ -1626,7 +1647,7 @@ bool Mesh::SaveOBJ(const String& fileName) const
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
template <typename T>
+7 -17
View File
@@ -60,9 +60,6 @@ public:
typedef SEACAVE::cList<Vertex,const Vertex&,0,8192,VIndex> VertexArr;
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<FIndex,FIndex,0,8,FIndex> FaceIdxArr;
@@ -157,9 +154,6 @@ public:
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)
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)
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)
@@ -200,8 +194,6 @@ public:
void ComputeNormalFaces();
void ComputeNormalVertices();
void ComputeColorFaces();
void SmoothNormalFaces(float fMaxGradient=25.f, float fOriginalWeight=0.5f, unsigned nIterations=3);
void GetEdgeFaces(VIndex, VIndex, FaceIdxArr&) const;
@@ -246,10 +238,6 @@ public:
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;
Vertex ComputeCentroid(FIndex) const;
@@ -274,8 +262,8 @@ public:
bool TransferTexture(Mesh& mesh, const FaceIdxArr& faceSubsetIndices={}, unsigned borderSize=3, unsigned textureSize=4096);
// file IO
bool Load(const String& fileName, bool *pBExistVertexColor=nullptr);
bool Save(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true) const;
bool Load(const String& fileName, bool bLoadUV=false);
bool Save(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true, bool bUseExistingUV=false) const;
bool 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);
@@ -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(Face& f, VIndex v) { const uint32_t idx(FindVertex(f, v)); ASSERT(idx != NO_ID); return f[idx]; }
void CheckUVValid();
protected:
bool LoadPLY(const String& fileName, bool *pBExistVertexColor);
bool LoadOBJ(const String& fileName);
bool LoadPLY(const String& fileName);
bool LoadOBJ(const String& fileName, bool bLoadUV);
bool LoadGLTF(const String& fileName, bool bBinary=true);
bool SavePLY(const String& fileName, const cList<String>& comments=cList<String>(), bool bBinary=true, bool bTexLossless=true) const;
bool SaveOBJ(const String& fileName) const;
bool SaveOBJ(const String& fileName, bool bUseExistingUV) const;
bool SaveGLTF(const String& fileName, bool bBinary=true) const;
#ifdef _USE_CUDA
+17 -6
View File
@@ -66,9 +66,6 @@ public:
std::map<std::string, std::unordered_set<int>> 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_test;
std::string base_path;
public:
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>>& delete_edge_faces_map,
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
bool TextureMesh(unsigned nResolutionLevel, unsigned nMinResolution, unsigned minCommonCameras=0, float fOutlierThreshold=0.f, float fRatioDataSmoothness=0.3f,
bool bGlobalSeamLeveling=true, bool bLocalSeamLeveling=true, unsigned nTextureSizeMultiple=0, unsigned nRectPackingHeuristic=3, Pixel8U colEmpty=Pixel8U(255,127,39),
float fSharpnessWeight=0.5f, int ignoreMaskLabel=-1, int maxTextureSize=0, const IIndexArr& views=IIndexArr(), const SEACAVE::String& basename = "", bool bOriginFaceview = false,
const std::string& inputFileName = "", const std::string& meshFileName = "", bool bExistVertexColor = false);
const std::string& inputFileName = "", const std::string& meshFileName = "", bool bUseExistingUV = false, const std::string& strUVMeshFileName = "");
std::string runPython(const std::string& command);
bool is_face_visible(const std::string& image_name, int face_index);
+14657 -2907
View File
File diff suppressed because it is too large. Load diff
+120
View File
@@ -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()
+5 -18
View File
@@ -4,8 +4,7 @@ import os
import numpy as np
from scipy.spatial.transform import Rotation
import sys
# sys.path.append("/home/algo/Documents/openMVS/openMVS/libs/MVS/utils")
sys.path.append("/root/code/openMVS/libs/MVS/utils")
sys.path.append("/home/algo/Documents/openMVS/openMVS/libs/MVS/utils")
from colmap_loader import read_cameras_text, read_images_text, read_int_text, write_int_text, read_indices_from_file
# from get_pose_matrix import get_w2c
import argparse
@@ -34,7 +33,6 @@ from typing import Dict, List, Set
import struct
import math
# import os
from pathlib import Path
CameraModel = collections.namedtuple(
"CameraModel", ["model_id", "model_name", "num_params"]
@@ -2176,8 +2174,6 @@ class ModelProcessor:
result3: 删除边面字典,包含图像名称和对应的删除边面列表
base_path: 基础文件路径
"""
os.makedirs(base_path, exist_ok = True)
print(f"save_occlusion_data {base_path}, {len(result1)}, {len(result2)}, {len(result3)}")
@@ -2201,12 +2197,9 @@ class ModelProcessor:
for image_name, face_list in result3.items():
delete_edge_faces_map[image_name] = set(face_list)
# 保存 visible_faces_map
try:
file_name = "_visible_faces_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file:
with open(base_path + "/_visible_faces_map.txt", "w", encoding='utf-8') as map_file:
for image_name, face_set in visible_faces_map.items():
# 写入图像名称和所有面ID,用空格分隔
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
@@ -2216,9 +2209,7 @@ class ModelProcessor:
# 保存 face_visible_relative
try:
file_name = "_face_visible_relative.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as relative_file:
with open(base_path + "/_face_visible_relative.txt", "w", encoding='utf-8') as relative_file:
for face in face_visible_relative:
relative_file.write(str(face) + "\n")
except IOError as e:
@@ -2226,9 +2217,7 @@ class ModelProcessor:
# 保存 edge_faces_map
try:
file_name = "_edge_faces_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file2:
with open(base_path + "/_edge_faces_map.txt", "w", encoding='utf-8') as map_file2:
for image_name, face_set in edge_faces_map.items():
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
map_file2.write(line)
@@ -2237,9 +2226,7 @@ class ModelProcessor:
# 保存 delete_edge_faces_map
try:
file_name = "_delete_edge_faces_map.txt"
file_path = Path(base_path) / file_name
with open(file_path, "w", encoding='utf-8') as map_file3:
with open(base_path + "/_delete_edge_faces_map.txt", "w", encoding='utf-8') as map_file3:
for image_name, face_set in delete_edge_faces_map.items():
line = image_name + " " + " ".join(str(face) for face in face_set) + "\n"
map_file3.write(line)
-33
View File
@@ -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}")