Separates a head mesh from a torso mesh, selectively remeshes the cut boundary region, and smooths the seam to produce a clean, intersection-free result.
Blender Python (PyMeshLab) Blender
────────────────── ────────────────────── ──────────────────────
bisect_head_torso.py → RemeshVertices.py → smooth_remeshed.py
Cut mesh at plane Remesh boundary region Smooth the seam
Export PLY + indices Output remeshed PLY Export final PLY
| Step | Produces |
|---|---|
| Step 1 | mesh_in_cut.ply — cut mesh (head removed) |
| Step 1 | mesh_in_vertices.txt — boundary vertex indices |
| Step 1 | selected_faces.txt — boundary face indices |
| Step 1 | cut_plane.json — cut plane geometry for step 3 |
| Step 2 | mesh_in_remeshed.ply — remeshed mesh |
| Step 2 | remesh_log_<timestamp>.txt — detailed remesh statistics |
| Step 3 | mesh_smoothed.ply — final smoothed mesh |
Step 1 & 3 — Blender
- Blender 3.6 or later (4.x recommended)
- 3D Print Toolbox extension enabled (for self-intersection checks in step 3)
- Blender 4.2+: Extensions → Get Extensions → search "3D Print Toolbox"
- Older: Edit → Preferences → Add-ons → search "3D Print Toolbox"
Step 2 — Python
- Python 3.10+
pymeshlab,numpy,scipy
pip install pymeshlab numpy scipyWhat it does: Cuts the mesh at a plane you define, removes the head geometry using a Boolean difference, selects the resulting boundary band of vertices, and exports everything needed for the next steps.
Setup in Blender:
- Import your mesh (e.g.
mesh_in.objor.ply) - Create a plane object — name it
<mesh_name>_plane(e.g.mesh_in_plane) - Position and orient the plane at the desired cut location
- The plane's local Z axis must point away from the head (toward the torso/body)
- Use the object's rotation to orient it correctly
- Open the Scripting workspace in Blender
Configure the script — edit these lines at the top of bisect_head_torso.py:
# ⚠️ Set this to your working directory (where outputs will be saved)
OUTPUT_DIR: str = "/Users/antonhoyer/Documents/git-repositories/head_bisection"
# Width of the boundary band selected around the cut (fraction of mesh diagonal).
# Increase if the selected band looks too narrow in the viewport.
MARGIN_FACTOR: float = 0.05
# Cut method: False = Boolean (recommended for clean meshes), True = Bisect (faster)
USE_BISECT: bool = FalseRun: Press Run Script in the Scripting workspace.
The script will:
- Cut the mesh at the plane
- Select a band of boundary vertices
- Export
<mesh_name>_cut.ply,<mesh_name>_vertices.txt,selected_faces.txt,cut_plane.json
After running, the mesh will be left in Edit Mode with the boundary selection visible so you can verify it looks correct before proceeding.
What it does: Loads the cut PLY, selects the boundary faces identified in step 1, dilates the selection by one ring of faces (with a spatial bounding-box clip to prevent it spreading across the whole mesh), and runs isotropic explicit remeshing on that region only. The rest of the mesh is untouched.
⏱ This step is slow. Expect several minutes for high-resolution meshes (~700k+ faces). Progress is printed to the console.
Configure the script — edit the bottom of RemeshVertices.py:
# ⚠️ Input: the cut PLY exported by step 1.
# Must match the filename produced by bisect_head_torso.py (<mesh_name>_cut.ply)
"mesh_in_cut.ply", # ← change "mesh_in" to your mesh name
# ⚠️ Output: remeshed PLY. This filename is used again in step 3.
"mesh_in_remeshed.ply", # ← change to match your mesh nameThe working directory is wherever you run the script from. The script loads
mesh_in_vertices.txt and selected_faces.txt from the current directory,
so either run it from OUTPUT_DIR or adjust the paths at the top:
vertices = np.loadtxt('mesh_in_vertices.txt', dtype=np.int64)
face_indices = np.loadtxt('selected_faces.txt', dtype=np.int64)Run:
cd /path/to/your/output_dir
python RemeshVertices.pyA timestamped log file (remesh_log_<timestamp>.txt) is written with full statistics.
Check the Boundary Containment section — it should show a low "outside region" ratio.
Remesh parameters (in local_remesh_from_indices):
ms.meshing_isotropic_explicit_remeshing(
iterations=20, # More = smoother triangles, slower
targetlen=ml.PercentageValue(0.100007), # Target edge length as % of mesh bbox
maxsurfdist=ml.PercentageValue(0.199988) # Max deviation from original surface
)What it does:
Imports the remeshed PLY into a fresh Blender scene, re-selects the boundary band
by world-space position (using the geometry saved in cut_plane.json), and applies
Blender's built-in Smooth operator to the selected vertices. Repeats until no
self-intersections remain, then attempts a geometric intersection repair if smoothing
alone is insufficient. Exports the final cleaned mesh.
Configure the script — edit these lines at the top of smooth_remeshed.py:
# ⚠️ Path to your working directory
WORK_DIR: str = "/Users/antonhoyer/Documents/git-repositories/head_bisection"
# ⚠️ Remeshed PLY from step 2 (used as fallback if cut_plane.json has no entry)
DEFAULT_REMESHED_PLY: str = os.path.join(WORK_DIR, "mesh_in_remeshed.ply")
# ⚠️ Output path for the smoothed final mesh
DEFAULT_SMOOTHED_PLY: str = os.path.join(WORK_DIR, "mesh_smoothed.ply")The script reads the remeshed PLY path from cut_plane.json if present (written
automatically by step 1). If your mesh name is different from mesh_in, make sure
cut_plane.json was generated by step 1 with the correct mesh name, or update
DEFAULT_REMESHED_PLY manually.
Smoothing parameters:
SMOOTH_ITERATIONS: int = 10 # Passes per round (higher = smoother per round)
SMOOTH_FACTOR: float = 0.5 # 0.0–1.0, identical to Blender's Smooth factor slider
MAX_SMOOTH_ROUNDS: int = 5 # Safety cap on smooth+check rounds before repairRun from Blender:
# Background mode (no UI):
blender --background --python smooth_remeshed.py
# With an open .blend file (useful for debugging — mesh will be visible):
blender your_file.blend --python smooth_remeshed.pyOr paste/run directly in Blender's Scripting workspace.
The script will print per-round intersection counts and whether it passed or required the geometric repair fallback.
| File | Purpose | Created by |
|---|---|---|
bisect_head_torso.py |
Cut mesh + export indices | — (run in Blender) |
RemeshVertices.py |
Selective remesh of boundary | — (run with Python) |
smooth_remeshed.py |
Seam smoothing + intersection repair | — (run in Blender) |
save_indices.py |
Standalone helper to export selected face indices from Blender manually | — |
mesh_in_cut.ply |
Cut mesh (head removed) | Step 1 |
mesh_in_vertices.txt |
Boundary vertex indices | Step 1 |
selected_faces.txt |
Boundary face indices | Step 1 |
cut_plane.json |
Cut plane world-space geometry | Step 1 |
mesh_in_remeshed.ply |
Selectively remeshed mesh | Step 2 |
remesh_log_*.txt |
Remesh statistics log | Step 2 |
mesh_smoothed.ply |
Final smoothed output mesh | Step 3 |
Remesh spreads over the whole mesh (dilation issue)
The BFS dilation is clipped to a spatial bounding box around the original selection.
Check the Faces within bbox line in the remesh log. If the number is very high,
your original selection may be too large or spread across the mesh.
No vertices selected in step 3
The margin_factor stored in cut_plane.json (from step 1) controls the selection
band width. If the remesher moved vertices significantly, the band may miss them.
Increase MARGIN_FACTOR in bisect_head_torso.py and re-run step 1 to regenerate
cut_plane.json.
Self-intersections remain after smoothing
The script automatically falls back to bpy.ops.mesh.intersect(use_self=True) which
cuts the mesh at the actual intersection lines and resolves them geometrically.
Increase MAX_SMOOTH_ROUNDS if you want more smoothing attempts before the repair.
Step 2 is very slow / appears stuck
Normal for large meshes. Progress is printed every chunk. The selection
re-injection (range-based) and the remesh itself (20 iterations) are the slow parts.
Check the console for Starting remeshing on N faces... — once you see that, the
remesher is running.