[{"data":1,"prerenderedAt":1196},["ShallowReactive",2],{"tutorial-principal-curvatures-of-a-mesh-in-python":3},{"id":4,"title":5,"author":6,"body":7,"date":1172,"description":1173,"extension":1174,"keywords":1175,"meta":1187,"navigation":144,"ogImage":1188,"path":1189,"published":144,"seo":1190,"stem":1191,"tags":1192,"__hash__":1195},"tutorials\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python.md","Fast and Robust Principal Curvatures of a Mesh in Python","Žiga Sajovic",{"type":8,"value":9,"toc":1161},"minimark",[10,28,48,52,59,64,89,99,103,170,184,187,229,233,259,265,268,293,296,338,341,345,348,377,383,398,457,460,464,471,510,516,578,581,707,710,714,722,752,758,761,764,939,943,946,954,977,980,1042,1045,1068,1076,1080,1110,1114,1118,1130,1135,1141,1153,1157],[11,12,13],"note",{},[14,15,16,17,22,23,27],"p",{},"Also available in ",[18,19,21],"a",{"href":20},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-cpp","C++"," and ",[18,24,26],{"href":25},"\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-javascript","JavaScript",".",[14,29,30,31,35,36,39,40,43,44,47],{},"Principal curvatures tell you how a surface bends at each point. ",[32,33,34],"code",{},"k0"," is the largest bend, ",[32,37,38],{},"k1"," the smallest. From them you get Gaussian curvature, mean curvature, and the shape index. ",[32,41,42],{},"trueform"," computes all of it per vertex. One ",[32,45,46],{},"pip install",". NumPy arrays in, NumPy arrays out. This tutorial covers principal curvatures, principal directions, the shape index, and how to read the signs.",[49,50],"article-cta",{":buttons":51},"[{\"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\u002Fgeometry#curvature-analysis\",\"variant\":\"soft\",\"color\":\"neutral\"},{\"label\":\"Try it live\",\"to\":\"https:\u002F\u002Ftrueform.polydera.com\u002Flive-examples\u002Fshape-histogram\",\"icon\":\"i-lucide-play\"}]",[14,53,54],{},[55,56],"img",{"alt":57,"src":58},"Mean curvature on the Stanford dragon: creases orange, flat areas teal","\u002Fimg\u002Ftutorials\u002Fprincipal-curvatures.png",[60,61,63],"h2",{"id":62},"install","Install",[65,66,71],"pre",{"className":67,"code":68,"language":69,"meta":70,"style":70},"language-bash shiki shiki-themes github-dark","pip install trueform\n","bash","",[32,72,73],{"__ignoreMap":70},[74,75,78,82,86],"span",{"class":76,"line":77},"line",1,[74,79,81],{"class":80},"svObZ","pip",[74,83,85],{"class":84},"sU2Wk"," install",[74,87,88],{"class":84}," trueform\n",[14,90,91,92,98],{},"See the ",[18,93,97],{"href":94,"rel":95},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fgetting-started\u002Finstallation",[96],"nofollow","installation guide"," for all options.",[60,100,102],{"id":101},"loading-the-mesh","Loading the mesh",[65,104,108],{"className":105,"code":106,"language":107,"meta":70,"style":70},"language-python shiki shiki-themes github-dark","import trueform as tf\nimport numpy as np\n\nmesh = tf.Mesh(*tf.read_stl(\"part.stl\"))\n","python",[32,109,110,126,139,146],{"__ignoreMap":70},[74,111,112,116,120,123],{"class":76,"line":77},[74,113,115],{"class":114},"snl16","import",[74,117,119],{"class":118},"s95oV"," trueform ",[74,121,122],{"class":114},"as",[74,124,125],{"class":118}," tf\n",[74,127,129,131,134,136],{"class":76,"line":128},2,[74,130,115],{"class":114},[74,132,133],{"class":118}," numpy ",[74,135,122],{"class":114},[74,137,138],{"class":118}," np\n",[74,140,142],{"class":76,"line":141},3,[74,143,145],{"emptyLinePlaceholder":144},true,"\n",[74,147,149,152,155,158,161,164,167],{"class":76,"line":148},4,[74,150,151],{"class":118},"mesh ",[74,153,154],{"class":114},"=",[74,156,157],{"class":118}," tf.Mesh(",[74,159,160],{"class":114},"*",[74,162,163],{"class":118},"tf.read_stl(",[74,165,166],{"class":84},"\"part.stl\"",[74,168,169],{"class":118},"))\n",[14,171,172,175,176,179,180,183],{},[32,173,174],{},"tf.read_obj"," works the same way. If you already have NumPy arrays, pass them to ",[32,177,178],{},"tf.Mesh(faces, points)",". Every function below also takes a plain ",[32,181,182],{},"(faces, points)"," tuple.",[14,185,186],{},"To follow along without a file, make a sphere of radius 2. Its curvature is 1\u002F2 everywhere, so you can check the numbers:",[65,188,190],{"className":105,"code":189,"language":107,"meta":70,"style":70},"faces, points = tf.make_sphere_mesh(2.0, 100, 200)\nmesh = tf.Mesh(faces, points)\n",[32,191,192,220],{"__ignoreMap":70},[74,193,194,197,199,202,206,209,212,214,217],{"class":76,"line":77},[74,195,196],{"class":118},"faces, points ",[74,198,154],{"class":114},[74,200,201],{"class":118}," tf.make_sphere_mesh(",[74,203,205],{"class":204},"sDLfK","2.0",[74,207,208],{"class":118},", ",[74,210,211],{"class":204},"100",[74,213,208],{"class":118},[74,215,216],{"class":204},"200",[74,218,219],{"class":118},")\n",[74,221,222,224,226],{"class":76,"line":128},[74,223,151],{"class":118},[74,225,154],{"class":114},[74,227,228],{"class":118}," tf.Mesh(faces, points)\n",[60,230,232],{"id":231},"principal-curvatures","Principal curvatures",[65,234,236],{"className":105,"code":235,"language":107,"meta":70,"style":70},"k0, k1 = tf.principal_curvatures(mesh)\n# k0, k1: np.ndarray, shape (num_points,)\n# k0 >= k1 at every point\n",[32,237,238,248,254],{"__ignoreMap":70},[74,239,240,243,245],{"class":76,"line":77},[74,241,242],{"class":118},"k0, k1 ",[74,244,154],{"class":114},[74,246,247],{"class":118}," tf.principal_curvatures(mesh)\n",[74,249,250],{"class":76,"line":128},[74,251,253],{"class":252},"sAwPA","# k0, k1: np.ndarray, shape (num_points,)\n",[74,255,256],{"class":76,"line":141},[74,257,258],{"class":252},"# k0 >= k1 at every point\n",[14,260,261],{},[55,262],{"alt":263,"src":264},"Principal curvatures on a carved pot from Thingi10K: Gaussian curvature k0 times k1, saddles in orange, domes and dents in teal","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-1-gaussian.png",[14,266,267],{},"On the sphere both are close to 0.5. Each vertex fits a quadric to its neighborhood. The neighborhood is the k-ring, two rings by default. A larger ring smooths out noise:",[65,269,271],{"className":105,"code":270,"language":107,"meta":70,"style":70},"k0, k1 = tf.principal_curvatures(mesh, k=3)\n",[32,272,273],{"__ignoreMap":70},[74,274,275,277,279,282,286,288,291],{"class":76,"line":77},[74,276,242],{"class":118},[74,278,154],{"class":114},[74,280,281],{"class":118}," tf.principal_curvatures(mesh, ",[74,283,285],{"class":284},"s9osk","k",[74,287,154],{"class":114},[74,289,290],{"class":204},"3",[74,292,219],{"class":118},[14,294,295],{},"Gaussian and mean curvature follow directly:",[65,297,299],{"className":105,"code":298,"language":107,"meta":70,"style":70},"gaussian = k0 * k1\nmean = 0.5 * (k0 + k1)\n",[32,300,301,316],{"__ignoreMap":70},[74,302,303,306,308,311,313],{"class":76,"line":77},[74,304,305],{"class":118},"gaussian ",[74,307,154],{"class":114},[74,309,310],{"class":118}," k0 ",[74,312,160],{"class":114},[74,314,315],{"class":118}," k1\n",[74,317,318,321,323,326,329,332,335],{"class":76,"line":128},[74,319,320],{"class":118},"mean ",[74,322,154],{"class":114},[74,324,325],{"class":204}," 0.5",[74,327,328],{"class":114}," *",[74,330,331],{"class":118}," (k0 ",[74,333,334],{"class":114},"+",[74,336,337],{"class":118}," k1)\n",[14,339,340],{},"On the sphere, Gaussian curvature is close to 0.25 and mean curvature close to 0.5.",[60,342,344],{"id":343},"principal-directions","Principal directions",[14,346,347],{},"Ask for the directions too:",[65,349,351],{"className":105,"code":350,"language":107,"meta":70,"style":70},"k0, k1, d0, d1 = tf.principal_curvatures(mesh, directions=True)\n# d0, d1: np.ndarray, shape (num_points, 3)\n",[32,352,353,372],{"__ignoreMap":70},[74,354,355,358,360,362,365,367,370],{"class":76,"line":77},[74,356,357],{"class":118},"k0, k1, d0, d1 ",[74,359,154],{"class":114},[74,361,281],{"class":118},[74,363,364],{"class":284},"directions",[74,366,154],{"class":114},[74,368,369],{"class":204},"True",[74,371,219],{"class":118},[74,373,374],{"class":76,"line":128},[74,375,376],{"class":252},"# d0, d1: np.ndarray, shape (num_points, 3)\n",[14,378,379],{},[55,380],{"alt":381,"src":382},"Principal directions on the carved pot's relief: the direction of k0 in pink and of k1 in orange at each vertex, following the ridges and grooves","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-2-directions.png",[14,384,385,388,389,208,391,394,395,397],{},[32,386,387],{},"d0"," is the direction of ",[32,390,34],{},[32,392,393],{},"d1"," the direction of ",[32,396,38],{},". Both are unit vectors, perpendicular to each other and to the vertex normal. You can check:",[65,399,401],{"className":105,"code":400,"language":107,"meta":70,"style":70},"n = tf.point_normals(mesh)\nnp.abs((d0 * d1).sum(axis=1)).max()   # close to 0\nnp.abs((d0 * n).sum(axis=1)).max()    # close to 0\n",[32,402,403,413,437],{"__ignoreMap":70},[74,404,405,408,410],{"class":76,"line":77},[74,406,407],{"class":118},"n ",[74,409,154],{"class":114},[74,411,412],{"class":118}," tf.point_normals(mesh)\n",[74,414,415,418,420,423,426,428,431,434],{"class":76,"line":128},[74,416,417],{"class":118},"np.abs((d0 ",[74,419,160],{"class":114},[74,421,422],{"class":118}," d1).sum(",[74,424,425],{"class":284},"axis",[74,427,154],{"class":114},[74,429,430],{"class":204},"1",[74,432,433],{"class":118},")).max()   ",[74,435,436],{"class":252},"# close to 0\n",[74,438,439,441,443,446,448,450,452,455],{"class":76,"line":141},[74,440,417],{"class":118},[74,442,160],{"class":114},[74,444,445],{"class":118}," n).sum(",[74,447,425],{"class":284},[74,449,154],{"class":114},[74,451,430],{"class":204},[74,453,454],{"class":118},")).max()    ",[74,456,436],{"class":252},[14,458,459],{},"Directions are useful for drawing hatching along a surface, for aligning tool paths, and for finding ridges and valleys.",[60,461,463],{"id":462},"shape-index","Shape index",[14,465,466,467,470],{},"The shape index folds the two curvatures into one number in ",[74,468,469],{},"-1, 1",". It describes the local shape, not its size. A small ball and a large ball have the same shape index.",[65,472,474],{"className":105,"code":473,"language":107,"meta":70,"style":70},"si = tf.shape_index(mesh)          # shape (num_points,)\nsi = tf.shape_index(mesh, k=3)     # larger neighborhood\n",[32,475,476,489],{"__ignoreMap":70},[74,477,478,481,483,486],{"class":76,"line":77},[74,479,480],{"class":118},"si ",[74,482,154],{"class":114},[74,484,485],{"class":118}," tf.shape_index(mesh)          ",[74,487,488],{"class":252},"# shape (num_points,)\n",[74,490,491,493,495,498,500,502,504,507],{"class":76,"line":128},[74,492,480],{"class":118},[74,494,154],{"class":114},[74,496,497],{"class":118}," tf.shape_index(mesh, ",[74,499,285],{"class":284},[74,501,154],{"class":114},[74,503,290],{"class":204},[74,505,506],{"class":118},")     ",[74,508,509],{"class":252},"# larger neighborhood\n",[14,511,512],{},[55,513],{"alt":514,"src":515},"Shape index on the carved pot: each vertex classed as cup, trough, saddle, ridge or cap, from dark orange through sand to pale teal","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-3-shape-index.png",[517,518,519,532],"table",{},[520,521,522],"thead",{},[523,524,525,529],"tr",{},[526,527,528],"th",{},"Range",[526,530,531],{},"Shape",[533,534,535,544,552,560,568],"tbody",{},[523,536,537,541],{},[538,539,540],"td",{},"[-1, -5\u002F8)",[538,542,543],{},"Cup",[523,545,546,549],{},[538,547,548],{},"[-5\u002F8, -3\u002F8)",[538,550,551],{},"Trough",[523,553,554,557],{},[538,555,556],{},"[-3\u002F8, 3\u002F8)",[538,558,559],{},"Saddle",[523,561,562,565],{},[538,563,564],{},"[3\u002F8, 5\u002F8)",[538,566,567],{},"Ridge",[523,569,570,575],{},[538,571,572],{},[74,573,574],{},"5\u002F8, 1",[538,576,577],{},"Cap",[14,579,580],{},"Classify every vertex:",[65,582,584],{"className":105,"code":583,"language":107,"meta":70,"style":70},"names = [\"cup\", \"trough\", \"saddle\", \"ridge\", \"cap\"]\ncls = np.digitize(si, [-5\u002F8, -3\u002F8, 3\u002F8, 5\u002F8])\ncounts = dict(zip(names, np.bincount(cls, minlength=5)))\n",[32,585,586,622,674],{"__ignoreMap":70},[74,587,588,591,593,596,599,601,604,606,609,611,614,616,619],{"class":76,"line":77},[74,589,590],{"class":118},"names ",[74,592,154],{"class":114},[74,594,595],{"class":118}," [",[74,597,598],{"class":84},"\"cup\"",[74,600,208],{"class":118},[74,602,603],{"class":84},"\"trough\"",[74,605,208],{"class":118},[74,607,608],{"class":84},"\"saddle\"",[74,610,208],{"class":118},[74,612,613],{"class":84},"\"ridge\"",[74,615,208],{"class":118},[74,617,618],{"class":84},"\"cap\"",[74,620,621],{"class":118},"]\n",[74,623,624,627,630,633,636,639,642,645,647,649,651,653,655,657,659,661,663,665,667,669,671],{"class":76,"line":128},[74,625,626],{"class":204},"cls",[74,628,629],{"class":114}," =",[74,631,632],{"class":118}," np.digitize(si, [",[74,634,635],{"class":114},"-",[74,637,638],{"class":204},"5",[74,640,641],{"class":114},"\u002F",[74,643,644],{"class":204},"8",[74,646,208],{"class":118},[74,648,635],{"class":114},[74,650,290],{"class":204},[74,652,641],{"class":114},[74,654,644],{"class":204},[74,656,208],{"class":118},[74,658,290],{"class":204},[74,660,641],{"class":114},[74,662,644],{"class":204},[74,664,208],{"class":118},[74,666,638],{"class":204},[74,668,641],{"class":114},[74,670,644],{"class":204},[74,672,673],{"class":118},"])\n",[74,675,676,679,681,684,687,690,693,695,697,700,702,704],{"class":76,"line":141},[74,677,678],{"class":118},"counts ",[74,680,154],{"class":114},[74,682,683],{"class":204}," dict",[74,685,686],{"class":118},"(",[74,688,689],{"class":204},"zip",[74,691,692],{"class":118},"(names, np.bincount(",[74,694,626],{"class":204},[74,696,208],{"class":118},[74,698,699],{"class":284},"minlength",[74,701,154],{"class":114},[74,703,638],{"class":204},[74,705,706],{"class":118},")))\n",[14,708,709],{},"On the sphere every vertex is a cap.",[60,711,713],{"id":712},"reading-the-signs","Reading the signs",[14,715,716,717,22,719,721],{},"Signs follow the face orientation. With outward-facing normals, a convex surface has positive curvature. On the sphere ",[32,718,34],{},[32,720,38],{}," are positive and the shape index is near 1. Flip the faces and the signs flip with them:",[65,723,725],{"className":105,"code":724,"language":107,"meta":70,"style":70},"flipped = tf.Mesh(faces[:, ::-1].copy(), points)\ntf.shape_index(flipped)   # near -1 on the same sphere\n",[32,726,727,744],{"__ignoreMap":70},[74,728,729,732,734,737,739,741],{"class":76,"line":77},[74,730,731],{"class":118},"flipped ",[74,733,154],{"class":114},[74,735,736],{"class":118}," tf.Mesh(faces[:, ::",[74,738,635],{"class":114},[74,740,430],{"class":204},[74,742,743],{"class":118},"].copy(), points)\n",[74,745,746,749],{"class":76,"line":128},[74,747,748],{"class":118},"tf.shape_index(flipped)   ",[74,750,751],{"class":252},"# near -1 on the same sphere\n",[14,753,754],{},[55,755],{"alt":756,"src":757},"Reading the signs on a cat figurine from Thingi10K: shape-index classes with outward normals on the left and with the faces flipped on the right, where caps turn into cups and ridges into troughs","\u002Fimg\u002Ftutorials\u002Fsteps\u002Fprincipal-curvatures-of-a-mesh-4-signs.png",[14,759,760],{},"If your results look inverted, check the winding first.",[14,762,763],{},"A height field shows all the shapes. Take a flat grid with +z normals and bend it:",[65,765,767],{"className":105,"code":766,"language":107,"meta":70,"style":70},"faces, points = tf.make_plane_mesh(2.0, 2.0, 100, 100)\nx, y = points[:, 0], points[:, 1]\n\npoints[:, 2] = x**2 - y**2          # saddle: shape index 0 at the center\nsaddle = tf.Mesh(faces, points.copy())\n\npoints[:, 2] = -(x**2)              # bends away from the normal: ridge, +0.5\nridge = tf.Mesh(faces, points.copy())\n\npoints[:, 2] = x**2                 # bends toward the normal: trough, -0.5\ntrough = tf.Mesh(faces, points.copy())\n",[32,768,769,794,814,818,852,863,868,894,904,909,929],{"__ignoreMap":70},[74,770,771,773,775,778,780,782,784,786,788,790,792],{"class":76,"line":77},[74,772,196],{"class":118},[74,774,154],{"class":114},[74,776,777],{"class":118}," tf.make_plane_mesh(",[74,779,205],{"class":204},[74,781,208],{"class":118},[74,783,205],{"class":204},[74,785,208],{"class":118},[74,787,211],{"class":204},[74,789,208],{"class":118},[74,791,211],{"class":204},[74,793,219],{"class":118},[74,795,796,799,801,804,807,810,812],{"class":76,"line":128},[74,797,798],{"class":118},"x, y ",[74,800,154],{"class":114},[74,802,803],{"class":118}," points[:, ",[74,805,806],{"class":204},"0",[74,808,809],{"class":118},"], points[:, ",[74,811,430],{"class":204},[74,813,621],{"class":118},[74,815,816],{"class":76,"line":141},[74,817,145],{"emptyLinePlaceholder":144},[74,819,820,823,826,829,831,834,837,839,842,845,847,849],{"class":76,"line":148},[74,821,822],{"class":118},"points[:, ",[74,824,825],{"class":204},"2",[74,827,828],{"class":118},"] ",[74,830,154],{"class":114},[74,832,833],{"class":118}," x",[74,835,836],{"class":114},"**",[74,838,825],{"class":204},[74,840,841],{"class":114}," -",[74,843,844],{"class":118}," y",[74,846,836],{"class":114},[74,848,825],{"class":204},[74,850,851],{"class":252},"          # saddle: shape index 0 at the center\n",[74,853,855,858,860],{"class":76,"line":854},5,[74,856,857],{"class":118},"saddle ",[74,859,154],{"class":114},[74,861,862],{"class":118}," tf.Mesh(faces, points.copy())\n",[74,864,866],{"class":76,"line":865},6,[74,867,145],{"emptyLinePlaceholder":144},[74,869,871,873,875,877,879,881,884,886,888,891],{"class":76,"line":870},7,[74,872,822],{"class":118},[74,874,825],{"class":204},[74,876,828],{"class":118},[74,878,154],{"class":114},[74,880,841],{"class":114},[74,882,883],{"class":118},"(x",[74,885,836],{"class":114},[74,887,825],{"class":204},[74,889,890],{"class":118},")              ",[74,892,893],{"class":252},"# bends away from the normal: ridge, +0.5\n",[74,895,897,900,902],{"class":76,"line":896},8,[74,898,899],{"class":118},"ridge ",[74,901,154],{"class":114},[74,903,862],{"class":118},[74,905,907],{"class":76,"line":906},9,[74,908,145],{"emptyLinePlaceholder":144},[74,910,912,914,916,918,920,922,924,926],{"class":76,"line":911},10,[74,913,822],{"class":118},[74,915,825],{"class":204},[74,917,828],{"class":118},[74,919,154],{"class":114},[74,921,833],{"class":118},[74,923,836],{"class":114},[74,925,825],{"class":204},[74,927,928],{"class":252},"                 # bends toward the normal: trough, -0.5\n",[74,930,932,935,937],{"class":76,"line":931},11,[74,933,934],{"class":118},"trough ",[74,936,154],{"class":114},[74,938,862],{"class":118},[60,940,942],{"id":941},"working-with-results","Working with results",[14,944,945],{},"All results are NumPy arrays with one value per point, in the mesh's dtype. Use them as colors, as weights, or as fields for other tools.",[14,947,948,949,953],{},"Draw the lines where the surface changes from saddle to ridge with ",[18,950,952],{"href":951},"\u002Ftutorials\u002Fisocontours-on-a-triangle-mesh-in-python","isocontours",":",[65,955,957],{"className":105,"code":956,"language":107,"meta":70,"style":70},"paths, curve_points = tf.isocontours(mesh, si, 3\u002F8)\n",[32,958,959],{"__ignoreMap":70},[74,960,961,964,966,969,971,973,975],{"class":76,"line":77},[74,962,963],{"class":118},"paths, curve_points ",[74,965,154],{"class":114},[74,967,968],{"class":118}," tf.isocontours(mesh, si, ",[74,970,290],{"class":204},[74,972,641],{"class":114},[74,974,644],{"class":204},[74,976,219],{"class":118},[14,978,979],{},"Cut the mesh into regions by shape with isobands:",[65,981,983],{"className":105,"code":982,"language":107,"meta":70,"style":70},"cuts = np.array([-3\u002F8, 3\u002F8], dtype=si.dtype)\n(region_faces, region_points), labels, face_labels = \\\n    tf.isobands(mesh, si, cuts)\n# labels: 0 concave, 1 saddle, 2 convex\n",[32,984,985,1022,1032,1037],{"__ignoreMap":70},[74,986,987,990,992,995,997,999,1001,1003,1005,1007,1009,1011,1014,1017,1019],{"class":76,"line":77},[74,988,989],{"class":118},"cuts ",[74,991,154],{"class":114},[74,993,994],{"class":118}," np.array([",[74,996,635],{"class":114},[74,998,290],{"class":204},[74,1000,641],{"class":114},[74,1002,644],{"class":204},[74,1004,208],{"class":118},[74,1006,290],{"class":204},[74,1008,641],{"class":114},[74,1010,644],{"class":204},[74,1012,1013],{"class":118},"], ",[74,1015,1016],{"class":284},"dtype",[74,1018,154],{"class":114},[74,1020,1021],{"class":118},"si.dtype)\n",[74,1023,1024,1027,1029],{"class":76,"line":128},[74,1025,1026],{"class":118},"(region_faces, region_points), labels, face_labels ",[74,1028,154],{"class":114},[74,1030,1031],{"class":118}," \\\n",[74,1033,1034],{"class":76,"line":141},[74,1035,1036],{"class":118},"    tf.isobands(mesh, si, cuts)\n",[74,1038,1039],{"class":76,"line":148},[74,1040,1041],{"class":252},"# labels: 0 concave, 1 saddle, 2 convex\n",[14,1043,1044],{},"Flag sharp spots for inspection:",[65,1046,1048],{"className":105,"code":1047,"language":107,"meta":70,"style":70},"sharp = np.flatnonzero(np.maximum(np.abs(k0), np.abs(k1)) > 10.0)\n",[32,1049,1050],{"__ignoreMap":70},[74,1051,1052,1055,1057,1060,1063,1066],{"class":76,"line":77},[74,1053,1054],{"class":118},"sharp ",[74,1056,154],{"class":114},[74,1058,1059],{"class":118}," np.flatnonzero(np.maximum(np.abs(k0), np.abs(k1)) ",[74,1061,1062],{"class":114},">",[74,1064,1065],{"class":204}," 10.0",[74,1067,219],{"class":118},[14,1069,1070,1071,1075],{},"Combine curvature with ",[18,1072,1074],{"href":1073},"\u002Ftutorials\u002Ffast-geodesic-distances-in-python","geodesic distances"," to look only at the neighborhood of a point.",[60,1077,1079],{"id":1078},"related-tutorials","Related tutorials",[1081,1082,1083,1090,1096,1103],"ul",{},[1084,1085,1086,1089],"li",{},[18,1087,1088],{"href":1073},"Fast and Accurate Geodesic Distances in Python"," — distance along the surface from one seed or many.",[1084,1091,1092,1095],{},[18,1093,1094],{"href":951},"Fast and Robust Isocontours on a Triangle Mesh in Python"," — curves and bands of equal value on a scalar field.",[1084,1097,1098,1102],{},[18,1099,1101],{"href":1100},"\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.",[1084,1104,1105,1109],{},[18,1106,1108],{"href":1107},"\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.",[60,1111,1113],{"id":1112},"performance","Performance",[1115,1116],"headline-numbers",{":items":1117},"[{\"value\":\"27.7ms\",\"label\":\"Principal curvatures\",\"detail\":\"1M faces, Python\"},{\"value\":\"70×\",\"label\":\"Faster than libigl\",\"detail\":\"1M faces, Python (1,941 ms)\"},{\"value\":\"51×\",\"label\":\"Faster than libigl\",\"detail\":\"1M faces, C++\"}]",[14,1119,1120,1125,1126],{},[74,1121,1124],{"className":1122},[1123],"tag-pill","Apple M4 Max"," ",[74,1127,1129],{"className":1128},[1123],"16 threads",[1131,1132],"data-table",{":headers":1133,":highlight":430,":rows":1134},"[\"1M faces\",\"trueform\",\"libigl\",\"Speedup\"]","[[\"Python\",\"27.7 ms\",\"1,941 ms\",\"70×\"],[\"C++\",\"30.1 ms\",\"1,520 ms\",\"51×\"]]",[14,1136,1137],{},[55,1138],{"alt":1139,"src":1140},"Benchmark chart: trueform against other Python libraries, principal curvatures","\u002Fimg\u002Falgorithms\u002Fbenchmark-principal-curvatures-python-2026.png",[14,1142,1143,1147,1148],{},[18,1144,1146],{"href":1145},"\u002Ftrueform\u002Fbenchmarks","Full benchmarks"," · ",[18,1149,1152],{"href":1150,"rel":1151},"https:\u002F\u002Ftrueform.polydera.com\u002Fpy\u002Fmodules\u002Fgeometry#curvature-analysis",[96],"Documentation",[1154,1155],"cite-as",{"author":1156,"title":5},"Sajovic, {\\v{Z}}iga",[1158,1159,1160],"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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":70,"searchDepth":128,"depth":128,"links":1162},[1163,1164,1165,1166,1167,1168,1169,1170,1171],{"id":62,"depth":128,"text":63},{"id":101,"depth":128,"text":102},{"id":231,"depth":128,"text":232},{"id":343,"depth":128,"text":344},{"id":462,"depth":128,"text":463},{"id":712,"depth":128,"text":713},{"id":941,"depth":128,"text":942},{"id":1078,"depth":128,"text":1079},{"id":1112,"depth":128,"text":1113},"2026-10-31","Compute principal curvatures, principal directions, Gaussian and mean curvature, and the shape index of a triangle mesh in Python. One pip install, NumPy arrays in and out.","md",[1176,1177,1178,1179,1180,1181,1182,1183,1184,1185,1186],"principal curvature Python","mesh curvature Python","Gaussian curvature mesh Python","mean curvature mesh Python","principal directions mesh","shape index mesh","libigl principal_curvature alternative","trimesh curvature alternative","PyMeshLab curvature","Open3D curvature","numpy mesh curvature",{},"\u002Fimg\u002Fog\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python.png","\u002Ftutorials\u002Fprincipal-curvatures-of-a-mesh-in-python",{"title":5,"description":1173},"tutorials\u002Fprincipal-curvatures-of-a-mesh-in-python",[107,1193,1194],"curvature","tutorial","HvCFNZFuhuhv10oxWFqN78SsgY8MDDRAOuGxJYf4T6g",1793517812008]