reconstruct3d 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ """Calibración de la matriz intrínseca a partir de un video de un tablero.
2
+
3
+ Graba un video moviendo un patrón de ajedrez (chessboard) frente a la cámara,
4
+ cubriendo distintas zonas/ángulos del encuadre, y este script estima K y los
5
+ coeficientes de distorsión con `cv2.calibrateCamera`. Escribe (o crea) el
6
+ `camera.json` que consume el resto del pipeline.
7
+
8
+ A diferencia de pegar una K a mano, aquí `proc_size` queda fijado a la resolución
9
+ NATIVA del video de calibración, así que el punto principal cae cerca del centro
10
+ y no hay riesgo de desalinear K con la resolución de proceso.
11
+
12
+ Uso directo:
13
+ uv run python calibrate.py data/calib.mp4 # automático
14
+ uv run python calibrate.py data/calib.mp4 --board 9x6 --square 0.025
15
+ uv run python calibrate.py data/calib.mp4 --interactive # revisar vistas a mano
16
+
17
+ Vía pipeline:
18
+ uv run python pipeline.py calibrate data/calib.mp4
19
+ uv run python pipeline.py all data/video.mp4 --calibrate data/calib.mp4
20
+ """
21
+ import argparse
22
+ import json
23
+ import os
24
+
25
+ import cv2
26
+ import numpy as np
27
+ from tqdm import tqdm
28
+
29
+ DEFAULT_CAMERA_JSON = "camera.json"
30
+
31
+
32
+ def _parse_board(s):
33
+ """'9x6' -> (9, 6) = (esquinas internas en X, en Y)."""
34
+ parts = str(s).lower().replace("×", "x").split("x")
35
+ if len(parts) != 2:
36
+ raise ValueError(f"Formato de tablero inválido: '{s}'. Usa COLSxROWS, p.ej. 9x6.")
37
+ return int(parts[0]), int(parts[1])
38
+
39
+
40
+ def collect_corners(video_path, board, square_size, k_skip=10, max_views=40,
41
+ min_spacing=0.04, interactive=False):
42
+ """Recorre el video, detecta el tablero y junta correspondencias 3D-2D.
43
+
44
+ Solo acepta una vista si sus esquinas difieren lo suficiente de las ya
45
+ aceptadas (min_spacing, fracción de la diagonal de imagen), para forzar
46
+ variedad de poses y no saturar con frames casi idénticos.
47
+
48
+ Devuelve (objpoints, imgpoints, image_size, frames_aceptados).
49
+ """
50
+ cols, rows = board
51
+ # Patrón 3D del tablero (Z=0), escalado por el lado real del cuadrado.
52
+ objp = np.zeros((rows * cols, 3), np.float32)
53
+ objp[:, :2] = np.mgrid[0:cols, 0:rows].T.reshape(-1, 2)
54
+ objp *= float(square_size)
55
+
56
+ cap = cv2.VideoCapture(video_path)
57
+ if not cap.isOpened():
58
+ raise RuntimeError(f"No se pudo abrir el video: {video_path}")
59
+ n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
60
+ w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
61
+ h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
62
+ diag = float(np.hypot(w, h))
63
+
64
+ flags = (cv2.CALIB_CB_ADAPTIVE_THRESH | cv2.CALIB_CB_NORMALIZE_IMAGE |
65
+ cv2.CALIB_CB_FAST_CHECK)
66
+ criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
67
+
68
+ objpoints, imgpoints, accepted_centers, used_frames = [], [], [], []
69
+ print(f"[calib] video {w}x{h}, {n_frames} frames | tablero {cols}x{rows} | "
70
+ f"cuadrado={square_size} | revisando 1 de cada {k_skip}")
71
+
72
+ idxs = range(0, n_frames, k_skip)
73
+ for i in tqdm(idxs, desc="Buscando tablero"):
74
+ cap.set(cv2.CAP_PROP_POS_FRAMES, i)
75
+ ret, frame = cap.read()
76
+ if not ret:
77
+ break
78
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
79
+ found, corners = cv2.findChessboardCorners(gray, (cols, rows), flags)
80
+ if not found:
81
+ continue
82
+ corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
83
+ center = corners.reshape(-1, 2).mean(axis=0)
84
+
85
+ # ¿suficientemente distinta de las vistas ya aceptadas?
86
+ if accepted_centers:
87
+ d = min(np.linalg.norm(center - c) for c in accepted_centers)
88
+ if d < min_spacing * diag and not interactive:
89
+ continue
90
+
91
+ if interactive:
92
+ vis = frame.copy()
93
+ cv2.drawChessboardCorners(vis, (cols, rows), corners, found)
94
+ cv2.putText(vis, f"aceptadas={len(objpoints)} [a]ceptar [s]altar [q]terminar",
95
+ (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)
96
+ cv2.imshow("calibracion", vis)
97
+ key = cv2.waitKey(0) & 0xFF
98
+ if key == ord('q'):
99
+ break
100
+ if key != ord('a'):
101
+ continue
102
+
103
+ objpoints.append(objp.copy())
104
+ imgpoints.append(corners)
105
+ accepted_centers.append(center)
106
+ used_frames.append(i)
107
+ if len(objpoints) >= max_views:
108
+ break
109
+
110
+ cap.release()
111
+ if interactive:
112
+ cv2.destroyAllWindows()
113
+ return objpoints, imgpoints, (w, h), used_frames
114
+
115
+
116
+ def calibrate_from_video(video_path, board=(9, 6), square_size=0.025, k_skip=10,
117
+ max_views=40, interactive=False):
118
+ """Estima K + distorsión. Devuelve un dict listo para camera.json."""
119
+ objpoints, imgpoints, image_size, used = collect_corners(
120
+ video_path, board, square_size, k_skip=k_skip,
121
+ max_views=max_views, interactive=interactive,
122
+ )
123
+ n = len(objpoints)
124
+ if n < 5:
125
+ raise RuntimeError(
126
+ f"Solo se detectó el tablero en {n} vistas (mínimo 5, ideal >=15). "
127
+ "Graba el patrón cubriendo más zonas y ángulos del encuadre, o baja --k-skip.")
128
+
129
+ print(f"[calib] calibrando con {n} vistas (frames: {used[:8]}{'...' if n > 8 else ''})")
130
+ rms, K, dist, _rvecs, _tvecs = cv2.calibrateCamera(
131
+ objpoints, imgpoints, image_size, None, None)
132
+
133
+ w, h = image_size
134
+ print(f"[calib] RMS reproyección: {rms:.4f} px")
135
+ print(f"[calib] K=[fx={K[0,0]:.2f} fy={K[1,1]:.2f} cx={K[0,2]:.2f} cy={K[1,2]:.2f}] "
136
+ f"(centro imagen ~ {w/2:.0f},{h/2:.0f})")
137
+
138
+ return {
139
+ "_calibracion": (f"Generado por calibrate.py desde '{os.path.basename(video_path)}'. "
140
+ f"RMS reproyección={rms:.4f}px, {n} vistas, tablero {board[0]}x{board[1]}, "
141
+ f"cuadrado={square_size}."),
142
+ "K": K.tolist(),
143
+ "dist_coeffs": dist.ravel().tolist(),
144
+ "proc_size": [int(w), int(h)],
145
+ }
146
+
147
+
148
+ def save_camera_json(camera_dict, path=DEFAULT_CAMERA_JSON):
149
+ """Escribe el camera.json (haciendo backup si ya existía)."""
150
+ if os.path.exists(path):
151
+ bak = path + ".bak"
152
+ os.replace(path, bak)
153
+ print(f"[calib] {path} existía -> respaldado en {bak}")
154
+ with open(path, "w") as f:
155
+ json.dump(camera_dict, f, indent=2, ensure_ascii=False)
156
+ print(f"[calib] camera.json escrito en {path}")
157
+ return path
158
+
159
+
160
+ def find_calibration_source(search_dirs=("data/calib", "data", ".")):
161
+ """Busca un video de calibración para el modo auto-detección.
162
+
163
+ Devuelve la ruta del primer candidato cuyo nombre sugiere calibración
164
+ (contiene 'calib'), o None. Usado por el pipeline cuando no hay camera.json.
165
+ """
166
+ exts = (".mp4", ".mov", ".avi", ".mkv")
167
+ for d in search_dirs:
168
+ if not os.path.isdir(d):
169
+ continue
170
+ for fn in sorted(os.listdir(d)):
171
+ if "calib" in fn.lower() and fn.lower().endswith(exts):
172
+ return os.path.join(d, fn)
173
+ return None
174
+
175
+
176
+ def run_calibration(video_path, board=(9, 6), square_size=0.025, k_skip=10,
177
+ max_views=40, interactive=False, out=DEFAULT_CAMERA_JSON):
178
+ """Calibra y guarda camera.json. Devuelve la ruta escrita."""
179
+ cam = calibrate_from_video(video_path, board=board, square_size=square_size,
180
+ k_skip=k_skip, max_views=max_views, interactive=interactive)
181
+ return save_camera_json(cam, out)
182
+
183
+
184
+ if __name__ == "__main__":
185
+ p = argparse.ArgumentParser(description="Calibración de cámara desde un video de tablero.")
186
+ p.add_argument("video", help="Video del patrón de ajedrez (mp4/MOV/avi).")
187
+ p.add_argument("--board", default="9x6", type=_parse_board,
188
+ help="Esquinas internas COLSxROWS del tablero (default 9x6).")
189
+ p.add_argument("--square", default=0.025, type=float,
190
+ help="Lado real del cuadrado en metros (default 0.025). Fija la escala métrica.")
191
+ p.add_argument("--k-skip", default=10, type=int, help="Revisar 1 de cada k frames (default 10).")
192
+ p.add_argument("--max-views", default=40, type=int, help="Máx. vistas a usar (default 40).")
193
+ p.add_argument("--interactive", action="store_true",
194
+ help="Revisar y aceptar cada vista a mano (ventana OpenCV).")
195
+ p.add_argument("--out", default=DEFAULT_CAMERA_JSON, help="Ruta de salida (default camera.json).")
196
+ args = p.parse_args()
197
+ run_calibration(args.video, board=args.board, square_size=args.square,
198
+ k_skip=args.k_skip, max_views=args.max_views,
199
+ interactive=args.interactive, out=args.out)
@@ -0,0 +1,22 @@
1
+ cmake_minimum_required(VERSION 3.16)
2
+ project(mesh_reconstruct CXX)
3
+
4
+ set(CMAKE_CXX_STANDARD 17)
5
+ set(CMAKE_CXX_STANDARD_REQUIRED ON)
6
+ if(NOT CMAKE_BUILD_TYPE)
7
+ set(CMAKE_BUILD_TYPE Release)
8
+ endif()
9
+
10
+ find_package(CGAL REQUIRED)
11
+
12
+ add_executable(mesh_reconstruct mesh_reconstruct.cpp)
13
+ target_link_libraries(mesh_reconstruct PRIVATE CGAL::CGAL)
14
+
15
+ # Eigen es necesario para el solver de Poisson. Si está disponible, se enlaza el
16
+ # soporte de CGAL; Advancing Front no lo necesita.
17
+ find_package(Eigen3 QUIET)
18
+ if(TARGET CGAL::Eigen3_support)
19
+ target_link_libraries(mesh_reconstruct PRIVATE CGAL::Eigen3_support)
20
+ elseif(Eigen3_FOUND)
21
+ target_link_libraries(mesh_reconstruct PRIVATE Eigen3::Eigen)
22
+ endif()
@@ -0,0 +1,312 @@
1
+ // mesh_reconstruct.cpp — Reconstrucción de malla desde una nube de puntos densa
2
+ // usando CGAL. Lee un PLY (puntos + color), limpia la nube (outliers +
3
+ // simplificación + suavizado), reconstruye una superficie y la POST-PROCESA
4
+ // (elimina islas de ruido + suavizado de malla) antes de escribir el PLY.
5
+ //
6
+ // Métodos de reconstrucción:
7
+ // - afront (Advancing Front): interpola los puntos, respeta bordes abiertos
8
+ // (ideal para fachadas). Fiel pero hereda el ruido como picos.
9
+ // - poisson (Poisson screened): superficie implícita suave y cerrada; tolera
10
+ // mucho mejor el ruido. Necesita normales (se estiman).
11
+ //
12
+ // El color SIEMPRE se transfiere desde la nube original (vecino más cercano),
13
+ // así que sobrevive al suavizado/remallado.
14
+ //
15
+ // Uso:
16
+ // mesh_reconstruct INPUT.ply OUTPUT.ply [opciones]
17
+ // --method afront|poisson (default afront)
18
+ // --outlier-pct P elimina P% de outliers (default 2.0; 0=off)
19
+ // --simplify CELL grid simplify (default 0=auto~2x spacing; <0=off)
20
+ // --smooth K jet smooth de PUNTOS con K vecinos (default 0=off)
21
+ // --mesh-smooth ITERS suavizado tangencial de la MALLA (default 0=off)
22
+ // --min-component FRAC elimina componentes < FRAC*caras (default 0.0=off)
23
+ // --afront-radius R longitud máx. de arista relativa al spacing (default 5.0)
24
+ // --poisson-spacing-mult M multiplica el spacing de Poisson (default 1.0)
25
+ #include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
26
+ #include <CGAL/Point_set_3.h>
27
+ #include <CGAL/Point_set_3/IO.h>
28
+ #include <CGAL/remove_outliers.h>
29
+ #include <CGAL/grid_simplify_point_set.h>
30
+ #include <CGAL/jet_estimate_normals.h>
31
+ #include <CGAL/mst_orient_normals.h>
32
+ #include <CGAL/jet_smooth_point_set.h>
33
+ #include <CGAL/compute_average_spacing.h>
34
+ #include <CGAL/advancing_front_surface_reconstruction.h>
35
+ #include <CGAL/poisson_surface_reconstruction.h>
36
+ #include <CGAL/Surface_mesh.h>
37
+ #include <CGAL/Search_traits_3.h>
38
+ #include <CGAL/Search_traits_adapter.h>
39
+ #include <CGAL/Orthogonal_k_neighbor_search.h>
40
+ #include <CGAL/property_map.h>
41
+ #include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
42
+ #include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
43
+ #include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
44
+ #include <CGAL/Polygon_mesh_processing/connected_components.h>
45
+ #include <CGAL/Polygon_mesh_processing/tangential_relaxation.h>
46
+ #include <CGAL/boost/graph/helpers.h>
47
+
48
+ #include <array>
49
+ #include <cstdint>
50
+ #include <cstdlib>
51
+ #include <fstream>
52
+ #include <iostream>
53
+ #include <numeric>
54
+ #include <string>
55
+ #include <vector>
56
+
57
+ typedef CGAL::Exact_predicates_inexact_constructions_kernel Kernel;
58
+ typedef Kernel::Point_3 Point;
59
+ typedef Kernel::Vector_3 Vector;
60
+ typedef CGAL::Point_set_3<Point, Vector> Point_set;
61
+ typedef CGAL::Surface_mesh<Point> SMesh;
62
+ typedef std::array<unsigned char, 3> Color;
63
+ typedef CGAL::Parallel_if_available_tag Concurrency_tag;
64
+ namespace PMP = CGAL::Polygon_mesh_processing;
65
+
66
+ // ----------------------------------------------------------------------------
67
+ struct Args {
68
+ std::string input, output;
69
+ std::string method = "afront";
70
+ double outlier_pct = 2.0;
71
+ double simplify = 0.0; // 0 = auto, <0 = off
72
+ int smooth = 0; // jet smooth de puntos
73
+ int mesh_smooth = 0; // suavizado tangencial de la malla
74
+ double min_component = 0.0; // fracción de caras; 0 = off
75
+ double afront_radius = 5.0;
76
+ double poisson_spacing_mult = 1.0;
77
+ };
78
+
79
+ static Args parse(int argc, char** argv) {
80
+ Args a;
81
+ if (argc < 3) {
82
+ std::cerr << "uso: mesh_reconstruct INPUT.ply OUTPUT.ply [opciones]\n";
83
+ std::exit(2);
84
+ }
85
+ a.input = argv[1];
86
+ a.output = argv[2];
87
+ for (int i = 3; i < argc; ++i) {
88
+ std::string k = argv[i];
89
+ auto next = [&]() -> const char* { return (i + 1 < argc) ? argv[++i] : "0"; };
90
+ if (k == "--method") a.method = next();
91
+ else if (k == "--outlier-pct") a.outlier_pct = std::atof(next());
92
+ else if (k == "--simplify") a.simplify = std::atof(next());
93
+ else if (k == "--smooth") a.smooth = std::atoi(next());
94
+ else if (k == "--mesh-smooth") a.mesh_smooth = std::atoi(next());
95
+ else if (k == "--min-component") a.min_component = std::atof(next());
96
+ else if (k == "--afront-radius") a.afront_radius = std::atof(next());
97
+ else if (k == "--poisson-spacing-mult") a.poisson_spacing_mult = std::atof(next());
98
+ else std::cerr << "[warn] opción desconocida: " << k << "\n";
99
+ }
100
+ return a;
101
+ }
102
+
103
+ static bool get_color_maps(const Point_set& ps,
104
+ Point_set::Property_map<unsigned char>& r,
105
+ Point_set::Property_map<unsigned char>& g,
106
+ Point_set::Property_map<unsigned char>& b) {
107
+ auto or_ = ps.property_map<unsigned char>("red");
108
+ auto og_ = ps.property_map<unsigned char>("green");
109
+ auto ob_ = ps.property_map<unsigned char>("blue");
110
+ if (!or_ || !og_ || !ob_) return false;
111
+ r = *or_; g = *og_; b = *ob_;
112
+ return true;
113
+ }
114
+
115
+ // Color por vecino más cercano: KD-tree indexado sobre (in_pts, in_cols).
116
+ static std::vector<Color> transfer_colors(const std::vector<Point>& in_pts,
117
+ const std::vector<Color>& in_cols,
118
+ const std::vector<Point>& out_V) {
119
+ typedef CGAL::Search_traits_3<Kernel> Base;
120
+ typedef CGAL::Pointer_property_map<Point>::const_type PMap;
121
+ typedef CGAL::Search_traits_adapter<std::size_t, PMap, Base> Traits;
122
+ typedef CGAL::Orthogonal_k_neighbor_search<Traits> KNN;
123
+ typedef KNN::Tree Tree;
124
+ typedef KNN::Distance Distance;
125
+
126
+ PMap pmap = CGAL::make_property_map(in_pts);
127
+ std::vector<std::size_t> idx(in_pts.size());
128
+ std::iota(idx.begin(), idx.end(), 0);
129
+ Tree tree(idx.begin(), idx.end(), KNN::Splitter(), Traits(pmap));
130
+ Distance dist(pmap);
131
+
132
+ std::vector<Color> out(out_V.size(), Color{200, 200, 200});
133
+ for (std::size_t i = 0; i < out_V.size(); ++i) {
134
+ KNN search(tree, out_V[i], 1, 0, true, dist);
135
+ if (search.begin() != search.end())
136
+ out[i] = in_cols[search.begin()->first];
137
+ }
138
+ return out;
139
+ }
140
+
141
+ static bool write_ply(const std::string& path,
142
+ const std::vector<Point>& V,
143
+ const std::vector<Color>& C,
144
+ const std::vector<std::array<std::size_t, 3>>& F) {
145
+ std::ofstream out(path);
146
+ if (!out) return false;
147
+ const bool has_color = (C.size() == V.size());
148
+ out << "ply\nformat ascii 1.0\n";
149
+ out << "element vertex " << V.size() << "\n";
150
+ out << "property float x\nproperty float y\nproperty float z\n";
151
+ if (has_color)
152
+ out << "property uchar red\nproperty uchar green\nproperty uchar blue\n";
153
+ out << "element face " << F.size() << "\n";
154
+ out << "property list uchar int vertex_indices\nend_header\n";
155
+ for (std::size_t i = 0; i < V.size(); ++i) {
156
+ out << V[i].x() << " " << V[i].y() << " " << V[i].z();
157
+ if (has_color)
158
+ out << " " << (int)C[i][0] << " " << (int)C[i][1] << " " << (int)C[i][2];
159
+ out << "\n";
160
+ }
161
+ for (const auto& f : F)
162
+ out << "3 " << f[0] << " " << f[1] << " " << f[2] << "\n";
163
+ return true;
164
+ }
165
+
166
+ // Post-procesa la malla in-place: elimina componentes pequeñas y suaviza.
167
+ static void postprocess(SMesh& sm, const Args& a) {
168
+ if (a.min_component > 0) {
169
+ std::size_t nfaces = sm.number_of_faces();
170
+ std::size_t thr = (std::size_t)std::max(1.0, a.min_component * nfaces);
171
+ std::size_t removed = PMP::keep_large_connected_components(sm, thr);
172
+ sm.collect_garbage();
173
+ std::cerr << "[mesh] componentes < " << thr << " caras eliminadas: " << removed
174
+ << " -> caras " << sm.number_of_faces() << "\n";
175
+ }
176
+ if (a.mesh_smooth > 0) {
177
+ // Constreñir los vértices de borde para no encoger la superficie abierta.
178
+ auto vcm = sm.add_property_map<SMesh::Vertex_index, bool>("v:constrained", false).first;
179
+ for (auto h : halfedges(sm))
180
+ if (CGAL::is_border(h, sm)) vcm[target(h, sm)] = true;
181
+ PMP::tangential_relaxation(
182
+ sm, CGAL::parameters::number_of_iterations(a.mesh_smooth)
183
+ .vertex_is_constrained_map(vcm));
184
+ std::cerr << "[mesh] suavizado tangencial de malla: " << a.mesh_smooth
185
+ << " iteraciones\n";
186
+ }
187
+ }
188
+
189
+ // ----------------------------------------------------------------------------
190
+ int main(int argc, char** argv) {
191
+ Args a = parse(argc, argv);
192
+
193
+ Point_set points;
194
+ std::cerr << "[mesh] leyendo " << a.input << " ...\n";
195
+ if (!CGAL::IO::read_point_set(a.input, points)) {
196
+ std::cerr << "[mesh] ERROR: no se pudo leer " << a.input << "\n";
197
+ return 1;
198
+ }
199
+ std::cerr << "[mesh] puntos leídos: " << points.size() << "\n";
200
+ if (points.empty()) return 1;
201
+
202
+ // Guardar la nube original (con color) para transferir color al final.
203
+ Point_set::Property_map<unsigned char> cr, cg, cb;
204
+ bool has_color = get_color_maps(points, cr, cg, cb);
205
+ std::cerr << "[mesh] color en la nube: " << (has_color ? "sí" : "no") << "\n";
206
+ std::vector<Point> orig_pts;
207
+ std::vector<Color> orig_cols;
208
+ if (has_color) {
209
+ orig_pts.reserve(points.size());
210
+ for (auto idx : points) { orig_pts.push_back(points.point(idx)); orig_cols.push_back({cr[idx], cg[idx], cb[idx]}); }
211
+ }
212
+
213
+ // 1) Outliers.
214
+ if (a.outlier_pct > 0) {
215
+ auto rm = CGAL::remove_outliers<Concurrency_tag>(
216
+ points, 24, CGAL::parameters::threshold_percent(a.outlier_pct));
217
+ std::size_t before = points.size();
218
+ points.remove(rm, points.end());
219
+ points.collect_garbage();
220
+ std::cerr << "[mesh] outliers eliminados: " << (before - points.size())
221
+ << " -> quedan " << points.size() << "\n";
222
+ }
223
+
224
+ double spacing = CGAL::compute_average_spacing<Concurrency_tag>(points, 6);
225
+ std::cerr << "[mesh] average spacing ~ " << spacing << "\n";
226
+
227
+ // 2) Grid simplify (densidad uniforme + denoise). 0 = auto(2x spacing), <0 = off.
228
+ if (a.simplify >= 0) {
229
+ double cell = (a.simplify > 0) ? a.simplify : 2.0 * spacing;
230
+ auto rm = CGAL::grid_simplify_point_set(points, cell);
231
+ std::size_t before = points.size();
232
+ points.remove(rm, points.end());
233
+ points.collect_garbage();
234
+ std::cerr << "[mesh] grid simplify (celda=" << cell << "): " << before
235
+ << " -> " << points.size() << "\n";
236
+ }
237
+
238
+ // 3) Jet smooth de puntos opcional (denoise antes de reconstruir).
239
+ if (a.smooth > 0) {
240
+ CGAL::jet_smooth_point_set<Concurrency_tag>(points, a.smooth);
241
+ std::cerr << "[mesh] jet smooth de puntos con " << a.smooth << " vecinos\n";
242
+ }
243
+
244
+ // --- Reconstrucción -> Surface_mesh ---
245
+ SMesh sm;
246
+ if (a.method == "afront") {
247
+ std::vector<Point> pts;
248
+ pts.reserve(points.size());
249
+ for (auto idx : points) pts.push_back(points.point(idx));
250
+ std::cerr << "[mesh] Advancing Front sobre " << pts.size() << " puntos...\n";
251
+ std::vector<std::array<std::size_t, 3>> facets;
252
+ CGAL::advancing_front_surface_reconstruction(
253
+ pts.begin(), pts.end(), std::back_inserter(facets));
254
+ std::cerr << "[mesh] triángulos (crudos): " << facets.size() << "\n";
255
+ PMP::repair_polygon_soup(pts, facets);
256
+ PMP::orient_polygon_soup(pts, facets);
257
+ PMP::polygon_soup_to_polygon_mesh(pts, facets, sm);
258
+ } else if (a.method == "poisson") {
259
+ points.add_normal_map();
260
+ std::cerr << "[mesh] estimando normales (jet)...\n";
261
+ CGAL::jet_estimate_normals<Concurrency_tag>(points, 18);
262
+ auto unoriented = CGAL::mst_orient_normals(points, 18);
263
+ points.remove(unoriented, points.end());
264
+ points.collect_garbage();
265
+ std::cerr << "[mesh] normales orientadas; puntos: " << points.size() << "\n";
266
+ double sp = spacing * a.poisson_spacing_mult;
267
+ std::cerr << "[mesh] Poisson (spacing=" << sp << ")...\n";
268
+ if (!CGAL::poisson_surface_reconstruction_delaunay(
269
+ points.begin(), points.end(), points.point_map(),
270
+ points.normal_map(), sm, sp)) {
271
+ std::cerr << "[mesh] ERROR: Poisson falló\n";
272
+ return 1;
273
+ }
274
+ } else {
275
+ std::cerr << "[mesh] ERROR: método desconocido '" << a.method
276
+ << "' (usa afront|poisson)\n";
277
+ return 2;
278
+ }
279
+ std::cerr << "[mesh] malla: " << sm.number_of_vertices() << " vértices, "
280
+ << sm.number_of_faces() << " caras\n";
281
+
282
+ // --- Post-procesado (limpieza + suavizado) ---
283
+ postprocess(sm, a);
284
+
285
+ // --- Volcado: vértices + caras, con color transferido de la nube original ---
286
+ std::vector<Point> V;
287
+ std::vector<std::array<std::size_t, 3>> F;
288
+ std::vector<std::size_t> vmap(sm.number_of_vertices() ? (*(--sm.vertices().end()) + 1) : 0, 0);
289
+ for (auto v : sm.vertices()) { vmap[(std::size_t)v] = V.size(); V.push_back(sm.point(v)); }
290
+ for (auto f : sm.faces()) {
291
+ std::array<std::size_t, 3> tri; int k = 0;
292
+ for (auto v : CGAL::vertices_around_face(sm.halfedge(f), sm))
293
+ if (k < 3) tri[k++] = vmap[(std::size_t)v];
294
+ if (k == 3) F.push_back(tri);
295
+ }
296
+ if (F.empty()) { std::cerr << "[mesh] ERROR: malla vacía\n"; return 1; }
297
+
298
+ std::vector<Color> C;
299
+ if (has_color && !orig_pts.empty()) {
300
+ std::cerr << "[mesh] transfiriendo color por vecino más cercano...\n";
301
+ C = transfer_colors(orig_pts, orig_cols, V);
302
+ }
303
+
304
+ std::cerr << "[mesh] escribiendo " << a.output << " (" << V.size()
305
+ << " vértices, " << F.size() << " triángulos)\n";
306
+ if (!write_ply(a.output, V, C, F)) {
307
+ std::cerr << "[mesh] ERROR al escribir " << a.output << "\n";
308
+ return 1;
309
+ }
310
+ std::cerr << "[mesh] OK\n";
311
+ return 0;
312
+ }