[{"data":1,"prerenderedAt":1130},["ShallowReactive",2],{"tutorial-fast-connected-components-in-python":3},{"id":4,"title":5,"author":6,"body":7,"date":1104,"description":1105,"extension":1106,"keywords":1107,"meta":1120,"navigation":136,"ogImage":1121,"path":1122,"published":136,"seo":1123,"stem":1124,"tags":1125,"__hash__":1129},"tutorials\u002Ftutorials\u002Ffast-connected-components-in-python.md","Fast Connected Components in Python","Žiga Sajovic",{"type":8,"value":9,"toc":1093},"minimark",[10,28,40,44,51,56,81,91,95,162,179,182,342,346,357,383,389,398,416,419,434,455,459,465,519,525,528,592,604,608,611,723,729,739,757,761,767,822,828,834,937,940,947,962,966,980,1008,1012,1041,1045,1048,1052,1064,1067,1073,1085,1089],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Ffast-connected-components-in-cpp","C++"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Ffast-connected-components-in-javascript","JavaScript",".",[14,29,30,31,35,36,39],{},"A scanned or exported mesh often holds more than one piece, and some of those pieces have holes. Two questions come first: which faces belong together, and where are the open edges. ",[32,33,34],"code",{},"trueform"," answers both with one ",[32,37,38],{},"pip install",". NumPy arrays in, NumPy arrays out. This page covers labeling connected components, splitting a mesh into parts, keeping the largest part, and extracting boundary loops for holes and open edges.",[41,42],"article-cta",{":buttons":43},"[{\"label\":\"trueform\",\"to\":\"\u002Ftrueform\",\"icon\":\"i-lucide-cpu\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"GitHub\",\"to\":\"https:\u002F\u002Fgithub.com\u002Fpolydera\u002Ftrueform\",\"icon\":\"i-simple-icons-github\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Documentation\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Ftopology#connected-components\",\"variant\":\"soft\",\"color\":\"neutral\"}]",[14,45,46],{},[47,48],"img",{"alt":49,"src":50},"The Stanford dragon split into separate connected components, with their boundary loops","\u002Fimg\u002Ftutorials\u002Fconnected-components.png",[52,53,55],"h2",{"id":54},"install","Install",[57,58,63],"pre",{"className":59,"code":60,"language":61,"meta":62,"style":62},"language-bash shiki shiki-themes github-dark","pip install trueform\n","bash","",[32,64,65],{"__ignoreMap":62},[66,67,70,74,78],"span",{"class":68,"line":69},"line",1,[66,71,73],{"class":72},"svObZ","pip",[66,75,77],{"class":76},"sU2Wk"," install",[66,79,80],{"class":76}," trueform\n",[14,82,83,84,90],{},"See the ",[18,85,89],{"href":86,"rel":87},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fgetting-started\u002Finstallation",[88],"nofollow","installation guide"," for all options.",[52,92,94],{"id":93},"loading-the-mesh","Loading the mesh",[57,96,100],{"className":97,"code":98,"language":99,"meta":62,"style":62},"language-python shiki shiki-themes github-dark","import numpy as np\nimport trueform as tf\n\nmesh = tf.Mesh(*tf.read_stl(\"parts.stl\"))\n","python",[32,101,102,118,131,138],{"__ignoreMap":62},[66,103,104,108,112,115],{"class":68,"line":69},[66,105,107],{"class":106},"snl16","import",[66,109,111],{"class":110},"s95oV"," numpy ",[66,113,114],{"class":106},"as",[66,116,117],{"class":110}," np\n",[66,119,121,123,126,128],{"class":68,"line":120},2,[66,122,107],{"class":106},[66,124,125],{"class":110}," trueform ",[66,127,114],{"class":106},[66,129,130],{"class":110}," tf\n",[66,132,134],{"class":68,"line":133},3,[66,135,137],{"emptyLinePlaceholder":136},true,"\n",[66,139,141,144,147,150,153,156,159],{"class":68,"line":140},4,[66,142,143],{"class":110},"mesh ",[66,145,146],{"class":106},"=",[66,148,149],{"class":110}," tf.Mesh(",[66,151,152],{"class":106},"*",[66,154,155],{"class":110},"tf.read_stl(",[66,157,158],{"class":76},"\"parts.stl\"",[66,160,161],{"class":110},"))\n",[14,163,164,167,168,171,172,175,176,27],{},[32,165,166],{},"tf.read_stl"," returns ",[32,169,170],{},"(faces, points)"," as NumPy arrays, with duplicate vertices merged so that faces share edges. ",[32,173,174],{},"tf.read_obj"," works the same way. If you already have arrays, pass them to ",[32,177,178],{},"tf.Mesh(faces, points)",[14,180,181],{},"To build a two-part test file without a scan, join a sphere and a moved box:",[57,183,185],{"className":97,"code":184,"language":99,"meta":62,"style":62},"sphere = tf.Mesh(*tf.make_sphere_mesh(1.0))\nbox = tf.Mesh(*tf.make_box_mesh(1.0, 1.0, 1.0))\n\nT = np.eye(4, dtype=np.float32)\nT[:3, 3] = [3.0, 0.0, 0.0]\nbox.transformation = T\n\ntf.write_stl(tf.concatenated([sphere, box]), \"parts.stl\")\nmesh = tf.Mesh(*tf.read_stl(\"parts.stl\"))\n",[32,186,187,207,234,238,262,298,309,314,325],{"__ignoreMap":62},[66,188,189,192,194,196,198,201,205],{"class":68,"line":69},[66,190,191],{"class":110},"sphere ",[66,193,146],{"class":106},[66,195,149],{"class":110},[66,197,152],{"class":106},[66,199,200],{"class":110},"tf.make_sphere_mesh(",[66,202,204],{"class":203},"sDLfK","1.0",[66,206,161],{"class":110},[66,208,209,212,214,216,218,221,223,226,228,230,232],{"class":68,"line":120},[66,210,211],{"class":110},"box ",[66,213,146],{"class":106},[66,215,149],{"class":110},[66,217,152],{"class":106},[66,219,220],{"class":110},"tf.make_box_mesh(",[66,222,204],{"class":203},[66,224,225],{"class":110},", ",[66,227,204],{"class":203},[66,229,225],{"class":110},[66,231,204],{"class":203},[66,233,161],{"class":110},[66,235,236],{"class":68,"line":133},[66,237,137],{"emptyLinePlaceholder":136},[66,239,240,243,245,248,251,253,257,259],{"class":68,"line":140},[66,241,242],{"class":110},"T ",[66,244,146],{"class":106},[66,246,247],{"class":110}," np.eye(",[66,249,250],{"class":203},"4",[66,252,225],{"class":110},[66,254,256],{"class":255},"s9osk","dtype",[66,258,146],{"class":106},[66,260,261],{"class":110},"np.float32)\n",[66,263,265,268,271,273,275,278,280,283,286,288,291,293,295],{"class":68,"line":264},5,[66,266,267],{"class":110},"T[:",[66,269,270],{"class":203},"3",[66,272,225],{"class":110},[66,274,270],{"class":203},[66,276,277],{"class":110},"] ",[66,279,146],{"class":106},[66,281,282],{"class":110}," [",[66,284,285],{"class":203},"3.0",[66,287,225],{"class":110},[66,289,290],{"class":203},"0.0",[66,292,225],{"class":110},[66,294,290],{"class":203},[66,296,297],{"class":110},"]\n",[66,299,301,304,306],{"class":68,"line":300},6,[66,302,303],{"class":110},"box.transformation ",[66,305,146],{"class":106},[66,307,308],{"class":110}," T\n",[66,310,312],{"class":68,"line":311},7,[66,313,137],{"emptyLinePlaceholder":136},[66,315,317,320,322],{"class":68,"line":316},8,[66,318,319],{"class":110},"tf.write_stl(tf.concatenated([sphere, box]), ",[66,321,158],{"class":76},[66,323,324],{"class":110},")\n",[66,326,328,330,332,334,336,338,340],{"class":68,"line":327},9,[66,329,143],{"class":110},[66,331,146],{"class":106},[66,333,149],{"class":110},[66,335,152],{"class":106},[66,337,155],{"class":110},[66,339,158],{"class":76},[66,341,161],{"class":110},[52,343,345],{"id":344},"labeling-connected-components","Labeling connected components",[14,347,348,349,352,353,356],{},"Two faces belong to the same component when a chain of shared edges connects them. ",[32,350,351],{},"mesh.face_link"," lists, for each face, the faces across its edges. ",[32,354,355],{},"tf.label_connected_components"," turns that graph into labels:",[57,358,360],{"className":97,"code":359,"language":99,"meta":62,"style":62},"n_components, labels = tf.label_connected_components(mesh.face_link)\n# n_components == 2\n# labels.shape == (772,)  int32, one label per face, 0 .. n_components - 1\n",[32,361,362,372,378],{"__ignoreMap":62},[66,363,364,367,369],{"class":68,"line":69},[66,365,366],{"class":110},"n_components, labels ",[66,368,146],{"class":106},[66,370,371],{"class":110}," tf.label_connected_components(mesh.face_link)\n",[66,373,374],{"class":68,"line":120},[66,375,377],{"class":376},"sAwPA","# n_components == 2\n",[66,379,380],{"class":68,"line":133},[66,381,382],{"class":376},"# labels.shape == (772,)  int32, one label per face, 0 .. n_components - 1\n",[14,384,385],{},[47,386],{"alt":387,"src":388},"Step: labeling connected components. The Utah teapot colored by component label: body, lid, spout and handle are four separate shells","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-1-labels.png",[14,390,391,393,394,397],{},[32,392,351],{}," is built on first access and cached on the mesh. Face counts per component are one ",[32,395,396],{},"np.bincount",":",[57,399,401],{"className":97,"code":400,"language":99,"meta":62,"style":62},"sizes = np.bincount(labels)   # array([760,  12])\n",[32,402,403],{"__ignoreMap":62},[66,404,405,408,410,413],{"class":68,"line":69},[66,406,407],{"class":110},"sizes ",[66,409,146],{"class":106},[66,411,412],{"class":110}," np.bincount(labels)   ",[66,414,415],{"class":376},"# array([760,  12])\n",[14,417,418],{},"For components that only touch at a vertex to count as one, label the vertex graph instead. The labels are then per vertex:",[57,420,422],{"className":97,"code":421,"language":99,"meta":62,"style":62},"n_components, vertex_labels = tf.label_connected_components(mesh.vertex_link)\n",[32,423,424],{"__ignoreMap":62},[66,425,426,429,431],{"class":68,"line":69},[66,427,428],{"class":110},"n_components, vertex_labels ",[66,430,146],{"class":106},[66,432,433],{"class":110}," tf.label_connected_components(mesh.vertex_link)\n",[14,435,436,438,439,442,443,446,447,450,451,454],{},[32,437,355],{}," takes any graph, not only meshes: an ",[32,440,441],{},"(N, K)"," int array of neighbor indices with ",[32,444,445],{},"-1"," for no neighbor, or a ",[32,448,449],{},"tf.OffsetBlockedArray"," for variable neighbor counts. On a graph with many components, more than 500, pass ",[32,452,453],{},"expected_number_of_components"," so it uses the sequential algorithm.",[52,456,458],{"id":457},"splitting-into-parts","Splitting into parts",[14,460,461,464],{},[32,462,463],{},"tf.split_into_components"," groups faces by label and returns one standalone mesh per label, with unused points dropped and indices renumbered:",[57,466,468],{"className":97,"code":467,"language":99,"meta":62,"style":62},"components, component_labels = tf.split_into_components(mesh, labels)\n\nfor (faces, points), label in zip(components, component_labels):\n    print(label, faces.shape, points.shape)\n# 0 (760, 3) (382, 3)\n# 1 (12, 3) (8, 3)\n",[32,469,470,480,484,501,509,514],{"__ignoreMap":62},[66,471,472,475,477],{"class":68,"line":69},[66,473,474],{"class":110},"components, component_labels ",[66,476,146],{"class":106},[66,478,479],{"class":110}," tf.split_into_components(mesh, labels)\n",[66,481,482],{"class":68,"line":120},[66,483,137],{"emptyLinePlaceholder":136},[66,485,486,489,492,495,498],{"class":68,"line":133},[66,487,488],{"class":106},"for",[66,490,491],{"class":110}," (faces, points), label ",[66,493,494],{"class":106},"in",[66,496,497],{"class":203}," zip",[66,499,500],{"class":110},"(components, component_labels):\n",[66,502,503,506],{"class":68,"line":140},[66,504,505],{"class":203},"    print",[66,507,508],{"class":110},"(label, faces.shape, points.shape)\n",[66,510,511],{"class":68,"line":264},[66,512,513],{"class":376},"# 0 (760, 3) (382, 3)\n",[66,515,516],{"class":68,"line":300},[66,517,518],{"class":376},"# 1 (12, 3) (8, 3)\n",[14,520,521],{},[47,522],{"alt":523,"src":524},"Step: splitting into parts. The teapot split into four standalone meshes, one per label, pulled apart","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-2-split.png",[14,526,527],{},"Keep only the largest part, a common clean-up for scans with floating debris:",[57,529,531],{"className":97,"code":530,"language":99,"meta":62,"style":62},"largest = np.argmax(np.bincount(labels))\nindex = int(np.flatnonzero(component_labels == largest)[0])\n\nfaces, points = components[index]\ntf.write_stl((faces, points), \"largest.stl\")\n",[32,532,533,543,568,572,582],{"__ignoreMap":62},[66,534,535,538,540],{"class":68,"line":69},[66,536,537],{"class":110},"largest ",[66,539,146],{"class":106},[66,541,542],{"class":110}," np.argmax(np.bincount(labels))\n",[66,544,545,548,550,553,556,559,562,565],{"class":68,"line":120},[66,546,547],{"class":110},"index ",[66,549,146],{"class":106},[66,551,552],{"class":203}," int",[66,554,555],{"class":110},"(np.flatnonzero(component_labels ",[66,557,558],{"class":106},"==",[66,560,561],{"class":110}," largest)[",[66,563,564],{"class":203},"0",[66,566,567],{"class":110},"])\n",[66,569,570],{"class":68,"line":133},[66,571,137],{"emptyLinePlaceholder":136},[66,573,574,577,579],{"class":68,"line":140},[66,575,576],{"class":110},"faces, points ",[66,578,146],{"class":106},[66,580,581],{"class":110}," components[index]\n",[66,583,584,587,590],{"class":68,"line":264},[66,585,586],{"class":110},"tf.write_stl((faces, points), ",[66,588,589],{"class":76},"\"largest.stl\"",[66,591,324],{"class":110},[14,593,594,595,598,599,603],{},"The labels do not have to come from connectivity. Any per-face ",[32,596,597],{},"int32"," array works, for example the labels a ",[18,600,602],{"href":601},"\u002Ftutorials\u002Ffast-mesh-booleans-in-python","mesh boolean"," returns.",[52,605,607],{"id":606},"finding-holes-and-open-edges","Finding holes and open edges",[14,609,610],{},"A boundary edge belongs to only one face. On a closed mesh there are none. Start with a quick check:",[57,612,614],{"className":97,"code":613,"language":99,"meta":62,"style":62},"faces, points = tf.make_plane_mesh(4.0, 4.0, 8, 8)\n\ncenters = points[faces].mean(axis=1)\nkeep = np.linalg.norm(centers[:, :2], axis=1) > 0.8   # cut a hole in the middle\nsheet = tf.Mesh(faces[keep], points)\n\ntf.is_closed(sheet)   # False\n",[32,615,616,643,647,667,701,711,715],{"__ignoreMap":62},[66,617,618,620,622,625,628,630,632,634,637,639,641],{"class":68,"line":69},[66,619,576],{"class":110},[66,621,146],{"class":106},[66,623,624],{"class":110}," tf.make_plane_mesh(",[66,626,627],{"class":203},"4.0",[66,629,225],{"class":110},[66,631,627],{"class":203},[66,633,225],{"class":110},[66,635,636],{"class":203},"8",[66,638,225],{"class":110},[66,640,636],{"class":203},[66,642,324],{"class":110},[66,644,645],{"class":68,"line":120},[66,646,137],{"emptyLinePlaceholder":136},[66,648,649,652,654,657,660,662,665],{"class":68,"line":133},[66,650,651],{"class":110},"centers ",[66,653,146],{"class":106},[66,655,656],{"class":110}," points[faces].mean(",[66,658,659],{"class":255},"axis",[66,661,146],{"class":106},[66,663,664],{"class":203},"1",[66,666,324],{"class":110},[66,668,669,672,674,677,680,683,685,687,689,692,695,698],{"class":68,"line":140},[66,670,671],{"class":110},"keep ",[66,673,146],{"class":106},[66,675,676],{"class":110}," np.linalg.norm(centers[:, :",[66,678,679],{"class":203},"2",[66,681,682],{"class":110},"], ",[66,684,659],{"class":255},[66,686,146],{"class":106},[66,688,664],{"class":203},[66,690,691],{"class":110},") ",[66,693,694],{"class":106},">",[66,696,697],{"class":203}," 0.8",[66,699,700],{"class":376},"   # cut a hole in the middle\n",[66,702,703,706,708],{"class":68,"line":264},[66,704,705],{"class":110},"sheet ",[66,707,146],{"class":106},[66,709,710],{"class":110}," tf.Mesh(faces[keep], points)\n",[66,712,713],{"class":68,"line":300},[66,714,137],{"emptyLinePlaceholder":136},[66,716,717,720],{"class":68,"line":311},[66,718,719],{"class":110},"tf.is_closed(sheet)   ",[66,721,722],{"class":376},"# False\n",[14,724,725],{},[47,726],{"alt":727,"src":728},"Step: finding holes and open edges. The underside of the Stanford bunny, an open scan, with its boundary edges in pink","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-3-holes.png",[14,730,731,734,735,738],{},[32,732,733],{},"tf.boundary_edges"," returns the open edges as an ",[32,736,737],{},"(N, 2)"," array of vertex pairs:",[57,740,742],{"className":97,"code":741,"language":99,"meta":62,"style":62},"edges = tf.boundary_edges(sheet)   # shape (48, 2)\n",[32,743,744],{"__ignoreMap":62},[66,745,746,749,751,754],{"class":68,"line":69},[66,747,748],{"class":110},"edges ",[66,750,146],{"class":106},[66,752,753],{"class":110}," tf.boundary_edges(sheet)   ",[66,755,756],{"class":376},"# shape (48, 2)\n",[52,758,760],{"id":759},"boundary-loops","Boundary loops",[14,762,763,766],{},[32,764,765],{},"tf.boundary_paths"," chains the open edges into loops. Each loop is a list of mesh vertex indices. A closed loop repeats its first vertex at the end:",[57,768,770],{"className":97,"code":769,"language":99,"meta":62,"style":62},"paths = tf.boundary_paths(sheet)\nlen(paths)                    # 2: the outer rim and the hole\n\nfor path in paths:\n    loop = points[path]       # loop coordinates, first point repeated last\n",[32,771,772,782,793,797,809],{"__ignoreMap":62},[66,773,774,777,779],{"class":68,"line":69},[66,775,776],{"class":110},"paths ",[66,778,146],{"class":106},[66,780,781],{"class":110}," tf.boundary_paths(sheet)\n",[66,783,784,787,790],{"class":68,"line":120},[66,785,786],{"class":203},"len",[66,788,789],{"class":110},"(paths)                    ",[66,791,792],{"class":376},"# 2: the outer rim and the hole\n",[66,794,795],{"class":68,"line":133},[66,796,137],{"emptyLinePlaceholder":136},[66,798,799,801,804,806],{"class":68,"line":140},[66,800,488],{"class":106},[66,802,803],{"class":110}," path ",[66,805,494],{"class":106},[66,807,808],{"class":110}," paths:\n",[66,810,811,814,816,819],{"class":68,"line":264},[66,812,813],{"class":110},"    loop ",[66,815,146],{"class":106},[66,817,818],{"class":110}," points[path]       ",[66,820,821],{"class":376},"# loop coordinates, first point repeated last\n",[14,823,824],{},[47,825],{"alt":826,"src":827},"Step: boundary loops. The bunny's open edges chained into five loops, each in its own color","\u002Fimg\u002Ftutorials\u002Fsteps\u002Ffast-connected-components-4-loops.png",[14,829,830,833],{},[32,831,832],{},"tf.boundary_curves"," returns the same loops as standalone geometry: a points array with only the boundary vertices, and loops that index into it:",[57,835,837],{"className":97,"code":836,"language":99,"meta":62,"style":62},"paths, curve_points = tf.boundary_curves(sheet)\n\nfor ids in paths:\n    loop = curve_points[ids]\n    perimeter = np.linalg.norm(np.diff(loop, axis=0), axis=1).sum()\n    print(len(ids) - 1, \"edges, perimeter\", perimeter)\n# 32 edges, perimeter 16.0\n# 16 edges, perimeter 9.656855\n",[32,838,839,849,853,864,873,901,927,932],{"__ignoreMap":62},[66,840,841,844,846],{"class":68,"line":69},[66,842,843],{"class":110},"paths, curve_points ",[66,845,146],{"class":106},[66,847,848],{"class":110}," tf.boundary_curves(sheet)\n",[66,850,851],{"class":68,"line":120},[66,852,137],{"emptyLinePlaceholder":136},[66,854,855,857,860,862],{"class":68,"line":133},[66,856,488],{"class":106},[66,858,859],{"class":110}," ids ",[66,861,494],{"class":106},[66,863,808],{"class":110},[66,865,866,868,870],{"class":68,"line":140},[66,867,813],{"class":110},[66,869,146],{"class":106},[66,871,872],{"class":110}," curve_points[ids]\n",[66,874,875,878,880,883,885,887,889,892,894,896,898],{"class":68,"line":264},[66,876,877],{"class":110},"    perimeter ",[66,879,146],{"class":106},[66,881,882],{"class":110}," np.linalg.norm(np.diff(loop, ",[66,884,659],{"class":255},[66,886,146],{"class":106},[66,888,564],{"class":203},[66,890,891],{"class":110},"), ",[66,893,659],{"class":255},[66,895,146],{"class":106},[66,897,664],{"class":203},[66,899,900],{"class":110},").sum()\n",[66,902,903,905,908,910,913,916,919,921,924],{"class":68,"line":300},[66,904,505],{"class":203},[66,906,907],{"class":110},"(",[66,909,786],{"class":203},[66,911,912],{"class":110},"(ids) ",[66,914,915],{"class":106},"-",[66,917,918],{"class":203}," 1",[66,920,225],{"class":110},[66,922,923],{"class":76},"\"edges, perimeter\"",[66,925,926],{"class":110},", perimeter)\n",[66,928,929],{"class":68,"line":311},[66,930,931],{"class":376},"# 32 edges, perimeter 16.0\n",[66,933,934],{"class":68,"line":316},[66,935,936],{"class":376},"# 16 edges, perimeter 9.656855\n",[14,938,939],{},"On a single sheet, the longest loop is usually the outer rim and the rest are holes. On a closed part, every loop is a hole.",[14,941,942,943,946],{},"For more detail per loop, ",[32,944,945],{},"tf.boundary_rims"," returns each rim's vertex walk, the face along each rim edge, and whether the rim closes:",[57,948,950],{"className":97,"code":949,"language":99,"meta":62,"style":62},"vertices, rim_faces, closed = tf.boundary_rims(sheet)\n",[32,951,952],{"__ignoreMap":62},[66,953,954,957,959],{"class":68,"line":69},[66,955,956],{"class":110},"vertices, rim_faces, closed ",[66,958,146],{"class":106},[66,960,961],{"class":110}," tf.boundary_rims(sheet)\n",[52,963,965],{"id":964},"working-with-results","Working with results",[14,967,968,969,971,972,975,976,979],{},"Labels, paths and points are NumPy arrays or ",[32,970,449],{}," blocks that index like lists. Write a part with ",[32,973,974],{},"tf.write_stl"," or ",[32,977,978],{},"tf.write_obj",". Common next steps:",[981,982,983,992,1000],"ul",{},[984,985,986,987,27],"li",{},"Fill the holes the loops describe: ",[18,988,991],{"href":989,"rel":990},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Ffill",[88],"fill holes in a mesh",[984,993,994,995,27],{},"Check a cleaned part for crossing faces: ",[18,996,999],{"href":997,"rel":998},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fintersect",[88],"detect self-intersections",[984,1001,1002,1003,27],{},"Even out triangle sizes before simulation: ",[18,1004,1007],{"href":1005,"rel":1006},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fremesh",[88],"isotropic remeshing",[52,1009,1011],{"id":1010},"related-tutorials","Related tutorials",[981,1013,1014,1020,1027,1034],{},[984,1015,1016,1019],{},[18,1017,1018],{"href":601},"Fast Mesh Booleans in Python"," — exact union, intersection and difference of two meshes.",[984,1021,1022,1026],{},[18,1023,1025],{"href":1024},"\u002Ftutorials\u002Ffast-stl-and-obj-reading-in-python","Fast STL and OBJ Reading in Python"," — load and write meshes, the first step of every pipeline.",[984,1028,1029,1033],{},[18,1030,1032],{"href":1031},"\u002Ftutorials\u002Ffast-closest-point-on-a-mesh-in-python","Fast and Robust Closest Point on a Mesh in Python"," — nearest surface point for a batch of queries.",[984,1035,1036,1040],{},[18,1037,1039],{"href":1038},"\u002Ftutorials\u002Fmesh-slicing-and-cross-sections-in-python","Fast and Robust Mesh Slicing and Cross-Sections in Python"," — plane sections, layers and slabs of a mesh.",[52,1042,1044],{"id":1043},"performance","Performance",[14,1046,1047],{},"The benchmark labels edge-connected faces on 2,000 production meshes of 100,368 to 938,940 triangles, against trimesh. Building the connectivity is inside the timer for both.",[1049,1050],"headline-numbers",{":items":1051},"[{\"value\":\"2.9ms\",\"label\":\"Connected components\",\"detail\":\"Geometric mean, 2,000 meshes\"},{\"value\":\"41.6ms\",\"label\":\"trimesh\",\"detail\":\"Same meshes, same task\"},{\"value\":\"15×\",\"label\":\"Faster than trimesh\",\"detail\":\"Geometric mean wall time\"}]",[14,1053,1054,1059,1060],{},[66,1055,1058],{"className":1056},[1057],"tag-pill","Apple M4 Max"," ",[66,1061,1063],{"className":1062},[1057],"16 threads",[14,1065,1066],{},"There is no published benchmark for boundary loops, so that part of the page makes no speed claim.",[14,1068,1069],{},[47,1070],{"alt":1071,"src":1072},"Benchmark chart: trueform against other Python libraries, connected components","\u002Fimg\u002Falgorithms\u002Fbenchmark-connected-components-python-2026.png",[14,1074,1075,1079,1080],{},[18,1076,1078],{"href":1077},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1081,1084],{"href":1082,"rel":1083},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Ftopology#connected-components",[88],"Documentation",[1086,1087],"cite-as",{"author":1088,"title":5},"Sajovic, {\\v{Z}}iga",[1090,1091,1092],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":62,"searchDepth":120,"depth":120,"links":1094},[1095,1096,1097,1098,1099,1100,1101,1102,1103],{"id":54,"depth":120,"text":55},{"id":93,"depth":120,"text":94},{"id":344,"depth":120,"text":345},{"id":457,"depth":120,"text":458},{"id":606,"depth":120,"text":607},{"id":759,"depth":120,"text":760},{"id":964,"depth":120,"text":965},{"id":1010,"depth":120,"text":1011},{"id":1043,"depth":120,"text":1044},"2026-10-31","Label the connected components of a triangle mesh, split it into separate parts, and find its holes and open edges as boundary loops, in Python. One pip install, NumPy arrays in and out.","md",[1108,1109,1110,1111,1112,1113,1114,1115,1116,1117,1118,1119],"mesh connected components Python","split mesh into parts Python","separate mesh shells","mesh boundary loops Python","find holes in mesh Python","open edges mesh Python","mesh boundary edges numpy","trimesh split alternative","trimesh connected_components alternative","PyMeshLab split connected components alternative","Open3D cluster_connected_triangles alternative","VTK connectivity filter alternative",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Ffast-connected-components-in-python.png","\u002Ftutorials\u002Ffast-connected-components-in-python",{"title":5,"description":1105},"tutorials\u002Ffast-connected-components-in-python",[99,1126,1127,1128],"topology","connected-components","tutorial","jRAHVdcTpfC2QFT-8DUeqilWj1ayUxNWm5WODxUPTL0",1793517812008]