From af4ee23ee5c5caa68461028f3f4f0827b06e1950 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 20 Aug 2025 16:53:16 +1200 Subject: [PATCH 01/61] Added missing argument to split segment function --- src/scaffoldmaker/utils/networkmesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index fd027d99..2723a826 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -167,7 +167,7 @@ def isPatch(self): """ return self._isPatch - def split(self, splitNetworkNode): + def split(self, splitNetworkNode, isPatch): """ Split segment to finish at splitNetworkNode, returning remainder as a new NetworkSegment. :param splitNetworkNode: NetworkNode to split segment at. @@ -175,7 +175,7 @@ def split(self, splitNetworkNode): """ index = self._networkNodes.index(splitNetworkNode, 1, -1) # throws exception if not an interior node splitNetworkNode.setInteriorSegment(None) - nextSegment = NetworkSegment(self._networkNodes[index:], self._nodeVersions[index:]) + nextSegment = NetworkSegment(self._networkNodes[index:], self._nodeVersions[index:], isPatch) self._networkNodes = self._networkNodes[:index + 1] self._nodeVersions = self._nodeVersions[:index + 1] return nextSegment @@ -249,7 +249,7 @@ def build(self, structureString): if networkNode: interiorSegment = networkNode.getInteriorSegment() if interiorSegment: - nextSegment = interiorSegment.split(networkNode) + nextSegment = interiorSegment.split(networkNode, isPatch) index = self._networkSegments.index(interiorSegment) + 1 self._networkSegments.insert(index, nextSegment) else: From c505fda13caf369f6111479418d531f5180c7889 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 21 Aug 2025 14:19:43 +1200 Subject: [PATCH 02/61] Separated arm into branchium and antebrachium --- src/scaffoldmaker/annotation/body_terms.py | 8 ++- .../meshtypes/meshtype_3d_wholebody2.py | 58 ++++++++++++++----- src/scaffoldmaker/scaffolds.py | 4 +- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 80de066b..73c2b280 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -10,8 +10,14 @@ ("abdominopelvic cavity", "UBERON:0035819"), ("upper limb", "UBERON:0001460"), ("left upper limb", "UBERON:8300002", "FMA:7186"), + ("left brachium", ""), + ("left antebrachium", ""), + ("left hand", ""), ("left upper limb skin epidermis outer surface", "ILX:0796504"), ("right upper limb", "UBERON:8300001", "FMA:7185"), + ("right brachium", ""), + ("right antebrachium", ""), + ("right hand", ""), ("right upper limb skin epidermis outer surface", "ILX:0796503"), ("body", "UBERON:0000468", "ILX:0101370"), ("core", ""), @@ -25,7 +31,7 @@ ("lower limb", "UBERON:0000978"), ("left lower limb", "UBERON:8300004", "FMA:24981"), ("left lower limb skin epidermis outer surface", "ILX:0796506"), - ("right lower limb ", "UBERON:8300003", "FMA:24980"), + ("right lower limb", "UBERON:8300003", "FMA:24980"), ("right lower limb skin epidermis outer surface", "ILX:0796505"), ("foot", "ILX:0745450", "FMA:9664"), ("neck", "UBERON:0000974", "ILX:0733967"), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 374d7d9e..8c3afad6 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -37,14 +37,18 @@ def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = parameterSetName options["Structure"] = ( - "1-2-3-4," - "4-5-6.1," - "6.2-14-15-16-17-18-19,19-20," - "6.3-21-22-23-24-25-26,26-27," - "6.1-7-8-9," - "9-10-11-12-13.1," - "13.2-28-29-30-31-32,32-33-34," - "13.3-35-36-37-38-39,39-40-41") + "1-2-3-4," # Head + "4-5-6.1," # Neck + "6.2-14-15-16-17," # Left brachium + "17-18-19," # Left antebrachium + "19-20," # Left hand + "6.3-21-22-23-24," # Right brachium + "24-25-26," # Right antebrachium + "26-27," # Right hand + "6.1-7-8-9," # Thorax + "9-10-11-12-13.1," # Abdomen + "13.2-28-29-30-31-32,32-33-34," # Left lower limb + "13.3-35-36-37-38-39,39-40-41") # Left upper limb options["Define inner coordinates"] = True options["Head depth"] = 2.0 options["Head length"] = 2.2 @@ -215,6 +219,7 @@ def generateBaseMesh(cls, region, options): fieldmodule = region.getFieldmodule() mesh = fieldmodule.findMeshByDimension(1) + # set up element annotations bodyGroup = AnnotationGroup(region, get_body_term("body")) headGroup = AnnotationGroup(region, get_body_term("head")) @@ -222,18 +227,26 @@ def generateBaseMesh(cls, region, options): armGroup = AnnotationGroup(region, get_body_term("upper limb")) armToHandGroup = AnnotationGroup(region, ("arm to hand", "")) leftArmGroup = AnnotationGroup(region, get_body_term("left upper limb")) + leftBrachiumGroup = AnnotationGroup(region, get_body_term("left brachium")) + leftAntebrachiumGroup = AnnotationGroup(region, get_body_term("left antebrachium")) + leftHandGroup = AnnotationGroup(region, get_body_term("left hand")) rightArmGroup = AnnotationGroup(region, get_body_term("right upper limb")) + rightBrachiumGroup = AnnotationGroup(region, get_body_term("right brachium")) + rightAntebrachiumGroup = AnnotationGroup(region, get_body_term("right antebrachium")) + rightHandGroup = AnnotationGroup(region, get_body_term("right hand")) handGroup = AnnotationGroup(region, get_body_term("hand")) thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) abdomenGroup = AnnotationGroup(region, get_body_term("abdomen")) legGroup = AnnotationGroup(region, get_body_term("lower limb")) legToFootGroup = AnnotationGroup(region, ("leg to foot", "")) leftLegGroup = AnnotationGroup(region, get_body_term("left lower limb")) - rightLegGroup = AnnotationGroup(region, get_body_term("right lower limb ")) + rightLegGroup = AnnotationGroup(region, get_body_term("right lower limb")) footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, armToHandGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, + leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, + rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, legGroup, legToFootGroup, leftLegGroup, rightLegGroup, footGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 @@ -253,20 +266,37 @@ def generateBaseMesh(cls, region, options): elementIdentifier += 1 left = 0 right = 1 + brachiumElementsCount = 4 + antebrachiumElementsCount = 2 armToHandElementsCount = 6 handElementsCount = 1 armMeshGroup = armGroup.getMeshGroup(mesh) armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) + for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup - meshGroups = [bodyMeshGroup, armMeshGroup, armToHandMeshGroup, sideArmGroup.getMeshGroup(mesh)] - for e in range(armToHandElementsCount): + sideBrachiumGroup = leftBrachiumGroup if (side == left) else rightBrachiumGroup + sideAntebrachiumGroup = leftAntebrachiumGroup if (side == left) else rightAntebrachiumGroup + sideHandGroup = leftHandGroup if (side == left) else rightHandGroup + # Setup brachium elements + meshGroups = [bodyMeshGroup, armMeshGroup, armToHandMeshGroup, + sideArmGroup.getMeshGroup(mesh), sideBrachiumGroup.getMeshGroup(mesh)] + for e in range(brachiumElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + # Setup antebrachium elements + meshGroups = [bodyMeshGroup, armMeshGroup, armToHandMeshGroup, + sideArmGroup.getMeshGroup(mesh), sideAntebrachiumGroup.getMeshGroup(mesh)] + for e in range(antebrachiumElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - meshGroups = [bodyMeshGroup, armMeshGroup, handMeshGroup, sideArmGroup.getMeshGroup(mesh)] + # Setup hand elements + meshGroups = [bodyMeshGroup, armMeshGroup, handMeshGroup, sideHandGroup.getMeshGroup(mesh)] for e in range(handElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: @@ -274,12 +304,14 @@ def generateBaseMesh(cls, region, options): elementIdentifier += 1 thoraxElementsCount = 3 abdomenElementsCount = 4 + # Setup thorax elements meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] for e in range(thoraxElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + # Setup abdomen elements meshGroups = [bodyMeshGroup, abdomenGroup.getMeshGroup(mesh)] for e in range(abdomenElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -411,7 +443,7 @@ def generateBaseMesh(cls, region, options): armSide = [-sinArmAngle, cosArmAngle, 0.0] armFront = cross(armDirn, armSide) d1 = mult(armDirn, armScale) - # set leg versions 2 (left) and 3 (right) on leg junction node, and intermediate shoulder node + # set arm versions 2 (left) and 3 (right) on arm junction node, and intermediate shoulder node sd1 = interpolateLagrangeHermiteDerivative(sx, x, d1, 0.0) nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=armScale)[0:2] arcLengths = [getCubicHermiteArcLength(nx[i], nd1[i], nx[i + 1], nd1[i + 1]) for i in range(2)] diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 5922df37..88a8fae0 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -72,7 +72,7 @@ class Scaffolds(object): def __init__(self): self._allScaffoldTypes = [ MeshType_1d_bifurcationtree1, - MeshType_1d_uterus_network_layout1, + MeshType_1d_human_body_network_layout1, MeshType_1d_network_layout1, MeshType_1d_path1, MeshType_2d_plate1, @@ -128,7 +128,7 @@ def __init__(self): MeshType_3d_tubeseptum1, MeshType_3d_uterus1, MeshType_3d_wholebody1, - MeshType_3d_wholebody2 + MeshType_3d_wholebody2, ] self._allPrivateScaffoldTypes = [ MeshType_1d_human_body_network_layout1, From 784292bbaebf8a36825fcb62b3ebe4faae5819dc Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 28 Aug 2025 12:57:06 +1200 Subject: [PATCH 03/61] Separated arm node construction into brachium and antebrachium --- .../meshtypes/meshtype_3d_wholebody2.py | 122 ++++++++++++++---- 1 file changed, 95 insertions(+), 27 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 8c3afad6..fb8b8ab2 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,7 +1,7 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub +from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node @@ -39,10 +39,12 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Structure"] = ( "1-2-3-4," # Head "4-5-6.1," # Neck - "6.2-14-15-16-17," # Left brachium + "6.2-14-15," # Left shoulder + "15-16-17," # Left brachium "17-18-19," # Left antebrachium "19-20," # Left hand - "6.3-21-22-23-24," # Right brachium + "6.3-21-22," # Right shoulder + "22-23-24," # Right brachium "24-25-26," # Right antebrachium "26-27," # Right hand "6.1-7-8-9," # Thorax @@ -56,7 +58,10 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Neck length"] = 1.3 options["Shoulder drop"] = 1.0 options["Shoulder width"] = 4.5 - options["Arm lateral angle degrees"] = 10.0 + options["Left arm lateral angle degrees"] = 10.0 + options["Right arm lateral angle degrees"] = 10.0 + options["Left elbow lateral angle degrees"] = 10.0 + options["Right elbow lateral angle degrees"] = 10.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -92,7 +97,10 @@ def getOrderedOptionNames(cls): "Neck length", "Shoulder drop", "Shoulder width", - "Arm lateral angle degrees", + "Left arm lateral angle degrees", + "Right arm lateral angle degrees", + "Left elbow lateral angle degrees", + "Right elbow lateral angle degrees", "Arm length", "Arm top diameter", "Arm twist angle degrees", @@ -161,7 +169,10 @@ def checkOptions(cls, options): elif options[key] > 0.9: options[key] = 0.9 for key, angleRange in { - "Arm lateral angle degrees": (-60.0, 200.0), + "Left arm lateral angle degrees": (-60.0, 200.0), + "Right arm lateral angle degrees": (-60.0, 200.0), + "Left elbow lateral angle degrees": (-60.0, 200.0), + "Right elbow lateral angle degrees": (-60.0, 200.0), "Arm twist angle degrees": (-90.0, 90.0), "Leg lateral angle degrees": (-20.0, 60.0) }.items(): @@ -187,7 +198,10 @@ def generateBaseMesh(cls, region, options): neckLength = options["Neck length"] shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] - armAngleRadians = math.radians(options["Arm lateral angle degrees"]) + armLeftAngleRadians = math.radians(options["Left arm lateral angle degrees"]) + armRigthAngleRadians = math.radians(options["Right arm lateral angle degrees"]) + elbowLeftAngleRadians = math.radians(options["Left elbow lateral angle degrees"]) + elbowRigthAngleRadians = math.radians(options["Right elbow lateral angle degrees"]) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) @@ -273,7 +287,7 @@ def generateBaseMesh(cls, region, options): armMeshGroup = armGroup.getMeshGroup(mesh) armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) - + elbowJunctionNodeIdentifier = [] #0 for left, 1 for right for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup sideBrachiumGroup = leftBrachiumGroup if (side == left) else rightBrachiumGroup @@ -295,6 +309,7 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + elbowJunctionNodeIdentifier.append(elementIdentifier) # Setup hand elements meshGroups = [bodyMeshGroup, armMeshGroup, handMeshGroup, sideHandGroup.getMeshGroup(mesh)] for e in range(handElementsCount): @@ -421,22 +436,24 @@ def generateBaseMesh(cls, region, options): px = [abdomenStartX + abdomenLength, 0.0, 0.0] # arms - # rotate shoulder with arm, pivoting about shoulder drop below arm junction on network - # this has the realistic effect of shoulders becoming narrower with higher angles - # initial shoulder rotation with arm is negligible, hence: - shoulderRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) - # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation - shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) - shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians - armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) - nonHandArmLength = armLength - handLength - armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count - d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) - d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) for side in (left, right): + # Shoulder rotation + # rotate shoulder with arm, pivoting about shoulder drop below arm junction on network + # this has the realistic effect of shoulders becoming narrower with higher angles + # initial shoulder rotation with arm is negligible, hence: + armAngleRadians = armLeftAngleRadians if (side == left) else armRigthAngleRadians + shoulderRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) + # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation + shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) + shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians + nonHandArmLength = armLength - handLength + armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count + d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) + d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) armAngle = armAngleRadians if (side == left) else -armAngleRadians cosArmAngle = math.cos(armAngle) sinArmAngle = math.sin(armAngle) + armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) armStartY = (halfShoulderWidth if (side == left) else -halfShoulderWidth) * math.cos(shoulderAngleRadians) x = [armStartX, armStartY, 0.0] armDirn = [cosArmAngle, sinArmAngle, 0.0] @@ -485,10 +502,11 @@ def generateBaseMesh(cls, region, options): sid13 = mult(sd13, innerProportionDefault) innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sid12) innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sid13) - # main part of arm to wrist + # Arm twist elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) - for i in range(armToHandElementsCount - 1): + # Setting brachium coordinates + for i in range(brachiumElementsCount - 1): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -497,8 +515,8 @@ def generateBaseMesh(cls, region, options): halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: twistAngle = 0.0 - elif i == (armToHandElementsCount - 2): - twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians + # elif i == (brachiumElementsCount - 2): + # twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians else: twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i if twistAngle == 0.0: @@ -515,7 +533,53 @@ def generateBaseMesh(cls, region, options): mult(armSide, halfWidth * sinTwistAngle)) d12 = set_magnitude(d2, d12_mag) d13 = set_magnitude(d3, d13_mag) - if i < (armToHandElementsCount - 2): + # if i < (brachiumElementsCount - 2): + # d12 = add(d12, set_magnitude(d3, -halfThickness * elementTwistAngle)) + # d13 = add(d13, set_magnitude(d2, halfWidth * elementTwistAngle)) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + # Setting antebrachium coordinates + # Elbow rotation + elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians + elbowRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) + cosElbowAngle = math.cos(elbowAngleRadians) + sinElbowAngle = math.sin(elbowAngleRadians) + # Antebrachium start position + antebrachiumStartX = armStartX + d1[0] * (brachiumElementsCount - 1) + antebrachiumStartY = armStartY + d1[1] * (brachiumElementsCount - 1) + antebrachiumStartZ = d1[2] * (brachiumElementsCount - 1) + elbowPosition = [antebrachiumStartX, antebrachiumStartY, antebrachiumStartZ] + for i in range(antebrachiumElementsCount): + xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = [antebrachiumStartX + d1[0] * i, antebrachiumStartY + d1[1] * i, antebrachiumStartZ * i] + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + if i == (antebrachiumElementsCount - 1): + twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians + else: + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i + brachiumElementsCount - 1) + if twistAngle == 0.0: + d2 = mult(armSide, halfThickness) + d3 = mult(armFront, halfWidth) + d12 = mult(armSide, d12_mag) + d13 = mult(armFront, d13_mag) + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + d2 = sub(mult(armSide, halfThickness * cosTwistAngle), + mult(armFront, halfThickness * sinTwistAngle)) + d3 = add(mult(armFront, halfWidth * cosTwistAngle), + mult(armSide, halfWidth * sinTwistAngle)) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) + if i < (antebrachiumElementsCount - 1): d12 = add(d12, set_magnitude(d3, -halfThickness * elementTwistAngle)) d13 = add(d13, set_magnitude(d2, halfWidth * elementTwistAngle)) id2 = mult(d2, innerProportionDefault) @@ -525,10 +589,14 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # hand + # Elbow rotation + + # antebrachiumNodeIdentifiers = [] + # Hand twist assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) + # hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians @@ -960,7 +1028,7 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): annotationGroups, region, get_body_term("left lower limb skin epidermis outer surface")) leftLegSkinGroup.getMeshGroup(mesh2d).addElementsConditional( fieldmodule.createFieldAnd(leftLegGroup.getGroup(), is_exterior)) - rightLegGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right lower limb ")) + rightLegGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right lower limb")) rightLegSkinGroup = findOrCreateAnnotationGroupForTerm( annotationGroups, region, get_body_term("right lower limb skin epidermis outer surface")) rightLegSkinGroup.getMeshGroup(mesh2d).addElementsConditional( From caae694880b09efcf9b900d134a8ffb1760ab517 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 1 Oct 2025 15:11:09 +1300 Subject: [PATCH 04/61] Added body markers, second version of elbow coordinates --- .../meshtypes/meshtype_3d_wholebody2.py | 246 ++++++++++++++---- 1 file changed, 190 insertions(+), 56 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index fb8b8ab2..8288c508 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,12 +1,13 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude +from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude, axis_angle_to_rotation_matrix, matrix_vector_mult from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates +from cmlibs.utils.zinc.finiteelement import get_maximum_node_identifier from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node from scaffoldmaker.annotation.annotationgroup import ( - AnnotationGroup, findOrCreateAnnotationGroupForTerm, getAnnotationGroupForTerm) + AnnotationGroup, findOrCreateAnnotationGroupForTerm, getAnnotationGroupForTerm, evaluateAnnotationMarkerNearestMeshLocation) from scaffoldmaker.annotation.body_terms import get_body_term from scaffoldmaker.meshtypes.meshtype_1d_network_layout1 import MeshType_1d_network_layout1 from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base @@ -16,6 +17,7 @@ sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import BodyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData +# from scaffoldmaker.utils.human_network_layout import import math @@ -40,17 +42,18 @@ def getDefaultOptions(cls, parameterSetName="Default"): "1-2-3-4," # Head "4-5-6.1," # Neck "6.2-14-15," # Left shoulder - "15-16-17," # Left brachium - "17-18-19," # Left antebrachium + "15-16-17.1," # Left brachium + "17.2-18-19," # Left antebrachium "19-20," # Left hand "6.3-21-22," # Right shoulder - "22-23-24," # Right brachium - "24-25-26," # Right antebrachium + "22-23-24.1," # Right brachium + "24.2-25-26," # Right antebrachium "26-27," # Right hand "6.1-7-8-9," # Thorax "9-10-11-12-13.1," # Abdomen "13.2-28-29-30-31-32,32-33-34," # Left lower limb - "13.3-35-36-37-38-39,39-40-41") # Left upper limb + "13.3-35-36-37-38-39,39-40-41" # Left upper limb + ) options["Define inner coordinates"] = True options["Head depth"] = 2.0 options["Head length"] = 2.2 @@ -60,8 +63,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Shoulder width"] = 4.5 options["Left arm lateral angle degrees"] = 10.0 options["Right arm lateral angle degrees"] = 10.0 - options["Left elbow lateral angle degrees"] = 10.0 - options["Right elbow lateral angle degrees"] = 10.0 + options["Left elbow lateral angle degrees"] = 90.0 + options["Right elbow lateral angle degrees"] = 45.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -80,6 +83,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 + options["Left ankle lateral angle degrees"] = 60.0 + options["Right ankle lateral angle degrees"] = 90.0 options["Foot height"] = 1.25 options["Foot length"] = 2.5 options["Foot thickness"] = 0.3 @@ -123,6 +128,8 @@ def getOrderedOptionNames(cls): "Foot length", "Foot thickness", "Foot width", + "Left ankle lateral angle degrees", + "Right ankle lateral angle degrees", "Inner proportion default", "Inner proportion head" ] @@ -171,8 +178,10 @@ def checkOptions(cls, options): for key, angleRange in { "Left arm lateral angle degrees": (-60.0, 200.0), "Right arm lateral angle degrees": (-60.0, 200.0), - "Left elbow lateral angle degrees": (-60.0, 200.0), - "Right elbow lateral angle degrees": (-60.0, 200.0), + "Left elbow lateral angle degrees": (0.0, 150.0), + "Right elbow lateral angle degrees": (0.0, 150.0), + "Left ankle lateral angle degrees": (60.0, 140.0), + "Right ankle lateral angle degrees": (60.0, 140.0), "Arm twist angle degrees": (-90.0, 90.0), "Leg lateral angle degrees": (-20.0, 60.0) }.items(): @@ -220,13 +229,16 @@ def generateBaseMesh(cls, region, options): legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] + ankleLeftAngleRadians = math.radians( options["Left ankle lateral angle degrees"]) + ankleRigthAngleRadians = math.radians(options["Right ankle lateral angle degrees"]) footHeight = options["Foot height"] footLength = options["Foot length"] halfFootThickness = 0.5 * options["Foot thickness"] halfFootWidth = 0.5 * options["Foot width"] innerProportionDefault = options["Inner proportion default"] innerProportionHead = options["Inner proportion head"] - + # Store coordinates for kinematic tree markers + options['Kinematic tree'] = {} networkMesh = NetworkMesh(structure) networkMesh.create1DLayoutMesh(region) @@ -390,6 +402,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 armJunctionNodeIdentifier = nodeIdentifier + options['Kinematic tree']['thorax_top'] = x thoraxScale = thoraxLength / thoraxElementsCount thoraxStartX = headLength + neckLength @@ -417,7 +430,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12) nodeIdentifier += 1 - + abdomenScale = abdomenLength / abdomenElementsCount d2 = [0.0, halfTorsoWidth, 0.0] d3 = [0.0, 0.0, halfTorsoDepth] @@ -434,7 +447,7 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 legJunctionNodeIdentifier = nodeIdentifier - 1 px = [abdomenStartX + abdomenLength, 0.0, 0.0] - + options['Kinematic tree']['lumbar_body'] = px # arms for side in (left, right): # Shoulder rotation @@ -455,7 +468,8 @@ def generateBaseMesh(cls, region, options): sinArmAngle = math.sin(armAngle) armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) armStartY = (halfShoulderWidth if (side == left) else -halfShoulderWidth) * math.cos(shoulderAngleRadians) - x = [armStartX, armStartY, 0.0] + armStart = [armStartX, armStartY, 0.0] + x = armStart.copy() armDirn = [cosArmAngle, sinArmAngle, 0.0] armSide = [-sinArmAngle, cosArmAngle, 0.0] armFront = cross(armDirn, armSide) @@ -467,6 +481,8 @@ def generateBaseMesh(cls, region, options): sd2_list = [] sd3_list = [] sNodeIdentifiers = [] + side_label = 'l' if (side == left) else 'r' + options['Kinematic tree']['humerus_' + side_label] = nx[1] for i in range(2): sNodeIdentifiers.append(nodeIdentifier if (i > 0) else armJunctionNodeIdentifier) node = nodes.findNodeByIdentifier(sNodeIdentifiers[-1]) @@ -510,13 +526,12 @@ def generateBaseMesh(cls, region, options): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = [armStartX + d1[0] * i, armStartY + d1[1] * i, d1[2] * i] + # x = [armStartX + d1[0] * i, armStartY + d1[1] * i, d1[2] * i] + x = add(armStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: twistAngle = 0.0 - # elif i == (brachiumElementsCount - 2): - # twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians else: twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i if twistAngle == 0.0: @@ -533,9 +548,6 @@ def generateBaseMesh(cls, region, options): mult(armSide, halfWidth * sinTwistAngle)) d12 = set_magnitude(d2, d12_mag) d13 = set_magnitude(d3, d13_mag) - # if i < (brachiumElementsCount - 2): - # d12 = add(d12, set_magnitude(d3, -halfThickness * elementTwistAngle)) - # d13 = add(d13, set_magnitude(d2, halfWidth * elementTwistAngle)) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) @@ -545,38 +557,101 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # Setting antebrachium coordinates # Elbow rotation + # Updating frame of reference wrt rotation angle (using d2 as rotation axis) elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians - elbowRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) - cosElbowAngle = math.cos(elbowAngleRadians) - sinElbowAngle = math.sin(elbowAngleRadians) - # Antebrachium start position - antebrachiumStartX = armStartX + d1[0] * (brachiumElementsCount - 1) - antebrachiumStartY = armStartY + d1[1] * (brachiumElementsCount - 1) - antebrachiumStartZ = d1[2] * (brachiumElementsCount - 1) - elbowPosition = [antebrachiumStartX, antebrachiumStartY, antebrachiumStartZ] + rotationMatrixNode = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) + rotationMatrixD2 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) + antebrachiumDirn = matrix_vector_mult(rotationMatrixNode, armDirn) + antebrachiumSide = armSide + antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) + elbowFront = matrix_vector_mult(rotationMatrixD2, armFront) + # brachiumD1 = d1 + # antebrachiumD1 = mult(antebrachiumDirn, armScale) + d1 = mult(antebrachiumDirn, armScale) + # The elbow is positioned between the last brahcium and the first antebrachium node + # brachiumEnd = x.copy() + # elbowPosition = sub(x, mult(armDirn, 1.5*halfThickness)) + # elbowPosition = add(x, mult(antebrachiumDirn, 1.5*halfThickness)) + elbowPosition = x.copy() #node 17 + # Set alternative derivartives for the elbow node + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # halfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth + halfWidth # Custom half width for wider elbow + sd1 = mult(antebrachiumDirn, armScale) + # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) + if twistAngle == 0.0: + sd2 = mult(antebrachiumSide, halfThickness) + sd3 = mult(elbowFront, halfWidth) + sd12 = mult(antebrachiumSide, d12_mag) + sd13 = mult(elbowFront, d13_mag) + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + sd2 = sub(mult(antebrachiumSide, halfThickness * cosTwistAngle), + mult(elbowFront, halfThickness * sinTwistAngle)) + sd3 = add(mult(elbowFront, halfWidth * cosTwistAngle), + mult(antebrachiumSide, halfWidth * sinTwistAngle)) + sd12 = set_magnitude(d2, d12_mag) + sd13 = set_magnitude(d3, d13_mag) + if i < (antebrachiumElementsCount - 1): + sd12 = add(d12, set_magnitude(d3, -halfThickness * elementTwistAngle)) + + sd13 = add(d13, set_magnitude(d2, halfWidth * elementTwistAngle)) + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + sid12 = mult(sd12, innerProportionDefault) + sid13 = mult(sd13, innerProportionDefault) + version = 2 + # for field in (coordinates, innerCoordinates): + # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3, sid12, sid13) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sd2) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sid2) + + # antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) + # antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) + antebrachiumStart = add(elbowPosition, mult(antebrachiumDirn, armScale)) + # elbowNodes = [brachiumEnd, antebrachiumStart] + # math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius) + # Normalize d1 to get close to arc length + # elbowD1 = smoothCubicHermiteDerivativesLine( + # [brachiumEnd, antebrachiumStart], [brachiumD1, antebrachiumD1],fixAllDirections=True, fixStartDerivative=True + # )[1] + # d1 = antebrachiumD1 for i in range(antebrachiumElementsCount): xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = [antebrachiumStartX + d1[0] * i, antebrachiumStartY + d1[1] * i, antebrachiumStartZ * i] + # if i == 0: + # x = elbowPosition + # d1 = mult(antebrachiumDirn, 1.5* halfThickness) + # elif i == 1: + # x = antebrachiumStart + # d1 = antebrachiumD1 + # else: + # x = add(antebrachiumStart, mult(d1, i)) + + x = add(antebrachiumStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # halfWidth = halfWidth + anteFront = antebrachiumFront if i == (antebrachiumElementsCount - 1): twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians else: twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i + brachiumElementsCount - 1) if twistAngle == 0.0: - d2 = mult(armSide, halfThickness) - d3 = mult(armFront, halfWidth) - d12 = mult(armSide, d12_mag) - d13 = mult(armFront, d13_mag) + d2 = mult(antebrachiumSide, halfThickness) + d3 = mult(anteFront, halfWidth) + d12 = mult(antebrachiumSide, d12_mag) + d13 = mult(anteFront, d13_mag) else: cosTwistAngle = math.cos(twistAngle) sinTwistAngle = math.sin(twistAngle) - d2 = sub(mult(armSide, halfThickness * cosTwistAngle), - mult(armFront, halfThickness * sinTwistAngle)) - d3 = add(mult(armFront, halfWidth * cosTwistAngle), - mult(armSide, halfWidth * sinTwistAngle)) + d2 = sub(mult(antebrachiumSide, halfThickness * cosTwistAngle), + mult(anteFront, halfThickness * sinTwistAngle)) + d3 = add(mult(anteFront, halfWidth * cosTwistAngle), + mult(antebrachiumSide, halfWidth * sinTwistAngle)) d12 = set_magnitude(d2, d12_mag) d13 = set_magnitude(d3, d13_mag) if i < (antebrachiumElementsCount - 1): @@ -589,27 +664,24 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Elbow rotation - - # antebrachiumNodeIdentifiers = [] # Hand twist assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) # hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] - hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] + hx = add(x, mult(antebrachiumDirn, handLength)) hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians if twistAngle == 0.0: hd2 = set_magnitude(d2, halfHandThickness) - hd3 = [0.0, 0.0, halfHandWidth] + hd3 = set_magnitude(d3, halfHandWidth) else: cosTwistAngle = math.cos(twistAngle) sinTwistAngle = math.sin(twistAngle) - hd2 = sub(mult(armSide, halfHandThickness * cosTwistAngle), - mult(armFront, halfHandThickness * sinTwistAngle)) - hd3 = add(mult(armFront, halfHandWidth * cosTwistAngle), - mult(armSide, halfHandWidth * sinTwistAngle)) + hd2 = sub(mult(antebrachiumSide, halfHandThickness * cosTwistAngle), + mult(antebrachiumFront, halfHandThickness * sinTwistAngle)) + hd3 = add(mult(antebrachiumFront, halfHandWidth * cosTwistAngle), + mult(antebrachiumSide, halfHandWidth * sinTwistAngle)) hid2 = mult(hd2, innerProportionDefault) hid3 = mult(hd3, innerProportionDefault) setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) @@ -667,17 +739,40 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # foot - fx = [x, - add(add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)), - [0.0, 0.0, legBottomRadius]), - add(add(legStart, mult(legDirn, legLength - halfFootThickness)), - [0.0, 0.0, footLength - legBottomRadius])] + # fx = [x, + # add(add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)), + # [0.0, 0.0, legBottomRadius]), + # add(add(legStart, mult(legDirn, legLength - halfFootThickness)), + # [0.0, 0.0, footLength - legBottomRadius])] + anklePosition = add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)) + ankleAngleRadians = ankleLeftAngleRadians if (side == left) else ankleRigthAngleRadians + rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians - math.pi/2)) + # We need to create a 45* angle between d1 and d3 to avoid deformations + rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (math.pi/4)) + cosAnkleAngle = math.cos(ankleAngleRadians) + sinAnkleAngle = math.sin(ankleAngleRadians) + footd1 = [-cosAnkleAngle, 0, sinAnkleAngle] + footd2 = mult(legSide, halfFootWidth) + footd3 = matrix_vector_mult(rotationMatrixAnkle, footd1) + # footd2 = matrix_vector_mult(rotationMatrixAnkle, footd2) + fx = [ + x, + add(anklePosition, mult(footd1, legBottomRadius)), + add(anklePosition, mult(footd1, footLength - legBottomRadius)), + + ] + # fd1 = smoothCubicHermiteDerivativesLine( + # fx, [d1, [0.0, 0.0, 0.5 * footLength], [0.0, 0.0, 0.5 * footLength]], + # fixAllDirections=True, fixStartDerivative=True) + fd1 = [d1, mult(footd1, 0.5*footLength), mult(footd1, 0.5*footLength)] fd1 = smoothCubicHermiteDerivativesLine( - fx, [d1, [0.0, 0.0, 0.5 * footLength], [0.0, 0.0, 0.5 * footLength]], - fixAllDirections=True, fixStartDerivative=True) - fd2 = [d2, mult(legSide, halfFootWidth), mult(legSide, halfFootWidth)] + fx, fd1, fixAllDirections=True, fixStartDerivative=True + ) + fd2 = [d2, footd2, footd2] fd3 = [d3, - set_magnitude(sub(legFront, legDirn), + # set_magnitude(sub(legFront, legDirn), + # math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius), + set_magnitude(footd3, math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius), set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness)] fd12 = sub(fd2[2], fd2[1]) @@ -692,6 +787,8 @@ def generateBaseMesh(cls, region, options): fid3 = mult(fd3[i], innerProportionDefault) setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3, fid12, fid13) nodeIdentifier += 1 + side_label = 'l' if (side == left) else 'r' + options['Kinematic tree']['toes_' + side_label] = fx[i] return annotationGroups, networkMesh @@ -986,6 +1083,40 @@ def generateBaseMesh(cls, region, options): is_abdominal_cavity = fieldmodule.createFieldAnd(abdomenGroup.getGroup(), coreGroup.getGroup()) abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) + # Kinematic tree markers + + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + node_identifier = max(1, get_maximum_node_identifier(nodes) + 1) + # node = stickman_nodes.findNodeByIdentifier(6) + # mesh = fieldmodule.findMeshByDimension(meshDimension) + coordinates = find_or_create_field_coordinates(fieldmodule) + # fieldcache = fieldmodule.createFieldcache() + # fieldcache.setNode(node) + # marker_positions = coordinates.getNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, 3)[1] + # marker_names = 'thorax_stickman' + # const_coordinates_field = fieldmodule.createFieldConstant([0.0, 0.0, 0.0]) + stickman_markers = options['Body network layout']._scaffoldSettings['Kinematic tree'] + for marker_name, marker_position in stickman_markers.items(): + marker_group = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, (marker_name, ""), isMarker=True + ) + marker_group.createMarkerNode( + node_identifier, coordinates, marker_position + ) + # annotationGroups.append(marker_group) + # marker_locations = [] + # for marker_name, marker_position in zip([marker_names], [marker_positions]): + # const_coordinates_field.assignReal(fieldcache, marker_position) + # # marker_location = evaluateAnnotationMarkerNearestMeshLocation( + # # fieldmodule, fieldcache, marker_position, coordinates, mesh + # # ) + # marker_group = findOrCreateAnnotationGroupForTerm( + # annotationGroups, region, (marker_name, ""), isMarker=True + # ) + # markerNode = marker_group.createMarkerNode( + # node_identifier, coordinates, marker_position + # ) + # annotationGroups.append(marker_group) return annotationGroups, None @classmethod @@ -1077,6 +1208,9 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): is_spinal_cord = fieldmodule.createFieldAnd(is_core_shell, is_left_right_dorsal) spinalCordGroup.getMeshGroup(mesh1d).addElementsConditional(is_spinal_cord) + # Kinematic tree markers + + def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): """ From 33d30c1191c1dee631ee56f380ed070189a3ef8c Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 8 Oct 2025 10:59:21 +1300 Subject: [PATCH 05/61] Fixed a fold in the shoulder (also removed v2 from elbow nodes) --- src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 8288c508..4f6e1525 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -42,12 +42,12 @@ def getDefaultOptions(cls, parameterSetName="Default"): "1-2-3-4," # Head "4-5-6.1," # Neck "6.2-14-15," # Left shoulder - "15-16-17.1," # Left brachium - "17.2-18-19," # Left antebrachium + "15-16-" # Left brachium + "17-18-19," # Left antebrachium "19-20," # Left hand "6.3-21-22," # Right shoulder - "22-23-24.1," # Right brachium - "24.2-25-26," # Right antebrachium + "22-23-" # Right brachium + "24-25-26," # Right antebrachium "26-27," # Right hand "6.1-7-8-9," # Thorax "9-10-11-12-13.1," # Abdomen From 8f273a6fc939d485b15d4ea2e079472dca662cde Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 8 Oct 2025 11:06:17 +1300 Subject: [PATCH 06/61] Fixed a fold in the shoulder (also removed v2 from elbow nodes) --- .../meshtypes/meshtype_3d_wholebody2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 8288c508..320e942a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -41,13 +41,13 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Structure"] = ( "1-2-3-4," # Head "4-5-6.1," # Neck - "6.2-14-15," # Left shoulder - "15-16-17.1," # Left brachium - "17.2-18-19," # Left antebrachium + "6.2-14-" # Left shoulder + "15-16-" # Left brachium + "17-18-" # Left antebrachium "19-20," # Left hand - "6.3-21-22," # Right shoulder - "22-23-24.1," # Right brachium - "24.2-25-26," # Right antebrachium + "6.3-21-" # Right shoulder + "22-23-" # Right brachium + "24-25-" # Right antebrachium "26-27," # Right hand "6.1-7-8-9," # Thorax "9-10-11-12-13.1," # Abdomen From 74962b6eafc1671ff758a8e57f1d43e7635288a9 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 8 Oct 2025 11:08:01 +1300 Subject: [PATCH 07/61] Rewrote network layourt structure --- src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index e89b93f1..320e942a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -41,7 +41,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Structure"] = ( "1-2-3-4," # Head "4-5-6.1," # Neck -<<<<<<< HEAD "6.2-14-" # Left shoulder "15-16-" # Left brachium "17-18-" # Left antebrachium @@ -49,15 +48,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): "6.3-21-" # Right shoulder "22-23-" # Right brachium "24-25-" # Right antebrachium -======= - "6.2-14-15," # Left shoulder - "15-16-" # Left brachium - "17-18-19," # Left antebrachium - "19-20," # Left hand - "6.3-21-22," # Right shoulder - "22-23-" # Right brachium - "24-25-26," # Right antebrachium ->>>>>>> 33d30c1191c1dee631ee56f380ed070189a3ef8c "26-27," # Right hand "6.1-7-8-9," # Thorax "9-10-11-12-13.1," # Abdomen From b2efdd6add4d535c3ca6350390f876ac79e938e6 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 8 Oct 2025 14:39:40 +1300 Subject: [PATCH 08/61] Automated the construction of the wholebody network layout. --- .../meshtypes/meshtype_3d_wholebody2.py | 93 +++++-------- .../utils/human_network_layout.py | 123 ++++++++++++++++++ 2 files changed, 157 insertions(+), 59 deletions(-) create mode 100644 src/scaffoldmaker/utils/human_network_layout.py diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 320e942a..4948ce4c 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -17,6 +17,7 @@ sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import BodyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData +from scaffoldmaker.utils.human_network_layout import constructNetworkLayoutStructure, humanElementCounts # from scaffoldmaker.utils.human_network_layout import import math @@ -38,22 +39,23 @@ def getParameterSetNames(cls): def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = parameterSetName - options["Structure"] = ( - "1-2-3-4," # Head - "4-5-6.1," # Neck - "6.2-14-" # Left shoulder - "15-16-" # Left brachium - "17-18-" # Left antebrachium - "19-20," # Left hand - "6.3-21-" # Right shoulder - "22-23-" # Right brachium - "24-25-" # Right antebrachium - "26-27," # Right hand - "6.1-7-8-9," # Thorax - "9-10-11-12-13.1," # Abdomen - "13.2-28-29-30-31-32,32-33-34," # Left lower limb - "13.3-35-36-37-38-39,39-40-41" # Left upper limb - ) + options["Structure"] = constructNetworkLayoutStructure(humanElementCounts) + # ( + # "1-2-3-4," # Head + # "4-5-6.1," # Neck + # "6.2-14-" # Left shoulder + # "15-16-" # Left brachium + # "17-18-" # Left antebrachium + # "19-20," # Left hand + # "6.3-21-" # Right shoulder + # "22-23-" # Right brachium + # "24-25-" # Right antebrachium + # "26-27," # Right hand + # "6.1-7-8-9," # Thorax + # "9-10-11-12-13.1," # Abdomen + # "13.2-28-29-30-31-32,32-33-34," # Left lower limb + # "13.3-35-36-37-38-39,39-40-41" # Left upper limb + # ) options["Define inner coordinates"] = True options["Head depth"] = 2.0 options["Head length"] = 2.2 @@ -63,8 +65,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Shoulder width"] = 4.5 options["Left arm lateral angle degrees"] = 10.0 options["Right arm lateral angle degrees"] = 10.0 - options["Left elbow lateral angle degrees"] = 90.0 - options["Right elbow lateral angle degrees"] = 45.0 + options["Left elbow lateral angle degrees"] = 45.0 + options["Right elbow lateral angle degrees"] = 90.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -83,7 +85,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left ankle lateral angle degrees"] = 60.0 + options["Left ankle lateral angle degrees"] = 90.0 options["Right ankle lateral angle degrees"] = 90.0 options["Foot height"] = 1.25 options["Foot length"] = 2.5 @@ -568,49 +570,19 @@ def generateBaseMesh(cls, region, options): # brachiumD1 = d1 # antebrachiumD1 = mult(antebrachiumDirn, armScale) d1 = mult(antebrachiumDirn, armScale) - # The elbow is positioned between the last brahcium and the first antebrachium node + # The elbow is positioned between the last brachium and the first antebrachium node # brachiumEnd = x.copy() - # elbowPosition = sub(x, mult(armDirn, 1.5*halfThickness)) - # elbowPosition = add(x, mult(antebrachiumDirn, 1.5*halfThickness)) + elbowPosition = sub(x, mult(armDirn, 1.5*halfThickness)) + elbowPosition = add(x, mult(antebrachiumDirn, 1.5*halfThickness)) elbowPosition = x.copy() #node 17 # Set alternative derivartives for the elbow node - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # halfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth + halfWidth # Custom half width for wider elbow + # halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + elbowHalfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth # Custom half width for wider elbow sd1 = mult(antebrachiumDirn, armScale) - # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) - if twistAngle == 0.0: - sd2 = mult(antebrachiumSide, halfThickness) - sd3 = mult(elbowFront, halfWidth) - sd12 = mult(antebrachiumSide, d12_mag) - sd13 = mult(elbowFront, d13_mag) - else: - cosTwistAngle = math.cos(twistAngle) - sinTwistAngle = math.sin(twistAngle) - sd2 = sub(mult(antebrachiumSide, halfThickness * cosTwistAngle), - mult(elbowFront, halfThickness * sinTwistAngle)) - sd3 = add(mult(elbowFront, halfWidth * cosTwistAngle), - mult(antebrachiumSide, halfWidth * sinTwistAngle)) - sd12 = set_magnitude(d2, d12_mag) - sd13 = set_magnitude(d3, d13_mag) - if i < (antebrachiumElementsCount - 1): - sd12 = add(d12, set_magnitude(d3, -halfThickness * elementTwistAngle)) - - sd13 = add(d13, set_magnitude(d2, halfWidth * elementTwistAngle)) - sid2 = mult(sd2, innerProportionDefault) - sid3 = mult(sd3, innerProportionDefault) - sid12 = mult(sd12, innerProportionDefault) - sid13 = mult(sd13, innerProportionDefault) - version = 2 - # for field in (coordinates, innerCoordinates): - # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) - setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) - setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3, sid12, sid13) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sd2) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sid2) - - # antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) - # antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) - antebrachiumStart = add(elbowPosition, mult(antebrachiumDirn, armScale)) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) + antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) + antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) + # antebrachiumStart = add(elbowPosition, mult(antebrachiumDirn, armScale)) # elbowNodes = [brachiumEnd, antebrachiumStart] # math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius) # Normalize d1 to get close to arc length @@ -633,7 +605,7 @@ def generateBaseMesh(cls, region, options): x = add(antebrachiumStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + halfWidth = elbowHalfWidth if (i ==0) else xi * halfWristWidth + (1.0 - xi) * armTopRadius # halfWidth = halfWidth anteFront = antebrachiumFront if i == (antebrachiumElementsCount - 1): @@ -1124,6 +1096,7 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): """ Add face annotation groups from the highest dimension mesh. Must have defined faces and added subelements for highest dimension groups. + :param region: Zinc region containing model. :param options: Dict containing options. See getDefaultOptions(). :param annotationGroups: List of annotation groups for top-level elements. @@ -1215,6 +1188,7 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): """ Assign node field parameters x, d1, d2, d3 of field. + :param field: Field parameters to assign. :param fieldcache: Fieldcache with node set. :param x: Parameters to set for Node.VALUE_LABEL_VALUE. @@ -1238,6 +1212,7 @@ def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None) def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=None, d13=None): """ Assign node field parameters d1, d2, d3 of field. + :param field: Field to assign parameters of. :param fieldcache: Fieldcache with node set. :param version: Version of d1, d2, d3 >= 1. diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py new file mode 100644 index 00000000..3d30c39d --- /dev/null +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -0,0 +1,123 @@ +#%% +# Number of elements per segment. Used to calculate the number of nodes per segment. +humanElementCounts = { + 'headElementsCount': 3, + 'neckElementsCount': 2, + 'brachiumElementsCount': 4, + 'antebrachiumElementsCount': 2, + 'handElementsCount': 1, + 'thoraxElementsCount': 3, + 'abdomenElementsCount': 4, + 'upperLegElementsCount': 3, + 'lowerLegElementsCount': 2, + 'footElementsCount': 2 +} + + +def addNodesToLayout(nodeCount:int, networkLayout:str, nodeIdentifier:int, endSegment=False, version=0): + """ + Construct a segment of the human network node + + :param nodeCount: Number of nodes to add. + :param networkLayout: String containing the current network layout. + :param nodeIdentifier: Integer denoting the current node. + :param endSegment: If true, adds a comma at the end of the segment. + :param version: If > 0, adds version number on the last node of the segment. + :return networklayout: String containing the updated layout. + :return nodeIdentifier: The updated nodeIdentifier after adding the segment. + """ + for i in range(nodeCount): + networkLayout = networkLayout + str(nodeIdentifier) + if i == nodeCount - 1: + if endSegment: + if version == 0: + segmentConnector = ',' + else: + segmentConnector = '.' + str(version) + ',' + networkLayout = networkLayout + segmentConnector + else: + networkLayout = networkLayout + '-' + nodeIdentifier += 1 + else: + networkLayout = networkLayout + '-' + nodeIdentifier += 1 + return networkLayout, nodeIdentifier + +def constructNetworkLayoutStructure(humanElementCounts:dict): + """ + Construct the network layout of the human wholebody scaffold. + The network layout consists of the following segments: + head, neck, thorax, abdomen, right/left arm, right/left leg. + Arms are subdivided into brachium, antebrachium and hand. + Legs are subdivided into upper leg, lower leg and foot. + + :param humanElementCounts: Dictionary containing the number of elements + corresponding to each segment. + :return humanNetworkLayout: String containing the network layout + """ + humanNetWorkLayout = "" + nodeIdentifier = 1 + # Head + humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['headElementsCount'], + humanNetWorkLayout, nodeIdentifier, endSegment=True) + # Neck + humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['neckElementsCount'], + humanNetWorkLayout, nodeIdentifier, endSegment=True, version=1) + neckJointNode = nodeIdentifier + # Thorax + humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '.1-' + nodeIdentifier += 1 + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['thoraxElementsCount'], + humanNetWorkLayout, nodeIdentifier, endSegment=True) + # Abdomen + humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['abdomenElementsCount'], + humanNetWorkLayout, nodeIdentifier, endSegment=True, version=1) + pelvisNodeJoint = nodeIdentifier + # Arms + for i in range(2): + version = 2 if (i == 0) else 3 #Left is 2, right is 3 + humanNetWorkLayout = humanNetWorkLayout + str(neckJointNode) + '.' + str(version) + '-' + nodeIdentifier += 1 + # Brachium + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['brachiumElementsCount'], + humanNetWorkLayout, nodeIdentifier) + # Antebrachium + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['antebrachiumElementsCount'], + humanNetWorkLayout, nodeIdentifier) + # Hand + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['handElementsCount'], + humanNetWorkLayout, nodeIdentifier, endSegment=True) + #Legs + for i in range(2): + version = 2 if (i == 0) else 3 #Left is 2, right is 3 + humanNetWorkLayout = humanNetWorkLayout + str(pelvisNodeJoint) + '.' + str(version) + '-' + nodeIdentifier += 1 + # Upper leg + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['upperLegElementsCount'], + humanNetWorkLayout, nodeIdentifier) + # Lower leg + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['lowerLegElementsCount'], + humanNetWorkLayout, nodeIdentifier) + # Feet + humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanElementCounts['footElementsCount'], + humanNetWorkLayout, nodeIdentifier, endSegment=True) + return humanNetWorkLayout + +# humanNetWorkLayout.replace(',', ',\n').splitlines() + From baf48a32fe3160020f114703d06fba8b3683749d Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 8 Oct 2025 16:32:52 +1300 Subject: [PATCH 09/61] Added rough elbow coordinates --- .../meshtypes/meshtype_3d_wholebody2.py | 133 ++++++++++-------- 1 file changed, 75 insertions(+), 58 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 4948ce4c..030b47cb 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -40,22 +40,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = parameterSetName options["Structure"] = constructNetworkLayoutStructure(humanElementCounts) - # ( - # "1-2-3-4," # Head - # "4-5-6.1," # Neck - # "6.2-14-" # Left shoulder - # "15-16-" # Left brachium - # "17-18-" # Left antebrachium - # "19-20," # Left hand - # "6.3-21-" # Right shoulder - # "22-23-" # Right brachium - # "24-25-" # Right antebrachium - # "26-27," # Right hand - # "6.1-7-8-9," # Thorax - # "9-10-11-12-13.1," # Abdomen - # "13.2-28-29-30-31-32,32-33-34," # Left lower limb - # "13.3-35-36-37-38-39,39-40-41" # Left upper limb - # ) options["Define inner coordinates"] = True options["Head depth"] = 2.0 options["Head length"] = 2.2 @@ -278,30 +262,45 @@ def generateBaseMesh(cls, region, options): legGroup, legToFootGroup, leftLegGroup, rightLegGroup, footGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 - headElementsCount = 3 + headElementsCount = humanElementCounts['headElementsCount'] meshGroups = [bodyMeshGroup, headGroup.getMeshGroup(mesh)] for e in range(headElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - neckElementsCount = 2 + neckElementsCount = humanElementCounts['neckElementsCount'] meshGroups = [bodyMeshGroup, neckGroup.getMeshGroup(mesh)] for e in range(neckElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + thoraxElementsCount = humanElementCounts['thoraxElementsCount'] + abdomenElementsCount = humanElementCounts['abdomenElementsCount'] + # Setup thorax elements + meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] + for e in range(thoraxElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + # Setup abdomen elements + meshGroups = [bodyMeshGroup, abdomenGroup.getMeshGroup(mesh)] + for e in range(abdomenElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 left = 0 right = 1 - brachiumElementsCount = 4 - antebrachiumElementsCount = 2 - armToHandElementsCount = 6 - handElementsCount = 1 + brachiumElementsCount = humanElementCounts['brachiumElementsCount'] + antebrachiumElementsCount = humanElementCounts['antebrachiumElementsCount'] + handElementsCount = humanElementCounts['handElementsCount'] + armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount armMeshGroup = armGroup.getMeshGroup(mesh) armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) - elbowJunctionNodeIdentifier = [] #0 for left, 1 for right for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup sideBrachiumGroup = leftBrachiumGroup if (side == left) else rightBrachiumGroup @@ -323,7 +322,6 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - elbowJunctionNodeIdentifier.append(elementIdentifier) # Setup hand elements meshGroups = [bodyMeshGroup, armMeshGroup, handMeshGroup, sideHandGroup.getMeshGroup(mesh)] for e in range(handElementsCount): @@ -331,24 +329,10 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - thoraxElementsCount = 3 - abdomenElementsCount = 4 - # Setup thorax elements - meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] - for e in range(thoraxElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 - # Setup abdomen elements - meshGroups = [bodyMeshGroup, abdomenGroup.getMeshGroup(mesh)] - for e in range(abdomenElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 - legToFootElementsCount = 5 - footElementsCount = 2 + upperLegElementsCount = humanElementCounts['upperLegElementsCount'] + lowerLegElementsCount = humanElementCounts['lowerLegElementsCount'] + footElementsCount = humanElementCounts['footElementsCount'] + legToFootElementsCount = upperLegElementsCount + lowerLegElementsCount legMeshGroup = legGroup.getMeshGroup(mesh) legToFootMeshGroup = legToFootGroup.getMeshGroup(mesh) footMeshGroup = footGroup.getMeshGroup(mesh) @@ -524,11 +508,10 @@ def generateBaseMesh(cls, region, options): elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) # Setting brachium coordinates - for i in range(brachiumElementsCount - 1): + for i in range(brachiumElementsCount - 2): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - # x = [armStartX + d1[0] * i, armStartY + d1[1] * i, d1[2] * i] x = add(armStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius @@ -557,7 +540,6 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Setting antebrachium coordinates # Elbow rotation # Updating frame of reference wrt rotation angle (using d2 as rotation axis) elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians @@ -566,20 +548,55 @@ def generateBaseMesh(cls, region, options): antebrachiumDirn = matrix_vector_mult(rotationMatrixNode, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) + elbowDirn = matrix_vector_mult(rotationMatrixD2, armDirn) + elbowSide = armSide elbowFront = matrix_vector_mult(rotationMatrixD2, armFront) - # brachiumD1 = d1 - # antebrachiumD1 = mult(antebrachiumDirn, armScale) - d1 = mult(antebrachiumDirn, armScale) - # The elbow is positioned between the last brachium and the first antebrachium node - # brachiumEnd = x.copy() - elbowPosition = sub(x, mult(armDirn, 1.5*halfThickness)) - elbowPosition = add(x, mult(antebrachiumDirn, 1.5*halfThickness)) - elbowPosition = x.copy() #node 17 + """ + elbow node coordinates are setup independently + """ + # Node position + xi = (brachiumElementsCount - 1) / (armToHandElementsCount - 2) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + # elbowPosition = sub(x, mult(armDirn, 1.5*halfThickness)) + # elbowPosition = add(x, mult(antebrachiumDirn, 1.5*halfThickness)) + # elbowPosition = x.copy() + elbowPosition = add(x, mult(armDirn, 0.85*armScale)) + elbowPosition = add(elbowPosition, mult(antebrachiumDirn, 0.15*armScale)) + # Derivatives + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth + halfWidth + d1 = elbowDirn.copy() + if i == 0: + twistAngle = 0.0 + else: + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i + if twistAngle == 0.0: + d2 = mult(elbowSide, halfThickness) + d3 = mult(elbowFront, halfWidth) + d12 = mult(elbowSide, d12_mag) + d13 = mult(elbowFront, d13_mag) + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + d2 = sub(mult(elbowSide, halfThickness * cosTwistAngle), + mult(elbowFront, halfThickness * sinTwistAngle)) + d3 = add(mult(elbowFront, halfWidth * cosTwistAngle), + mult(elbowSide, halfWidth * sinTwistAngle)) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, elbowPosition, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, elbowPosition, d1, id2, id3, id12, id13) + nodeIdentifier += 1 # Set alternative derivartives for the elbow node # halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - elbowHalfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth # Custom half width for wider elbow - sd1 = mult(antebrachiumDirn, armScale) - field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) + # elbowHalfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth # Custom half width for wider elbow + # sd1 = mult(antebrachiumDirn, armScale) + # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) # antebrachiumStart = add(elbowPosition, mult(antebrachiumDirn, armScale)) @@ -589,7 +606,7 @@ def generateBaseMesh(cls, region, options): # elbowD1 = smoothCubicHermiteDerivativesLine( # [brachiumEnd, antebrachiumStart], [brachiumD1, antebrachiumD1],fixAllDirections=True, fixStartDerivative=True # )[1] - # d1 = antebrachiumD1 + d1 = matrix_vector_mult(rotationMatrixNode, armDirn) for i in range(antebrachiumElementsCount): xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) @@ -605,7 +622,7 @@ def generateBaseMesh(cls, region, options): x = add(antebrachiumStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - halfWidth = elbowHalfWidth if (i ==0) else xi * halfWristWidth + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius # halfWidth = halfWidth anteFront = antebrachiumFront if i == (antebrachiumElementsCount - 1): From 4c8deb5905ca3c9ddc0d7ea415bf486a758e64aa Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 8 Oct 2025 16:33:18 +1300 Subject: [PATCH 10/61] Corrected variable name on network layout --- .../utils/human_network_layout.py | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 3d30c39d..483ac5a6 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -55,69 +55,70 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): corresponding to each segment. :return humanNetworkLayout: String containing the network layout """ - humanNetWorkLayout = "" + humanNetworkLayout = "" nodeIdentifier = 1 # Head - humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '-' + humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['headElementsCount'], - humanNetWorkLayout, nodeIdentifier, endSegment=True) + humanNetworkLayout, nodeIdentifier, endSegment=True) # Neck - humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '-' + humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['neckElementsCount'], - humanNetWorkLayout, nodeIdentifier, endSegment=True, version=1) + humanNetworkLayout, nodeIdentifier, endSegment=True, version=1) neckJointNode = nodeIdentifier # Thorax - humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '.1-' + humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '.1-' nodeIdentifier += 1 - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['thoraxElementsCount'], - humanNetWorkLayout, nodeIdentifier, endSegment=True) + humanNetworkLayout, nodeIdentifier, endSegment=True) # Abdomen - humanNetWorkLayout = humanNetWorkLayout + str(nodeIdentifier) + '-' + humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['abdomenElementsCount'], - humanNetWorkLayout, nodeIdentifier, endSegment=True, version=1) + humanNetworkLayout, nodeIdentifier, endSegment=True, version=1) pelvisNodeJoint = nodeIdentifier # Arms for i in range(2): version = 2 if (i == 0) else 3 #Left is 2, right is 3 - humanNetWorkLayout = humanNetWorkLayout + str(neckJointNode) + '.' + str(version) + '-' + humanNetworkLayout = humanNetworkLayout + str(neckJointNode) + '.' + str(version) + '-' nodeIdentifier += 1 # Brachium - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['brachiumElementsCount'], - humanNetWorkLayout, nodeIdentifier) + humanNetworkLayout, nodeIdentifier) # Antebrachium - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['antebrachiumElementsCount'], - humanNetWorkLayout, nodeIdentifier) + humanNetworkLayout, nodeIdentifier) # Hand - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['handElementsCount'], - humanNetWorkLayout, nodeIdentifier, endSegment=True) + humanNetworkLayout, nodeIdentifier, endSegment=True) #Legs for i in range(2): version = 2 if (i == 0) else 3 #Left is 2, right is 3 - humanNetWorkLayout = humanNetWorkLayout + str(pelvisNodeJoint) + '.' + str(version) + '-' + humanNetworkLayout = humanNetworkLayout + str(pelvisNodeJoint) + '.' + str(version) + '-' nodeIdentifier += 1 # Upper leg - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['upperLegElementsCount'], - humanNetWorkLayout, nodeIdentifier) + humanNetworkLayout, nodeIdentifier) # Lower leg - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['lowerLegElementsCount'], - humanNetWorkLayout, nodeIdentifier) + humanNetworkLayout, nodeIdentifier) # Feet - humanNetWorkLayout, nodeIdentifier = addNodesToLayout( + humanNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['footElementsCount'], - humanNetWorkLayout, nodeIdentifier, endSegment=True) - return humanNetWorkLayout + humanNetworkLayout, nodeIdentifier, endSegment=True) + humanNetworkLayout = humanNetworkLayout[:-1] #Remove a comma at the end + return humanNetworkLayout # humanNetWorkLayout.replace(',', ',\n').splitlines() From 6caccca424460f39930aa61a47cb7b17fd8b0ebf Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 9 Oct 2025 11:26:13 +1300 Subject: [PATCH 11/61] Updated the construction of the network layout to fix a bug (the top of the thorax would show up as exterior in the scaffold) --- .../meshtypes/meshtype_3d_wholebody2.py | 55 ++++++-------- .../utils/human_network_layout.py | 74 ++++++++++--------- 2 files changed, 63 insertions(+), 66 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 030b47cb..3de59d4d 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -17,7 +17,7 @@ sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import BodyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData -from scaffoldmaker.utils.human_network_layout import constructNetworkLayoutStructure, humanElementCounts +from scaffoldmaker.utils.human_network_layout import constructNetworkLayoutStructure, humanElementCounts, constructNetworkLayoutStructure2 # from scaffoldmaker.utils.human_network_layout import import math @@ -276,22 +276,6 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - thoraxElementsCount = humanElementCounts['thoraxElementsCount'] - abdomenElementsCount = humanElementCounts['abdomenElementsCount'] - # Setup thorax elements - meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] - for e in range(thoraxElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 - # Setup abdomen elements - meshGroups = [bodyMeshGroup, abdomenGroup.getMeshGroup(mesh)] - for e in range(abdomenElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 left = 0 right = 1 brachiumElementsCount = humanElementCounts['brachiumElementsCount'] @@ -329,6 +313,22 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + thoraxElementsCount = humanElementCounts['thoraxElementsCount'] + abdomenElementsCount = humanElementCounts['abdomenElementsCount'] + # Setup thorax elements + meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] + for e in range(thoraxElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + # Setup abdomen elements + meshGroups = [bodyMeshGroup, abdomenGroup.getMeshGroup(mesh)] + for e in range(abdomenElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 upperLegElementsCount = humanElementCounts['upperLegElementsCount'] lowerLegElementsCount = humanElementCounts['lowerLegElementsCount'] footElementsCount = humanElementCounts['footElementsCount'] @@ -558,13 +558,11 @@ def generateBaseMesh(cls, region, options): xi = (brachiumElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - # elbowPosition = sub(x, mult(armDirn, 1.5*halfThickness)) - # elbowPosition = add(x, mult(antebrachiumDirn, 1.5*halfThickness)) - # elbowPosition = x.copy() elbowPosition = add(x, mult(armDirn, 0.85*armScale)) - elbowPosition = add(elbowPosition, mult(antebrachiumDirn, 0.15*armScale)) + elbowPosition = add(elbowPosition, mult(elbowDirn, 0.15*armScale)) # Derivatives halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth + halfWidth d1 = elbowDirn.copy() if i == 0: @@ -597,8 +595,10 @@ def generateBaseMesh(cls, region, options): # elbowHalfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth # Custom half width for wider elbow # sd1 = mult(antebrachiumDirn, armScale) # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) - antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) - antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) + # antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) + # antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) + antebrachiumStart = add(elbowPosition, mult(elbowDirn, 0.1*armScale)) + antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, 0.9*armScale)) # antebrachiumStart = add(elbowPosition, mult(antebrachiumDirn, armScale)) # elbowNodes = [brachiumEnd, antebrachiumStart] # math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius) @@ -611,15 +611,6 @@ def generateBaseMesh(cls, region, options): xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - # if i == 0: - # x = elbowPosition - # d1 = mult(antebrachiumDirn, 1.5* halfThickness) - # elif i == 1: - # x = antebrachiumStart - # d1 = antebrachiumD1 - # else: - # x = add(antebrachiumStart, mult(d1, i)) - x = add(antebrachiumStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 483ac5a6..a4dfcdf7 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -14,7 +14,7 @@ } -def addNodesToLayout(nodeCount:int, networkLayout:str, nodeIdentifier:int, endSegment=False, version=0): +def createSegment(nodeCount:int, segmentStart:str, nodeIdentifier:int, endSegment=False, version=0): """ Construct a segment of the human network node @@ -27,21 +27,21 @@ def addNodesToLayout(nodeCount:int, networkLayout:str, nodeIdentifier:int, endSe :return nodeIdentifier: The updated nodeIdentifier after adding the segment. """ for i in range(nodeCount): - networkLayout = networkLayout + str(nodeIdentifier) + segmentLayout = segmentStart + str(nodeIdentifier) if i == nodeCount - 1: if endSegment: if version == 0: segmentConnector = ',' else: segmentConnector = '.' + str(version) + ',' - networkLayout = networkLayout + segmentConnector + segmentLayout = segmentLayout + segmentConnector else: - networkLayout = networkLayout + '-' + segmentLayout = segmentLayout + '-' nodeIdentifier += 1 else: - networkLayout = networkLayout + '-' + segmentLayout = segmentLayout + '-' nodeIdentifier += 1 - return networkLayout, nodeIdentifier + return segmentLayout, nodeIdentifier def constructNetworkLayoutStructure(humanElementCounts:dict): """ @@ -55,70 +55,76 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): corresponding to each segment. :return humanNetworkLayout: String containing the network layout """ - humanNetworkLayout = "" nodeIdentifier = 1 # Head - humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '-' + headNetworkLayout = str(nodeIdentifier) + '-' nodeIdentifier += 1 - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + headNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['headElementsCount'], - humanNetworkLayout, nodeIdentifier, endSegment=True) + headNetworkLayout, nodeIdentifier, endSegment=True) # Neck - humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '-' + necknNetworkLayout = str(nodeIdentifier) + '-' nodeIdentifier += 1 - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + necknNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['neckElementsCount'], - humanNetworkLayout, nodeIdentifier, endSegment=True, version=1) + necknNetworkLayout, nodeIdentifier, endSegment=True, version=1) neckJointNode = nodeIdentifier # Thorax - humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '.1-' + thoraxNetworkLayout = str(nodeIdentifier) + '.1-' nodeIdentifier += 1 - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + thoraxNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['thoraxElementsCount'], - humanNetworkLayout, nodeIdentifier, endSegment=True) + thoraxNetworkLayout, nodeIdentifier, endSegment=True) # Abdomen - humanNetworkLayout = humanNetworkLayout + str(nodeIdentifier) + '-' + abdomenNetworkLayout = str(nodeIdentifier) + '-' nodeIdentifier += 1 - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + abdomenNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['abdomenElementsCount'], - humanNetworkLayout, nodeIdentifier, endSegment=True, version=1) + abdomenNetworkLayout, nodeIdentifier, endSegment=True, version=1) pelvisNodeJoint = nodeIdentifier # Arms + armNetworkLayouts = [] for i in range(2): version = 2 if (i == 0) else 3 #Left is 2, right is 3 - humanNetworkLayout = humanNetworkLayout + str(neckJointNode) + '.' + str(version) + '-' + armNetworkLayout = str(neckJointNode) + '.' + str(version) + '-' nodeIdentifier += 1 # Brachium - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + armNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['brachiumElementsCount'], - humanNetworkLayout, nodeIdentifier) + armNetworkLayout, nodeIdentifier) # Antebrachium - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + armNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['antebrachiumElementsCount'], - humanNetworkLayout, nodeIdentifier) + armNetworkLayout, nodeIdentifier) # Hand - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + armNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['handElementsCount'], - humanNetworkLayout, nodeIdentifier, endSegment=True) + armNetworkLayout, nodeIdentifier, endSegment=True) + armNetworkLayouts.append(armNetworkLayout) #Legs + legNetworkLayouts = [] for i in range(2): version = 2 if (i == 0) else 3 #Left is 2, right is 3 - humanNetworkLayout = humanNetworkLayout + str(pelvisNodeJoint) + '.' + str(version) + '-' + legNetworkLayout = str(pelvisNodeJoint) + '.' + str(version) + '-' nodeIdentifier += 1 # Upper leg - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + legNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['upperLegElementsCount'], - humanNetworkLayout, nodeIdentifier) + legNetworkLayout, nodeIdentifier) # Lower leg - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + legNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['lowerLegElementsCount'], - humanNetworkLayout, nodeIdentifier) + legNetworkLayout, nodeIdentifier) # Feet - humanNetworkLayout, nodeIdentifier = addNodesToLayout( + legNetworkLayout, nodeIdentifier = addNodesToLayout( humanElementCounts['footElementsCount'], - humanNetworkLayout, nodeIdentifier, endSegment=True) + legNetworkLayout, nodeIdentifier, endSegment=True) + legNetworkLayouts.append(legNetworkLayout) + humanNetworkLayout = headNetworkLayout + necknNetworkLayout + \ + armNetworkLayouts[0] + armNetworkLayouts[1] + thoraxNetworkLayout + \ + abdomenNetworkLayout + legNetworkLayouts[0] + legNetworkLayouts[1] humanNetworkLayout = humanNetworkLayout[:-1] #Remove a comma at the end return humanNetworkLayout -# humanNetWorkLayout.replace(',', ',\n').splitlines() +# humanNetworkLayout.replace(',', ',\n').splitlines() From f63863d519de6912318d64c67915f8fa70748825 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 9 Oct 2025 11:55:22 +1300 Subject: [PATCH 12/61] Fixed another bug with the construction of the network layout --- .../meshtypes/meshtype_3d_wholebody2.py | 2 +- .../utils/human_network_layout.py | 45 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 3de59d4d..f009249f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -17,7 +17,7 @@ sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import BodyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData -from scaffoldmaker.utils.human_network_layout import constructNetworkLayoutStructure, humanElementCounts, constructNetworkLayoutStructure2 +from scaffoldmaker.utils.human_network_layout import constructNetworkLayoutStructure, humanElementCounts # from scaffoldmaker.utils.human_network_layout import import math diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index a4dfcdf7..957402ba 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -14,7 +14,7 @@ } -def createSegment(nodeCount:int, segmentStart:str, nodeIdentifier:int, endSegment=False, version=0): +def createSegment(nodeCount:int, networkLayout:str, nodeIdentifier:int, endSegment=False, version=0): """ Construct a segment of the human network node @@ -27,21 +27,21 @@ def createSegment(nodeCount:int, segmentStart:str, nodeIdentifier:int, endSegmen :return nodeIdentifier: The updated nodeIdentifier after adding the segment. """ for i in range(nodeCount): - segmentLayout = segmentStart + str(nodeIdentifier) + networkLayout = networkLayout + str(nodeIdentifier) if i == nodeCount - 1: if endSegment: if version == 0: segmentConnector = ',' else: segmentConnector = '.' + str(version) + ',' - segmentLayout = segmentLayout + segmentConnector + networkLayout = networkLayout + segmentConnector else: - segmentLayout = segmentLayout + '-' + networkLayout = networkLayout + '-' nodeIdentifier += 1 else: - segmentLayout = segmentLayout + '-' + networkLayout = networkLayout + '-' nodeIdentifier += 1 - return segmentLayout, nodeIdentifier + return networkLayout, nodeIdentifier def constructNetworkLayoutStructure(humanElementCounts:dict): """ @@ -59,72 +59,71 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): # Head headNetworkLayout = str(nodeIdentifier) + '-' nodeIdentifier += 1 - headNetworkLayout, nodeIdentifier = addNodesToLayout( + headNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['headElementsCount'], headNetworkLayout, nodeIdentifier, endSegment=True) # Neck necknNetworkLayout = str(nodeIdentifier) + '-' nodeIdentifier += 1 - necknNetworkLayout, nodeIdentifier = addNodesToLayout( + necknNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['neckElementsCount'], necknNetworkLayout, nodeIdentifier, endSegment=True, version=1) neckJointNode = nodeIdentifier # Thorax thoraxNetworkLayout = str(nodeIdentifier) + '.1-' nodeIdentifier += 1 - thoraxNetworkLayout, nodeIdentifier = addNodesToLayout( + thoraxNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['thoraxElementsCount'], thoraxNetworkLayout, nodeIdentifier, endSegment=True) # Abdomen abdomenNetworkLayout = str(nodeIdentifier) + '-' nodeIdentifier += 1 - abdomenNetworkLayout, nodeIdentifier = addNodesToLayout( + abdomenNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['abdomenElementsCount'], abdomenNetworkLayout, nodeIdentifier, endSegment=True, version=1) pelvisNodeJoint = nodeIdentifier # Arms - armNetworkLayouts = [] + arms = [] for i in range(2): version = 2 if (i == 0) else 3 #Left is 2, right is 3 armNetworkLayout = str(neckJointNode) + '.' + str(version) + '-' nodeIdentifier += 1 # Brachium - armNetworkLayout, nodeIdentifier = addNodesToLayout( + armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['brachiumElementsCount'], armNetworkLayout, nodeIdentifier) # Antebrachium - armNetworkLayout, nodeIdentifier = addNodesToLayout( + armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['antebrachiumElementsCount'], armNetworkLayout, nodeIdentifier) # Hand - armNetworkLayout, nodeIdentifier = addNodesToLayout( + armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['handElementsCount'], armNetworkLayout, nodeIdentifier, endSegment=True) - armNetworkLayouts.append(armNetworkLayout) + arms.append(armNetworkLayout) #Legs - legNetworkLayouts = [] + legs = [] for i in range(2): version = 2 if (i == 0) else 3 #Left is 2, right is 3 legNetworkLayout = str(pelvisNodeJoint) + '.' + str(version) + '-' nodeIdentifier += 1 # Upper leg - legNetworkLayout, nodeIdentifier = addNodesToLayout( + legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['upperLegElementsCount'], legNetworkLayout, nodeIdentifier) # Lower leg - legNetworkLayout, nodeIdentifier = addNodesToLayout( + legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['lowerLegElementsCount'], legNetworkLayout, nodeIdentifier) # Feet - legNetworkLayout, nodeIdentifier = addNodesToLayout( + legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['footElementsCount'], legNetworkLayout, nodeIdentifier, endSegment=True) - legNetworkLayouts.append(legNetworkLayout) - humanNetworkLayout = headNetworkLayout + necknNetworkLayout + \ - armNetworkLayouts[0] + armNetworkLayouts[1] + thoraxNetworkLayout + \ - abdomenNetworkLayout + legNetworkLayouts[0] + legNetworkLayouts[1] + legs.append(legNetworkLayout) + humanNetworkLayout = headNetworkLayout + necknNetworkLayout + arms[0] + arms[1] + thoraxNetworkLayout + abdomenNetworkLayout + legs[0] + legs[1] humanNetworkLayout = humanNetworkLayout[:-1] #Remove a comma at the end return humanNetworkLayout +constructNetworkLayoutStructure(humanElementCounts).replace(',', ',\n').splitlines() # humanNetworkLayout.replace(',', ',\n').splitlines() From a3af6bc57d3b01aeaaaf9da030af164a0a78412e Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 9 Oct 2025 15:23:56 +1300 Subject: [PATCH 13/61] finishing setup of elbow coordinates --- .../meshtypes/meshtype_3d_wholebody2.py | 59 ++++++++----------- 1 file changed, 25 insertions(+), 34 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index f009249f..634e94ed 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -59,7 +59,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Hand length"] = 2.0 options["Hand thickness"] = 0.2 options["Hand width"] = 1.0 - options["Thorax length"] = 2.5 + options["Thorax length"] = 2.5 options["Abdomen length"] = 3.0 options["Torso depth"] = 2.5 options["Torso width"] = 3.2 @@ -540,8 +540,9 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Elbow rotation # Updating frame of reference wrt rotation angle (using d2 as rotation axis) + # Nodes in the antebrachium are rotated acoording to the elbow angle + # The special elbow node is rotated by half that angle, to give a smoother transition elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians rotationMatrixNode = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) rotationMatrixD2 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) @@ -551,20 +552,22 @@ def generateBaseMesh(cls, region, options): elbowDirn = matrix_vector_mult(rotationMatrixD2, armDirn) elbowSide = armSide elbowFront = matrix_vector_mult(rotationMatrixD2, armFront) - """ - elbow node coordinates are setup independently - """ - # Node position + # Elbow node field parameters are allocated separately from the rest of the brachium xi = (brachiumElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - elbowPosition = add(x, mult(armDirn, 0.85*armScale)) - elbowPosition = add(elbowPosition, mult(elbowDirn, 0.15*armScale)) - # Derivatives + # Ideally, the elbow node has the same d1 direction as the rest of the brachium + # However, doing so causes a distortion in the network layout + # This node is also moved 'forward' in the elbow direction (see above) + # The 0.8/0.2 values were chosen by visual inspection of the scaffold + elbowPosition = add(x, mult(armDirn, 0.8*armScale)) + elbowPosition = add(elbowPosition, mult(elbowDirn, 0.2*armScale)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # The elbow node uses a special width value during the transition + # Which 'fattens' the scaffold around the elbow depending on the level of rotation halfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth + halfWidth - d1 = elbowDirn.copy() + d1 = mult(elbowDirn, armScale) if i == 0: twistAngle = 0.0 else: @@ -590,23 +593,13 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, elbowPosition, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, elbowPosition, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Set alternative derivartives for the elbow node - # halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # elbowHalfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth # Custom half width for wider elbow - # sd1 = mult(antebrachiumDirn, armScale) - # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, elbowPosition) - # antebrachiumStart = add(elbowPosition, mult(armDirn, 1.5* halfThickness)) - # antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, armScale - 1.5* halfThickness)) - antebrachiumStart = add(elbowPosition, mult(elbowDirn, 0.1*armScale)) - antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, 0.9*armScale)) - # antebrachiumStart = add(elbowPosition, mult(antebrachiumDirn, armScale)) - # elbowNodes = [brachiumEnd, antebrachiumStart] - # math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius) - # Normalize d1 to get close to arc length - # elbowD1 = smoothCubicHermiteDerivativesLine( - # [brachiumEnd, antebrachiumStart], [brachiumD1, antebrachiumD1],fixAllDirections=True, fixStartDerivative=True - # )[1] - d1 = matrix_vector_mult(rotationMatrixNode, armDirn) + # Antebrachium nodes start below the elbow position + # As with the elbow node, the first antebrachium node is position + # via a linear combination of the elbow and antebrachium d1 + # The values were chosen to produce the smoothest curve in the network layout. + antebrachiumStart = add(elbowPosition, mult(elbowDirn, 0.5*armScale)) + antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, 0.5*armScale)) + d1 = mult(antebrachiumDirn, armScale) for i in range(antebrachiumElementsCount): xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) @@ -614,23 +607,21 @@ def generateBaseMesh(cls, region, options): x = add(antebrachiumStart, mult(d1, i)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # halfWidth = halfWidth - anteFront = antebrachiumFront if i == (antebrachiumElementsCount - 1): twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians else: twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i + brachiumElementsCount - 1) if twistAngle == 0.0: d2 = mult(antebrachiumSide, halfThickness) - d3 = mult(anteFront, halfWidth) + d3 = mult(antebrachiumFront, halfWidth) d12 = mult(antebrachiumSide, d12_mag) - d13 = mult(anteFront, d13_mag) + d13 = mult(antebrachiumFront, d13_mag) else: cosTwistAngle = math.cos(twistAngle) sinTwistAngle = math.sin(twistAngle) d2 = sub(mult(antebrachiumSide, halfThickness * cosTwistAngle), - mult(anteFront, halfThickness * sinTwistAngle)) - d3 = add(mult(anteFront, halfWidth * cosTwistAngle), + mult(antebrachiumFront, halfThickness * sinTwistAngle)) + d3 = add(mult(antebrachiumFront, halfWidth * cosTwistAngle), mult(antebrachiumSide, halfWidth * sinTwistAngle)) d12 = set_magnitude(d2, d12_mag) d13 = set_magnitude(d3, d13_mag) @@ -813,7 +804,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 - options["Number of elements along arm to hand"] = 5 + options["Number of elements along arm to hand"] = 6 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 4 options["Number of elements along foot"] = 2 From 7538b0b9ad6346b4c68582b7fcb1d6b5bf72eb1c Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 10 Oct 2025 11:17:11 +1300 Subject: [PATCH 14/61] Separated the leg and foot segments in the network layout --- .../meshtypes/meshtype_3d_wholebody2.py | 24 +++++++------------ .../utils/human_network_layout.py | 5 ++-- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 634e94ed..9505436c 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -560,8 +560,9 @@ def generateBaseMesh(cls, region, options): # However, doing so causes a distortion in the network layout # This node is also moved 'forward' in the elbow direction (see above) # The 0.8/0.2 values were chosen by visual inspection of the scaffold - elbowPosition = add(x, mult(armDirn, 0.8*armScale)) - elbowPosition = add(elbowPosition, mult(elbowDirn, 0.2*armScale)) + d1 = add(mult(armDirn,0.8), mult(elbowDirn,0.2)) + d1 = set_magnitude(d1, armScale) + elbowPosition = add(x, d1) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius # The elbow node uses a special width value during the transition @@ -594,11 +595,14 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, elbowPosition, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Antebrachium nodes start below the elbow position - # As with the elbow node, the first antebrachium node is position + # As with the elbow node, the first antebrachium node is positioned # via a linear combination of the elbow and antebrachium d1 # The values were chosen to produce the smoothest curve in the network layout. - antebrachiumStart = add(elbowPosition, mult(elbowDirn, 0.5*armScale)) - antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, 0.5*armScale)) + d1 = add(mult(elbowDirn,0.5), mult(antebrachiumDirn,0.5)) + d1 = set_magnitude(d1, armScale) + antebrachiumStart = add(elbowPosition, d1) + # antebrachiumStart = add(elbowPosition, mult(elbowDirn, 0.5*armScale)) + # antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, 0.5*armScale)) d1 = mult(antebrachiumDirn, armScale) for i in range(antebrachiumElementsCount): xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) @@ -639,7 +643,6 @@ def generateBaseMesh(cls, region, options): assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - # hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] hx = add(x, mult(antebrachiumDirn, handLength)) hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians @@ -658,7 +661,6 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) setNodeFieldParameters(innerCoordinates, fieldcache, hx, hd1, hid2, hid3) nodeIdentifier += 1 - # legs legStartX = abdomenStartX + abdomenLength + pelvisDrop nonFootLegLength = legLength - footHeight @@ -710,11 +712,6 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # foot - # fx = [x, - # add(add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)), - # [0.0, 0.0, legBottomRadius]), - # add(add(legStart, mult(legDirn, legLength - halfFootThickness)), - # [0.0, 0.0, footLength - legBottomRadius])] anklePosition = add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)) ankleAngleRadians = ankleLeftAngleRadians if (side == left) else ankleRigthAngleRadians rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians - math.pi/2)) @@ -1180,9 +1177,6 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): is_spinal_cord = fieldmodule.createFieldAnd(is_core_shell, is_left_right_dorsal) spinalCordGroup.getMeshGroup(mesh1d).addElementsConditional(is_spinal_cord) - # Kinematic tree markers - - def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): """ diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 957402ba..da23a09d 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -114,8 +114,10 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): # Lower leg legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['lowerLegElementsCount'], - legNetworkLayout, nodeIdentifier) + legNetworkLayout, nodeIdentifier, endSegment=True) # Feet + legNetworkLayout = legNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['footElementsCount'], legNetworkLayout, nodeIdentifier, endSegment=True) @@ -125,5 +127,4 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): return humanNetworkLayout constructNetworkLayoutStructure(humanElementCounts).replace(',', ',\n').splitlines() -# humanNetworkLayout.replace(',', ',\n').splitlines() From 13e6d4ea35ec264e0878e892ea666f6d856e4a03 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 10 Oct 2025 12:46:35 +1300 Subject: [PATCH 15/61] Tweaked elbow width and separated the arm into two segments in the network layout --- .../meshtypes/meshtype_3d_wholebody2.py | 42 ++++++++++--------- .../utils/human_network_layout.py | 8 +++- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 9505436c..7b1e195e 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -552,23 +552,35 @@ def generateBaseMesh(cls, region, options): elbowDirn = matrix_vector_mult(rotationMatrixD2, armDirn) elbowSide = armSide elbowFront = matrix_vector_mult(rotationMatrixD2, armFront) - # Elbow node field parameters are allocated separately from the rest of the brachium - xi = (brachiumElementsCount - 1) / (armToHandElementsCount - 2) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) # Ideally, the elbow node has the same d1 direction as the rest of the brachium # However, doing so causes a distortion in the network layout # This node is also moved 'forward' in the elbow direction (see above) # The 0.8/0.2 values were chosen by visual inspection of the scaffold - d1 = add(mult(armDirn,0.8), mult(elbowDirn,0.2)) - d1 = set_magnitude(d1, armScale) - elbowPosition = add(x, d1) + elbowPosition = add(mult(armDirn,0.8), mult(elbowDirn,0.2)) + elbowPosition = set_magnitude(elbowPosition, armScale) + elbowPosition = add(x, elbowPosition) + elbowd1 = mult(elbowDirn, armScale) + # Antebrachium nodes start below the elbow position + # As with the elbow node, the first antebrachium node is positioned + # via a linear combination of the elbow and antebrachium d1 + # The values were chosen to produce the smoothest curve in the network layout. + antebrachiumStart = add(mult(elbowDirn,0.5), mult(antebrachiumDirn,0.5)) + antebrachiumStart = set_magnitude(antebrachiumStart, armScale) + antebrachiumStart = add(elbowPosition, antebrachiumStart) + # Smooth d1 at the elbow + d1 = smoothCubicHermiteDerivativesLine( + [x, elbowPosition], + [d1, elbowd1], fixAllDirections=True, fixStartDerivative=True + )[1] + # Elbow node field parameters are allocated separately from the rest of the brachium + xi = (brachiumElementsCount - 2) / (armToHandElementsCount - 2) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius # The elbow node uses a special width value during the transition # Which 'fattens' the scaffold around the elbow depending on the level of rotation - halfWidth = math.sin(elbowAngleRadians)*(math.sqrt(2)-1)*halfWidth + halfWidth - d1 = mult(elbowDirn, armScale) + halfWidth = math.sin(elbowAngleRadians)*(0.25)*halfWidth + halfWidth if i == 0: twistAngle = 0.0 else: @@ -594,15 +606,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, elbowPosition, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, elbowPosition, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Antebrachium nodes start below the elbow position - # As with the elbow node, the first antebrachium node is positioned - # via a linear combination of the elbow and antebrachium d1 - # The values were chosen to produce the smoothest curve in the network layout. - d1 = add(mult(elbowDirn,0.5), mult(antebrachiumDirn,0.5)) - d1 = set_magnitude(d1, armScale) - antebrachiumStart = add(elbowPosition, d1) - # antebrachiumStart = add(elbowPosition, mult(elbowDirn, 0.5*armScale)) - # antebrachiumStart = add(antebrachiumStart, mult(antebrachiumDirn, 0.5*armScale)) + # Change d1 to the antebrachium direction d1 = mult(antebrachiumDirn, armScale) for i in range(antebrachiumElementsCount): xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) @@ -801,7 +805,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 - options["Number of elements along arm to hand"] = 6 + options["Number of elements along arm to hand"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 4 options["Number of elements along foot"] = 2 diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index da23a09d..0ea7b50c 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -91,12 +91,16 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): # Brachium armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['brachiumElementsCount'], - armNetworkLayout, nodeIdentifier) + armNetworkLayout, nodeIdentifier, endSegment=True) # Antebrachium + armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['antebrachiumElementsCount'], - armNetworkLayout, nodeIdentifier) + armNetworkLayout, nodeIdentifier, endSegment=True) # Hand + armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['handElementsCount'], armNetworkLayout, nodeIdentifier, endSegment=True) From e9cb318b72a73e6610789367fdd509fde2b0d128 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 13 Oct 2025 15:22:10 +1300 Subject: [PATCH 16/61] Added an extra node to the elbow --- .../meshtypes/meshtype_3d_wholebody2.py | 131 ++++++++++-------- .../utils/human_network_layout.py | 3 +- 2 files changed, 75 insertions(+), 59 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 7b1e195e..8f676e9f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -279,9 +279,10 @@ def generateBaseMesh(cls, region, options): left = 0 right = 1 brachiumElementsCount = humanElementCounts['brachiumElementsCount'] + elbowElementsCount = humanElementCounts['elbowElementsCount'] antebrachiumElementsCount = humanElementCounts['antebrachiumElementsCount'] handElementsCount = humanElementCounts['handElementsCount'] - armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount + armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount - elbowElementsCount + 1 armMeshGroup = armGroup.getMeshGroup(mesh) armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) @@ -508,7 +509,7 @@ def generateBaseMesh(cls, region, options): elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) # Setting brachium coordinates - for i in range(brachiumElementsCount - 2): + for i in range(brachiumElementsCount - elbowElementsCount - 1): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -545,67 +546,83 @@ def generateBaseMesh(cls, region, options): # The special elbow node is rotated by half that angle, to give a smoother transition elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians rotationMatrixNode = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) - rotationMatrixD2 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) + rotationMatrixD2 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/3) antebrachiumDirn = matrix_vector_mult(rotationMatrixNode, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) elbowDirn = matrix_vector_mult(rotationMatrixD2, armDirn) elbowSide = armSide - elbowFront = matrix_vector_mult(rotationMatrixD2, armFront) + elbowd3 = [] + elbowd3.append(matrix_vector_mult(rotationMatrixD2, armFront)) + elbowd3.append(matrix_vector_mult(rotationMatrixD2, elbowd3[-1])) # Ideally, the elbow node has the same d1 direction as the rest of the brachium # However, doing so causes a distortion in the network layout # This node is also moved 'forward' in the elbow direction (see above) # The 0.8/0.2 values were chosen by visual inspection of the scaffold - elbowPosition = add(mult(armDirn,0.8), mult(elbowDirn,0.2)) - elbowPosition = set_magnitude(elbowPosition, armScale) - elbowPosition = add(x, elbowPosition) - elbowd1 = mult(elbowDirn, armScale) + for field in (coordinates, innerCoordinates): + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, mult(d1, 0.5)) + elbowPosition = [] + elbowd1 = [] + elbowDir = add(mult(armDirn,0.99), mult(elbowDirn,0.01)) + elbowDir = set_magnitude(elbowDir, 0.5*armScale) + elbowPosition.append(add(x, elbowDir)) + elbowDir = add(mult(armDirn,0.01), mult(elbowDirn,0.99)) + elbowDir = set_magnitude(elbowDir, 0.5*armScale) + elbowPosition.append(add(elbowPosition[-1], elbowDir)) + + elbowd1.append(mult(elbowDirn, 0.5*armScale)) + elbowd1.append(mult(antebrachiumDirn, 1*armScale)) + # elbowPosition # Antebrachium nodes start below the elbow position # As with the elbow node, the first antebrachium node is positioned # via a linear combination of the elbow and antebrachium d1 # The values were chosen to produce the smoothest curve in the network layout. - antebrachiumStart = add(mult(elbowDirn,0.5), mult(antebrachiumDirn,0.5)) - antebrachiumStart = set_magnitude(antebrachiumStart, armScale) - antebrachiumStart = add(elbowPosition, antebrachiumStart) + # antebrachiumStart = add(mult(elbowDirn,0.5), mult(antebrachiumDirn,0.5)) + antebrachiumStart = set_magnitude(antebrachiumDirn, 1*armScale) + antebrachiumStart = add(elbowPosition[-1], antebrachiumStart) # Smooth d1 at the elbow - d1 = smoothCubicHermiteDerivativesLine( - [x, elbowPosition], - [d1, elbowd1], fixAllDirections=True, fixStartDerivative=True - )[1] + # d1 = smoothCubicHermiteDerivativesLine( + # [x, elbowPosition], + # [d1, elbowd1], fixAllDirections=True, fixStartDerivative=True + # )[1] # Elbow node field parameters are allocated separately from the rest of the brachium - xi = (brachiumElementsCount - 2) / (armToHandElementsCount - 2) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # The elbow node uses a special width value during the transition - # Which 'fattens' the scaffold around the elbow depending on the level of rotation - halfWidth = math.sin(elbowAngleRadians)*(0.25)*halfWidth + halfWidth - if i == 0: - twistAngle = 0.0 - else: - twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i - if twistAngle == 0.0: - d2 = mult(elbowSide, halfThickness) - d3 = mult(elbowFront, halfWidth) - d12 = mult(elbowSide, d12_mag) - d13 = mult(elbowFront, d13_mag) - else: - cosTwistAngle = math.cos(twistAngle) - sinTwistAngle = math.sin(twistAngle) - d2 = sub(mult(elbowSide, halfThickness * cosTwistAngle), - mult(elbowFront, halfThickness * sinTwistAngle)) - d3 = add(mult(elbowFront, halfWidth * cosTwistAngle), - mult(elbowSide, halfWidth * sinTwistAngle)) - d12 = set_magnitude(d2, d12_mag) - d13 = set_magnitude(d3, d13_mag) - id2 = mult(d2, innerProportionDefault) - id3 = mult(d3, innerProportionDefault) - id12 = mult(d12, innerProportionDefault) - id13 = mult(d13, innerProportionDefault) - setNodeFieldParameters(coordinates, fieldcache, elbowPosition, d1, d2, d3, d12, d13) - setNodeFieldParameters(innerCoordinates, fieldcache, elbowPosition, d1, id2, id3, id12, id13) - nodeIdentifier += 1 + for i in range(elbowElementsCount): + xi = (brachiumElementsCount - elbowElementsCount - 1) / (armToHandElementsCount - 2) + x = elbowPosition[i] + d1 = elbowd1[i] + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # The elbow node uses a special width value + # Which 'fattens' the scaffold around the elbow depending on the level of rotation + halfWidth = math.sin(elbowAngleRadians)*(0.25)*halfWidth + halfWidth + elbowFront = elbowd3[i] + if i == 0: + twistAngle = 0.0 + else: + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i + if twistAngle == 0.0: + d2 = mult(elbowSide, halfThickness) + d3 = mult(elbowFront, halfWidth) + d12 = mult(elbowSide, d12_mag) + d13 = mult(elbowFront, d13_mag) + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + d2 = sub(mult(elbowSide, halfThickness * cosTwistAngle), + mult(elbowFront, halfThickness * sinTwistAngle)) + d3 = add(mult(elbowFront, halfWidth * cosTwistAngle), + mult(elbowSide, halfWidth * sinTwistAngle)) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 # Change d1 to the antebrachium direction d1 = mult(antebrachiumDirn, armScale) for i in range(antebrachiumElementsCount): @@ -718,37 +735,35 @@ def generateBaseMesh(cls, region, options): # foot anklePosition = add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)) ankleAngleRadians = ankleLeftAngleRadians if (side == left) else ankleRigthAngleRadians - rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians - math.pi/2)) + rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians)) # We need to create a 45* angle between d1 and d3 to avoid deformations - rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (math.pi/4)) + rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians/2)) cosAnkleAngle = math.cos(ankleAngleRadians) sinAnkleAngle = math.sin(ankleAngleRadians) footd1 = [-cosAnkleAngle, 0, sinAnkleAngle] footd2 = mult(legSide, halfFootWidth) footd3 = matrix_vector_mult(rotationMatrixAnkle, footd1) # footd2 = matrix_vector_mult(rotationMatrixAnkle, footd2) + # This positioning of the food nodes bends edge connecting the leg and the foot + # Which allows the scaffold to better capture the shape of the calcaneus fx = [ x, - add(anklePosition, mult(footd1, legBottomRadius)), + add(anklePosition, mult(footd1, 0)), add(anklePosition, mult(footd1, footLength - legBottomRadius)), ] - # fd1 = smoothCubicHermiteDerivativesLine( - # fx, [d1, [0.0, 0.0, 0.5 * footLength], [0.0, 0.0, 0.5 * footLength]], - # fixAllDirections=True, fixStartDerivative=True) fd1 = [d1, mult(footd1, 0.5*footLength), mult(footd1, 0.5*footLength)] fd1 = smoothCubicHermiteDerivativesLine( fx, fd1, fixAllDirections=True, fixStartDerivative=True ) fd2 = [d2, footd2, footd2] fd3 = [d3, - # set_magnitude(sub(legFront, legDirn), - # math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius), set_magnitude(footd3, math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius), - set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness)] + set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness) + ] fd12 = sub(fd2[2], fd2[1]) - fd13 = sub(fd3[2], fd3[1]) + fd13 = sub(fd3[2], fd3[1]) fid12 = mult(fd12, innerProportionDefault) fid13 = mult(fd13, innerProportionDefault) for i in range(1, 3): diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 0ea7b50c..dd7ae5c4 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -3,7 +3,8 @@ humanElementCounts = { 'headElementsCount': 3, 'neckElementsCount': 2, - 'brachiumElementsCount': 4, + 'brachiumElementsCount': 5, + 'elbowElementsCount': 2, 'antebrachiumElementsCount': 2, 'handElementsCount': 1, 'thoraxElementsCount': 3, From 0b92a40112cf33b55e50140e5695145c8ce6aed8 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 13 Oct 2025 15:55:58 +1300 Subject: [PATCH 17/61] Separated the 3d annotation groups from the arm --- .../meshtypes/meshtype_3d_wholebody2.py | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 8f676e9f..2946399b 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -237,7 +237,7 @@ def generateBaseMesh(cls, region, options): headGroup = AnnotationGroup(region, get_body_term("head")) neckGroup = AnnotationGroup(region, get_body_term("neck")) armGroup = AnnotationGroup(region, get_body_term("upper limb")) - armToHandGroup = AnnotationGroup(region, ("arm to hand", "")) + # armToHandGroup = AnnotationGroup(region, ("arm to hand", "")) leftArmGroup = AnnotationGroup(region, get_body_term("left upper limb")) leftBrachiumGroup = AnnotationGroup(region, get_body_term("left brachium")) leftAntebrachiumGroup = AnnotationGroup(region, get_body_term("left antebrachium")) @@ -255,7 +255,7 @@ def generateBaseMesh(cls, region, options): rightLegGroup = AnnotationGroup(region, get_body_term("right lower limb")) footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, - armGroup, armToHandGroup, leftArmGroup, rightArmGroup, handGroup, + armGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, @@ -284,7 +284,7 @@ def generateBaseMesh(cls, region, options): handElementsCount = humanElementCounts['handElementsCount'] armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount - elbowElementsCount + 1 armMeshGroup = armGroup.getMeshGroup(mesh) - armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) + # armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup @@ -292,7 +292,7 @@ def generateBaseMesh(cls, region, options): sideAntebrachiumGroup = leftAntebrachiumGroup if (side == left) else rightAntebrachiumGroup sideHandGroup = leftHandGroup if (side == left) else rightHandGroup # Setup brachium elements - meshGroups = [bodyMeshGroup, armMeshGroup, armToHandMeshGroup, + meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideBrachiumGroup.getMeshGroup(mesh)] for e in range(brachiumElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -300,7 +300,7 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 # Setup antebrachium elements - meshGroups = [bodyMeshGroup, armMeshGroup, armToHandMeshGroup, + meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideAntebrachiumGroup.getMeshGroup(mesh)] for e in range(antebrachiumElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -820,7 +820,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 - options["Number of elements along arm to hand"] = 3 + options["Number of elements along brachium"] = 5 + options["Number of elements along antebrachium"] = 2 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 4 options["Number of elements along foot"] = 2 @@ -838,7 +839,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 2 options["Number of elements along thorax"] = 3 options["Number of elements along abdomen"] = 3 - options["Number of elements along arm to hand"] = 6 + options["Number of elements along brachium"] = 6 + options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 6 options["Number of elements along foot"] = 2 @@ -850,7 +852,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 2 options["Number of elements along thorax"] = 4 options["Number of elements along abdomen"] = 4 - options["Number of elements along arm to hand"] = 8 + options["Number of elements along brachium"] = 6 + options["Number of elements along antebrachium"] = 4 options["Number of elements along hand"] = 2 options["Number of elements along leg to foot"] = 8 options["Number of elements along foot"] = 3 @@ -871,7 +874,8 @@ def getOrderedOptionNames(cls): "Number of elements along neck", "Number of elements along thorax", "Number of elements along abdomen", - "Number of elements along arm to hand", + "Number of elements along brachium", + "Number of elements along antebrachium", "Number of elements along hand", "Number of elements along leg to foot", "Number of elements along foot", @@ -919,7 +923,8 @@ def checkOptions(cls, options): "Number of elements along neck", "Number of elements along thorax", "Number of elements along abdomen", - "Number of elements along arm to hand", + "Number of elements along brachium", + "Number of elements along antebrachium", "Number of elements along hand", "Number of elements along leg to foot", "Number of elements along foot" @@ -974,7 +979,8 @@ def generateBaseMesh(cls, region, options): elementsCountAlongNeck = options["Number of elements along neck"] elementsCountAlongThorax = options["Number of elements along thorax"] elementsCountAlongAbdomen = options["Number of elements along abdomen"] - elementsCountAlongArmToHand = options["Number of elements along arm to hand"] + elementsCountAlongBrachium = options["Number of elements along brachium"] + elementsCountAlongAntebrachium = options["Number of elements along antebrachium"] elementsCountAlongHand = options["Number of elements along hand"] elementsCountAlongLegToFoot = options["Number of elements along leg to foot"] elementsCountAlongFoot = options["Number of elements along foot"] @@ -1012,8 +1018,11 @@ def generateBaseMesh(cls, region, options): alongCount = elementsCountAlongAbdomen aroundCount = elementsCountAroundTorso coreBoundaryScalingMode = 2 - elif "arm to hand" in name: - alongCount = elementsCountAlongArmToHand + elif " brachium" in name: + alongCount = elementsCountAlongBrachium + aroundCount = elementsCountAroundArm + elif " antebrachium" in name: + alongCount = elementsCountAlongAntebrachium aroundCount = elementsCountAroundArm elif "hand" in name: alongCount = elementsCountAlongHand From 47a23dd77700e414da14ed49924319f34c0b821a Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Tue, 14 Oct 2025 15:15:16 +1300 Subject: [PATCH 18/61] Added an elbow region in the arm --- src/scaffoldmaker/annotation/body_terms.py | 2 + .../meshtypes/meshtype_3d_wholebody2.py | 77 +++++++++++++------ .../utils/human_network_layout.py | 8 +- 3 files changed, 62 insertions(+), 25 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 73c2b280..98dc4f04 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -11,11 +11,13 @@ ("upper limb", "UBERON:0001460"), ("left upper limb", "UBERON:8300002", "FMA:7186"), ("left brachium", ""), + ("left elbow", ""), ("left antebrachium", ""), ("left hand", ""), ("left upper limb skin epidermis outer surface", "ILX:0796504"), ("right upper limb", "UBERON:8300001", "FMA:7185"), ("right brachium", ""), + ("right elbow", ""), ("right antebrachium", ""), ("right hand", ""), ("right upper limb skin epidermis outer surface", "ILX:0796503"), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 2946399b..dac4ecec 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -241,10 +241,12 @@ def generateBaseMesh(cls, region, options): leftArmGroup = AnnotationGroup(region, get_body_term("left upper limb")) leftBrachiumGroup = AnnotationGroup(region, get_body_term("left brachium")) leftAntebrachiumGroup = AnnotationGroup(region, get_body_term("left antebrachium")) + leftElbowGroup = AnnotationGroup(region, get_body_term("left elbow")) leftHandGroup = AnnotationGroup(region, get_body_term("left hand")) rightArmGroup = AnnotationGroup(region, get_body_term("right upper limb")) rightBrachiumGroup = AnnotationGroup(region, get_body_term("right brachium")) rightAntebrachiumGroup = AnnotationGroup(region, get_body_term("right antebrachium")) + rightElbowGroup = AnnotationGroup(region, get_body_term("right elbow")) rightHandGroup = AnnotationGroup(region, get_body_term("right hand")) handGroup = AnnotationGroup(region, get_body_term("hand")) thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) @@ -257,8 +259,8 @@ def generateBaseMesh(cls, region, options): annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, - leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, - rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, + leftBrachiumGroup, leftElbowGroup, leftAntebrachiumGroup, leftHandGroup, + rightBrachiumGroup, rightElbowGroup, rightAntebrachiumGroup, rightHandGroup, legGroup, legToFootGroup, leftLegGroup, rightLegGroup, footGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 @@ -282,7 +284,7 @@ def generateBaseMesh(cls, region, options): elbowElementsCount = humanElementCounts['elbowElementsCount'] antebrachiumElementsCount = humanElementCounts['antebrachiumElementsCount'] handElementsCount = humanElementCounts['handElementsCount'] - armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount - elbowElementsCount + 1 + armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount + 1 #all elbow nodes count as 1 armMeshGroup = armGroup.getMeshGroup(mesh) # armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) @@ -290,6 +292,7 @@ def generateBaseMesh(cls, region, options): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup sideBrachiumGroup = leftBrachiumGroup if (side == left) else rightBrachiumGroup sideAntebrachiumGroup = leftAntebrachiumGroup if (side == left) else rightAntebrachiumGroup + sideElbowGroup = leftElbowGroup if (side == left) else rightElbowGroup sideHandGroup = leftHandGroup if (side == left) else rightHandGroup # Setup brachium elements meshGroups = [bodyMeshGroup, armMeshGroup, @@ -299,6 +302,14 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + # Setup elbow elements + meshGroups = [bodyMeshGroup, armMeshGroup, + sideArmGroup.getMeshGroup(mesh), sideElbowGroup.getMeshGroup(mesh)] + for e in range(elbowElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 # Setup antebrachium elements meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideAntebrachiumGroup.getMeshGroup(mesh)] @@ -509,7 +520,7 @@ def generateBaseMesh(cls, region, options): elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) # Setting brachium coordinates - for i in range(brachiumElementsCount - elbowElementsCount - 1): + for i in range(brachiumElementsCount - 1): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -546,38 +557,45 @@ def generateBaseMesh(cls, region, options): # The special elbow node is rotated by half that angle, to give a smoother transition elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians rotationMatrixNode = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) - rotationMatrixD2 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/3) + rotationMatrixD2 = axis_angle_to_rotation_matrix( + mult(d2, -1), elbowAngleRadians/(elbowElementsCount)) antebrachiumDirn = matrix_vector_mult(rotationMatrixNode, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) elbowDirn = matrix_vector_mult(rotationMatrixD2, armDirn) elbowSide = armSide elbowd3 = [] + elbowd1 = [] elbowd3.append(matrix_vector_mult(rotationMatrixD2, armFront)) elbowd3.append(matrix_vector_mult(rotationMatrixD2, elbowd3[-1])) + elbowd1.append(set_magnitude(antebrachiumDirn, 1*armScale)) + elbowd1.append(set_magnitude(antebrachiumDirn, 1*armScale)) + # The d1 on the last brachium node is reduced to fit the scale of the first + # elbow node + # for field in (coordinates, innerCoordinates): + # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, mult(d1, 0.5)) # Ideally, the elbow node has the same d1 direction as the rest of the brachium # However, doing so causes a distortion in the network layout # This node is also moved 'forward' in the elbow direction (see above) # The 0.8/0.2 values were chosen by visual inspection of the scaffold - for field in (coordinates, innerCoordinates): - field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, mult(d1, 0.5)) elbowPosition = [] - elbowd1 = [] - elbowDir = add(mult(armDirn,0.99), mult(elbowDirn,0.01)) - elbowDir = set_magnitude(elbowDir, 0.5*armScale) + + # elbowDir = add(mult(armDirn,0.99), mult(elbowDirn,0.01)) + elbowDir = armDirn + elbowDir = set_magnitude(armDirn, 0.5*armScale) elbowPosition.append(add(x, elbowDir)) - elbowDir = add(mult(armDirn,0.01), mult(elbowDirn,0.99)) - elbowDir = set_magnitude(elbowDir, 0.5*armScale) + # elbowDir = add(mult(armDirn,0.01), mult(elbowDirn,0.99)) + elbowDir = elbowDirn + elbowDir = set_magnitude(elbowd1[1], 1*armScale) elbowPosition.append(add(elbowPosition[-1], elbowDir)) - elbowd1.append(mult(elbowDirn, 0.5*armScale)) - elbowd1.append(mult(antebrachiumDirn, 1*armScale)) - # elbowPosition - # Antebrachium nodes start below the elbow position + # elbowd1.append(mult(elbowDirn, 0.5*armScale)) + # elbowd1.append(mult(antebrachiumDirn, 1*armScale)) + + # Antebrachium nodes starts after the last elbow node # As with the elbow node, the first antebrachium node is positioned # via a linear combination of the elbow and antebrachium d1 # The values were chosen to produce the smoothest curve in the network layout. - # antebrachiumStart = add(mult(elbowDirn,0.5), mult(antebrachiumDirn,0.5)) antebrachiumStart = set_magnitude(antebrachiumDirn, 1*armScale) antebrachiumStart = add(elbowPosition[-1], antebrachiumStart) # Smooth d1 at the elbow @@ -587,16 +605,19 @@ def generateBaseMesh(cls, region, options): # )[1] # Elbow node field parameters are allocated separately from the rest of the brachium for i in range(elbowElementsCount): - xi = (brachiumElementsCount - elbowElementsCount - 1) / (armToHandElementsCount - 2) + xi = (brachiumElementsCount + 4*i - 1) / (armToHandElementsCount - 2) x = elbowPosition[i] d1 = elbowd1[i] node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0) -1) + halfWidth + xi = (brachiumElementsCount + 1*i - 1) / (armToHandElementsCount - 2) + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius # The elbow node uses a special width value # Which 'fattens' the scaffold around the elbow depending on the level of rotation - halfWidth = math.sin(elbowAngleRadians)*(0.25)*halfWidth + halfWidth + # halfWidth = math.sin(elbowAngleRadians)*(0.25)*halfWidth + halfWidth elbowFront = elbowd3[i] if i == 0: twistAngle = 0.0 @@ -626,7 +647,7 @@ def generateBaseMesh(cls, region, options): # Change d1 to the antebrachium direction d1 = mult(antebrachiumDirn, armScale) for i in range(antebrachiumElementsCount): - xi = (i + brachiumElementsCount - 1) / (armToHandElementsCount - 2) + xi = (i + brachiumElementsCount + elbowElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = add(antebrachiumStart, mult(d1, i)) @@ -820,7 +841,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 - options["Number of elements along brachium"] = 5 + options["Number of elements along brachium"] = 3 + options["Number of elements along elbow"] = 2 options["Number of elements along antebrachium"] = 2 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 4 @@ -839,7 +861,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 2 options["Number of elements along thorax"] = 3 options["Number of elements along abdomen"] = 3 - options["Number of elements along brachium"] = 6 + options["Number of elements along brachium"] = 4 + options["Number of elements along elbow"] = 3 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 6 @@ -852,7 +875,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 2 options["Number of elements along thorax"] = 4 options["Number of elements along abdomen"] = 4 - options["Number of elements along brachium"] = 6 + options["Number of elements along brachium"] = 5 + options["Number of elements along elbow"] = 4 options["Number of elements along antebrachium"] = 4 options["Number of elements along hand"] = 2 options["Number of elements along leg to foot"] = 8 @@ -924,6 +948,7 @@ def checkOptions(cls, options): "Number of elements along thorax", "Number of elements along abdomen", "Number of elements along brachium", + "Number of elements along elbow", "Number of elements along antebrachium", "Number of elements along hand", "Number of elements along leg to foot", @@ -980,6 +1005,7 @@ def generateBaseMesh(cls, region, options): elementsCountAlongThorax = options["Number of elements along thorax"] elementsCountAlongAbdomen = options["Number of elements along abdomen"] elementsCountAlongBrachium = options["Number of elements along brachium"] + elementsCountAlongElbow = options["Number of elements along elbow"] elementsCountAlongAntebrachium = options["Number of elements along antebrachium"] elementsCountAlongHand = options["Number of elements along hand"] elementsCountAlongLegToFoot = options["Number of elements along leg to foot"] @@ -1021,6 +1047,9 @@ def generateBaseMesh(cls, region, options): elif " brachium" in name: alongCount = elementsCountAlongBrachium aroundCount = elementsCountAroundArm + elif " elbow" in name: + alongCount = elementsCountAlongElbow + aroundCount = elementsCountAroundArm elif " antebrachium" in name: alongCount = elementsCountAlongAntebrachium aroundCount = elementsCountAroundArm diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index dd7ae5c4..f57f2d23 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -3,7 +3,7 @@ humanElementCounts = { 'headElementsCount': 3, 'neckElementsCount': 2, - 'brachiumElementsCount': 5, + 'brachiumElementsCount': 3, 'elbowElementsCount': 2, 'antebrachiumElementsCount': 2, 'handElementsCount': 1, @@ -93,6 +93,12 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['brachiumElementsCount'], armNetworkLayout, nodeIdentifier, endSegment=True) + # Elbow + armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 + armNetworkLayout, nodeIdentifier = createSegment( + humanElementCounts['elbowElementsCount'], + armNetworkLayout, nodeIdentifier, endSegment=True) # Antebrachium armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 From 6892bc32d0111a17425c1860fd4f6b2af268be22 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Tue, 14 Oct 2025 16:02:41 +1300 Subject: [PATCH 19/61] Figured out how to get the right angle at the elbow --- .../meshtypes/meshtype_3d_wholebody2.py | 34 +++++++++++-------- .../utils/human_network_layout.py | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index dac4ecec..85f48975 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -51,7 +51,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Right arm lateral angle degrees"] = 10.0 options["Left elbow lateral angle degrees"] = 45.0 options["Right elbow lateral angle degrees"] = 90.0 - options["Arm length"] = 7.5 + options["Upper Arm length"] = 4.5 + options["Lower Arm length"] = 3 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 options["Wrist thickness"] = 0.5 @@ -92,7 +93,8 @@ def getOrderedOptionNames(cls): "Right arm lateral angle degrees", "Left elbow lateral angle degrees", "Right elbow lateral angle degrees", - "Arm length", + "Upper Arm length", + "Lower Arm length", "Arm top diameter", "Arm twist angle degrees", "Wrist thickness", @@ -130,7 +132,8 @@ def checkOptions(cls, options): "Neck length", "Shoulder drop", "Shoulder width", - "Arm length", + "Upper Arm length", + "Lower Arm length", "Arm top diameter", "Wrist thickness", "Wrist width", @@ -197,7 +200,8 @@ def generateBaseMesh(cls, region, options): armRigthAngleRadians = math.radians(options["Right arm lateral angle degrees"]) elbowLeftAngleRadians = math.radians(options["Left elbow lateral angle degrees"]) elbowRigthAngleRadians = math.radians(options["Right elbow lateral angle degrees"]) - armLength = options["Arm length"] + upperArmLength = options["Upper Arm length"] + lowerArmLength = options["Lower Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) halfWristThickness = 0.5 * options["Wrist thickness"] @@ -457,8 +461,10 @@ def generateBaseMesh(cls, region, options): # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians - nonHandArmLength = armLength - handLength - armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count + nonHandArmLength = upperArmLength + lowerArmLength - handLength + lowerArmScale = lowerArmLength / (antebrachiumElementsCount + (elbowElementsCount/2)) # 2 == shoulder elements count + upperArmScale = upperArmLength / (brachiumElementsCount + (elbowElementsCount/2) - 2) # 2 == shoulder elements count + # armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) armAngle = armAngleRadians if (side == left) else -armAngleRadians @@ -471,10 +477,10 @@ def generateBaseMesh(cls, region, options): armDirn = [cosArmAngle, sinArmAngle, 0.0] armSide = [-sinArmAngle, cosArmAngle, 0.0] armFront = cross(armDirn, armSide) - d1 = mult(armDirn, armScale) + d1 = mult(armDirn, upperArmScale) # set arm versions 2 (left) and 3 (right) on arm junction node, and intermediate shoulder node sd1 = interpolateLagrangeHermiteDerivative(sx, x, d1, 0.0) - nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=armScale)[0:2] + nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=upperArmScale)[0:2] arcLengths = [getCubicHermiteArcLength(nx[i], nd1[i], nx[i + 1], nd1[i + 1]) for i in range(2)] sd2_list = [] sd3_list = [] @@ -568,8 +574,8 @@ def generateBaseMesh(cls, region, options): elbowd1 = [] elbowd3.append(matrix_vector_mult(rotationMatrixD2, armFront)) elbowd3.append(matrix_vector_mult(rotationMatrixD2, elbowd3[-1])) - elbowd1.append(set_magnitude(antebrachiumDirn, 1*armScale)) - elbowd1.append(set_magnitude(antebrachiumDirn, 1*armScale)) + elbowd1.append(set_magnitude(antebrachiumDirn, 1*lowerArmScale)) + elbowd1.append(set_magnitude(antebrachiumDirn, 1*lowerArmScale)) # The d1 on the last brachium node is reduced to fit the scale of the first # elbow node # for field in (coordinates, innerCoordinates): @@ -582,11 +588,11 @@ def generateBaseMesh(cls, region, options): # elbowDir = add(mult(armDirn,0.99), mult(elbowDirn,0.01)) elbowDir = armDirn - elbowDir = set_magnitude(armDirn, 0.5*armScale) + elbowDir = set_magnitude(armDirn, (1/3)*(upperArmScale+lowerArmScale)) elbowPosition.append(add(x, elbowDir)) # elbowDir = add(mult(armDirn,0.01), mult(elbowDirn,0.99)) elbowDir = elbowDirn - elbowDir = set_magnitude(elbowd1[1], 1*armScale) + elbowDir = set_magnitude(elbowd1[1], (2/3)*(upperArmScale+lowerArmScale)) elbowPosition.append(add(elbowPosition[-1], elbowDir)) # elbowd1.append(mult(elbowDirn, 0.5*armScale)) @@ -596,7 +602,7 @@ def generateBaseMesh(cls, region, options): # As with the elbow node, the first antebrachium node is positioned # via a linear combination of the elbow and antebrachium d1 # The values were chosen to produce the smoothest curve in the network layout. - antebrachiumStart = set_magnitude(antebrachiumDirn, 1*armScale) + antebrachiumStart = set_magnitude(antebrachiumDirn, 1*lowerArmScale) antebrachiumStart = add(elbowPosition[-1], antebrachiumStart) # Smooth d1 at the elbow # d1 = smoothCubicHermiteDerivativesLine( @@ -645,7 +651,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Change d1 to the antebrachium direction - d1 = mult(antebrachiumDirn, armScale) + d1 = mult(antebrachiumDirn, lowerArmScale) for i in range(antebrachiumElementsCount): xi = (i + brachiumElementsCount + elbowElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index f57f2d23..7c14ca01 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -5,7 +5,7 @@ 'neckElementsCount': 2, 'brachiumElementsCount': 3, 'elbowElementsCount': 2, - 'antebrachiumElementsCount': 2, + 'antebrachiumElementsCount': 1, 'handElementsCount': 1, 'thoraxElementsCount': 3, 'abdomenElementsCount': 4, From af38d2ac5e20bf6f7b6c8c48124401c2138d8a0b Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 15 Oct 2025 16:40:39 +1300 Subject: [PATCH 20/61] Got rid ot the second elbow node, made it part of the antebrachium --- .../meshtypes/meshtype_3d_wholebody2.py | 177 +++++++----------- .../utils/human_network_layout.py | 18 +- 2 files changed, 72 insertions(+), 123 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 85f48975..89640b81 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -51,8 +51,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Right arm lateral angle degrees"] = 10.0 options["Left elbow lateral angle degrees"] = 45.0 options["Right elbow lateral angle degrees"] = 90.0 - options["Upper Arm length"] = 4.5 - options["Lower Arm length"] = 3 + options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 options["Wrist thickness"] = 0.5 @@ -93,8 +92,7 @@ def getOrderedOptionNames(cls): "Right arm lateral angle degrees", "Left elbow lateral angle degrees", "Right elbow lateral angle degrees", - "Upper Arm length", - "Lower Arm length", + "Arm length", "Arm top diameter", "Arm twist angle degrees", "Wrist thickness", @@ -132,8 +130,7 @@ def checkOptions(cls, options): "Neck length", "Shoulder drop", "Shoulder width", - "Upper Arm length", - "Lower Arm length", + "Arm length", "Arm top diameter", "Wrist thickness", "Wrist width", @@ -200,8 +197,7 @@ def generateBaseMesh(cls, region, options): armRigthAngleRadians = math.radians(options["Right arm lateral angle degrees"]) elbowLeftAngleRadians = math.radians(options["Left elbow lateral angle degrees"]) elbowRigthAngleRadians = math.radians(options["Right elbow lateral angle degrees"]) - upperArmLength = options["Upper Arm length"] - lowerArmLength = options["Lower Arm length"] + armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) halfWristThickness = 0.5 * options["Wrist thickness"] @@ -288,7 +284,7 @@ def generateBaseMesh(cls, region, options): elbowElementsCount = humanElementCounts['elbowElementsCount'] antebrachiumElementsCount = humanElementCounts['antebrachiumElementsCount'] handElementsCount = humanElementCounts['handElementsCount'] - armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount + 1 #all elbow nodes count as 1 + armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount #all elbow nodes count as 1 armMeshGroup = armGroup.getMeshGroup(mesh) # armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) @@ -306,14 +302,14 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - # Setup elbow elements - meshGroups = [bodyMeshGroup, armMeshGroup, - sideArmGroup.getMeshGroup(mesh), sideElbowGroup.getMeshGroup(mesh)] - for e in range(elbowElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 + # # Setup elbow elements + # meshGroups = [bodyMeshGroup, armMeshGroup, + # sideArmGroup.getMeshGroup(mesh), sideElbowGroup.getMeshGroup(mesh)] + # for e in range(elbowElementsCount): + # element = mesh.findElementByIdentifier(elementIdentifier) + # for meshGroup in meshGroups: + # meshGroup.addElement(element) + # elementIdentifier += 1 # Setup antebrachium elements meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideAntebrachiumGroup.getMeshGroup(mesh)] @@ -461,10 +457,10 @@ def generateBaseMesh(cls, region, options): # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians - nonHandArmLength = upperArmLength + lowerArmLength - handLength - lowerArmScale = lowerArmLength / (antebrachiumElementsCount + (elbowElementsCount/2)) # 2 == shoulder elements count - upperArmScale = upperArmLength / (brachiumElementsCount + (elbowElementsCount/2) - 2) # 2 == shoulder elements count - # armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count + nonHandArmLength = armLength - handLength + # lowerArmScale = lowerArmLength / (antebrachiumElementsCount) # 2 == shoulder elements count + # upperArmScale = upperArmLength / (brachiumElementsCount - 2) # 2 == shoulder elements count + armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) armAngle = armAngleRadians if (side == left) else -armAngleRadians @@ -477,10 +473,10 @@ def generateBaseMesh(cls, region, options): armDirn = [cosArmAngle, sinArmAngle, 0.0] armSide = [-sinArmAngle, cosArmAngle, 0.0] armFront = cross(armDirn, armSide) - d1 = mult(armDirn, upperArmScale) + d1 = mult(armDirn, armScale) # set arm versions 2 (left) and 3 (right) on arm junction node, and intermediate shoulder node sd1 = interpolateLagrangeHermiteDerivative(sx, x, d1, 0.0) - nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=upperArmScale)[0:2] + nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=armScale)[0:2] arcLengths = [getCubicHermiteArcLength(nx[i], nd1[i], nx[i + 1], nd1[i + 1]) for i in range(2)] sd2_list = [] sd3_list = [] @@ -563,96 +559,56 @@ def generateBaseMesh(cls, region, options): # The special elbow node is rotated by half that angle, to give a smoother transition elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians rotationMatrixNode = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) - rotationMatrixD2 = axis_angle_to_rotation_matrix( - mult(d2, -1), elbowAngleRadians/(elbowElementsCount)) + rotationMatrixD3 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) antebrachiumDirn = matrix_vector_mult(rotationMatrixNode, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) - elbowDirn = matrix_vector_mult(rotationMatrixD2, armDirn) + elbowDirn = matrix_vector_mult(rotationMatrixD3, armDirn) elbowSide = armSide - elbowd3 = [] - elbowd1 = [] - elbowd3.append(matrix_vector_mult(rotationMatrixD2, armFront)) - elbowd3.append(matrix_vector_mult(rotationMatrixD2, elbowd3[-1])) - elbowd1.append(set_magnitude(antebrachiumDirn, 1*lowerArmScale)) - elbowd1.append(set_magnitude(antebrachiumDirn, 1*lowerArmScale)) - # The d1 on the last brachium node is reduced to fit the scale of the first - # elbow node - # for field in (coordinates, innerCoordinates): - # field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, mult(d1, 0.5)) - # Ideally, the elbow node has the same d1 direction as the rest of the brachium - # However, doing so causes a distortion in the network layout - # This node is also moved 'forward' in the elbow direction (see above) - # The 0.8/0.2 values were chosen by visual inspection of the scaffold - elbowPosition = [] - - # elbowDir = add(mult(armDirn,0.99), mult(elbowDirn,0.01)) - elbowDir = armDirn - elbowDir = set_magnitude(armDirn, (1/3)*(upperArmScale+lowerArmScale)) - elbowPosition.append(add(x, elbowDir)) - # elbowDir = add(mult(armDirn,0.01), mult(elbowDirn,0.99)) - elbowDir = elbowDirn - elbowDir = set_magnitude(elbowd1[1], (2/3)*(upperArmScale+lowerArmScale)) - elbowPosition.append(add(elbowPosition[-1], elbowDir)) - - # elbowd1.append(mult(elbowDirn, 0.5*armScale)) - # elbowd1.append(mult(antebrachiumDirn, 1*armScale)) - - # Antebrachium nodes starts after the last elbow node - # As with the elbow node, the first antebrachium node is positioned - # via a linear combination of the elbow and antebrachium d1 - # The values were chosen to produce the smoothest curve in the network layout. - antebrachiumStart = set_magnitude(antebrachiumDirn, 1*lowerArmScale) - antebrachiumStart = add(elbowPosition[-1], antebrachiumStart) - # Smooth d1 at the elbow - # d1 = smoothCubicHermiteDerivativesLine( - # [x, elbowPosition], - # [d1, elbowd1], fixAllDirections=True, fixStartDerivative=True - # )[1] + elbowFront = matrix_vector_mult(rotationMatrixD3, armFront) + elbowPosition = add(x, set_magnitude(armDirn, armScale)) # Elbow node field parameters are allocated separately from the rest of the brachium - for i in range(elbowElementsCount): - xi = (brachiumElementsCount + 4*i - 1) / (armToHandElementsCount - 2) - x = elbowPosition[i] - d1 = elbowd1[i] - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0) -1) + halfWidth - xi = (brachiumElementsCount + 1*i - 1) / (armToHandElementsCount - 2) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - # The elbow node uses a special width value - # Which 'fattens' the scaffold around the elbow depending on the level of rotation - # halfWidth = math.sin(elbowAngleRadians)*(0.25)*halfWidth + halfWidth - elbowFront = elbowd3[i] - if i == 0: - twistAngle = 0.0 - else: - twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i - if twistAngle == 0.0: - d2 = mult(elbowSide, halfThickness) - d3 = mult(elbowFront, halfWidth) - d12 = mult(elbowSide, d12_mag) - d13 = mult(elbowFront, d13_mag) - else: - cosTwistAngle = math.cos(twistAngle) - sinTwistAngle = math.sin(twistAngle) - d2 = sub(mult(elbowSide, halfThickness * cosTwistAngle), - mult(elbowFront, halfThickness * sinTwistAngle)) - d3 = add(mult(elbowFront, halfWidth * cosTwistAngle), - mult(elbowSide, halfWidth * sinTwistAngle)) - d12 = set_magnitude(d2, d12_mag) - d13 = set_magnitude(d3, d13_mag) - id2 = mult(d2, innerProportionDefault) - id3 = mult(d3, innerProportionDefault) - id12 = mult(d12, innerProportionDefault) - id13 = mult(d13, innerProportionDefault) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) - nodeIdentifier += 1 + x = add(x, set_magnitude(armDirn, armScale)) + d1 = set_magnitude(antebrachiumDirn, armScale) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + # The elbow node uses a special width value + # Which 'fattens' the scaffold around the elbow depending on the level of rotation + xi = (brachiumElementsCount - 1) / (armToHandElementsCount - 2) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0) -1) + halfWidth + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + if i == 0: + twistAngle = 0.0 + else: + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i + if twistAngle == 0.0: + d2 = mult(elbowSide, halfThickness) + d3 = mult(elbowFront, halfWidth) + d12 = mult(elbowSide, d12_mag) + d13 = mult(elbowFront, d13_mag) + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + d2 = sub(mult(elbowSide, halfThickness * cosTwistAngle), + mult(elbowFront, halfThickness * sinTwistAngle)) + d3 = add(mult(elbowFront, halfWidth * cosTwistAngle), + mult(elbowSide, halfWidth * sinTwistAngle)) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + # Antebrachium nodes starts after the last elbow node + antebrachiumStart = set_magnitude(antebrachiumDirn, 1*armScale) + antebrachiumStart = add(elbowPosition, antebrachiumStart) # Change d1 to the antebrachium direction - d1 = mult(antebrachiumDirn, lowerArmScale) - for i in range(antebrachiumElementsCount): + d1 = mult(antebrachiumDirn, armScale) + for i in range(antebrachiumElementsCount - 1): xi = (i + brachiumElementsCount + elbowElementsCount - 1) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -848,7 +804,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 options["Number of elements along brachium"] = 3 - options["Number of elements along elbow"] = 2 options["Number of elements along antebrachium"] = 2 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 4 @@ -868,7 +823,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along thorax"] = 3 options["Number of elements along abdomen"] = 3 options["Number of elements along brachium"] = 4 - options["Number of elements along elbow"] = 3 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 6 @@ -882,7 +836,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along thorax"] = 4 options["Number of elements along abdomen"] = 4 options["Number of elements along brachium"] = 5 - options["Number of elements along elbow"] = 4 options["Number of elements along antebrachium"] = 4 options["Number of elements along hand"] = 2 options["Number of elements along leg to foot"] = 8 @@ -1011,7 +964,6 @@ def generateBaseMesh(cls, region, options): elementsCountAlongThorax = options["Number of elements along thorax"] elementsCountAlongAbdomen = options["Number of elements along abdomen"] elementsCountAlongBrachium = options["Number of elements along brachium"] - elementsCountAlongElbow = options["Number of elements along elbow"] elementsCountAlongAntebrachium = options["Number of elements along antebrachium"] elementsCountAlongHand = options["Number of elements along hand"] elementsCountAlongLegToFoot = options["Number of elements along leg to foot"] @@ -1053,9 +1005,6 @@ def generateBaseMesh(cls, region, options): elif " brachium" in name: alongCount = elementsCountAlongBrachium aroundCount = elementsCountAroundArm - elif " elbow" in name: - alongCount = elementsCountAlongElbow - aroundCount = elementsCountAroundArm elif " antebrachium" in name: alongCount = elementsCountAlongAntebrachium aroundCount = elementsCountAroundArm diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 7c14ca01..0cfb88ee 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -3,9 +3,9 @@ humanElementCounts = { 'headElementsCount': 3, 'neckElementsCount': 2, - 'brachiumElementsCount': 3, - 'elbowElementsCount': 2, - 'antebrachiumElementsCount': 1, + 'brachiumElementsCount': 4, + 'elbowElementsCount': 0, + 'antebrachiumElementsCount': 3, 'handElementsCount': 1, 'thoraxElementsCount': 3, 'abdomenElementsCount': 4, @@ -93,12 +93,12 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['brachiumElementsCount'], armNetworkLayout, nodeIdentifier, endSegment=True) - # Elbow - armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' - nodeIdentifier += 1 - armNetworkLayout, nodeIdentifier = createSegment( - humanElementCounts['elbowElementsCount'], - armNetworkLayout, nodeIdentifier, endSegment=True) + # # Elbow + # armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' + # nodeIdentifier += 1 + # armNetworkLayout, nodeIdentifier = createSegment( + # humanElementCounts['elbowElementsCount'], + # armNetworkLayout, nodeIdentifier, endSegment=True) # Antebrachium armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 From b78050d9798cd35a4cfaefec7ab0c9315721d0ed Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 16 Oct 2025 13:06:32 +1300 Subject: [PATCH 21/61] Fixed the d3 and d2 at the elbow not being orthogonal with a non-zero twist angle --- .../meshtypes/meshtype_3d_wholebody2.py | 65 +++++++++---------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 89640b81..9ee16cea 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,7 +1,7 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude, axis_angle_to_rotation_matrix, matrix_vector_mult +from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude, axis_angle_to_rotation_matrix, matrix_vector_mult, dot from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates from cmlibs.utils.zinc.finiteelement import get_maximum_node_identifier from cmlibs.zinc.element import Element @@ -554,6 +554,20 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + # Elbow node field parameters are allocated separately from the rest of the amr + # Updating d2 and d3 for the elbow node + i += 1 + xi = i / (armToHandElementsCount - 2) + if twistAngle == 0.0: + d2 = armSide + d3 = armFront + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + d2 = sub(mult(armSide, cosTwistAngle), + mult(armFront, sinTwistAngle)) + d3 = add(mult(armFront, cosTwistAngle), + mult(armSide, sinTwistAngle)) # Updating frame of reference wrt rotation angle (using d2 as rotation axis) # Nodes in the antebrachium are rotated acoording to the elbow angle # The special elbow node is rotated by half that angle, to give a smoother transition @@ -564,42 +578,27 @@ def generateBaseMesh(cls, region, options): antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) elbowDirn = matrix_vector_mult(rotationMatrixD3, armDirn) - elbowSide = armSide - elbowFront = matrix_vector_mult(rotationMatrixD3, armFront) + elbowSide = d2 + elbowFront = matrix_vector_mult(rotationMatrixD3, d3) elbowPosition = add(x, set_magnitude(armDirn, armScale)) - # Elbow node field parameters are allocated separately from the rest of the brachium - x = add(x, set_magnitude(armDirn, armScale)) - d1 = set_magnitude(antebrachiumDirn, armScale) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) + xi = i / (armToHandElementsCount - 2) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius # The elbow node uses a special width value # Which 'fattens' the scaffold around the elbow depending on the level of rotation - xi = (brachiumElementsCount - 1) / (armToHandElementsCount - 2) - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0) -1) + halfWidth halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - if i == 0: - twistAngle = 0.0 - else: - twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * i - if twistAngle == 0.0: - d2 = mult(elbowSide, halfThickness) - d3 = mult(elbowFront, halfWidth) - d12 = mult(elbowSide, d12_mag) - d13 = mult(elbowFront, d13_mag) - else: - cosTwistAngle = math.cos(twistAngle) - sinTwistAngle = math.sin(twistAngle) - d2 = sub(mult(elbowSide, halfThickness * cosTwistAngle), - mult(elbowFront, halfThickness * sinTwistAngle)) - d3 = add(mult(elbowFront, halfWidth * cosTwistAngle), - mult(elbowSide, halfWidth * sinTwistAngle)) - d12 = set_magnitude(d2, d12_mag) - d13 = set_magnitude(d3, d13_mag) + d2 = set_magnitude(elbowSide, halfThickness) + d3 = set_magnitude(elbowFront, halfWidth) + d12 = set_magnitude(elbowSide, d12_mag) + d13 = set_magnitude(elbowFront, d13_mag) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) id13 = mult(d13, innerProportionDefault) + x = add(x, set_magnitude(armDirn, armScale)) + d1 = set_magnitude(antebrachiumDirn, armScale) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 @@ -608,17 +607,17 @@ def generateBaseMesh(cls, region, options): antebrachiumStart = add(elbowPosition, antebrachiumStart) # Change d1 to the antebrachium direction d1 = mult(antebrachiumDirn, armScale) - for i in range(antebrachiumElementsCount - 1): - xi = (i + brachiumElementsCount + elbowElementsCount - 1) / (armToHandElementsCount - 2) + for i in range(brachiumElementsCount, armToHandElementsCount - 1): + xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i)) + x = add(antebrachiumStart, mult(d1, i - brachiumElementsCount)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - if i == (antebrachiumElementsCount - 1): + if i == 0: twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians else: - twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i + brachiumElementsCount - 1) + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i - 1) if twistAngle == 0.0: d2 = mult(antebrachiumSide, halfThickness) d3 = mult(antebrachiumFront, halfWidth) From addffb8bc9637b210239023196ab83a02e77573a Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 16 Oct 2025 13:22:59 +1300 Subject: [PATCH 22/61] Cleaned up elbow rotation code --- .../meshtypes/meshtype_3d_wholebody2.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 9ee16cea..19017b2c 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -555,9 +555,8 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Elbow node field parameters are allocated separately from the rest of the amr - # Updating d2 and d3 for the elbow node - i += 1 - xi = i / (armToHandElementsCount - 2) + # Calculating initial d2 and d3 before rotation, just necessary in case there + # is a non-zero twist angle if twistAngle == 0.0: d2 = armSide d3 = armFront @@ -569,24 +568,28 @@ def generateBaseMesh(cls, region, options): d3 = add(mult(armFront, cosTwistAngle), mult(armSide, sinTwistAngle)) # Updating frame of reference wrt rotation angle (using d2 as rotation axis) - # Nodes in the antebrachium are rotated acoording to the elbow angle - # The special elbow node is rotated by half that angle, to give a smoother transition elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians - rotationMatrixNode = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) - rotationMatrixD3 = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) - antebrachiumDirn = matrix_vector_mult(rotationMatrixNode, armDirn) + rotationMatrixElbow = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) + rotationMatrixElbowHalf = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) + antebrachiumDirn = matrix_vector_mult(rotationMatrixElbow, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) - elbowDirn = matrix_vector_mult(rotationMatrixD3, armDirn) + # The d3 direction in the elbow node is rotated by half this angle + # To ensure a better transition at this node. + elbowDirn = antebrachiumDirn elbowSide = d2 - elbowFront = matrix_vector_mult(rotationMatrixD3, d3) - elbowPosition = add(x, set_magnitude(armDirn, armScale)) + elbowFront = matrix_vector_mult(rotationMatrixElbowHalf, d3) + # Elbow node position does not depend on the rotation angle + elbowPosition = add(x, d1) + i += 1 xi = i / (armToHandElementsCount - 2) halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius # The elbow node uses a special width value # Which 'fattens' the scaffold around the elbow depending on the level of rotation - halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0) -1) + halfWidth + halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0)-1) + halfWidth halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + x = elbowPosition + d1 = set_magnitude(elbowDirn, armScale) d2 = set_magnitude(elbowSide, halfThickness) d3 = set_magnitude(elbowFront, halfWidth) d12 = set_magnitude(elbowSide, d12_mag) @@ -595,18 +598,15 @@ def generateBaseMesh(cls, region, options): id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) id13 = mult(d13, innerProportionDefault) - x = add(x, set_magnitude(armDirn, armScale)) - d1 = set_magnitude(antebrachiumDirn, armScale) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Antebrachium nodes starts after the last elbow node - antebrachiumStart = set_magnitude(antebrachiumDirn, 1*armScale) - antebrachiumStart = add(elbowPosition, antebrachiumStart) - # Change d1 to the antebrachium direction d1 = mult(antebrachiumDirn, armScale) + antebrachiumStart = add(elbowPosition, d1) + # Change d1 to the antebrachium direction for i in range(brachiumElementsCount, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) From c815706349fb1623ca197d73c4f79aad2686af83 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 16 Oct 2025 13:25:01 +1300 Subject: [PATCH 23/61] Removed elbow from human network layout --- src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py | 6 +++--- src/scaffoldmaker/utils/human_network_layout.py | 7 ------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 19017b2c..0c252748 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -554,9 +554,9 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Elbow node field parameters are allocated separately from the rest of the amr - # Calculating initial d2 and d3 before rotation, just necessary in case there - # is a non-zero twist angle + # Elbow node field parameters are allocated separately from the rest of the arm + # Calculating initial d2 and d3 before rotation + # Necessary in case there is is a non-zero twist angle if twistAngle == 0.0: d2 = armSide d3 = armFront diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 0cfb88ee..00475f8b 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -4,7 +4,6 @@ 'headElementsCount': 3, 'neckElementsCount': 2, 'brachiumElementsCount': 4, - 'elbowElementsCount': 0, 'antebrachiumElementsCount': 3, 'handElementsCount': 1, 'thoraxElementsCount': 3, @@ -93,12 +92,6 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['brachiumElementsCount'], armNetworkLayout, nodeIdentifier, endSegment=True) - # # Elbow - # armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' - # nodeIdentifier += 1 - # armNetworkLayout, nodeIdentifier = createSegment( - # humanElementCounts['elbowElementsCount'], - # armNetworkLayout, nodeIdentifier, endSegment=True) # Antebrachium armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 From 407d9ef4d8a13634ed0a33e96ef5bec60fc907ce Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 16 Oct 2025 14:28:38 +1300 Subject: [PATCH 24/61] Added lower and upper leg annotation groups to the stickman --- src/scaffoldmaker/annotation/body_terms.py | 4 +++ .../meshtypes/meshtype_3d_wholebody2.py | 28 +++++++++++++++---- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 98dc4f04..70742780 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -32,8 +32,12 @@ ("left", ""), ("lower limb", "UBERON:0000978"), ("left lower limb", "UBERON:8300004", "FMA:24981"), + ("left upper leg", ""), + ("left lower leg", ""), ("left lower limb skin epidermis outer surface", "ILX:0796506"), ("right lower limb", "UBERON:8300003", "FMA:24980"), + ("right upper leg", ""), + ("right lower leg", ""), ("right lower limb skin epidermis outer surface", "ILX:0796505"), ("foot", "ILX:0745450", "FMA:9664"), ("neck", "UBERON:0000974", "ILX:0733967"), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 0c252748..997f6d9a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -254,14 +254,21 @@ def generateBaseMesh(cls, region, options): legGroup = AnnotationGroup(region, get_body_term("lower limb")) legToFootGroup = AnnotationGroup(region, ("leg to foot", "")) leftLegGroup = AnnotationGroup(region, get_body_term("left lower limb")) + leftUpperLegGroup = AnnotationGroup(region, get_body_term("left upper leg")) + leftLowerLegGroup = AnnotationGroup(region, get_body_term("left lower leg")) rightLegGroup = AnnotationGroup(region, get_body_term("right lower limb")) + rightUpperLegGroup = AnnotationGroup(region, get_body_term("right upper leg")) + rightLowerLegGroup = AnnotationGroup(region, get_body_term("right lower leg")) footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, leftBrachiumGroup, leftElbowGroup, leftAntebrachiumGroup, leftHandGroup, rightBrachiumGroup, rightElbowGroup, rightAntebrachiumGroup, rightHandGroup, - legGroup, legToFootGroup, leftLegGroup, rightLegGroup, footGroup] + legGroup, legToFootGroup, + leftLegGroup, leftUpperLegGroup, leftLowerLegGroup, + rightLegGroup, rightUpperLegGroup, rightLowerLegGroup, + footGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 headElementsCount = humanElementCounts['headElementsCount'] @@ -281,7 +288,6 @@ def generateBaseMesh(cls, region, options): left = 0 right = 1 brachiumElementsCount = humanElementCounts['brachiumElementsCount'] - elbowElementsCount = humanElementCounts['elbowElementsCount'] antebrachiumElementsCount = humanElementCounts['antebrachiumElementsCount'] handElementsCount = humanElementCounts['handElementsCount'] armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount #all elbow nodes count as 1 @@ -350,8 +356,20 @@ def generateBaseMesh(cls, region, options): footMeshGroup = footGroup.getMeshGroup(mesh) for side in (left, right): sideLegGroup = leftLegGroup if (side == left) else rightLegGroup - meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, sideLegGroup.getMeshGroup(mesh)] - for e in range(legToFootElementsCount): + sideUpperLegGroup = leftUpperLegGroup if (side == left) else rightUpperLegGroup + sideLowerLegGroup = leftLowerLegGroup if (side == left) else rightLowerLegGroup + # Upper leg + meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, + sideLegGroup.getMeshGroup(mesh), sideUpperLegGroup.getMeshGroup(mesh)] + for e in range(upperLegElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + # Lower leg + meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, + sideLegGroup.getMeshGroup(mesh), sideLowerLegGroup.getMeshGroup(mesh)] + for e in range(lowerLegElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) @@ -556,7 +574,7 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # Elbow node field parameters are allocated separately from the rest of the arm # Calculating initial d2 and d3 before rotation - # Necessary in case there is is a non-zero twist angle + # Necessary in case there is a non-zero twist angle if twistAngle == 0.0: d2 = armSide d3 = armFront From e23a6af27e87397913f578857509b4cfea8c8bb1 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 16 Oct 2025 16:13:35 +1300 Subject: [PATCH 25/61] Added knee rotation --- .../meshtypes/meshtype_3d_wholebody2.py | 129 ++++++++++++++---- .../utils/human_network_layout.py | 4 +- 2 files changed, 107 insertions(+), 26 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 997f6d9a..651bd86b 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -69,6 +69,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 + options["Left knee lateral angle degrees"] = 45.0 + options["Right knee lateral angle degrees"] = 90.0 options["Left ankle lateral angle degrees"] = 90.0 options["Right ankle lateral angle degrees"] = 90.0 options["Foot height"] = 1.25 @@ -110,6 +112,8 @@ def getOrderedOptionNames(cls): "Leg length", "Leg top diameter", "Leg bottom diameter", + "Left knee lateral angle degrees", + "Right knee lateral angle degrees", "Foot height", "Foot length", "Foot thickness", @@ -146,6 +150,8 @@ def checkOptions(cls, options): "Leg length", "Leg top diameter", "Leg bottom diameter", + "Left knee lateral angle degrees", + "Right knee lateral angle degrees", "Foot height", "Foot length", "Foot thickness", @@ -166,6 +172,8 @@ def checkOptions(cls, options): "Right arm lateral angle degrees": (-60.0, 200.0), "Left elbow lateral angle degrees": (0.0, 150.0), "Right elbow lateral angle degrees": (0.0, 150.0), + "Left knee lateral angle degrees": (0.0, 150.0), + "Right knee lateral angle degrees": (0.0, 150.0), "Left ankle lateral angle degrees": (60.0, 140.0), "Right ankle lateral angle degrees": (60.0, 140.0), "Arm twist angle degrees": (-90.0, 90.0), @@ -215,6 +223,8 @@ def generateBaseMesh(cls, region, options): legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] + kneeLeftAngleRadians = math.radians(options["Left knee lateral angle degrees"]) + kneeRigthAngleRadians = math.radians(options["Right knee lateral angle degrees"]) ankleLeftAngleRadians = math.radians( options["Left ankle lateral angle degrees"]) ankleRigthAngleRadians = math.radians(options["Right ankle lateral angle degrees"]) footHeight = options["Foot height"] @@ -587,23 +597,23 @@ def generateBaseMesh(cls, region, options): mult(armSide, sinTwistAngle)) # Updating frame of reference wrt rotation angle (using d2 as rotation axis) elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians - rotationMatrixElbow = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) - rotationMatrixElbowHalf = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) - antebrachiumDirn = matrix_vector_mult(rotationMatrixElbow, armDirn) + elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) + elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) + antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) # The d3 direction in the elbow node is rotated by half this angle # To ensure a better transition at this node. elbowDirn = antebrachiumDirn elbowSide = d2 - elbowFront = matrix_vector_mult(rotationMatrixElbowHalf, d3) + elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) # Elbow node position does not depend on the rotation angle - elbowPosition = add(x, d1) + elbowPosition = add(x, set_magnitude(d1, armScale)) i += 1 xi = i / (armToHandElementsCount - 2) - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius # The elbow node uses a special width value # Which 'fattens' the scaffold around the elbow depending on the level of rotation + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0)-1) + halfWidth halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius x = elbowPosition @@ -621,7 +631,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Antebrachium nodes starts after the last elbow node + # Antebrachium nodes starts after the elbow node d1 = mult(antebrachiumDirn, armScale) antebrachiumStart = add(elbowPosition, d1) # Change d1 to the antebrachium direction @@ -718,12 +728,12 @@ def generateBaseMesh(cls, region, options): id12 = mult(d12, innerProportionDefault) d13 = [0.0, 0.0, d13_mag] id13 = mult(d13, innerProportionDefault) - # main part of leg to ankle - for i in range(legToFootElementsCount): + # Upper leg + for i in range(upperLegElementsCount): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = [legStartX + d1[0] * i, legStartY + d1[1] * i, d1[2] * i] + x = add(legStart, mult(d1, i)) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = mult(legSide, radius) d3 = [0.0, 0.0, radius] @@ -732,20 +742,80 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + # knee + # Updating frame of reference wrt rotation angle (using d2 as rotation axis) + kneeAngleRadians = kneeLeftAngleRadians if (side == left) else kneeRigthAngleRadians + kneeRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeAngleRadians) + kneeHalfrotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeAngleRadians/2) + lowerLegDirn = matrix_vector_mult(kneeRotationMatrix, d1) + lowerLegSide = legSide + lowerLegFront = cross(lowerLegDirn, lowerLegSide) + # The d3 direction in the knee node is rotated by half this angle + # To ensure a better transition at this node. + kneeDirn = lowerLegDirn + kneeSide = d2 + kneeFront = matrix_vector_mult(kneeHalfrotationMatrix, d3) + # Knee node position does not depend on the rotation angle + kneePosition = add(x, set_magnitude(d1, legScale - 1.5 * radius)) + i += 1 + xi = i / legToFootElementsCount + radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius + radius = math.sin(kneeAngleRadians)*radius*(math.sqrt(2.0)-1) + radius + x = kneePosition + d1 = set_magnitude(kneeDirn, legScale) + d2 = set_magnitude(kneeSide, radius) + d3 = set_magnitude(kneeFront, radius) + d12 = set_magnitude(kneeSide, d12_mag) + d13 = set_magnitude(kneeFront, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + # Lower leg + d1 = set_magnitude(lowerLegDirn, legScale) + lowerLegStart = add(kneePosition, d1) + for i in range(upperLegElementsCount, legToFootElementsCount - 1): + xi = i / legToFootElementsCount + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = add(lowerLegStart, mult(d1, i - upperLegElementsCount)) + radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius + d2 = set_magnitude(lowerLegSide, radius) + d3 = set_magnitude(lowerLegFront, radius) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 # foot - anklePosition = add(legStart, mult(legDirn, legLength - 1.5 * halfFootThickness)) + # Updating frame of reference wrt rotation angle (using d2 as rotation axis) ankleAngleRadians = ankleLeftAngleRadians if (side == left) else ankleRigthAngleRadians - rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians)) - # We need to create a 45* angle between d1 and d3 to avoid deformations - rotationMatrixAnkle = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians/2)) + ankleRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians)) + ankleHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians/2)) + # The d3 direction in the ankle node is rotated by half this angle + # To ensure a better transition at this node. + ankleDirn = matrix_vector_mult(ankleRotationMatrix, d1) + ankleSide = d2 + ankleFront = cross(ankleDirn, ankleSide) + cosAnkleAngle = math.cos(ankleAngleRadians) sinAnkleAngle = math.sin(ankleAngleRadians) - footd1 = [-cosAnkleAngle, 0, sinAnkleAngle] - footd2 = mult(legSide, halfFootWidth) - footd3 = matrix_vector_mult(rotationMatrixAnkle, footd1) + + footDirn = ankleDirn + footSide = d2 + footFront = matrix_vector_mult(ankleHalfRotationMatrix, d3) + footd1 = footDirn + footd2 = footSide + footd3 = footFront # footd2 = matrix_vector_mult(rotationMatrixAnkle, footd2) # This positioning of the food nodes bends edge connecting the leg and the foot # Which allows the scaffold to better capture the shape of the calcaneus + anklePosition = add(x, set_magnitude(d1, legScale - 1.5 * halfFootThickness)) fx = [ x, add(anklePosition, mult(footd1, 0)), @@ -823,7 +893,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 3 options["Number of elements along antebrachium"] = 2 options["Number of elements along hand"] = 1 - options["Number of elements along leg to foot"] = 4 + options["Number of elements along upper leg"] = 3 + options["Number of elements along lower leg"] = 2 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 @@ -842,7 +913,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 4 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 - options["Number of elements along leg to foot"] = 6 + options["Number of elements along upper leg"] = 3 + options["Number of elements along lower leg"] = 2 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 16 options["Number of elements around torso"] = 16 @@ -855,7 +927,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 5 options["Number of elements along antebrachium"] = 4 options["Number of elements along hand"] = 2 - options["Number of elements along leg to foot"] = 8 + options["Number of elements along upper leg"] = 3 + options["Number of elements along lower leg"] = 2 options["Number of elements along foot"] = 3 options["Number of elements around head"] = 20 options["Number of elements around torso"] = 20 @@ -877,7 +950,8 @@ def getOrderedOptionNames(cls): "Number of elements along brachium", "Number of elements along antebrachium", "Number of elements along hand", - "Number of elements along leg to foot", + "Number of elements along upper leg", + "Number of elements along lower leg", "Number of elements along foot", "Number of elements around head", "Number of elements around torso", @@ -927,7 +1001,8 @@ def checkOptions(cls, options): "Number of elements along elbow", "Number of elements along antebrachium", "Number of elements along hand", - "Number of elements along leg to foot", + "Number of elements along upper leg", + "Number of elements along lower leg", "Number of elements along foot" ]: if options[key] < 1: @@ -983,7 +1058,8 @@ def generateBaseMesh(cls, region, options): elementsCountAlongBrachium = options["Number of elements along brachium"] elementsCountAlongAntebrachium = options["Number of elements along antebrachium"] elementsCountAlongHand = options["Number of elements along hand"] - elementsCountAlongLegToFoot = options["Number of elements along leg to foot"] + elementsCountAlongUpperLeg = options["Number of elements along upper leg"] + elementsCountAlongLowerLeg = options["Number of elements along lower leg"] elementsCountAlongFoot = options["Number of elements along foot"] elementsCountAroundHead = options["Number of elements around head"] elementsCountAroundTorso = options["Number of elements around torso"] @@ -1028,8 +1104,11 @@ def generateBaseMesh(cls, region, options): elif "hand" in name: alongCount = elementsCountAlongHand aroundCount = elementsCountAroundArm - elif "leg to foot" in name: - alongCount = elementsCountAlongLegToFoot + elif "upper leg" in name: + alongCount = elementsCountAlongUpperLeg + aroundCount = elementsCountAroundLeg + elif "lower leg" in name: + alongCount = elementsCountAlongLowerLeg aroundCount = elementsCountAroundLeg elif "foot" in name: alongCount = elementsCountAlongFoot diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 00475f8b..57113663 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -114,8 +114,10 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): # Upper leg legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['upperLegElementsCount'], - legNetworkLayout, nodeIdentifier) + legNetworkLayout, nodeIdentifier, endSegment=True) # Lower leg + legNetworkLayout = legNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['lowerLegElementsCount'], legNetworkLayout, nodeIdentifier, endSegment=True) From bcd5d311718534c5dd820292dee5d5e67882990d Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 17 Oct 2025 16:25:54 +1300 Subject: [PATCH 26/61] Got beaten my simple trigonometry --- .../meshtypes/meshtype_3d_wholebody2.py | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 651bd86b..286e2d54 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -69,7 +69,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee lateral angle degrees"] = 45.0 + options["Left knee lateral angle degrees"] = 120.0 options["Right knee lateral angle degrees"] = 90.0 options["Left ankle lateral angle degrees"] = 90.0 options["Right ankle lateral angle degrees"] = 90.0 @@ -150,8 +150,6 @@ def checkOptions(cls, options): "Leg length", "Leg top diameter", "Leg bottom diameter", - "Left knee lateral angle degrees", - "Right knee lateral angle degrees", "Foot height", "Foot length", "Foot thickness", @@ -750,21 +748,25 @@ def generateBaseMesh(cls, region, options): lowerLegDirn = matrix_vector_mult(kneeRotationMatrix, d1) lowerLegSide = legSide lowerLegFront = cross(lowerLegDirn, lowerLegSide) + rotationFactor = math.sin(kneeAngleRadians)*(math.sqrt(2)-1) + # rotationFactor = math.sin(kneeAngleRadians)*(math.sqrt(2)-1) + # The d3 direction in the knee node is rotated by half this angle # To ensure a better transition at this node. kneeDirn = lowerLegDirn kneeSide = d2 kneeFront = matrix_vector_mult(kneeHalfrotationMatrix, d3) # Knee node position does not depend on the rotation angle - kneePosition = add(x, set_magnitude(d1, legScale - 1.5 * radius)) + i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - radius = math.sin(kneeAngleRadians)*radius*(math.sqrt(2.0)-1) + radius + kneeRadius = radius/math.sin(kneeAngleRadians/2) + kneePosition = add(x, set_magnitude(d1, legScale - 2.0*rotationFactor * kneeRadius)) x = kneePosition - d1 = set_magnitude(kneeDirn, legScale) - d2 = set_magnitude(kneeSide, radius) - d3 = set_magnitude(kneeFront, radius) + d1 = set_magnitude(kneeDirn, legScale + 2.0*rotationFactor*kneeRadius) + d2 = set_magnitude(kneeSide, kneeRadius) + d3 = set_magnitude(kneeFront, kneeRadius) d12 = set_magnitude(kneeSide, d12_mag) d13 = set_magnitude(kneeFront, d13_mag) id2 = mult(d2, innerProportionDefault) @@ -777,13 +779,16 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Lower leg - d1 = set_magnitude(lowerLegDirn, legScale) lowerLegStart = add(kneePosition, d1) - for i in range(upperLegElementsCount, legToFootElementsCount - 1): + d1 = set_magnitude(lowerLegDirn, legScale) + for i in range(upperLegElementsCount + 1, legToFootElementsCount): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(lowerLegStart, mult(d1, i - upperLegElementsCount)) + x = add(lowerLegStart, mult(d1, i - (upperLegElementsCount + 1))) + # if (i == legToFootElementsCount): + # # x = add(x, set_magnitude(d1, 1.5 * radius)) + # d1 = set_magnitude(d1, legScale + 1.5 * radius) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = set_magnitude(lowerLegSide, radius) d3 = set_magnitude(lowerLegFront, radius) @@ -815,14 +820,14 @@ def generateBaseMesh(cls, region, options): # footd2 = matrix_vector_mult(rotationMatrixAnkle, footd2) # This positioning of the food nodes bends edge connecting the leg and the foot # Which allows the scaffold to better capture the shape of the calcaneus - anklePosition = add(x, set_magnitude(d1, legScale - 1.5 * halfFootThickness)) + anklePosition = add(x, set_magnitude(d1, legScale - 1.5*halfFootThickness)) fx = [ x, - add(anklePosition, mult(footd1, 0)), - add(anklePosition, mult(footd1, footLength - legBottomRadius)), + add(anklePosition, set_magnitude(footd1, 0)), + add(anklePosition, set_magnitude(footd1, footLength - legBottomRadius)), ] - fd1 = [d1, mult(footd1, 0.5*footLength), mult(footd1, 0.5*footLength)] + fd1 = [d1, set_magnitude(footd1, 0.5*footLength), set_magnitude(footd1, 0.5*footLength)] fd1 = smoothCubicHermiteDerivativesLine( fx, fd1, fixAllDirections=True, fixStartDerivative=True ) @@ -998,7 +1003,6 @@ def checkOptions(cls, options): "Number of elements along thorax", "Number of elements along abdomen", "Number of elements along brachium", - "Number of elements along elbow", "Number of elements along antebrachium", "Number of elements along hand", "Number of elements along upper leg", From 19b92afe2da254102aa0160e89c685584a41152c Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 20 Oct 2025 16:12:21 +1300 Subject: [PATCH 27/61] Added foot element groups, corrected ankle position and fixed uneven upper leg elements by getting rid of the leg to foot group --- src/scaffoldmaker/annotation/body_terms.py | 2 + .../meshtypes/meshtype_3d_wholebody2.py | 105 +++++++++--------- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 70742780..fe6f3ca7 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -34,10 +34,12 @@ ("left lower limb", "UBERON:8300004", "FMA:24981"), ("left upper leg", ""), ("left lower leg", ""), + ("left foot", ""), ("left lower limb skin epidermis outer surface", "ILX:0796506"), ("right lower limb", "UBERON:8300003", "FMA:24980"), ("right upper leg", ""), ("right lower leg", ""), + ("right foot", ""), ("right lower limb skin epidermis outer surface", "ILX:0796505"), ("foot", "ILX:0745450", "FMA:9664"), ("neck", "UBERON:0000974", "ILX:0733967"), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 286e2d54..c831ee32 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -69,8 +69,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee lateral angle degrees"] = 120.0 - options["Right knee lateral angle degrees"] = 90.0 + options["Left knee flexion degrees"] = 120.0 + options["Right knee flexion degrees"] = 90.0 options["Left ankle lateral angle degrees"] = 90.0 options["Right ankle lateral angle degrees"] = 90.0 options["Foot height"] = 1.25 @@ -112,8 +112,8 @@ def getOrderedOptionNames(cls): "Leg length", "Leg top diameter", "Leg bottom diameter", - "Left knee lateral angle degrees", - "Right knee lateral angle degrees", + "Left knee flexion degrees", + "Right knee flexion degrees", "Foot height", "Foot length", "Foot thickness", @@ -170,8 +170,8 @@ def checkOptions(cls, options): "Right arm lateral angle degrees": (-60.0, 200.0), "Left elbow lateral angle degrees": (0.0, 150.0), "Right elbow lateral angle degrees": (0.0, 150.0), - "Left knee lateral angle degrees": (0.0, 150.0), - "Right knee lateral angle degrees": (0.0, 150.0), + "Left knee flexion degrees": (0.0, 140.0), + "Right knee flexion degrees": (0.0, 140.0), "Left ankle lateral angle degrees": (60.0, 140.0), "Right ankle lateral angle degrees": (60.0, 140.0), "Arm twist angle degrees": (-90.0, 90.0), @@ -221,10 +221,10 @@ def generateBaseMesh(cls, region, options): legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] - kneeLeftAngleRadians = math.radians(options["Left knee lateral angle degrees"]) - kneeRigthAngleRadians = math.radians(options["Right knee lateral angle degrees"]) - ankleLeftAngleRadians = math.radians( options["Left ankle lateral angle degrees"]) - ankleRigthAngleRadians = math.radians(options["Right ankle lateral angle degrees"]) + kneeLeftFlexionRadians = math.radians(options["Left knee flexion degrees"]) + kneeRightFlexionRadians = math.radians(options["Right knee flexion degrees"]) + ankleLeftFlexionRadians = math.radians( options["Left ankle lateral angle degrees"]) + ankleRightFlexionRadians = math.radians(options["Right ankle lateral angle degrees"]) footHeight = options["Foot height"] footLength = options["Foot length"] halfFootThickness = 0.5 * options["Foot thickness"] @@ -249,12 +249,12 @@ def generateBaseMesh(cls, region, options): leftArmGroup = AnnotationGroup(region, get_body_term("left upper limb")) leftBrachiumGroup = AnnotationGroup(region, get_body_term("left brachium")) leftAntebrachiumGroup = AnnotationGroup(region, get_body_term("left antebrachium")) - leftElbowGroup = AnnotationGroup(region, get_body_term("left elbow")) + # leftElbowGroup = AnnotationGroup(region, get_body_term("left elbow")) leftHandGroup = AnnotationGroup(region, get_body_term("left hand")) rightArmGroup = AnnotationGroup(region, get_body_term("right upper limb")) rightBrachiumGroup = AnnotationGroup(region, get_body_term("right brachium")) rightAntebrachiumGroup = AnnotationGroup(region, get_body_term("right antebrachium")) - rightElbowGroup = AnnotationGroup(region, get_body_term("right elbow")) + # rightElbowGroup = AnnotationGroup(region, get_body_term("right elbow")) rightHandGroup = AnnotationGroup(region, get_body_term("right hand")) handGroup = AnnotationGroup(region, get_body_term("hand")) thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) @@ -264,19 +264,20 @@ def generateBaseMesh(cls, region, options): leftLegGroup = AnnotationGroup(region, get_body_term("left lower limb")) leftUpperLegGroup = AnnotationGroup(region, get_body_term("left upper leg")) leftLowerLegGroup = AnnotationGroup(region, get_body_term("left lower leg")) + leftFootGroup = AnnotationGroup(region, get_body_term("left foot")) rightLegGroup = AnnotationGroup(region, get_body_term("right lower limb")) rightUpperLegGroup = AnnotationGroup(region, get_body_term("right upper leg")) rightLowerLegGroup = AnnotationGroup(region, get_body_term("right lower leg")) + rightFootGroup = AnnotationGroup(region, get_body_term("right foot")) footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, - leftBrachiumGroup, leftElbowGroup, leftAntebrachiumGroup, leftHandGroup, - rightBrachiumGroup, rightElbowGroup, rightAntebrachiumGroup, rightHandGroup, - legGroup, legToFootGroup, - leftLegGroup, leftUpperLegGroup, leftLowerLegGroup, - rightLegGroup, rightUpperLegGroup, rightLowerLegGroup, - footGroup] + leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, + rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, + legGroup, footGroup, + leftLegGroup, leftUpperLegGroup, leftLowerLegGroup, leftFootGroup, + rightLegGroup, rightUpperLegGroup, rightLowerLegGroup, rightFootGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 headElementsCount = humanElementCounts['headElementsCount'] @@ -306,7 +307,7 @@ def generateBaseMesh(cls, region, options): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup sideBrachiumGroup = leftBrachiumGroup if (side == left) else rightBrachiumGroup sideAntebrachiumGroup = leftAntebrachiumGroup if (side == left) else rightAntebrachiumGroup - sideElbowGroup = leftElbowGroup if (side == left) else rightElbowGroup + # sideElbowGroup = leftElbowGroup if (side == left) else rightElbowGroup sideHandGroup = leftHandGroup if (side == left) else rightHandGroup # Setup brachium elements meshGroups = [bodyMeshGroup, armMeshGroup, @@ -316,14 +317,6 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - # # Setup elbow elements - # meshGroups = [bodyMeshGroup, armMeshGroup, - # sideArmGroup.getMeshGroup(mesh), sideElbowGroup.getMeshGroup(mesh)] - # for e in range(elbowElementsCount): - # element = mesh.findElementByIdentifier(elementIdentifier) - # for meshGroup in meshGroups: - # meshGroup.addElement(element) - # elementIdentifier += 1 # Setup antebrachium elements meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideAntebrachiumGroup.getMeshGroup(mesh)] @@ -366,8 +359,9 @@ def generateBaseMesh(cls, region, options): sideLegGroup = leftLegGroup if (side == left) else rightLegGroup sideUpperLegGroup = leftUpperLegGroup if (side == left) else rightUpperLegGroup sideLowerLegGroup = leftLowerLegGroup if (side == left) else rightLowerLegGroup + sideFootGroup = leftFootGroup if (side == left) else rightFootGroup # Upper leg - meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, + meshGroups = [bodyMeshGroup, legMeshGroup, sideLegGroup.getMeshGroup(mesh), sideUpperLegGroup.getMeshGroup(mesh)] for e in range(upperLegElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -375,14 +369,15 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 # Lower leg - meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, + meshGroups = [bodyMeshGroup, legMeshGroup, sideLegGroup.getMeshGroup(mesh), sideLowerLegGroup.getMeshGroup(mesh)] for e in range(lowerLegElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - meshGroups = [bodyMeshGroup, legMeshGroup, footMeshGroup, sideLegGroup.getMeshGroup(mesh)] + # Foot + meshGroups = [bodyMeshGroup, legMeshGroup, footMeshGroup, sideFootGroup.getMeshGroup(mesh)] for e in range(footElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: @@ -742,30 +737,31 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # knee # Updating frame of reference wrt rotation angle (using d2 as rotation axis) - kneeAngleRadians = kneeLeftAngleRadians if (side == left) else kneeRigthAngleRadians - kneeRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeAngleRadians) - kneeHalfrotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeAngleRadians/2) + kneeFlexionRadians = kneeLeftFlexionRadians if (side == left) else kneeRightFlexionRadians + # Angle between upper and lower leg + kneeJointAngleRadians = math.pi - kneeFlexionRadians + kneeRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeFlexionRadians) + kneeHalfrotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeFlexionRadians/2) lowerLegDirn = matrix_vector_mult(kneeRotationMatrix, d1) lowerLegSide = legSide lowerLegFront = cross(lowerLegDirn, lowerLegSide) - rotationFactor = math.sin(kneeAngleRadians)*(math.sqrt(2)-1) - # rotationFactor = math.sin(kneeAngleRadians)*(math.sqrt(2)-1) - - # The d3 direction in the knee node is rotated by half this angle + # The d3 direction in the knee node is rotated by half the flexion angle # To ensure a better transition at this node. kneeDirn = lowerLegDirn kneeSide = d2 kneeFront = matrix_vector_mult(kneeHalfrotationMatrix, d3) - # Knee node position does not depend on the rotation angle - + # This rotation factor is used to adjust the position of the knee node relative + # to the angle of flexion, and ensures a proper transition between the upper and lower leg + rotationFactor = 2.0*math.sin(kneeFlexionRadians)*(math.sqrt(2)-1) i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - kneeRadius = radius/math.sin(kneeAngleRadians/2) - kneePosition = add(x, set_magnitude(d1, legScale - 2.0*rotationFactor * kneeRadius)) + # d3 direction is fattened at the joint to ensure a proper transition in the tube network + kneeRadius = radius/math.sin(kneeJointAngleRadians/2) + kneePosition = add(x, set_magnitude(d1, legScale - rotationFactor*kneeRadius)) x = kneePosition - d1 = set_magnitude(kneeDirn, legScale + 2.0*rotationFactor*kneeRadius) - d2 = set_magnitude(kneeSide, kneeRadius) + d1 = set_magnitude(kneeDirn, legScale + rotationFactor*kneeRadius) + d2 = set_magnitude(kneeSide, radius) d3 = set_magnitude(kneeFront, kneeRadius) d12 = set_magnitude(kneeSide, d12_mag) d13 = set_magnitude(kneeFront, d13_mag) @@ -799,28 +795,31 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # foot # Updating frame of reference wrt rotation angle (using d2 as rotation axis) - ankleAngleRadians = ankleLeftAngleRadians if (side == left) else ankleRigthAngleRadians - ankleRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians)) - ankleHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleAngleRadians/2)) + ankleFlexionRadians = ankleLeftFlexionRadians if (side == left) else ankleRightFlexionRadians + # Angle between lower leg and foot + ankleJointAngleRadians = math.pi - ankleFlexionRadians + ankleRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleFlexionRadians)) + ankleHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleFlexionRadians/2)) # The d3 direction in the ankle node is rotated by half this angle # To ensure a better transition at this node. ankleDirn = matrix_vector_mult(ankleRotationMatrix, d1) ankleSide = d2 ankleFront = cross(ankleDirn, ankleSide) - - cosAnkleAngle = math.cos(ankleAngleRadians) - sinAnkleAngle = math.sin(ankleAngleRadians) footDirn = ankleDirn footSide = d2 footFront = matrix_vector_mult(ankleHalfRotationMatrix, d3) + # + footd1 = footDirn - footd2 = footSide + footd2 = set_magnitude(footSide, halfFootWidth) footd3 = footFront + rotationFactor = 2.0*math.sin(ankleFlexionRadians)*(math.sqrt(2)- 1) + ankleThickness = halfFootThickness/math.sin(ankleJointAngleRadians/2) # footd2 = matrix_vector_mult(rotationMatrixAnkle, footd2) # This positioning of the food nodes bends edge connecting the leg and the foot # Which allows the scaffold to better capture the shape of the calcaneus - anklePosition = add(x, set_magnitude(d1, legScale - 1.5*halfFootThickness)) + anklePosition = add(x, set_magnitude(d1, legScale - 1*rotationFactor*footHeight)) fx = [ x, add(anklePosition, set_magnitude(footd1, 0)), @@ -834,7 +833,7 @@ def generateBaseMesh(cls, region, options): fd2 = [d2, footd2, footd2] fd3 = [d3, set_magnitude(footd3, - math.sqrt(2.0 * halfFootThickness * halfFootThickness) + legBottomRadius), + ankleThickness + legBottomRadius), set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness) ] fd12 = sub(fd2[2], fd2[1]) @@ -918,7 +917,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 4 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 - options["Number of elements along upper leg"] = 3 + options["Number of elements along upper leg"] = 2 options["Number of elements along lower leg"] = 2 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 16 From 321d174ccec6f430f51b73237f85d1094dd9043a Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Tue, 21 Oct 2025 11:33:31 +1300 Subject: [PATCH 28/61] Homogenized the elbow flexion code with the knee and ankle --- .../meshtypes/meshtype_3d_wholebody2.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index c831ee32..edd271bf 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -201,8 +201,8 @@ def generateBaseMesh(cls, region, options): halfShoulderWidth = 0.5 * options["Shoulder width"] armLeftAngleRadians = math.radians(options["Left arm lateral angle degrees"]) armRigthAngleRadians = math.radians(options["Right arm lateral angle degrees"]) - elbowLeftAngleRadians = math.radians(options["Left elbow lateral angle degrees"]) - elbowRigthAngleRadians = math.radians(options["Right elbow lateral angle degrees"]) + elbowLeftFlexionRadians = math.radians(options["Left elbow lateral angle degrees"]) + elbowRigthFlexionRadians = math.radians(options["Right elbow lateral angle degrees"]) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) @@ -589,9 +589,10 @@ def generateBaseMesh(cls, region, options): d3 = add(mult(armFront, cosTwistAngle), mult(armSide, sinTwistAngle)) # Updating frame of reference wrt rotation angle (using d2 as rotation axis) - elbowAngleRadians = elbowLeftAngleRadians if (side == left) else elbowRigthAngleRadians - elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians) - elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowAngleRadians/2) + elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRigthFlexionRadians + elbowJointAngleRadians = math.pi - elbowFlexionRadians + elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) + elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) @@ -600,17 +601,19 @@ def generateBaseMesh(cls, region, options): elbowDirn = antebrachiumDirn elbowSide = d2 elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) - # Elbow node position does not depend on the rotation angle - elbowPosition = add(x, set_magnitude(d1, armScale)) + # This rotation factor is used to adjust the position of the knee node relative + # to the angle of flexion, and ensures a proper transition between the upper and lower leg + rotationFactor = 1.0*math.sin(elbowFlexionRadians)*(math.sqrt(2)-1) i += 1 xi = i / (armToHandElementsCount - 2) # The elbow node uses a special width value # Which 'fattens' the scaffold around the elbow depending on the level of rotation halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - halfWidth = math.sin(elbowAngleRadians)*halfWidth*(math.sqrt(2.0)-1) + halfWidth + halfWidth = halfWidth/math.sin(elbowJointAngleRadians/2) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + elbowPosition = add(x, set_magnitude(d1, armScale - rotationFactor*halfWidth)) x = elbowPosition - d1 = set_magnitude(elbowDirn, armScale) + d1 = set_magnitude(elbowDirn, armScale + rotationFactor*halfWidth) d2 = set_magnitude(elbowSide, halfThickness) d3 = set_magnitude(elbowFront, halfWidth) d12 = set_magnitude(elbowSide, d12_mag) @@ -625,8 +628,8 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Antebrachium nodes starts after the elbow node - d1 = mult(antebrachiumDirn, armScale) antebrachiumStart = add(elbowPosition, d1) + d1 = mult(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction for i in range(brachiumElementsCount, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) From 55af4cc37666f5cb46300bdfab9029ccc6137b77 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 14:00:51 +1300 Subject: [PATCH 29/61] Fixed the length of the arm+hand --- .../meshtypes/meshtype_3d_wholebody2.py | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index edd271bf..6883141b 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -47,10 +47,12 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Neck length"] = 1.3 options["Shoulder drop"] = 1.0 options["Shoulder width"] = 4.5 + options["Left shoulder lateral angle degrees"] = 0.0 + options["Right shoulder lateral angle degrees"] = 0.0 options["Left arm lateral angle degrees"] = 10.0 options["Right arm lateral angle degrees"] = 10.0 - options["Left elbow lateral angle degrees"] = 45.0 - options["Right elbow lateral angle degrees"] = 90.0 + options["Left elbow lateral angle degrees"] = 0.0 + options["Right elbow lateral angle degrees"] = 0.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -90,6 +92,8 @@ def getOrderedOptionNames(cls): "Neck length", "Shoulder drop", "Shoulder width", + "Left shoulder lateral angle degrees", + "Right shoulder lateral angle degrees", "Left arm lateral angle degrees", "Right arm lateral angle degrees", "Left elbow lateral angle degrees", @@ -166,6 +170,8 @@ def checkOptions(cls, options): elif options[key] > 0.9: options[key] = 0.9 for key, angleRange in { + "Left shoulder lateral angle degrees": (-60.0, 180.0), + "Right shoulder lateral angle degrees": (-60.0, 180.0), "Left arm lateral angle degrees": (-60.0, 200.0), "Right arm lateral angle degrees": (-60.0, 200.0), "Left elbow lateral angle degrees": (0.0, 150.0), @@ -199,6 +205,8 @@ def generateBaseMesh(cls, region, options): neckLength = options["Neck length"] shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] + shoulderLeftFlexionRadians = math.radians(options["Left shoulder lateral angle degrees"]) + shoulderRightFlexionRadians = math.radians(options["Right shoulder lateral angle degrees"]) armLeftAngleRadians = math.radians(options["Left arm lateral angle degrees"]) armRigthAngleRadians = math.radians(options["Right arm lateral angle degrees"]) elbowLeftFlexionRadians = math.radians(options["Left elbow lateral angle degrees"]) @@ -310,7 +318,8 @@ def generateBaseMesh(cls, region, options): # sideElbowGroup = leftElbowGroup if (side == left) else rightElbowGroup sideHandGroup = leftHandGroup if (side == left) else rightHandGroup # Setup brachium elements - meshGroups = [bodyMeshGroup, armMeshGroup, + meshGroups = [bodyMeshGroup, + armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideBrachiumGroup.getMeshGroup(mesh)] for e in range(brachiumElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -318,7 +327,8 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 # Setup antebrachium elements - meshGroups = [bodyMeshGroup, armMeshGroup, + meshGroups = [bodyMeshGroup, + armMeshGroup, sideArmGroup.getMeshGroup(mesh), sideAntebrachiumGroup.getMeshGroup(mesh)] for e in range(antebrachiumElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -326,7 +336,9 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 # Setup hand elements - meshGroups = [bodyMeshGroup, armMeshGroup, handMeshGroup, sideHandGroup.getMeshGroup(mesh)] + meshGroups = [bodyMeshGroup, + armMeshGroup, + handMeshGroup, sideHandGroup.getMeshGroup(mesh)] for e in range(handElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: @@ -479,8 +491,6 @@ def generateBaseMesh(cls, region, options): shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians nonHandArmLength = armLength - handLength - # lowerArmScale = lowerArmLength / (antebrachiumElementsCount) # 2 == shoulder elements count - # upperArmScale = upperArmLength / (brachiumElementsCount - 2) # 2 == shoulder elements count armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) @@ -543,7 +553,7 @@ def generateBaseMesh(cls, region, options): elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) # Setting brachium coordinates - for i in range(brachiumElementsCount - 1): + for i in range(brachiumElementsCount - 2): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -588,12 +598,12 @@ def generateBaseMesh(cls, region, options): mult(armFront, sinTwistAngle)) d3 = add(mult(armFront, cosTwistAngle), mult(armSide, sinTwistAngle)) - # Updating frame of reference wrt rotation angle (using d2 as rotation axis) + # Updating frame of reference wrt flexion angle (using d2 as rotation axis) elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRigthFlexionRadians elbowJointAngleRadians = math.pi - elbowFlexionRadians elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) - antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) + antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) # The d3 direction in the elbow node is rotated by half this angle @@ -631,11 +641,11 @@ def generateBaseMesh(cls, region, options): antebrachiumStart = add(elbowPosition, d1) d1 = mult(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction - for i in range(brachiumElementsCount, armToHandElementsCount - 1): + for i in range(brachiumElementsCount - 1, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i - brachiumElementsCount)) + x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - 1))) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: From 7f1291792168d9bee850c10211acf9d417eb1403 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 16:56:54 +1300 Subject: [PATCH 30/61] Initial shoulder flexion (still needs to be worked on) --- .../meshtypes/meshtype_3d_wholebody2.py | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 6883141b..46e5dcc5 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -71,8 +71,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee flexion degrees"] = 120.0 - options["Right knee flexion degrees"] = 90.0 + options["Left knee flexion degrees"] = 0.0 + options["Right knee flexion degrees"] = 0.0 options["Left ankle lateral angle degrees"] = 90.0 options["Right ankle lateral angle degrees"] = 90.0 options["Foot height"] = 1.25 @@ -208,9 +208,9 @@ def generateBaseMesh(cls, region, options): shoulderLeftFlexionRadians = math.radians(options["Left shoulder lateral angle degrees"]) shoulderRightFlexionRadians = math.radians(options["Right shoulder lateral angle degrees"]) armLeftAngleRadians = math.radians(options["Left arm lateral angle degrees"]) - armRigthAngleRadians = math.radians(options["Right arm lateral angle degrees"]) + armRightAngleRadians = math.radians(options["Right arm lateral angle degrees"]) elbowLeftFlexionRadians = math.radians(options["Left elbow lateral angle degrees"]) - elbowRigthFlexionRadians = math.radians(options["Right elbow lateral angle degrees"]) + elbowRightFlexionRadians = math.radians(options["Right elbow lateral angle degrees"]) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) @@ -279,13 +279,13 @@ def generateBaseMesh(cls, region, options): rightFootGroup = AnnotationGroup(region, get_body_term("right foot")) footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, - armGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, - legGroup, footGroup, leftLegGroup, leftUpperLegGroup, leftLowerLegGroup, leftFootGroup, - rightLegGroup, rightUpperLegGroup, rightLowerLegGroup, rightFootGroup] + rightLegGroup, rightUpperLegGroup, rightLowerLegGroup, rightFootGroup, + armGroup, leftArmGroup, rightArmGroup, handGroup, + legGroup, footGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 headElementsCount = humanElementCounts['headElementsCount'] @@ -485,7 +485,7 @@ def generateBaseMesh(cls, region, options): # rotate shoulder with arm, pivoting about shoulder drop below arm junction on network # this has the realistic effect of shoulders becoming narrower with higher angles # initial shoulder rotation with arm is negligible, hence: - armAngleRadians = armLeftAngleRadians if (side == left) else armRigthAngleRadians + armAngleRadians = armLeftAngleRadians if (side == left) else armRightAngleRadians shoulderRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) @@ -500,7 +500,7 @@ def generateBaseMesh(cls, region, options): armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) armStartY = (halfShoulderWidth if (side == left) else -halfShoulderWidth) * math.cos(shoulderAngleRadians) armStart = [armStartX, armStartY, 0.0] - x = armStart.copy() + x = armStart armDirn = [cosArmAngle, sinArmAngle, 0.0] armSide = [-sinArmAngle, cosArmAngle, 0.0] armFront = cross(armDirn, armSide) @@ -552,8 +552,50 @@ def generateBaseMesh(cls, region, options): # Arm twist elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) + # Shoulder flexion + d2 = mult(armSide, armScale) + d3 = mult(armFront, armScale) + # Updating frame of reference wrt flexion angle (using d2 as rotation axis) + shoulderFlexionRadians = shoulderLeftFlexionRadians if (side == left) else shoulderRightFlexionRadians + shoulderJointAngleRadians = math.pi - shoulderFlexionRadians + shoulderRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), shoulderFlexionRadians) + shoulderHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), shoulderFlexionRadians/2) + armDirn = matrix_vector_mult(shoulderRotationMatrix, armDirn) + armSide = armSide + armFront = cross(armDirn, armSide) + # The d3 direction in the shoulder node is rotated by half this angle + # To ensure a better transition at this node. + shoulderDirn = armDirn + shoulderSide = armSide + shoulderFront = matrix_vector_mult(shoulderRotationMatrix, d3) + # This rotation factor is used to adjust the position of the knee node relative + # to the angle of flexion, and ensures a proper transition between the upper and lower leg + rotationFactor = 1.0*math.sin(shoulderFlexionRadians)*(math.sqrt(2)-1) + i = 0 + xi = i / (armToHandElementsCount - 2) + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # halfWidth = halfWidth/math.sin(shoulderJointAngleRadians/2) + shoulderPosition = add(armStart, set_magnitude(d1, 0)) + x = shoulderPosition + d1 = set_magnitude(shoulderDirn, armScale) + d2 = set_magnitude(shoulderSide, halfThickness) + d3 = set_magnitude(shoulderFront, halfWidth) + d12 = set_magnitude(shoulderSide, d12_mag) + d13 = set_magnitude(shoulderFront, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + # armStart = add(shoulderPosition,d1) + # d1 = mult(armDirn, armScale) # Setting brachium coordinates - for i in range(brachiumElementsCount - 2): + for i in range(1, brachiumElementsCount - 2): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -599,7 +641,7 @@ def generateBaseMesh(cls, region, options): d3 = add(mult(armFront, cosTwistAngle), mult(armSide, sinTwistAngle)) # Updating frame of reference wrt flexion angle (using d2 as rotation axis) - elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRigthFlexionRadians + elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRightFlexionRadians elbowJointAngleRadians = math.pi - elbowFlexionRadians elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) From aab77f09d57f62f3b533a0db80b1be294201e9fe Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 17:00:03 +1300 Subject: [PATCH 31/61] Standarized flexion and abduction names --- .../meshtypes/meshtype_3d_wholebody2.py | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 46e5dcc5..1b805466 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -47,12 +47,12 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Neck length"] = 1.3 options["Shoulder drop"] = 1.0 options["Shoulder width"] = 4.5 - options["Left shoulder lateral angle degrees"] = 0.0 - options["Right shoulder lateral angle degrees"] = 0.0 - options["Left arm lateral angle degrees"] = 10.0 - options["Right arm lateral angle degrees"] = 10.0 - options["Left elbow lateral angle degrees"] = 0.0 - options["Right elbow lateral angle degrees"] = 0.0 + options["Left shoulder abduction degrees"] = 0.0 + options["Right shoulder abduction degrees"] = 0.0 + options["Left arm flexion degrees"] = 10.0 + options["Right arm flexion degrees"] = 10.0 + options["Left elbow flexion degrees"] = 0.0 + options["Right elbow flexion degrees"] = 0.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -67,14 +67,14 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Torso width"] = 3.2 options["Pelvis drop"] = 1.5 options["Pelvis width"] = 2.0 - options["Leg lateral angle degrees"] = 10.0 + options["Leg abduction degrees"] = 10.0 options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 options["Left knee flexion degrees"] = 0.0 options["Right knee flexion degrees"] = 0.0 - options["Left ankle lateral angle degrees"] = 90.0 - options["Right ankle lateral angle degrees"] = 90.0 + options["Left ankle flexion degrees"] = 90.0 + options["Right ankle flexion degrees"] = 90.0 options["Foot height"] = 1.25 options["Foot length"] = 2.5 options["Foot thickness"] = 0.3 @@ -92,12 +92,12 @@ def getOrderedOptionNames(cls): "Neck length", "Shoulder drop", "Shoulder width", - "Left shoulder lateral angle degrees", - "Right shoulder lateral angle degrees", - "Left arm lateral angle degrees", - "Right arm lateral angle degrees", - "Left elbow lateral angle degrees", - "Right elbow lateral angle degrees", + "Left shoulder abduction degrees", + "Right shoulder abduction degrees", + "Left arm flexion degrees", + "Right arm flexion degrees", + "Left elbow flexion degrees", + "Right elbow flexion degrees", "Arm length", "Arm top diameter", "Arm twist angle degrees", @@ -112,7 +112,7 @@ def getOrderedOptionNames(cls): "Torso width", "Pelvis drop", "Pelvis width", - "Leg lateral angle degrees", + "Leg abduction degrees", "Leg length", "Leg top diameter", "Leg bottom diameter", @@ -122,8 +122,8 @@ def getOrderedOptionNames(cls): "Foot length", "Foot thickness", "Foot width", - "Left ankle lateral angle degrees", - "Right ankle lateral angle degrees", + "Left ankle flexion degrees", + "Right ankle flexion degrees", "Inner proportion default", "Inner proportion head" ] @@ -170,18 +170,18 @@ def checkOptions(cls, options): elif options[key] > 0.9: options[key] = 0.9 for key, angleRange in { - "Left shoulder lateral angle degrees": (-60.0, 180.0), - "Right shoulder lateral angle degrees": (-60.0, 180.0), - "Left arm lateral angle degrees": (-60.0, 200.0), - "Right arm lateral angle degrees": (-60.0, 200.0), - "Left elbow lateral angle degrees": (0.0, 150.0), - "Right elbow lateral angle degrees": (0.0, 150.0), + "Left shoulder abduction degrees": (-60.0, 180.0), + "Right shoulder abduction degrees": (-60.0, 180.0), + "Left arm flexion degrees": (-60.0, 200.0), + "Right arm flexion degrees": (-60.0, 200.0), + "Left elbow flexion degrees": (0.0, 150.0), + "Right elbow flexion degrees": (0.0, 150.0), "Left knee flexion degrees": (0.0, 140.0), "Right knee flexion degrees": (0.0, 140.0), - "Left ankle lateral angle degrees": (60.0, 140.0), - "Right ankle lateral angle degrees": (60.0, 140.0), + "Left ankle flexion degrees": (60.0, 140.0), + "Right ankle flexion degrees": (60.0, 140.0), "Arm twist angle degrees": (-90.0, 90.0), - "Leg lateral angle degrees": (-20.0, 60.0) + "Leg abduction degrees": (-20.0, 60.0) }.items(): if options[key] < angleRange[0]: options[key] = angleRange[0] @@ -205,12 +205,12 @@ def generateBaseMesh(cls, region, options): neckLength = options["Neck length"] shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] - shoulderLeftFlexionRadians = math.radians(options["Left shoulder lateral angle degrees"]) - shoulderRightFlexionRadians = math.radians(options["Right shoulder lateral angle degrees"]) - armLeftAngleRadians = math.radians(options["Left arm lateral angle degrees"]) - armRightAngleRadians = math.radians(options["Right arm lateral angle degrees"]) - elbowLeftFlexionRadians = math.radians(options["Left elbow lateral angle degrees"]) - elbowRightFlexionRadians = math.radians(options["Right elbow lateral angle degrees"]) + shoulderLeftFlexionRadians = math.radians(options["Left shoulder abduction degrees"]) + shoulderRightFlexionRadians = math.radians(options["Right shoulder abduction degrees"]) + armLeftAngleRadians = math.radians(options["Left arm flexion degrees"]) + armRightAngleRadians = math.radians(options["Right arm flexion degrees"]) + elbowLeftFlexionRadians = math.radians(options["Left elbow flexion degrees"]) + elbowRightFlexionRadians = math.radians(options["Right elbow flexion degrees"]) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) @@ -225,14 +225,14 @@ def generateBaseMesh(cls, region, options): halfTorsoWidth = 0.5 * options["Torso width"] pelvisDrop = options["Pelvis drop"] halfPelvisWidth = 0.5 * options["Pelvis width"] - legAngleRadians = math.radians(options["Leg lateral angle degrees"]) + legAngleRadians = math.radians(options["Leg abduction degrees"]) legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] kneeLeftFlexionRadians = math.radians(options["Left knee flexion degrees"]) kneeRightFlexionRadians = math.radians(options["Right knee flexion degrees"]) - ankleLeftFlexionRadians = math.radians( options["Left ankle lateral angle degrees"]) - ankleRightFlexionRadians = math.radians(options["Right ankle lateral angle degrees"]) + ankleLeftFlexionRadians = math.radians( options["Left ankle flexion degrees"]) + ankleRightFlexionRadians = math.radians(options["Right ankle flexion degrees"]) footHeight = options["Foot height"] footLength = options["Foot length"] halfFootThickness = 0.5 * options["Foot thickness"] From 7ded2cf1023603201baba7be32b7914d8d90d010 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 17:05:48 +1300 Subject: [PATCH 32/61] Cleaned up marker location code in 3d scaffold --- .../meshtypes/meshtype_3d_wholebody2.py | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 1b805466..b01a69b9 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -243,11 +243,8 @@ def generateBaseMesh(cls, region, options): options['Kinematic tree'] = {} networkMesh = NetworkMesh(structure) networkMesh.create1DLayoutMesh(region) - fieldmodule = region.getFieldmodule() mesh = fieldmodule.findMeshByDimension(1) - - # set up element annotations bodyGroup = AnnotationGroup(region, get_body_term("body")) headGroup = AnnotationGroup(region, get_body_term("head")) @@ -1218,18 +1215,10 @@ def generateBaseMesh(cls, region, options): abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) # Kinematic tree markers - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) node_identifier = max(1, get_maximum_node_identifier(nodes) + 1) - # node = stickman_nodes.findNodeByIdentifier(6) - # mesh = fieldmodule.findMeshByDimension(meshDimension) coordinates = find_or_create_field_coordinates(fieldmodule) - # fieldcache = fieldmodule.createFieldcache() - # fieldcache.setNode(node) - # marker_positions = coordinates.getNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, 3)[1] - # marker_names = 'thorax_stickman' - # const_coordinates_field = fieldmodule.createFieldConstant([0.0, 0.0, 0.0]) - stickman_markers = options['Body network layout']._scaffoldSettings['Kinematic tree'] + stickman_markers = networkLayout._scaffoldSettings['Kinematic tree'] for marker_name, marker_position in stickman_markers.items(): marker_group = findOrCreateAnnotationGroupForTerm( annotationGroups, region, (marker_name, ""), isMarker=True @@ -1237,20 +1226,6 @@ def generateBaseMesh(cls, region, options): marker_group.createMarkerNode( node_identifier, coordinates, marker_position ) - # annotationGroups.append(marker_group) - # marker_locations = [] - # for marker_name, marker_position in zip([marker_names], [marker_positions]): - # const_coordinates_field.assignReal(fieldcache, marker_position) - # # marker_location = evaluateAnnotationMarkerNearestMeshLocation( - # # fieldmodule, fieldcache, marker_position, coordinates, mesh - # # ) - # marker_group = findOrCreateAnnotationGroupForTerm( - # annotationGroups, region, (marker_name, ""), isMarker=True - # ) - # markerNode = marker_group.createMarkerNode( - # node_identifier, coordinates, marker_position - # ) - # annotationGroups.append(marker_group) return annotationGroups, None @classmethod From 721cc358422c710b0ab1e6fa85aaf1a1dfe813d5 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 17:16:38 +1300 Subject: [PATCH 33/61] Separated leg abduction into left and right legs --- .../meshtypes/meshtype_3d_wholebody2.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index b01a69b9..ffc7c523 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -47,10 +47,10 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Neck length"] = 1.3 options["Shoulder drop"] = 1.0 options["Shoulder width"] = 4.5 - options["Left shoulder abduction degrees"] = 0.0 - options["Right shoulder abduction degrees"] = 0.0 - options["Left arm flexion degrees"] = 10.0 - options["Right arm flexion degrees"] = 10.0 + options["Left shoulder flexion degrees"] = 0.0 + options["Right shoulder flexion degrees"] = 0.0 + options["Left shoulder abduction degrees"] = 10.0 + options["Right shoulder abduction degrees"] = 10.0 options["Left elbow flexion degrees"] = 0.0 options["Right elbow flexion degrees"] = 0.0 options["Arm length"] = 7.5 @@ -67,7 +67,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Torso width"] = 3.2 options["Pelvis drop"] = 1.5 options["Pelvis width"] = 2.0 - options["Leg abduction degrees"] = 10.0 + options["Left leg abduction degrees"] = 10.0 + options["Right leg abduction degrees"] = 10.0 options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 @@ -94,8 +95,8 @@ def getOrderedOptionNames(cls): "Shoulder width", "Left shoulder abduction degrees", "Right shoulder abduction degrees", - "Left arm flexion degrees", - "Right arm flexion degrees", + "Left shoulder flexion degrees", + "Right shoulder flexion degrees", "Left elbow flexion degrees", "Right elbow flexion degrees", "Arm length", @@ -112,7 +113,8 @@ def getOrderedOptionNames(cls): "Torso width", "Pelvis drop", "Pelvis width", - "Leg abduction degrees", + "Left leg abduction degrees", + "Right leg abduction degrees", "Leg length", "Leg top diameter", "Leg bottom diameter", @@ -172,8 +174,8 @@ def checkOptions(cls, options): for key, angleRange in { "Left shoulder abduction degrees": (-60.0, 180.0), "Right shoulder abduction degrees": (-60.0, 180.0), - "Left arm flexion degrees": (-60.0, 200.0), - "Right arm flexion degrees": (-60.0, 200.0), + "Left shoulder flexion degrees": (-60.0, 200.0), + "Right shoulder flexion degrees": (-60.0, 200.0), "Left elbow flexion degrees": (0.0, 150.0), "Right elbow flexion degrees": (0.0, 150.0), "Left knee flexion degrees": (0.0, 140.0), @@ -181,7 +183,8 @@ def checkOptions(cls, options): "Left ankle flexion degrees": (60.0, 140.0), "Right ankle flexion degrees": (60.0, 140.0), "Arm twist angle degrees": (-90.0, 90.0), - "Leg abduction degrees": (-20.0, 60.0) + "Left leg abduction degrees": (-20.0, 60.0), + "Right leg abduction degrees": (-20.0, 60.0) }.items(): if options[key] < angleRange[0]: options[key] = angleRange[0] @@ -205,10 +208,10 @@ def generateBaseMesh(cls, region, options): neckLength = options["Neck length"] shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] - shoulderLeftFlexionRadians = math.radians(options["Left shoulder abduction degrees"]) - shoulderRightFlexionRadians = math.radians(options["Right shoulder abduction degrees"]) - armLeftAngleRadians = math.radians(options["Left arm flexion degrees"]) - armRightAngleRadians = math.radians(options["Right arm flexion degrees"]) + shoulderLeftFlexionRadians = math.radians(options["Left shoulder flexion degrees"]) + shoulderRightFlexionRadians = math.radians(options["Right shoulder flexion degrees"]) + armLeftAngleRadians = math.radians(options["Left shoulder abduction degrees"]) + armRightAngleRadians = math.radians(options["Right shoulder abduction degrees"]) elbowLeftFlexionRadians = math.radians(options["Left elbow flexion degrees"]) elbowRightFlexionRadians = math.radians(options["Right elbow flexion degrees"]) armLength = options["Arm length"] @@ -225,7 +228,8 @@ def generateBaseMesh(cls, region, options): halfTorsoWidth = 0.5 * options["Torso width"] pelvisDrop = options["Pelvis drop"] halfPelvisWidth = 0.5 * options["Pelvis width"] - legAngleRadians = math.radians(options["Leg abduction degrees"]) + leftLegAbductionRadians = math.radians(options["Left leg abduction degrees"]) + rightLegAbductionRadians = math.radians(options["Right leg abduction degrees"]) legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] @@ -747,7 +751,7 @@ def generateBaseMesh(cls, region, options): pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] pid3 = mult(pd3, innerProportionDefault) for side in (left, right): - legAngle = legAngleRadians if (side == left) else -legAngleRadians + legAngle = leftLegAbductionRadians if (side == left) else -rightLegAbductionRadians cosLegAngle = math.cos(legAngle) sinLegAngle = math.sin(legAngle) legStartY = halfPelvisWidth if (side == left) else -halfPelvisWidth From 29773330909d8a13dde574a93a38efc9679d370b Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 17:24:42 +1300 Subject: [PATCH 34/61] Cleaned up comments --- src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index ffc7c523..04f54f82 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -747,7 +747,6 @@ def generateBaseMesh(cls, region, options): legScale = nonFootLegLength / (legToFootElementsCount - 1) d12_mag = (legBottomRadius - legTopRadius) / (armToHandElementsCount - 2) d13_mag = (legBottomRadius - legTopRadius) / (armToHandElementsCount - 2) - pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] pid3 = mult(pd3, innerProportionDefault) for side in (left, right): @@ -861,18 +860,17 @@ def generateBaseMesh(cls, region, options): ankleDirn = matrix_vector_mult(ankleRotationMatrix, d1) ankleSide = d2 ankleFront = cross(ankleDirn, ankleSide) - footDirn = ankleDirn footSide = d2 footFront = matrix_vector_mult(ankleHalfRotationMatrix, d3) - # - footd1 = footDirn footd2 = set_magnitude(footSide, halfFootWidth) footd3 = footFront + # This rotation factor is used to adjust 'fatten' the d3 direction at the ankle node + # As well as adjusting the position of the node depending on the + # angle of flexion, and ensures a proper transition between lower leg and the foot rotationFactor = 2.0*math.sin(ankleFlexionRadians)*(math.sqrt(2)- 1) ankleThickness = halfFootThickness/math.sin(ankleJointAngleRadians/2) - # footd2 = matrix_vector_mult(rotationMatrixAnkle, footd2) # This positioning of the food nodes bends edge connecting the leg and the foot # Which allows the scaffold to better capture the shape of the calcaneus anklePosition = add(x, set_magnitude(d1, legScale - 1*rotationFactor*footHeight)) @@ -888,8 +886,7 @@ def generateBaseMesh(cls, region, options): ) fd2 = [d2, footd2, footd2] fd3 = [d3, - set_magnitude(footd3, - ankleThickness + legBottomRadius), + set_magnitude(footd3, ankleThickness + legBottomRadius), set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness) ] fd12 = sub(fd2[2], fd2[1]) From 209fe330499a2a450a1a2c0b5b90575531383229 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 17:54:05 +1300 Subject: [PATCH 35/61] Removed 1d human network from public scaffold list --- src/scaffoldmaker/scaffolds.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 88a8fae0..60f33f85 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -72,7 +72,6 @@ class Scaffolds(object): def __init__(self): self._allScaffoldTypes = [ MeshType_1d_bifurcationtree1, - MeshType_1d_human_body_network_layout1, MeshType_1d_network_layout1, MeshType_1d_path1, MeshType_2d_plate1, From a6df8955d38d49949a380e6135ec99b32c21495b Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 22 Oct 2025 17:55:28 +1300 Subject: [PATCH 36/61] Removed output from file --- src/scaffoldmaker/utils/human_network_layout.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 57113663..7ae3d2b7 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -130,7 +130,4 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): legs.append(legNetworkLayout) humanNetworkLayout = headNetworkLayout + necknNetworkLayout + arms[0] + arms[1] + thoraxNetworkLayout + abdomenNetworkLayout + legs[0] + legs[1] humanNetworkLayout = humanNetworkLayout[:-1] #Remove a comma at the end - return humanNetworkLayout - -constructNetworkLayoutStructure(humanElementCounts).replace(',', ',\n').splitlines() - + return humanNetworkLayout \ No newline at end of file From a3cb93e3432d4c4f23006870e94e61deec851228 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 23 Oct 2025 14:37:47 +1300 Subject: [PATCH 37/61] Corrected a numbering error on the elbow node --- src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 04f54f82..d30d5205 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -596,7 +596,7 @@ def generateBaseMesh(cls, region, options): # armStart = add(shoulderPosition,d1) # d1 = mult(armDirn, armScale) # Setting brachium coordinates - for i in range(1, brachiumElementsCount - 2): + for i in range(1, brachiumElementsCount - 1): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -680,15 +680,16 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + options['Kinematic tree']['ulna_' + side_label] = x # Antebrachium nodes starts after the elbow node antebrachiumStart = add(elbowPosition, d1) d1 = mult(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction - for i in range(brachiumElementsCount - 1, armToHandElementsCount - 1): + for i in range(brachiumElementsCount, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - 1))) + x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount))) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: @@ -720,6 +721,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Hand twist + options['Kinematic tree']['wrist_' + side_label] = x assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -741,6 +743,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) setNodeFieldParameters(innerCoordinates, fieldcache, hx, hd1, hid2, hid3) nodeIdentifier += 1 + # legs legStartX = abdomenStartX + abdomenLength + pelvisDrop nonFootLegLength = legLength - footHeight From ac6573af685a72b026531369cf1ff9688304ac4f Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Thu, 30 Oct 2025 14:00:54 +1300 Subject: [PATCH 38/61] Updated the elbow code to have elbow node right in the middle --- .../meshtypes/meshtype_3d_wholebody2.py | 135 ++++++++++++------ .../utils/human_network_layout.py | 2 +- 2 files changed, 92 insertions(+), 45 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index d30d5205..257a3974 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -51,8 +51,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Right shoulder flexion degrees"] = 0.0 options["Left shoulder abduction degrees"] = 10.0 options["Right shoulder abduction degrees"] = 10.0 - options["Left elbow flexion degrees"] = 0.0 - options["Right elbow flexion degrees"] = 0.0 + options["Left elbow flexion degrees"] = 110.0 + options["Right elbow flexion degrees"] = 90.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -492,9 +492,10 @@ def generateBaseMesh(cls, region, options): shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians nonHandArmLength = armLength - handLength - armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count - d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) - d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) + shoulderElementCount = 2 + armScale = nonHandArmLength / (armToHandElementsCount - shoulderElementCount) + d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - shoulderElementCount) + d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - shoulderElementCount) armAngle = armAngleRadians if (side == left) else -armAngleRadians cosArmAngle = math.cos(armAngle) sinArmAngle = math.sin(armAngle) @@ -596,7 +597,7 @@ def generateBaseMesh(cls, region, options): # armStart = add(shoulderPosition,d1) # d1 = mult(armDirn, armScale) # Setting brachium coordinates - for i in range(1, brachiumElementsCount - 1): + for i in range(1, brachiumElementsCount - shoulderElementCount - 1): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -628,11 +629,17 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Elbow node field parameters are allocated separately from the rest of the arm + # Diagram to calculate elbow flexion + # 1 -- 2 -- 3 -- 4 -- 5 + # 1 and 2 are brachium, 3 is the elbow, 4 and 5 are antebrachium + # we fix the position of the nodes 1, 3 and 5, and calculate + # the position of 2 and 4 using the sampleCubicHermiteCurvesSmooth function. + # This process also gives us the correct d1 and d3 directions for nodes 1 to 5. + # Calculating initial d2 and d3 before rotation # Necessary in case there is a non-zero twist angle if twistAngle == 0.0: - d2 = armSide + d2 = armSide d3 = armFront else: cosTwistAngle = math.cos(twistAngle) @@ -646,50 +653,90 @@ def generateBaseMesh(cls, region, options): elbowJointAngleRadians = math.pi - elbowFlexionRadians elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) + # Calculating initial estimation for directions at the elbow node + elbowDirn = matrix_vector_mult(elbowHalfRotationMatrix, d1) + elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) + elbowSide = d2 + # Calculating direction for the antebrachium antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) - # The d3 direction in the elbow node is rotated by half this angle - # To ensure a better transition at this node. - elbowDirn = antebrachiumDirn - elbowSide = d2 - elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) - # This rotation factor is used to adjust the position of the knee node relative - # to the angle of flexion, and ensures a proper transition between the upper and lower leg - rotationFactor = 1.0*math.sin(elbowFlexionRadians)*(math.sqrt(2)-1) - i += 1 - xi = i / (armToHandElementsCount - 2) - # The elbow node uses a special width value - # Which 'fattens' the scaffold around the elbow depending on the level of rotation - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - halfWidth = halfWidth/math.sin(elbowJointAngleRadians/2) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - elbowPosition = add(x, set_magnitude(d1, armScale - rotationFactor*halfWidth)) - x = elbowPosition - d1 = set_magnitude(elbowDirn, armScale + rotationFactor*halfWidth) - d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, halfWidth) - d12 = set_magnitude(elbowSide, d12_mag) - d13 = set_magnitude(elbowFront, d13_mag) - id2 = mult(d2, innerProportionDefault) - id3 = mult(d3, innerProportionDefault) - id12 = mult(d12, innerProportionDefault) - id13 = mult(d13, innerProportionDefault) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) - nodeIdentifier += 1 + # This rotation factor is used to adjust the position of the joint node relative + # to the angle of flexion, and ensures a proper transition between the two parts + rotationFactor = math.sin(elbowFlexionRadians)*(math.sqrt(2)-1) + transitionNodes = 3 + jointPositions = [x] # 1 + elbowPosition = add(jointPositions[-1], set_magnitude(armDirn, armScale)) + elbowPosition = add(elbowPosition, set_magnitude(armDirn, armScale)) + exi = (i+2) / (armToHandElementsCount - 2) + ehalfdWidth = exi * halfWristWidth + (1.0 - exi) * armTopRadius + ehalfdWidth = ehalfdWidth * rotationFactor + jointPositions.append(add(elbowPosition, set_magnitude(elbowFront, ehalfdWidth))) + elbowPosition = add(elbowPosition, set_magnitude(antebrachiumDirn, armScale)) + elbowPosition = add(elbowPosition, set_magnitude(antebrachiumDirn, armScale)) + jointPositions.append(jointPositions) + jointDir = [d1, elbowDirn, antebrachiumDirn] + jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( + jointPositions, jointDir, transitionNodes + 1, + derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0:2] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, jointDirn[0]) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, jointDirn[0]) + # x0 = (i) / (armToHandElementsCount - 2) + # hw0 = x0 * halfWristWidth + (1.0 - x0) * armTopRadius + # x1 = (i+1) / (armToHandElementsCount - 2) + # hw1 = x1 * halfWristWidth + (1.0 - x1) * armTopRadius + # x3 = (i+3) / (armToHandElementsCount - 2) + # hw3 = x3 * halfWristWidth + (1.0 - x3) * armTopRadius + # x4 = (i+4) / (armToHandElementsCount - 2) + # hw4 = x4 * halfWristWidth + (1.0 - x4) * armTopRadius + # jointWidths = [ + # add(elbowNodes[0], set_magnitude(cross(elbowD1[0], d2), hw0)), + # add(elbowNodes[1], set_magnitude(cross(elbowD1[2], d2), hw1)), + # add(elbowNodes[3], set_magnitude(cross(elbowD1[2], d2), hw3)), + # add(elbowNodes[4], set_magnitude(cross(elbowD1[4], d2), hw4)), + # ] + # elbowD12 = [ + # elbowD1[0], + # elbowD1[1], + # elbowD1[3], + # elbowD1[4] + # ] + # elbowD3mag = sampleCubicHermiteCurvesSmooth( + # jointWidths, elbowD12, transitionNodes + 1, + # derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0][2] + for j in range(transitionNodes): + i += 1 + xi = i / (armToHandElementsCount - 2) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + if (j == 1): + halfWidth = ((0.9)*halfWidth)/math.sin(elbowJointAngleRadians/2) + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + x = jointPositions[j+1] + d1 = jointDirn[j+1] + elbowFront = cross(d1, d2) + d2 = set_magnitude(elbowSide, halfThickness) + d3 = set_magnitude(elbowFront, halfWidth) + d12 = set_magnitude(elbowSide, d12_mag) + d13 = set_magnitude(elbowFront, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 options['Kinematic tree']['ulna_' + side_label] = x # Antebrachium nodes starts after the elbow node - antebrachiumStart = add(elbowPosition, d1) + antebrachiumStart = jointPositions[-1] d1 = mult(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction - for i in range(brachiumElementsCount, armToHandElementsCount - 1): + for i in range(brachiumElementsCount - 2 + transitionNodes - 1, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount))) + x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - 2 + transitionNodes - 1))) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: @@ -950,8 +997,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 - options["Number of elements along brachium"] = 3 - options["Number of elements along antebrachium"] = 2 + options["Number of elements along brachium"] = 5 + options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along upper leg"] = 3 options["Number of elements along lower leg"] = 2 diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 7ae3d2b7..b200898b 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -3,7 +3,7 @@ humanElementCounts = { 'headElementsCount': 3, 'neckElementsCount': 2, - 'brachiumElementsCount': 4, + 'brachiumElementsCount': 5, 'antebrachiumElementsCount': 3, 'handElementsCount': 1, 'thoraxElementsCount': 3, From 754ad758d30c795ca6e6fca07e65d7a35df619c4 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 31 Oct 2025 12:48:58 +1300 Subject: [PATCH 39/61] Stable version of elbow with minimal bump --- .../meshtypes/meshtype_3d_wholebody2.py | 114 ++++++++---------- 1 file changed, 47 insertions(+), 67 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 257a3974..09c5d1bc 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -597,7 +597,7 @@ def generateBaseMesh(cls, region, options): # armStart = add(shoulderPosition,d1) # d1 = mult(armDirn, armScale) # Setting brachium coordinates - for i in range(1, brachiumElementsCount - shoulderElementCount - 1): + for i in range(1, brachiumElementsCount - shoulderElementCount): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -630,10 +630,12 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Diagram to calculate elbow flexion - # 1 -- 2 -- 3 -- 4 -- 5 - # 1 and 2 are brachium, 3 is the elbow, 4 and 5 are antebrachium - # we fix the position of the nodes 1, 3 and 5, and calculate - # the position of 2 and 4 using the sampleCubicHermiteCurvesSmooth function. + # 1 + # | + # 2 -- 3 + # 1 is brachium, 2 is the elbow, 4 is antebrachium + # we fix the position of the nodes 1 and 3, and calculate + # the position of 2 using the sampleCubicHermiteCurvesSmooth function. # This process also gives us the correct d1 and d3 directions for nodes 1 to 5. # Calculating initial d2 and d3 before rotation @@ -665,78 +667,57 @@ def generateBaseMesh(cls, region, options): # to the angle of flexion, and ensures a proper transition between the two parts rotationFactor = math.sin(elbowFlexionRadians)*(math.sqrt(2)-1) transitionNodes = 3 - jointPositions = [x] # 1 - elbowPosition = add(jointPositions[-1], set_magnitude(armDirn, armScale)) - elbowPosition = add(elbowPosition, set_magnitude(armDirn, armScale)) - exi = (i+2) / (armToHandElementsCount - 2) + jointPositions = [] + # jointPositions.append(sub(x, d1)) #1 + jointPositions.append(x) + exi = (i+1) / (armToHandElementsCount - 2) ehalfdWidth = exi * halfWristWidth + (1.0 - exi) * armTopRadius ehalfdWidth = ehalfdWidth * rotationFactor - jointPositions.append(add(elbowPosition, set_magnitude(elbowFront, ehalfdWidth))) - elbowPosition = add(elbowPosition, set_magnitude(antebrachiumDirn, armScale)) - elbowPosition = add(elbowPosition, set_magnitude(antebrachiumDirn, armScale)) - jointPositions.append(jointPositions) - jointDir = [d1, elbowDirn, antebrachiumDirn] + eDir = add(set_magnitude(armDirn, armScale), set_magnitude(elbowFront, ehalfdWidth)) + eDir = set_magnitude(eDir, armScale) + jointPositions.append(add(x, eDir)) # 2 + eDir = add(set_magnitude(elbowFront, -ehalfdWidth), set_magnitude(antebrachiumDirn, armScale)) + eDir = set_magnitude(eDir, armScale) + jointPositions.append(add(jointPositions[-1], eDir)) #3 + # jointPositions.append(add(jointPositions[-1], set_magnitude(antebrachiumDirn, armScale))) #3 + jointDir = [armDirn, armDirn, elbowDirn, antebrachiumDirn, antebrachiumDirn] + jointDir = [armDirn, elbowDirn, antebrachiumDirn] jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - jointPositions, jointDir, transitionNodes + 1, - derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0:2] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, jointDirn[0]) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, jointDirn[0]) - # x0 = (i) / (armToHandElementsCount - 2) - # hw0 = x0 * halfWristWidth + (1.0 - x0) * armTopRadius - # x1 = (i+1) / (armToHandElementsCount - 2) - # hw1 = x1 * halfWristWidth + (1.0 - x1) * armTopRadius - # x3 = (i+3) / (armToHandElementsCount - 2) - # hw3 = x3 * halfWristWidth + (1.0 - x3) * armTopRadius - # x4 = (i+4) / (armToHandElementsCount - 2) - # hw4 = x4 * halfWristWidth + (1.0 - x4) * armTopRadius - # jointWidths = [ - # add(elbowNodes[0], set_magnitude(cross(elbowD1[0], d2), hw0)), - # add(elbowNodes[1], set_magnitude(cross(elbowD1[2], d2), hw1)), - # add(elbowNodes[3], set_magnitude(cross(elbowD1[2], d2), hw3)), - # add(elbowNodes[4], set_magnitude(cross(elbowD1[4], d2), hw4)), - # ] - # elbowD12 = [ - # elbowD1[0], - # elbowD1[1], - # elbowD1[3], - # elbowD1[4] - # ] - # elbowD3mag = sampleCubicHermiteCurvesSmooth( - # jointWidths, elbowD12, transitionNodes + 1, - # derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0][2] - for j in range(transitionNodes): - i += 1 - xi = i / (armToHandElementsCount - 2) - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - if (j == 1): - halfWidth = ((0.9)*halfWidth)/math.sin(elbowJointAngleRadians/2) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - x = jointPositions[j+1] - d1 = jointDirn[j+1] - elbowFront = cross(d1, d2) - d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, halfWidth) - d12 = set_magnitude(elbowSide, d12_mag) - d13 = set_magnitude(elbowFront, d13_mag) - id2 = mult(d2, innerProportionDefault) - id3 = mult(d3, innerProportionDefault) - id12 = mult(d12, innerProportionDefault) - id13 = mult(d13, innerProportionDefault) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) - nodeIdentifier += 1 + jointPositions, jointDir, 2, derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0:2] + j = 1 + i += 1 + xi = i / (armToHandElementsCount - 2) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # if (j == 1): + halfWidth = (halfWidth)*(1/math.sin(elbowJointAngleRadians/2) - 0.5*rotationFactor) + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + x = jointPositions[j] + d1 = jointDirn[j] + elbowFront = cross(d1, d2) + d2 = set_magnitude(elbowSide, halfThickness) + d3 = set_magnitude(elbowFront, halfWidth) + d12 = set_magnitude(elbowSide, d12_mag) + d13 = set_magnitude(elbowFront, d13_mag) + d13 = add(d13, set_magnitude(d1, -2*halfWidth*math.sin(elbowJointAngleRadians/2))) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 options['Kinematic tree']['ulna_' + side_label] = x # Antebrachium nodes starts after the elbow node antebrachiumStart = jointPositions[-1] d1 = mult(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction - for i in range(brachiumElementsCount - 2 + transitionNodes - 1, armToHandElementsCount - 1): + for i in range(brachiumElementsCount - shoulderElementCount + transitionNodes - 2, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - 2 + transitionNodes - 1))) + x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - shoulderElementCount + transitionNodes - 2))) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: @@ -790,7 +771,6 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) setNodeFieldParameters(innerCoordinates, fieldcache, hx, hd1, hid2, hid3) nodeIdentifier += 1 - # legs legStartX = abdomenStartX + abdomenLength + pelvisDrop nonFootLegLength = legLength - footHeight From 85a1316082cd72fa80e1af0d2c997f656d6a431e Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 31 Oct 2025 13:55:35 +1300 Subject: [PATCH 40/61] eliminate the transitionNodes variable --- src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 09c5d1bc..5dde9ef7 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -666,7 +666,6 @@ def generateBaseMesh(cls, region, options): # This rotation factor is used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts rotationFactor = math.sin(elbowFlexionRadians)*(math.sqrt(2)-1) - transitionNodes = 3 jointPositions = [] # jointPositions.append(sub(x, d1)) #1 jointPositions.append(x) @@ -679,8 +678,6 @@ def generateBaseMesh(cls, region, options): eDir = add(set_magnitude(elbowFront, -ehalfdWidth), set_magnitude(antebrachiumDirn, armScale)) eDir = set_magnitude(eDir, armScale) jointPositions.append(add(jointPositions[-1], eDir)) #3 - # jointPositions.append(add(jointPositions[-1], set_magnitude(antebrachiumDirn, armScale))) #3 - jointDir = [armDirn, armDirn, elbowDirn, antebrachiumDirn, antebrachiumDirn] jointDir = [armDirn, elbowDirn, antebrachiumDirn] jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( jointPositions, jointDir, 2, derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0:2] @@ -713,11 +710,11 @@ def generateBaseMesh(cls, region, options): antebrachiumStart = jointPositions[-1] d1 = mult(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction - for i in range(brachiumElementsCount - shoulderElementCount + transitionNodes - 2, armToHandElementsCount - 1): + for i in range(brachiumElementsCount - shoulderElementCount + 1, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - shoulderElementCount + transitionNodes - 2))) + x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - shoulderElementCount + 1))) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: From d24cdd300e43256caad755cc550ae421af75f660 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 31 Oct 2025 14:37:01 +1300 Subject: [PATCH 41/61] look good enough here --- .../meshtypes/meshtype_3d_wholebody2.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 5dde9ef7..d4295897 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -665,37 +665,32 @@ def generateBaseMesh(cls, region, options): antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) # This rotation factor is used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts - rotationFactor = math.sin(elbowFlexionRadians)*(math.sqrt(2)-1) + rotationFactor = math.sin(elbowFlexionRadians)*(0.5) + rotationFactor2 = 1/math.sin(elbowJointAngleRadians/2) jointPositions = [] # jointPositions.append(sub(x, d1)) #1 jointPositions.append(x) - exi = (i+1) / (armToHandElementsCount - 2) - ehalfdWidth = exi * halfWristWidth + (1.0 - exi) * armTopRadius - ehalfdWidth = ehalfdWidth * rotationFactor - eDir = add(set_magnitude(armDirn, armScale), set_magnitude(elbowFront, ehalfdWidth)) + i += 1 + xi = i / (armToHandElementsCount - 2) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + eDir = add(set_magnitude(armDirn, armScale), set_magnitude(elbowFront, halfWidth*rotationFactor)) eDir = set_magnitude(eDir, armScale) jointPositions.append(add(x, eDir)) # 2 - eDir = add(set_magnitude(elbowFront, -ehalfdWidth), set_magnitude(antebrachiumDirn, armScale)) + eDir = add(set_magnitude(elbowFront, -halfWidth*rotationFactor), set_magnitude(antebrachiumDirn, armScale)) eDir = set_magnitude(eDir, armScale) jointPositions.append(add(jointPositions[-1], eDir)) #3 jointDir = [armDirn, elbowDirn, antebrachiumDirn] jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( jointPositions, jointDir, 2, derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0:2] - j = 1 - i += 1 - xi = i / (armToHandElementsCount - 2) - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # if (j == 1): - halfWidth = (halfWidth)*(1/math.sin(elbowJointAngleRadians/2) - 0.5*rotationFactor) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - x = jointPositions[j] - d1 = jointDirn[j] + x = jointPositions[1] + d1 = jointDirn[1] elbowFront = cross(d1, d2) d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, halfWidth) + d3 = set_magnitude(elbowFront, halfWidth*(rotationFactor2-0.5*rotationFactor)) d12 = set_magnitude(elbowSide, d12_mag) d13 = set_magnitude(elbowFront, d13_mag) - d13 = add(d13, set_magnitude(d1, -2*halfWidth*math.sin(elbowJointAngleRadians/2))) + d13 = add(d13, set_magnitude(d1, -halfWidth*rotationFactor2)) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) From 1b4dc08b32f92075f9dfe823231789f4559c99c9 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 31 Oct 2025 16:39:55 +1300 Subject: [PATCH 42/61] Cleaned up code, rotationFactors are standarized --- .../meshtypes/meshtype_3d_wholebody2.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index d4295897..ee166991 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -629,14 +629,13 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Diagram to calculate elbow flexion + # Diagram to calculate joint flexion # 1 # | # 2 -- 3 # 1 is brachium, 2 is the elbow, 4 is antebrachium # we fix the position of the nodes 1 and 3, and calculate - # the position of 2 using the sampleCubicHermiteCurvesSmooth function. - # This process also gives us the correct d1 and d3 directions for nodes 1 to 5. + # the position (and d1) of 2 using the sampleCubicHermiteCurvesSmooth function. # Calculating initial d2 and d3 before rotation # Necessary in case there is a non-zero twist angle @@ -663,21 +662,20 @@ def generateBaseMesh(cls, region, options): antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, armDirn) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) - # This rotation factor is used to adjust the position of the joint node relative + # These rotation factors are used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts - rotationFactor = math.sin(elbowFlexionRadians)*(0.5) + rotationFactor = (0.5)*math.sin(elbowFlexionRadians) rotationFactor2 = 1/math.sin(elbowJointAngleRadians/2) jointPositions = [] - # jointPositions.append(sub(x, d1)) #1 - jointPositions.append(x) + jointPositions.append(x) # 1 i += 1 xi = i / (armToHandElementsCount - 2) halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - eDir = add(set_magnitude(armDirn, armScale), set_magnitude(elbowFront, halfWidth*rotationFactor)) + eDir = add(set_magnitude(armDirn, armScale), set_magnitude(elbowFront, 1.5*halfWidth*rotationFactor)) eDir = set_magnitude(eDir, armScale) jointPositions.append(add(x, eDir)) # 2 - eDir = add(set_magnitude(elbowFront, -halfWidth*rotationFactor), set_magnitude(antebrachiumDirn, armScale)) + eDir = add(set_magnitude(elbowFront, -1.5*halfWidth*rotationFactor), set_magnitude(antebrachiumDirn, armScale)) eDir = set_magnitude(eDir, armScale) jointPositions.append(add(jointPositions[-1], eDir)) #3 jointDir = [armDirn, elbowDirn, antebrachiumDirn] @@ -687,7 +685,7 @@ def generateBaseMesh(cls, region, options): d1 = jointDirn[1] elbowFront = cross(d1, d2) d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, halfWidth*(rotationFactor2-0.5*rotationFactor)) + d3 = set_magnitude(elbowFront, halfWidth*(rotationFactor2-(1/1.5)*rotationFactor)) d12 = set_magnitude(elbowSide, d12_mag) d13 = set_magnitude(elbowFront, d13_mag) d13 = add(d13, set_magnitude(d1, -halfWidth*rotationFactor2)) From fdc9906a6da24730f67e38bd3df96e86de5284aa Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 3 Nov 2025 12:16:50 +1300 Subject: [PATCH 43/61] Cleaned up elbow rotation factor and elbow position --- .../meshtypes/meshtype_3d_wholebody2.py | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index ee166991..9fc529d8 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -572,7 +572,7 @@ def generateBaseMesh(cls, region, options): shoulderFront = matrix_vector_mult(shoulderRotationMatrix, d3) # This rotation factor is used to adjust the position of the knee node relative # to the angle of flexion, and ensures a proper transition between the upper and lower leg - rotationFactor = 1.0*math.sin(shoulderFlexionRadians)*(math.sqrt(2)-1) + flexionRotFactor = 1.0*math.sin(shoulderFlexionRadians)*(math.sqrt(2)-1) i = 0 xi = i / (armToHandElementsCount - 2) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius @@ -635,10 +635,11 @@ def generateBaseMesh(cls, region, options): # 2 -- 3 # 1 is brachium, 2 is the elbow, 4 is antebrachium # we fix the position of the nodes 1 and 3, and calculate - # the position (and d1) of 2 using the sampleCubicHermiteCurvesSmooth function. - + # the position (and d1) of 2 using the sampleCubicHermiteCurvesSmooth function. + # The frame at the joint node is rotated by half the flexion angle # Calculating initial d2 and d3 before rotation # Necessary in case there is a non-zero twist angle + d1 = armDirn if twistAngle == 0.0: d2 = armSide d3 = armFront @@ -659,36 +660,59 @@ def generateBaseMesh(cls, region, options): elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) elbowSide = d2 # Calculating direction for the antebrachium - antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, armDirn) + antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) antebrachiumSide = armSide antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) # These rotation factors are used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts - rotationFactor = (0.5)*math.sin(elbowFlexionRadians) - rotationFactor2 = 1/math.sin(elbowJointAngleRadians/2) + rotationCoeff = 0.25 + flexionRotFactor = 1*math.sin(elbowJointAngleRadians) + jointRotFactor = 1/math.sin(elbowJointAngleRadians/2) jointPositions = [] jointPositions.append(x) # 1 i += 1 xi = i / (armToHandElementsCount - 2) halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - eDir = add(set_magnitude(armDirn, armScale), set_magnitude(elbowFront, 1.5*halfWidth*rotationFactor)) - eDir = set_magnitude(eDir, armScale) - jointPositions.append(add(x, eDir)) # 2 - eDir = add(set_magnitude(elbowFront, -1.5*halfWidth*rotationFactor), set_magnitude(antebrachiumDirn, armScale)) - eDir = set_magnitude(eDir, armScale) - jointPositions.append(add(jointPositions[-1], eDir)) #3 + # The joint node is not set directly at the corner + # Instead it is nudged forward in the d3 direction + # To get as smooth of a line as possible between node #1 and #3 + jointAdjustDir = add( + set_magnitude(elbowFront, rotationCoeff*halfWidth*flexionRotFactor), + set_magnitude(antebrachiumDirn, rotationCoeff*halfWidth*flexionRotFactor), + ) + jointAdjustDir = add( + set_magnitude(armDirn, armScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, armScale) + jointPositions.append(add(x, jointAdjustDir)) # 2 + jointAdjustDir = add( + set_magnitude(elbowDirn, rotationCoeff*halfWidth*flexionRotFactor), + set_magnitude(armDirn, rotationCoeff*halfWidth*flexionRotFactor), + ) + jointAdjustDir = add( + set_magnitude(antebrachiumDirn, armScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, armScale) + jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 jointDir = [armDirn, elbowDirn, antebrachiumDirn] jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - jointPositions, jointDir, 2, derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale)[0:2] + jointPositions, jointDir, 2, + derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale + )[0:2] + # Set coordiantes for joint node x = jointPositions[1] d1 = jointDirn[1] elbowFront = cross(d1, d2) d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, halfWidth*(rotationFactor2-(1/1.5)*rotationFactor)) + d3 = set_magnitude(elbowFront, halfWidth*(jointRotFactor-(rotationCoeff*flexionRotFactor))) d12 = set_magnitude(elbowSide, d12_mag) - d13 = set_magnitude(elbowFront, d13_mag) - d13 = add(d13, set_magnitude(d1, -halfWidth*rotationFactor2)) + d13 = add( + set_magnitude(elbowFront, d13_mag), + set_magnitude(d1, -halfWidth*jointRotFactor) + ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) From 9eab8bd5de7ae03a2b4b27ae8c41f80e0f963d5a Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 3 Nov 2025 14:18:16 +1300 Subject: [PATCH 44/61] Replicated elbow code at the knee, needs some tweaking still. --- .../meshtypes/meshtype_3d_wholebody2.py | 106 +++++++++++++----- 1 file changed, 78 insertions(+), 28 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 9fc529d8..e4362ee7 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -51,8 +51,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Right shoulder flexion degrees"] = 0.0 options["Left shoulder abduction degrees"] = 10.0 options["Right shoulder abduction degrees"] = 10.0 - options["Left elbow flexion degrees"] = 110.0 - options["Right elbow flexion degrees"] = 90.0 + options["Left elbow flexion degrees"] = 0.0 + options["Right elbow flexion degrees"] = 0.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -72,8 +72,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee flexion degrees"] = 0.0 - options["Right knee flexion degrees"] = 0.0 + options["Left knee flexion degrees"] = 90.0 + options["Right knee flexion degrees"] = 110.0 options["Left ankle flexion degrees"] = 90.0 options["Right ankle flexion degrees"] = 90.0 options["Foot height"] = 1.25 @@ -675,7 +675,7 @@ def generateBaseMesh(cls, region, options): halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius # The joint node is not set directly at the corner - # Instead it is nudged forward in the d3 direction + # Instead it is nudged forward towards the center of the joint # To get as smooth of a line as possible between node #1 and #3 jointAdjustDir = add( set_magnitude(elbowFront, rotationCoeff*halfWidth*flexionRotFactor), @@ -725,7 +725,7 @@ def generateBaseMesh(cls, region, options): options['Kinematic tree']['ulna_' + side_label] = x # Antebrachium nodes starts after the elbow node antebrachiumStart = jointPositions[-1] - d1 = mult(antebrachiumDirn, armScale) + d1 = set_magnitude(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction for i in range(brachiumElementsCount - shoulderElementCount + 1, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) @@ -821,7 +821,7 @@ def generateBaseMesh(cls, region, options): d13 = [0.0, 0.0, d13_mag] id13 = mult(d13, innerProportionDefault) # Upper leg - for i in range(upperLegElementsCount): + for i in range(upperLegElementsCount-1): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -835,35 +835,80 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # knee - # Updating frame of reference wrt rotation angle (using d2 as rotation axis) + # Diagram to calculate joint flexion + # 1 + # | + # 3 - 2 + # 1 is upper leg, 2 is the knee, 3 is the lower leg + # we fix the position of the nodes 1 and 3, and calculate + # the position (and d1) of 2 using the sampleCubicHermiteCurvesSmooth function. + # The frame at the joint node is rotated by half the flexion angle + # Calculating initial d2 and d3 before rotation + # Necessary in case there is a non-zero twist angle kneeFlexionRadians = kneeLeftFlexionRadians if (side == left) else kneeRightFlexionRadians # Angle between upper and lower leg kneeJointAngleRadians = math.pi - kneeFlexionRadians kneeRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeFlexionRadians) kneeHalfrotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeFlexionRadians/2) + # Calculating initial estimation for directions at the elbow node + kneeDirn = matrix_vector_mult(kneeHalfrotationMatrix, d1) + kneeSide = legSide + kneeFront = matrix_vector_mult(kneeHalfrotationMatrix, d3) + # Calculating directions for the lower leg lowerLegDirn = matrix_vector_mult(kneeRotationMatrix, d1) lowerLegSide = legSide lowerLegFront = cross(lowerLegDirn, lowerLegSide) - # The d3 direction in the knee node is rotated by half the flexion angle - # To ensure a better transition at this node. - kneeDirn = lowerLegDirn - kneeSide = d2 - kneeFront = matrix_vector_mult(kneeHalfrotationMatrix, d3) - # This rotation factor is used to adjust the position of the knee node relative - # to the angle of flexion, and ensures a proper transition between the upper and lower leg - rotationFactor = 2.0*math.sin(kneeFlexionRadians)*(math.sqrt(2)-1) + # These rotation factors are used to adjust the position of the joint node relative + # to the angle of flexion, and ensures a proper transition between the two parts + rotationCoeff = 0.3 + flexionRotFactor = 1*math.sin(kneeJointAngleRadians) + jointRotFactor = 1/math.sin(kneeJointAngleRadians/2) + jointPositions = [] + jointPositions.append(x) #1 i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - # d3 direction is fattened at the joint to ensure a proper transition in the tube network - kneeRadius = radius/math.sin(kneeJointAngleRadians/2) - kneePosition = add(x, set_magnitude(d1, legScale - rotationFactor*kneeRadius)) - x = kneePosition - d1 = set_magnitude(kneeDirn, legScale + rotationFactor*kneeRadius) + # The joint node is not set directly at the corner + # Instead it is nudged forward towards the center of the joint + # To get as smooth of a line as possible between node #1 and #3 + jointAdjustDir = add( + set_magnitude(kneeFront, -rotationCoeff*radius*flexionRotFactor), + set_magnitude(lowerLegDirn, rotationCoeff*radius*flexionRotFactor), + ) + jointAdjustDir = add( + set_magnitude(legDirn, legScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, legScale) + jointPositions.append(add(x, jointAdjustDir)) # 2 + jointAdjustDir = add( + set_magnitude(kneeDirn, rotationCoeff*radius*flexionRotFactor), + set_magnitude(legDirn, rotationCoeff*radius*flexionRotFactor), + ) + jointAdjustDir = add( + set_magnitude(lowerLegDirn, legScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, legScale) + jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 + jointDir = [legDirn, kneeDirn, lowerLegDirn] + jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( + jointPositions, jointDir, 2, + derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale + )[0:2] + # Set coordiantes for joint node + x = jointPositions[1] + d1 = jointDirn[1] + kneeFront = cross(d1, d2) d2 = set_magnitude(kneeSide, radius) - d3 = set_magnitude(kneeFront, kneeRadius) + d3 = set_magnitude(kneeFront, radius*(jointRotFactor-(rotationCoeff*flexionRotFactor))) d12 = set_magnitude(kneeSide, d12_mag) - d13 = set_magnitude(kneeFront, d13_mag) + d13 = set_magnitude(d1, legScale/jointRotFactor) + + # d13 = add( + # set_magnitude(kneeFront, d13_mag), + # set_magnitude(d1, 1.1*radius*jointRotFactor) + # ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) @@ -873,22 +918,27 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + options['Kinematic tree']['knee_' + side_label] = x # Lower leg - lowerLegStart = add(kneePosition, d1) + lowerLegStart = jointPositions[-1] d1 = set_magnitude(lowerLegDirn, legScale) - for i in range(upperLegElementsCount + 1, legToFootElementsCount): + for i in range(upperLegElementsCount, legToFootElementsCount): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(lowerLegStart, mult(d1, i - (upperLegElementsCount + 1))) + x = add(lowerLegStart, mult(d1, i - (upperLegElementsCount))) # if (i == legToFootElementsCount): # # x = add(x, set_magnitude(d1, 1.5 * radius)) # d1 = set_magnitude(d1, legScale + 1.5 * radius) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = set_magnitude(lowerLegSide, radius) d3 = set_magnitude(lowerLegFront, radius) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) + id12 = set_magnitude(d12, d12_mag) + id13 = set_magnitude(d13, d13_mag) setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 @@ -994,8 +1044,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 5 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 - options["Number of elements along upper leg"] = 3 - options["Number of elements along lower leg"] = 2 + options["Number of elements along upper leg"] = 4 + options["Number of elements along lower leg"] = 3 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 From 00c8c7e8ec8f81ad1980778688b3b63eda2eb7a6 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 3 Nov 2025 14:18:31 +1300 Subject: [PATCH 45/61] Updated number of elements for leg --- src/scaffoldmaker/utils/human_network_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index b200898b..5a2d2d48 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -8,8 +8,8 @@ 'handElementsCount': 1, 'thoraxElementsCount': 3, 'abdomenElementsCount': 4, - 'upperLegElementsCount': 3, - 'lowerLegElementsCount': 2, + 'upperLegElementsCount': 4, + 'lowerLegElementsCount': 3, 'footElementsCount': 2 } From cc2ada59686d2058ef59cfc96be6dcf71e2cfe13 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 5 Nov 2025 14:53:36 +1300 Subject: [PATCH 46/61] Added initial estimation for ankle flexion (still needs work) --- .../meshtypes/meshtype_3d_wholebody2.py | 187 ++++++++++++------ 1 file changed, 129 insertions(+), 58 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index e4362ee7..881254c6 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -51,8 +51,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Right shoulder flexion degrees"] = 0.0 options["Left shoulder abduction degrees"] = 10.0 options["Right shoulder abduction degrees"] = 10.0 - options["Left elbow flexion degrees"] = 0.0 - options["Right elbow flexion degrees"] = 0.0 + options["Left elbow flexion degrees"] = 90.0 + options["Right elbow flexion degrees"] = 110.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -72,8 +72,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee flexion degrees"] = 90.0 - options["Right knee flexion degrees"] = 110.0 + options["Left knee flexion degrees"] = 0.0 + options["Right knee flexion degrees"] = 0.0 options["Left ankle flexion degrees"] = 90.0 options["Right ankle flexion degrees"] = 90.0 options["Foot height"] = 1.25 @@ -668,6 +668,7 @@ def generateBaseMesh(cls, region, options): rotationCoeff = 0.25 flexionRotFactor = 1*math.sin(elbowJointAngleRadians) jointRotFactor = 1/math.sin(elbowJointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(elbowFlexionRadians/2) jointPositions = [] jointPositions.append(x) # 1 i += 1 @@ -711,7 +712,7 @@ def generateBaseMesh(cls, region, options): d12 = set_magnitude(elbowSide, d12_mag) d13 = add( set_magnitude(elbowFront, d13_mag), - set_magnitude(d1, -halfWidth*jointRotFactor) + set_magnitude(d1, -halfWidth*d13RotFactor) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -860,9 +861,10 @@ def generateBaseMesh(cls, region, options): lowerLegFront = cross(lowerLegDirn, lowerLegSide) # These rotation factors are used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.3 + rotationCoeff = 0.25 flexionRotFactor = 1*math.sin(kneeJointAngleRadians) jointRotFactor = 1/math.sin(kneeJointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(kneeFlexionRadians/2) jointPositions = [] jointPositions.append(x) #1 i += 1 @@ -901,14 +903,12 @@ def generateBaseMesh(cls, region, options): d1 = jointDirn[1] kneeFront = cross(d1, d2) d2 = set_magnitude(kneeSide, radius) - d3 = set_magnitude(kneeFront, radius*(jointRotFactor-(rotationCoeff*flexionRotFactor))) + d3 = set_magnitude(kneeFront, radius*(jointRotFactor-(0.35*flexionRotFactor))) d12 = set_magnitude(kneeSide, d12_mag) - d13 = set_magnitude(d1, legScale/jointRotFactor) - - # d13 = add( - # set_magnitude(kneeFront, d13_mag), - # set_magnitude(d1, 1.1*radius*jointRotFactor) - # ) + d13 = add( + set_magnitude(d3, d13_mag), + set_magnitude(d1, radius*d13RotFactor) + ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) @@ -949,54 +949,125 @@ def generateBaseMesh(cls, region, options): ankleJointAngleRadians = math.pi - ankleFlexionRadians ankleRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleFlexionRadians)) ankleHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleFlexionRadians/2)) - # The d3 direction in the ankle node is rotated by half this angle - # To ensure a better transition at this node. - ankleDirn = matrix_vector_mult(ankleRotationMatrix, d1) - ankleSide = d2 - ankleFront = cross(ankleDirn, ankleSide) - footDirn = ankleDirn - footSide = d2 - footFront = matrix_vector_mult(ankleHalfRotationMatrix, d3) - footd1 = footDirn - footd2 = set_magnitude(footSide, halfFootWidth) - footd3 = footFront - # This rotation factor is used to adjust 'fatten' the d3 direction at the ankle node - # As well as adjusting the position of the node depending on the - # angle of flexion, and ensures a proper transition between lower leg and the foot - rotationFactor = 2.0*math.sin(ankleFlexionRadians)*(math.sqrt(2)- 1) - ankleThickness = halfFootThickness/math.sin(ankleJointAngleRadians/2) + # Calculating initial estimation for directions at the elbow node + ankleDirn = matrix_vector_mult(ankleHalfRotationMatrix, d1) + ankleSide = legSide + ankleFront = matrix_vector_mult(ankleHalfRotationMatrix, d3) + # Calculating directions for the lower leg + footDirn = matrix_vector_mult(ankleRotationMatrix, d1) + footSide = legSide + footFront = matrix_vector_mult(ankleRotationMatrix, d3) + # These rotation factors are used to adjust the position of the joint node relative + # to the angle of flexion, and ensures a proper transition between the two parts + rotationCoeff = 0.2 + flexionRotFactor = 1*math.sin(ankleJointAngleRadians) + jointRotFactor = 1/math.sin(ankleJointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(ankleFlexionRadians/2) + jointPositions = [] + jointPositions.append(x) #1 + i += 1 + radius = halfFootThickness + legBottomRadius + # radius = legbohalfFootThickness + jointAdjustDir = add( + set_magnitude(ankleFront, rotationCoeff*radius*flexionRotFactor), + set_magnitude(footDirn, rotationCoeff*radius*flexionRotFactor), + ) + jointAdjustDir = add( + set_magnitude(lowerLegDirn, 0.8*footHeight), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, 0.8*footHeight) + jointPositions.append(add(x, jointAdjustDir)) # 2 + # radius = halfFootThickness + jointAdjustDir = add( + set_magnitude(ankleDirn, rotationCoeff*radius*flexionRotFactor), + set_magnitude(lowerLegDirn, rotationCoeff*radius*flexionRotFactor), + ) + jointAdjustDir = add( + set_magnitude(lowerLegDirn, 0.8*footHeight), + jointAdjustDir + ) + jointAdjustDir = add( + set_magnitude(footDirn, footLength), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, footLength) + jointPositions.append(add(jointPositions[0], jointAdjustDir)) #3 + jointDir = [lowerLegDirn, footDirn, footDirn] + jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( + jointPositions, jointDir, 2, + derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=footLength + )[0:2] + # Set coordiantes for joint node + x = jointPositions[1] + d1 = jointDirn[1] + # ankleFront = cross(d1, d2) + d2 = set_magnitude(ankleSide, halfFootWidth) + d3 = set_magnitude(ankleFront, radius*(jointRotFactor-(rotationCoeff*flexionRotFactor))) + d12 = set_magnitude(ankleSide, d12_mag) + d13 = add( + set_magnitude(lowerLegFront, d13_mag), + set_magnitude(d3, -radius*d13RotFactor) + ) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + side_label = 'l' if (side == left) else 'r' + options['Kinematic tree']['toes_' + side_label] = x + # Foot end node + x = jointPositions[2] + d1 = jointDirn[2] + d1 = computeCubicHermiteEndDerivative(jointPositions[1], jointDirn[1], jointPositions[2], jointDirn[2]) + d2 = set_magnitude(footSide, halfFootWidth) + d3 = set_magnitude(footFront, halfFootThickness) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = set_magnitude(d12, d12_mag) + id13 = set_magnitude(d13, d13_mag) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 # This positioning of the food nodes bends edge connecting the leg and the foot # Which allows the scaffold to better capture the shape of the calcaneus - anklePosition = add(x, set_magnitude(d1, legScale - 1*rotationFactor*footHeight)) - fx = [ - x, - add(anklePosition, set_magnitude(footd1, 0)), - add(anklePosition, set_magnitude(footd1, footLength - legBottomRadius)), + # anklePosition = add(x, set_magnitude(d1, legScale - 1*rotationFactor*footHeight)) + # fx = [ + # x, + # add(anklePosition, set_magnitude(footd1, 0)), + # add(anklePosition, set_magnitude(footd1, footLength - legBottomRadius)), - ] - fd1 = [d1, set_magnitude(footd1, 0.5*footLength), set_magnitude(footd1, 0.5*footLength)] - fd1 = smoothCubicHermiteDerivativesLine( - fx, fd1, fixAllDirections=True, fixStartDerivative=True - ) - fd2 = [d2, footd2, footd2] - fd3 = [d3, - set_magnitude(footd3, ankleThickness + legBottomRadius), - set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness) - ] - fd12 = sub(fd2[2], fd2[1]) - fd13 = sub(fd3[2], fd3[1]) - fid12 = mult(fd12, innerProportionDefault) - fid13 = mult(fd13, innerProportionDefault) - for i in range(1, 3): - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i], fd12, fd13) - fid2 = mult(fd2[i], innerProportionDefault) - fid3 = mult(fd3[i], innerProportionDefault) - setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3, fid12, fid13) - nodeIdentifier += 1 - side_label = 'l' if (side == left) else 'r' - options['Kinematic tree']['toes_' + side_label] = fx[i] + # ] + # fd1 = [d1, set_magnitude(footd1, 0.5*footLength), set_magnitude(footd1, 0.5*footLength)] + # fd1 = smoothCubicHermiteDerivativesLine( + # fx, fd1, fixAllDirections=True, fixStartDerivative=True + # ) + # fd2 = [d2, footd2, footd2] + # fd3 = [d3, + # set_magnitude(footd3, ankleThickness + legBottomRadius), + # set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness) + # ] + # fd12 = sub(fd2[2], fd2[1]) + # fd13 = sub(fd3[2], fd3[1]) + # fid12 = mult(fd12, innerProportionDefault) + # fid13 = mult(fd13, innerProportionDefault) + # for i in range(1, 3): + # node = nodes.findNodeByIdentifier(nodeIdentifier) + # fieldcache.setNode(node) + # setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i], fd12, fd13) + # fid2 = mult(fd2[i], innerProportionDefault) + # fid3 = mult(fd3[i], innerProportionDefault) + # setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3, fid12, fid13) + # nodeIdentifier += 1 + return annotationGroups, networkMesh From 5cd7b33c7a5db380cd4af4b7d42f5a49cacb9be7 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Tue, 11 Nov 2025 16:46:13 +1300 Subject: [PATCH 47/61] Added frontal hip flexion --- .../meshtypes/meshtype_3d_wholebody2.py | 172 +++++++++++++++--- .../utils/human_network_layout.py | 2 +- 2 files changed, 143 insertions(+), 31 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 881254c6..5b0eb221 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -65,15 +65,17 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Abdomen length"] = 3.0 options["Torso depth"] = 2.5 options["Torso width"] = 3.2 - options["Pelvis drop"] = 1.5 + options["Pelvis drop"] = 0.8 options["Pelvis width"] = 2.0 - options["Left leg abduction degrees"] = 10.0 - options["Right leg abduction degrees"] = 10.0 - options["Leg length"] = 10.0 + options["Left leg abduction degrees"] = 5.0 + options["Right leg abduction degrees"] = 5.0 + options["Left hip flexion degrees"] = 90.0 + options["Right hip flexion degrees"] = 90.0 + options["Leg length"] = 11.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee flexion degrees"] = 0.0 - options["Right knee flexion degrees"] = 0.0 + options["Left knee flexion degrees"] = 90.0 + options["Right knee flexion degrees"] = 90.0 options["Left ankle flexion degrees"] = 90.0 options["Right ankle flexion degrees"] = 90.0 options["Foot height"] = 1.25 @@ -115,6 +117,8 @@ def getOrderedOptionNames(cls): "Pelvis width", "Left leg abduction degrees", "Right leg abduction degrees", + "Left hip flexion degrees", + "Right hip flexion degrees", "Leg length", "Leg top diameter", "Leg bottom diameter", @@ -178,6 +182,8 @@ def checkOptions(cls, options): "Right shoulder flexion degrees": (-60.0, 200.0), "Left elbow flexion degrees": (0.0, 150.0), "Right elbow flexion degrees": (0.0, 150.0), + "Left hip flexion degrees": (0.0, 150.0), + "Right hip flexion degrees": (0.0, 150.0), "Left knee flexion degrees": (0.0, 140.0), "Right knee flexion degrees": (0.0, 140.0), "Left ankle flexion degrees": (60.0, 140.0), @@ -230,6 +236,8 @@ def generateBaseMesh(cls, region, options): halfPelvisWidth = 0.5 * options["Pelvis width"] leftLegAbductionRadians = math.radians(options["Left leg abduction degrees"]) rightLegAbductionRadians = math.radians(options["Right leg abduction degrees"]) + hipLeftFlexionRadians = math.radians(options["Left hip flexion degrees"]) + hipRightFlexionRadians = math.radians(options["Right hip flexion degrees"]) legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] @@ -434,11 +442,12 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 armJunctionNodeIdentifier = nodeIdentifier - options['Kinematic tree']['thorax_top'] = x + thoraxScale = thoraxLength / thoraxElementsCount thoraxStartX = headLength + neckLength sx = [thoraxStartX, 0.0, 0.0] + options['Kinematic tree']['thorax_top'] = sx for i in range(thoraxElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -479,7 +488,7 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 legJunctionNodeIdentifier = nodeIdentifier - 1 px = [abdomenStartX + abdomenLength, 0.0, 0.0] - options['Kinematic tree']['lumbar_body'] = px + # options['Kinematic tree']['lumbar_body'] = px # arms for side in (left, right): # Shoulder rotation @@ -678,9 +687,11 @@ def generateBaseMesh(cls, region, options): # The joint node is not set directly at the corner # Instead it is nudged forward towards the center of the joint # To get as smooth of a line as possible between node #1 and #3 + rotDisplacementFactor = rotationCoeff*halfWidth*flexionRotFactor + ventralFlexion = 1 jointAdjustDir = add( - set_magnitude(elbowFront, rotationCoeff*halfWidth*flexionRotFactor), - set_magnitude(antebrachiumDirn, rotationCoeff*halfWidth*flexionRotFactor), + set_magnitude(elbowFront, ventralFlexion*rotDisplacementFactor), + set_magnitude(antebrachiumDirn, rotDisplacementFactor), ) jointAdjustDir = add( set_magnitude(armDirn, armScale), @@ -689,8 +700,8 @@ def generateBaseMesh(cls, region, options): jointAdjustDir = set_magnitude(jointAdjustDir, armScale) jointPositions.append(add(x, jointAdjustDir)) # 2 jointAdjustDir = add( - set_magnitude(elbowDirn, rotationCoeff*halfWidth*flexionRotFactor), - set_magnitude(armDirn, rotationCoeff*halfWidth*flexionRotFactor), + set_magnitude(elbowDirn, rotDisplacementFactor), + set_magnitude(armDirn, rotDisplacementFactor), ) jointAdjustDir = add( set_magnitude(antebrachiumDirn, armScale), @@ -764,14 +775,14 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Hand twist - options['Kinematic tree']['wrist_' + side_label] = x + options['Kinematic tree']['hand_' + side_label] = x assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) hx = add(x, mult(antebrachiumDirn, handLength)) hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians - if twistAngle == 0.0: + if twistAngle >= 0.0: hd2 = set_magnitude(d2, halfHandThickness) hd3 = set_magnitude(d3, halfHandWidth) else: @@ -795,6 +806,7 @@ def generateBaseMesh(cls, region, options): pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] pid3 = mult(pd3, innerProportionDefault) for side in (left, right): + side_label = 'l' if (side == left) else 'r' legAngle = leftLegAbductionRadians if (side == left) else -rightLegAbductionRadians cosLegAngle = math.cos(legAngle) sinLegAngle = math.sin(legAngle) @@ -821,8 +833,10 @@ def generateBaseMesh(cls, region, options): id12 = mult(d12, innerProportionDefault) d13 = [0.0, 0.0, d13_mag] id13 = mult(d13, innerProportionDefault) + # options['Kinematic tree']['femur_' + side_label] = x # Upper leg - for i in range(upperLegElementsCount-1): + hipElementsCount = 1 + for i in range(hipElementsCount): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -835,6 +849,106 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + # Frontal hip flexion + # First node of the upper leg is fixed, rotation happens at the second node. + + # Updating frame of reference wrt flexion angle (using d2 as rotation axis) + hipFlexionRadians = hipLeftFlexionRadians if (side == left) else hipRightFlexionRadians + hipJointAngleRadians = math.pi - hipFlexionRadians + hipRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), hipFlexionRadians) + hipHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), hipFlexionRadians/2) + # Calculating initial estimation for directions at the joint node + hipDirn = matrix_vector_mult(hipHalfRotationMatrix, d1) + hipFront = matrix_vector_mult(hipHalfRotationMatrix, d3) + hipSide = d2 + # Calculating direction for the antebrachium + upperLegDirn = matrix_vector_mult(hipRotationMatrix, d1) + upperLegSide = legSide + upperLegFront = cross(upperLegDirn, upperLegSide) + # These rotation factors are used to adjust the position of the joint node relative + # to the angle of flexion, and ensures a proper transition between the two parts + rotationCoeff = 0.2 + flexionRotFactor = 1*math.sin(hipJointAngleRadians) + jointRotFactor = 1/math.sin(hipJointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(hipFlexionRadians/2) + jointPositions = [] + jointPositions.append(x) # 1 + i += 1 + xi = i / legToFootElementsCount + radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius + # The joint node is not set directly at the corner + # Instead it is nudged forward towards the center of the joint + # To get as smooth of a line as possible between node #1 and #3 + rotDisplacementFactor = rotationCoeff*radius*flexionRotFactor + ventralFlexion = 1 + jointAdjustDir = add( + set_magnitude(hipFront, ventralFlexion*rotDisplacementFactor), + set_magnitude(upperLegDirn, 2*rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(legDirn, legScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, legScale) + jointPositions.append(add(x, jointAdjustDir)) # 2 + jointAdjustDir = add( + set_magnitude(hipDirn, rotDisplacementFactor), + set_magnitude(legDirn, rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(upperLegDirn, legScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, legScale) + jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 + jointDir = [legDirn, hipDirn, upperLegDirn] + jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( + jointPositions, jointDir, 2, + derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale + )[0:2] + # Set coordiantes for joint node + x = jointPositions[1] + d1 = jointDirn[1] + hipFront = cross(d1, d2) + d2 = set_magnitude(hipSide, radius) + d3 = set_magnitude(hipFront, radius*jointRotFactor - 2*rotDisplacementFactor) + d12 = set_magnitude(hipSide, d12_mag) + d13 = add( + set_magnitude(hipFront, d13_mag), + set_magnitude(d1, -radius*d13RotFactor) + ) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + options['Kinematic tree']['femur_' + side_label] = x + # Antebrachium nodes starts after the elbow node + upperLegStart = jointPositions[-1] + d1 = set_magnitude(upperLegDirn, armScale) + + # Rest of upper leg + for i in range(hipElementsCount+1, upperLegElementsCount-1): + xi = i / legToFootElementsCount + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = add(upperLegStart, mult(d1, i - (hipElementsCount+1))) + radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius + d2 = set_magnitude(upperLegSide, radius) + d3 = set_magnitude(upperLegFront, radius) + d12 = set_magnitude(d2, d12_mag) + d13 = set_magnitude(d3, d13_mag) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = set_magnitude(d12, d12_mag) + id13 = set_magnitude(d13, d13_mag) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 # knee # Diagram to calculate joint flexion # 1 @@ -873,19 +987,21 @@ def generateBaseMesh(cls, region, options): # The joint node is not set directly at the corner # Instead it is nudged forward towards the center of the joint # To get as smooth of a line as possible between node #1 and #3 + rotDisplacementFactor = rotationCoeff*radius*flexionRotFactor + ventralFlexion = -1 jointAdjustDir = add( - set_magnitude(kneeFront, -rotationCoeff*radius*flexionRotFactor), - set_magnitude(lowerLegDirn, rotationCoeff*radius*flexionRotFactor), + set_magnitude(kneeFront, ventralFlexion*rotDisplacementFactor), + set_magnitude(lowerLegDirn, rotDisplacementFactor), ) jointAdjustDir = add( - set_magnitude(legDirn, legScale), + set_magnitude(upperLegDirn, legScale), jointAdjustDir ) jointAdjustDir = set_magnitude(jointAdjustDir, legScale) jointPositions.append(add(x, jointAdjustDir)) # 2 jointAdjustDir = add( - set_magnitude(kneeDirn, rotationCoeff*radius*flexionRotFactor), - set_magnitude(legDirn, rotationCoeff*radius*flexionRotFactor), + set_magnitude(kneeDirn, rotDisplacementFactor), + set_magnitude(upperLegDirn, rotDisplacementFactor), ) jointAdjustDir = add( set_magnitude(lowerLegDirn, legScale), @@ -893,7 +1009,7 @@ def generateBaseMesh(cls, region, options): ) jointAdjustDir = set_magnitude(jointAdjustDir, legScale) jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 - jointDir = [legDirn, kneeDirn, lowerLegDirn] + jointDir = [upperLegDirn, kneeDirn, lowerLegDirn] jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( jointPositions, jointDir, 2, derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale @@ -918,7 +1034,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - options['Kinematic tree']['knee_' + side_label] = x + options['Kinematic tree']['tibia_' + side_label] = x # Lower leg lowerLegStart = jointPositions[-1] d1 = set_magnitude(lowerLegDirn, legScale) @@ -927,9 +1043,6 @@ def generateBaseMesh(cls, region, options): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = add(lowerLegStart, mult(d1, i - (upperLegElementsCount))) - # if (i == legToFootElementsCount): - # # x = add(x, set_magnitude(d1, 1.5 * radius)) - # d1 = set_magnitude(d1, legScale + 1.5 * radius) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = set_magnitude(lowerLegSide, radius) d3 = set_magnitude(lowerLegFront, radius) @@ -1017,9 +1130,7 @@ def generateBaseMesh(cls, region, options): fieldcache.setNode(node) setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) - nodeIdentifier += 1 - side_label = 'l' if (side == left) else 'r' - options['Kinematic tree']['toes_' + side_label] = x + nodeIdentifier += 1 # Foot end node x = jointPositions[2] d1 = jointDirn[2] @@ -1037,6 +1148,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + options['Kinematic tree']['toes_' + side_label] = x # This positioning of the food nodes bends edge connecting the leg and the foot # Which allows the scaffold to better capture the shape of the calcaneus # anklePosition = add(x, set_magnitude(d1, legScale - 1*rotationFactor*footHeight)) @@ -1115,8 +1227,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 5 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 - options["Number of elements along upper leg"] = 4 - options["Number of elements along lower leg"] = 3 + options["Number of elements along upper leg"] = 6 + options["Number of elements along lower leg"] = 4 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 5a2d2d48..65b2d995 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -8,7 +8,7 @@ 'handElementsCount': 1, 'thoraxElementsCount': 3, 'abdomenElementsCount': 4, - 'upperLegElementsCount': 4, + 'upperLegElementsCount': 6, 'lowerLegElementsCount': 3, 'footElementsCount': 2 } From 4d2ae1d4aa1a44dc8d7cfb1b0350d9681f9de4bb Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 12 Nov 2025 14:39:23 +1300 Subject: [PATCH 48/61] Added hip element region --- src/scaffoldmaker/annotation/body_terms.py | 1 + .../meshtypes/meshtype_3d_wholebody2.py | 75 ++++++++++++------- .../utils/human_network_layout.py | 9 ++- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index fe6f3ca7..5c212f17 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -29,6 +29,7 @@ ("head core", ""), ("diaphragm", "UBERON:0001103", "ILX:0103194"), ("hand", "ILX:0104885", "FMA:9712"), + ("hip", ""), ("left", ""), ("lower limb", "UBERON:0000978"), ("left lower limb", "UBERON:8300004", "FMA:24981"), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 5b0eb221..fe0d4795 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -67,8 +67,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Torso width"] = 3.2 options["Pelvis drop"] = 0.8 options["Pelvis width"] = 2.0 - options["Left leg abduction degrees"] = 5.0 - options["Right leg abduction degrees"] = 5.0 + options["Left leg abduction degrees"] = 10.0 + options["Right leg abduction degrees"] = 10.0 options["Left hip flexion degrees"] = 90.0 options["Right hip flexion degrees"] = 90.0 options["Leg length"] = 11.0 @@ -79,7 +79,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Left ankle flexion degrees"] = 90.0 options["Right ankle flexion degrees"] = 90.0 options["Foot height"] = 1.25 - options["Foot length"] = 2.5 + options["Foot length"] = 2.0 options["Foot thickness"] = 0.3 options["Foot width"] = 1.0 options["Inner proportion default"] = 0.7 @@ -276,6 +276,7 @@ def generateBaseMesh(cls, region, options): handGroup = AnnotationGroup(region, get_body_term("hand")) thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) abdomenGroup = AnnotationGroup(region, get_body_term("abdomen")) + hipGroup = AnnotationGroup(region, get_body_term("hip")) legGroup = AnnotationGroup(region, get_body_term("lower limb")) legToFootGroup = AnnotationGroup(region, ("leg to foot", "")) leftLegGroup = AnnotationGroup(region, get_body_term("left lower limb")) @@ -288,7 +289,7 @@ def generateBaseMesh(cls, region, options): rightFootGroup = AnnotationGroup(region, get_body_term("right foot")) footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, - thoraxGroup, abdomenGroup, + thoraxGroup, abdomenGroup, hipGroup, leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, leftLegGroup, leftUpperLegGroup, leftLowerLegGroup, leftFootGroup, @@ -369,11 +370,13 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + hipElementsCount = humanElementCounts['hipElementsCount'] upperLegElementsCount = humanElementCounts['upperLegElementsCount'] lowerLegElementsCount = humanElementCounts['lowerLegElementsCount'] footElementsCount = humanElementCounts['footElementsCount'] - legToFootElementsCount = upperLegElementsCount + lowerLegElementsCount + legToFootElementsCount = hipElementsCount + upperLegElementsCount + lowerLegElementsCount legMeshGroup = legGroup.getMeshGroup(mesh) + hipMeshGroup = hipGroup.getMeshGroup(mesh) legToFootMeshGroup = legToFootGroup.getMeshGroup(mesh) footMeshGroup = footGroup.getMeshGroup(mesh) for side in (left, right): @@ -381,6 +384,13 @@ def generateBaseMesh(cls, region, options): sideUpperLegGroup = leftUpperLegGroup if (side == left) else rightUpperLegGroup sideLowerLegGroup = leftLowerLegGroup if (side == left) else rightLowerLegGroup sideFootGroup = leftFootGroup if (side == left) else rightFootGroup + # Hip + meshGroups = [bodyMeshGroup, legMeshGroup, hipMeshGroup] + for e in range(hipElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 # Upper leg meshGroups = [bodyMeshGroup, legMeshGroup, sideLegGroup.getMeshGroup(mesh), sideUpperLegGroup.getMeshGroup(mesh)] @@ -800,9 +810,9 @@ def generateBaseMesh(cls, region, options): # legs legStartX = abdomenStartX + abdomenLength + pelvisDrop nonFootLegLength = legLength - footHeight - legScale = nonFootLegLength / (legToFootElementsCount - 1) - d12_mag = (legBottomRadius - legTopRadius) / (armToHandElementsCount - 2) - d13_mag = (legBottomRadius - legTopRadius) / (armToHandElementsCount - 2) + legScale = nonFootLegLength / (legToFootElementsCount - 1) + d12_mag = (legBottomRadius - legTopRadius) / (legToFootElementsCount) + d13_mag = (legBottomRadius - legTopRadius) / (legToFootElementsCount) pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] pid3 = mult(pd3, innerProportionDefault) for side in (left, right): @@ -835,17 +845,21 @@ def generateBaseMesh(cls, region, options): id13 = mult(d13, innerProportionDefault) # options['Kinematic tree']['femur_' + side_label] = x # Upper leg - hipElementsCount = 1 - for i in range(hipElementsCount): + # hipElementsCount = 1 + for i in range(hipElementsCount-1): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = add(legStart, mult(d1, i)) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = mult(legSide, radius) - d3 = [0.0, 0.0, radius] + d3 = mult(legFront, radius) + d13 = mult(legFront, d13_mag) + d12 = mult(legSide, d12_mag) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 @@ -867,7 +881,7 @@ def generateBaseMesh(cls, region, options): upperLegFront = cross(upperLegDirn, upperLegSide) # These rotation factors are used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.2 + rotationCoeff = 0.1 flexionRotFactor = 1*math.sin(hipJointAngleRadians) jointRotFactor = 1/math.sin(hipJointAngleRadians/2) d13RotFactor = math.sqrt(2)*math.tan(hipFlexionRadians/2) @@ -901,21 +915,21 @@ def generateBaseMesh(cls, region, options): ) jointAdjustDir = set_magnitude(jointAdjustDir, legScale) jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 - jointDir = [legDirn, hipDirn, upperLegDirn] - jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - jointPositions, jointDir, 2, - derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale - )[0:2] + jointDirn = [legDirn, hipDirn, upperLegDirn] + # jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( + # jointPositions, jointDirn, 2, + # derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale + # )[0:2] # Set coordiantes for joint node x = jointPositions[1] - d1 = jointDirn[1] - hipFront = cross(d1, d2) + d1 = set_magnitude(hipDirn, legScale) + # hipFront = cross(d1, d2) d2 = set_magnitude(hipSide, radius) - d3 = set_magnitude(hipFront, radius*jointRotFactor - 2*rotDisplacementFactor) + d3 = set_magnitude(hipFront, radius*jointRotFactor - 2.5*rotDisplacementFactor) d12 = set_magnitude(hipSide, d12_mag) d13 = add( set_magnitude(hipFront, d13_mag), - set_magnitude(d1, -radius*d13RotFactor) + set_magnitude(d1, -(radius)/(legScale*jointRotFactor)) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -932,11 +946,12 @@ def generateBaseMesh(cls, region, options): d1 = set_magnitude(upperLegDirn, armScale) # Rest of upper leg - for i in range(hipElementsCount+1, upperLegElementsCount-1): + j = 0 + for i in range(hipElementsCount, hipElementsCount+upperLegElementsCount-1): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(upperLegStart, mult(d1, i - (hipElementsCount+1))) + x = add(upperLegStart, mult(d1, j)) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = set_magnitude(upperLegSide, radius) d3 = set_magnitude(upperLegFront, radius) @@ -949,6 +964,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + j += 1 # knee # Diagram to calculate joint flexion # 1 @@ -1038,11 +1054,12 @@ def generateBaseMesh(cls, region, options): # Lower leg lowerLegStart = jointPositions[-1] d1 = set_magnitude(lowerLegDirn, legScale) - for i in range(upperLegElementsCount, legToFootElementsCount): + j = 0 + for i in range(hipElementsCount+upperLegElementsCount, legToFootElementsCount): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(lowerLegStart, mult(d1, i - (upperLegElementsCount))) + x = add(lowerLegStart, mult(d1, j)) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = set_magnitude(lowerLegSide, radius) d3 = set_magnitude(lowerLegFront, radius) @@ -1055,6 +1072,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + j+=1 # foot # Updating frame of reference wrt rotation angle (using d2 as rotation axis) ankleFlexionRadians = ankleLeftFlexionRadians if (side == left) else ankleRightFlexionRadians @@ -1227,7 +1245,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along brachium"] = 5 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 - options["Number of elements along upper leg"] = 6 + options["Number of elements along hip"] = 2 + options["Number of elements along upper leg"] = 4 options["Number of elements along lower leg"] = 4 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 12 @@ -1391,6 +1410,7 @@ def generateBaseMesh(cls, region, options): elementsCountAlongBrachium = options["Number of elements along brachium"] elementsCountAlongAntebrachium = options["Number of elements along antebrachium"] elementsCountAlongHand = options["Number of elements along hand"] + elementsCountAlongHip = options["Number of elements along hip"] elementsCountAlongUpperLeg = options["Number of elements along upper leg"] elementsCountAlongLowerLeg = options["Number of elements along lower leg"] elementsCountAlongFoot = options["Number of elements along foot"] @@ -1437,6 +1457,9 @@ def generateBaseMesh(cls, region, options): elif "hand" in name: alongCount = elementsCountAlongHand aroundCount = elementsCountAroundArm + elif "hip" in name: + alongCount = elementsCountAlongHip + aroundCount = elementsCountAroundLeg elif "upper leg" in name: alongCount = elementsCountAlongUpperLeg aroundCount = elementsCountAroundLeg diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 65b2d995..823602c0 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -8,7 +8,8 @@ 'handElementsCount': 1, 'thoraxElementsCount': 3, 'abdomenElementsCount': 4, - 'upperLegElementsCount': 6, + 'hipElementsCount': 2, + 'upperLegElementsCount': 4, 'lowerLegElementsCount': 3, 'footElementsCount': 2 } @@ -111,7 +112,13 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): version = 2 if (i == 0) else 3 #Left is 2, right is 3 legNetworkLayout = str(pelvisNodeJoint) + '.' + str(version) + '-' nodeIdentifier += 1 + # Hip + legNetworkLayout, nodeIdentifier = createSegment( + humanElementCounts['hipElementsCount'], + legNetworkLayout, nodeIdentifier, endSegment=True) # Upper leg + legNetworkLayout = legNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['upperLegElementsCount'], legNetworkLayout, nodeIdentifier, endSegment=True) From a0f2e692e0845440df2ebebdca74b60b3085a9a0 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 19 Nov 2025 16:09:01 +1300 Subject: [PATCH 49/61] Added a function containing the directions for joint flexion --- .../meshtypes/meshtype_3d_wholebody2.py | 198 ++++++++++++------ 1 file changed, 139 insertions(+), 59 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index fe0d4795..18ac017a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -669,71 +669,83 @@ def generateBaseMesh(cls, region, options): mult(armFront, sinTwistAngle)) d3 = add(mult(armFront, cosTwistAngle), mult(armSide, sinTwistAngle)) + armSide = d2 + armFront = d3 + # Updating frame of reference wrt flexion angle (using d2 as rotation axis) elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRightFlexionRadians - elbowJointAngleRadians = math.pi - elbowFlexionRadians - elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) - elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) - # Calculating initial estimation for directions at the elbow node - elbowDirn = matrix_vector_mult(elbowHalfRotationMatrix, d1) - elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) - elbowSide = d2 - # Calculating direction for the antebrachium - antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) - antebrachiumSide = armSide - antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) - # These rotation factors are used to adjust the position of the joint node relative - # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.25 - flexionRotFactor = 1*math.sin(elbowJointAngleRadians) - jointRotFactor = 1/math.sin(elbowJointAngleRadians/2) - d13RotFactor = math.sqrt(2)*math.tan(elbowFlexionRadians/2) - jointPositions = [] - jointPositions.append(x) # 1 + # jointAngleRadians = math.pi - elbowFlexionRadians + # elbowJointAngleRadians = math.pi - elbowFlexionRadians + # elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) + # elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) + # # Calculating initial estimation for directions at the elbow node + # elbowDirn = matrix_vector_mult(elbowHalfRotationMatrix, d1) + # elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) + # elbowSide = d2 + # # Calculating direction for the antebrachium + # antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) + # antebrachiumSide = armSide + # antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) + # # These rotation factors are used to adjust the position of the joint node relative + # # to the angle of flexion, and ensures a proper transition between the two parts + # rotationCoeff = 0.25 + # flexionRotFactor = 1*math.sin(elbowJointAngleRadians) + # jointRotFactor = 1/math.sin(elbowJointAngleRadians/2) + # d13RotFactor = math.sqrt(2)*math.tan(elbowFlexionRadians/2) + # jointPositions = [] + # jointPositions.append(x) # 1 + # i += 1 + # xi = i / (armToHandElementsCount - 2) + # halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + # halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + # # The joint node is not set directly at the corner + # # Instead it is nudged forward towards the center of the joint + # # To get as smooth of a line as possible between node #1 and #3 + # rotDisplacementFactor = rotationCoeff*halfWidth*flexionRotFactor + # ventralFlexion = 1 + # jointAdjustDir = add( + # set_magnitude(elbowFront, ventralFlexion*rotDisplacementFactor), + # set_magnitude(antebrachiumDirn, rotDisplacementFactor), + # ) + # jointAdjustDir = add( + # set_magnitude(armDirn, armScale), + # jointAdjustDir + # ) + # jointAdjustDir = set_magnitude(jointAdjustDir, armScale) + # jointPositions.append(add(x, jointAdjustDir)) # 2 + # jointAdjustDir = add( + # set_magnitude(elbowDirn, rotDisplacementFactor), + # set_magnitude(armDirn, rotDisplacementFactor), + # ) + # jointAdjustDir = add( + # set_magnitude(antebrachiumDirn, armScale), + # jointAdjustDir + # ) + # jointAdjustDir = set_magnitude(jointAdjustDir, armScale) + # jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 + # jointDir = [armDirn, elbowDirn, antebrachiumDirn] + # jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( + # jointPositions, jointDir, 2, + # derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale + # )[0:2] + armDir = [armDirn, armSide, armFront] i += 1 xi = i / (armToHandElementsCount - 2) halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - # The joint node is not set directly at the corner - # Instead it is nudged forward towards the center of the joint - # To get as smooth of a line as possible between node #1 and #3 - rotDisplacementFactor = rotationCoeff*halfWidth*flexionRotFactor - ventralFlexion = 1 - jointAdjustDir = add( - set_magnitude(elbowFront, ventralFlexion*rotDisplacementFactor), - set_magnitude(antebrachiumDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(armDirn, armScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, armScale) - jointPositions.append(add(x, jointAdjustDir)) # 2 - jointAdjustDir = add( - set_magnitude(elbowDirn, rotDisplacementFactor), - set_magnitude(armDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(antebrachiumDirn, armScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, armScale) - jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 - jointDir = [armDirn, elbowDirn, antebrachiumDirn] - jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - jointPositions, jointDir, 2, - derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale - )[0:2] + rotationCoeff = 0.25 + [x, elbowDir, antebrachiumStart, antebrachiumDir, elbowd3_mag, elbowd13_mag] = getJointAndDistalFlexionFrames(elbowFlexionRadians, x, armDir,halfWidth, armScale, rotationCoeff,ventralFlexion=True) # Set coordiantes for joint node - x = jointPositions[1] - d1 = jointDirn[1] - elbowFront = cross(d1, d2) + # x = jointPositions[1] + elbowDirn, elbowSide, elbowFront = elbowDir + d1 = mult(elbowDirn, armScale) + # elbowFront = cross(d1, d2) d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, halfWidth*(jointRotFactor-(rotationCoeff*flexionRotFactor))) + d3 = set_magnitude(elbowFront, elbowd3_mag) d12 = set_magnitude(elbowSide, d12_mag) d13 = add( set_magnitude(elbowFront, d13_mag), - set_magnitude(d1, -halfWidth*d13RotFactor) + set_magnitude(d1, -elbowd13_mag) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -746,7 +758,8 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 options['Kinematic tree']['ulna_' + side_label] = x # Antebrachium nodes starts after the elbow node - antebrachiumStart = jointPositions[-1] + # antebrachiumStart = jointPositions[-1] + antebrachiumDirn, antebrachiumSide, antebrachiumFront = antebrachiumDir d1 = set_magnitude(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction for i in range(brachiumElementsCount - shoulderElementCount + 1, armToHandElementsCount - 1): @@ -881,7 +894,7 @@ def generateBaseMesh(cls, region, options): upperLegFront = cross(upperLegDirn, upperLegSide) # These rotation factors are used to adjust the position of the joint node relative # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.1 + rotationCoeff = 0.25 flexionRotFactor = 1*math.sin(hipJointAngleRadians) jointRotFactor = 1/math.sin(hipJointAngleRadians/2) d13RotFactor = math.sqrt(2)*math.tan(hipFlexionRadians/2) @@ -923,13 +936,13 @@ def generateBaseMesh(cls, region, options): # Set coordiantes for joint node x = jointPositions[1] d1 = set_magnitude(hipDirn, legScale) - # hipFront = cross(d1, d2) + hipFront = cross(d1, d2) d2 = set_magnitude(hipSide, radius) - d3 = set_magnitude(hipFront, radius*jointRotFactor - 2.5*rotDisplacementFactor) + d3 = set_magnitude(hipFront, radius*jointRotFactor - 1*rotDisplacementFactor) d12 = set_magnitude(hipSide, d12_mag) d13 = add( set_magnitude(hipFront, d13_mag), - set_magnitude(d1, -(radius)/(legScale*jointRotFactor)) + mult(d1, -radius*d13RotFactor) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -1665,3 +1678,70 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, d12) if d13: field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) + +def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, proximalDir, + frontScale, dirnScale, rotationCoeff,ventralFlexion=True): + jointAngleRadians = math.pi - jointFlexionRadians + proximalDirn, proximalSide, proximalFront = proximalDir + ventral = 1 if ventralFlexion else -1 + jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) + jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) + # Joint directions (frame is rotated by half the flexion angle) + jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) + jointSide = proximalSide + jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) + # Proximal directions + distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) + distalSide = proximalSide + distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) + # These rotation factors are used to adjust the position of the joint node relative + # to the angle of flexion, and ensures a proper transition between the two parts + flexionRotFactor = 1*math.sin(jointAngleRadians) + jointRotFactor = 1/math.sin(jointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(jointFlexionRadians/2) + # Diagram to calculate position of joint node with flexion + # 1 + # | + # 2 -- 3 + # 1 is the proximal node, 2 is the joint node, 3 is distal node + # we fix the position of the nodes 1 and 3, and calculate + # the position of 2 using the sampleCubicHermiteCurvesSmooth function. + nodePositions = [] + nodePositions.append(proximalNodePosition) #1 + # The joint node is not set directly at the corner + # Instead it is nudged forward towards the center of the tube + # To get as smooth of a line as possible between node #1 and #3 + rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor + jointAdjustDir = add( + set_magnitude(jointFront, ventral*rotDisplacementFactor), + set_magnitude(distalDirn, rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(proximalDirn, dirnScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, dirnScale) + nodePositions.append(add(nodePositions[-1], jointAdjustDir)) #2 + jointAdjustDir = add( + set_magnitude(jointDirn, rotDisplacementFactor), + set_magnitude(proximalDirn, rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(distalDirn, dirnScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, dirnScale) + nodePositions.append(add(nodePositions[-1], jointAdjustDir)) #3 + # nodeDir = [proximalDirn, jointDirn, distalDirn] + # nodePositions, nodeDirn = sampleCubicHermiteCurvesSmooth( + # nodePositions, jo, 2, + # derivativeMagnitudeStart=dirnScale, derivativeMagnitudeEnd=dirnScale + # )[0:2] + jointNodePosition = nodePositions[1] + jointDir = [jointDirn, jointSide, jointFront] + distalNodePosition = nodePositions[2] + distalDir = [distalDirn, distalSide, distalFront] + # Magnitudes + jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) + jointd13_mag = frontScale*d13RotFactor + return [jointNodePosition, jointDir, distalNodePosition, distalDir, jointd3_mag, jointd13_mag] \ No newline at end of file From 2c5167129d98891d8b4a8903df70ed608a62331c Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Wed, 19 Nov 2025 18:08:11 +1300 Subject: [PATCH 50/61] Updated hip and knee to use the new function to obtain flexion frames --- .../meshtypes/meshtype_3d_wholebody2.py | 240 +++--------------- 1 file changed, 40 insertions(+), 200 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 18ac017a..21143d35 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -671,81 +671,27 @@ def generateBaseMesh(cls, region, options): mult(armSide, sinTwistAngle)) armSide = d2 armFront = d3 - # Updating frame of reference wrt flexion angle (using d2 as rotation axis) elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRightFlexionRadians - # jointAngleRadians = math.pi - elbowFlexionRadians - # elbowJointAngleRadians = math.pi - elbowFlexionRadians - # elbowRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians) - # elbowHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), elbowFlexionRadians/2) - # # Calculating initial estimation for directions at the elbow node - # elbowDirn = matrix_vector_mult(elbowHalfRotationMatrix, d1) - # elbowFront = matrix_vector_mult(elbowHalfRotationMatrix, d3) - # elbowSide = d2 - # # Calculating direction for the antebrachium - # antebrachiumDirn = matrix_vector_mult(elbowRotationMatrix, d1) - # antebrachiumSide = armSide - # antebrachiumFront = cross(antebrachiumDirn, antebrachiumSide) - # # These rotation factors are used to adjust the position of the joint node relative - # # to the angle of flexion, and ensures a proper transition between the two parts - # rotationCoeff = 0.25 - # flexionRotFactor = 1*math.sin(elbowJointAngleRadians) - # jointRotFactor = 1/math.sin(elbowJointAngleRadians/2) - # d13RotFactor = math.sqrt(2)*math.tan(elbowFlexionRadians/2) - # jointPositions = [] - # jointPositions.append(x) # 1 - # i += 1 - # xi = i / (armToHandElementsCount - 2) - # halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - # # The joint node is not set directly at the corner - # # Instead it is nudged forward towards the center of the joint - # # To get as smooth of a line as possible between node #1 and #3 - # rotDisplacementFactor = rotationCoeff*halfWidth*flexionRotFactor - # ventralFlexion = 1 - # jointAdjustDir = add( - # set_magnitude(elbowFront, ventralFlexion*rotDisplacementFactor), - # set_magnitude(antebrachiumDirn, rotDisplacementFactor), - # ) - # jointAdjustDir = add( - # set_magnitude(armDirn, armScale), - # jointAdjustDir - # ) - # jointAdjustDir = set_magnitude(jointAdjustDir, armScale) - # jointPositions.append(add(x, jointAdjustDir)) # 2 - # jointAdjustDir = add( - # set_magnitude(elbowDirn, rotDisplacementFactor), - # set_magnitude(armDirn, rotDisplacementFactor), - # ) - # jointAdjustDir = add( - # set_magnitude(antebrachiumDirn, armScale), - # jointAdjustDir - # ) - # jointAdjustDir = set_magnitude(jointAdjustDir, armScale) - # jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 - # jointDir = [armDirn, elbowDirn, antebrachiumDirn] - # jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - # jointPositions, jointDir, 2, - # derivativeMagnitudeStart=armScale, derivativeMagnitudeEnd=armScale - # )[0:2] armDir = [armDirn, armSide, armFront] i += 1 xi = i / (armToHandElementsCount - 2) halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius rotationCoeff = 0.25 - [x, elbowDir, antebrachiumStart, antebrachiumDir, elbowd3_mag, elbowd13_mag] = getJointAndDistalFlexionFrames(elbowFlexionRadians, x, armDir,halfWidth, armScale, rotationCoeff,ventralFlexion=True) + [x, elbowDir, antebrachiumStart, antebrachiumDir, elbowd3_mag, elbowd13_mag] =\ + getJointAndDistalFlexionFrames(elbowFlexionRadians, x, armDir,halfWidth, armScale, rotationCoeff,ventralFlexion=True) # Set coordiantes for joint node # x = jointPositions[1] elbowDirn, elbowSide, elbowFront = elbowDir d1 = mult(elbowDirn, armScale) # elbowFront = cross(d1, d2) - d2 = set_magnitude(elbowSide, halfThickness) - d3 = set_magnitude(elbowFront, elbowd3_mag) - d12 = set_magnitude(elbowSide, d12_mag) + d2 = mult(elbowSide, halfThickness) + d3 = mult(elbowFront, elbowd3_mag) + d12 = mult(elbowSide, d12_mag) d13 = add( - set_magnitude(elbowFront, d13_mag), - set_magnitude(d1, -elbowd13_mag) + mult(elbowFront, d13_mag), + mult(elbowDirn, elbowd13_mag) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -877,72 +823,23 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Frontal hip flexion - # First node of the upper leg is fixed, rotation happens at the second node. - # Updating frame of reference wrt flexion angle (using d2 as rotation axis) hipFlexionRadians = hipLeftFlexionRadians if (side == left) else hipRightFlexionRadians - hipJointAngleRadians = math.pi - hipFlexionRadians - hipRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), hipFlexionRadians) - hipHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), hipFlexionRadians/2) - # Calculating initial estimation for directions at the joint node - hipDirn = matrix_vector_mult(hipHalfRotationMatrix, d1) - hipFront = matrix_vector_mult(hipHalfRotationMatrix, d3) - hipSide = d2 - # Calculating direction for the antebrachium - upperLegDirn = matrix_vector_mult(hipRotationMatrix, d1) - upperLegSide = legSide - upperLegFront = cross(upperLegDirn, upperLegSide) - # These rotation factors are used to adjust the position of the joint node relative - # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.25 - flexionRotFactor = 1*math.sin(hipJointAngleRadians) - jointRotFactor = 1/math.sin(hipJointAngleRadians/2) - d13RotFactor = math.sqrt(2)*math.tan(hipFlexionRadians/2) - jointPositions = [] - jointPositions.append(x) # 1 i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - # The joint node is not set directly at the corner - # Instead it is nudged forward towards the center of the joint - # To get as smooth of a line as possible between node #1 and #3 - rotDisplacementFactor = rotationCoeff*radius*flexionRotFactor - ventralFlexion = 1 - jointAdjustDir = add( - set_magnitude(hipFront, ventralFlexion*rotDisplacementFactor), - set_magnitude(upperLegDirn, 2*rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(legDirn, legScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, legScale) - jointPositions.append(add(x, jointAdjustDir)) # 2 - jointAdjustDir = add( - set_magnitude(hipDirn, rotDisplacementFactor), - set_magnitude(legDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(upperLegDirn, legScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, legScale) - jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 - jointDirn = [legDirn, hipDirn, upperLegDirn] - # jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - # jointPositions, jointDirn, 2, - # derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale - # )[0:2] - # Set coordiantes for joint node - x = jointPositions[1] - d1 = set_magnitude(hipDirn, legScale) - hipFront = cross(d1, d2) - d2 = set_magnitude(hipSide, radius) - d3 = set_magnitude(hipFront, radius*jointRotFactor - 1*rotDisplacementFactor) - d12 = set_magnitude(hipSide, d12_mag) + legDir = [legDirn, legSide, legFront] + rotationCoeff = 0.18 + [x, hipDir, upperLegStart, upperLegDir, hipd3_mag, hipd13_mag] =\ + getJointAndDistalFlexionFrames(hipFlexionRadians, x, legDir,radius, legScale, rotationCoeff,ventralFlexion=True) + hipDirn, hipSide, hipFront = hipDir + d1 = mult(hipDirn, legScale) + d2 = mult(hipSide, radius) + d3 = mult(hipFront, 0.9*hipd3_mag) + d12 = mult(hipSide, d12_mag) d13 = add( - set_magnitude(hipFront, d13_mag), - mult(d1, -radius*d13RotFactor) + mult(hipFront, d13_mag), + mult(hipDirn, 0.7*hipd13_mag) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -954,10 +851,8 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 options['Kinematic tree']['femur_' + side_label] = x - # Antebrachium nodes starts after the elbow node - upperLegStart = jointPositions[-1] + upperLegDirn, upperLegSide, upperLegFront = upperLegDir d1 = set_magnitude(upperLegDirn, armScale) - # Rest of upper leg j = 0 for i in range(hipElementsCount, hipElementsCount+upperLegElementsCount-1): @@ -966,10 +861,10 @@ def generateBaseMesh(cls, region, options): fieldcache.setNode(node) x = add(upperLegStart, mult(d1, j)) radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - d2 = set_magnitude(upperLegSide, radius) - d3 = set_magnitude(upperLegFront, radius) - d12 = set_magnitude(d2, d12_mag) - d13 = set_magnitude(d3, d13_mag) + d2 = mult(upperLegSide, radius) + d3 = mult(upperLegFront, radius) + d12 = mult(upperLegSide, d12_mag) + d13 = mult(upperLegFront, d13_mag) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = set_magnitude(d12, d12_mag) @@ -979,80 +874,23 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 j += 1 # knee - # Diagram to calculate joint flexion - # 1 - # | - # 3 - 2 - # 1 is upper leg, 2 is the knee, 3 is the lower leg - # we fix the position of the nodes 1 and 3, and calculate - # the position (and d1) of 2 using the sampleCubicHermiteCurvesSmooth function. - # The frame at the joint node is rotated by half the flexion angle - # Calculating initial d2 and d3 before rotation - # Necessary in case there is a non-zero twist angle kneeFlexionRadians = kneeLeftFlexionRadians if (side == left) else kneeRightFlexionRadians - # Angle between upper and lower leg - kneeJointAngleRadians = math.pi - kneeFlexionRadians - kneeRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeFlexionRadians) - kneeHalfrotationMatrix = axis_angle_to_rotation_matrix(mult(d2, 1), kneeFlexionRadians/2) - # Calculating initial estimation for directions at the elbow node - kneeDirn = matrix_vector_mult(kneeHalfrotationMatrix, d1) - kneeSide = legSide - kneeFront = matrix_vector_mult(kneeHalfrotationMatrix, d3) - # Calculating directions for the lower leg - lowerLegDirn = matrix_vector_mult(kneeRotationMatrix, d1) - lowerLegSide = legSide - lowerLegFront = cross(lowerLegDirn, lowerLegSide) - # These rotation factors are used to adjust the position of the joint node relative - # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.25 - flexionRotFactor = 1*math.sin(kneeJointAngleRadians) - jointRotFactor = 1/math.sin(kneeJointAngleRadians/2) - d13RotFactor = math.sqrt(2)*math.tan(kneeFlexionRadians/2) - jointPositions = [] - jointPositions.append(x) #1 + # # Set coordiantes for joint node i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - # The joint node is not set directly at the corner - # Instead it is nudged forward towards the center of the joint - # To get as smooth of a line as possible between node #1 and #3 - rotDisplacementFactor = rotationCoeff*radius*flexionRotFactor - ventralFlexion = -1 - jointAdjustDir = add( - set_magnitude(kneeFront, ventralFlexion*rotDisplacementFactor), - set_magnitude(lowerLegDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(upperLegDirn, legScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, legScale) - jointPositions.append(add(x, jointAdjustDir)) # 2 - jointAdjustDir = add( - set_magnitude(kneeDirn, rotDisplacementFactor), - set_magnitude(upperLegDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(lowerLegDirn, legScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, legScale) - jointPositions.append(add(jointPositions[-1], jointAdjustDir)) #3 - jointDir = [upperLegDirn, kneeDirn, lowerLegDirn] - jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - jointPositions, jointDir, 2, - derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=legScale - )[0:2] - # Set coordiantes for joint node - x = jointPositions[1] - d1 = jointDirn[1] - kneeFront = cross(d1, d2) - d2 = set_magnitude(kneeSide, radius) - d3 = set_magnitude(kneeFront, radius*(jointRotFactor-(0.35*flexionRotFactor))) - d12 = set_magnitude(kneeSide, d12_mag) + upperLegDir = [upperLegDirn, upperLegSide, upperLegFront] + rotationCoeff = 0.25 + [x, kneeDir, lowerLegStart, lowerLegDir, kneed3_mag, kneed13_mag] =\ + getJointAndDistalFlexionFrames(kneeFlexionRadians, x, upperLegDir,radius, legScale, rotationCoeff,ventralFlexion=False) + kneeDirn, kneeSide, kneeFront = kneeDir + d1 = mult(kneeDirn, legScale) + d2 = mult(kneeSide, radius) + d3 = mult(kneeFront, kneed3_mag) + d12 = mult(kneeSide, d12_mag) d13 = add( set_magnitude(d3, d13_mag), - set_magnitude(d1, radius*d13RotFactor) + set_magnitude(d1, kneed13_mag) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -1065,7 +903,7 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 options['Kinematic tree']['tibia_' + side_label] = x # Lower leg - lowerLegStart = jointPositions[-1] + lowerLegDirn, lowerLegSide, lowerLegFront = lowerLegDir d1 = set_magnitude(lowerLegDirn, legScale) j = 0 for i in range(hipElementsCount+upperLegElementsCount, legToFootElementsCount): @@ -1143,6 +981,7 @@ def generateBaseMesh(cls, region, options): derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=footLength )[0:2] # Set coordiantes for joint node + x = jointPositions[1] d1 = jointDirn[1] # ankleFront = cross(d1, d2) @@ -1255,12 +1094,13 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 + options["Number of elements along shoulder"] = 2 options["Number of elements along brachium"] = 5 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along hip"] = 2 - options["Number of elements along upper leg"] = 4 - options["Number of elements along lower leg"] = 4 + options["Number of elements along upper leg"] = 3 + options["Number of elements along lower leg"] = 3 options["Number of elements along foot"] = 2 options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 @@ -1743,5 +1583,5 @@ def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, pr distalDir = [distalDirn, distalSide, distalFront] # Magnitudes jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) - jointd13_mag = frontScale*d13RotFactor + jointd13_mag = -1*ventral*frontScale*d13RotFactor return [jointNodePosition, jointDir, distalNodePosition, distalDir, jointd3_mag, jointd13_mag] \ No newline at end of file From 9167bedbf3f1d7d9a622348b80f21425ce79056c Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 21 Nov 2025 14:11:44 +1300 Subject: [PATCH 51/61] Updated foot flexion script --- src/scaffoldmaker/annotation/body_terms.py | 2 + .../meshtypes/meshtype_3d_wholebody2.py | 239 +++++++----------- .../utils/human_network_layout.py | 13 +- 3 files changed, 109 insertions(+), 145 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 5c212f17..1121ac8d 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -10,12 +10,14 @@ ("abdominopelvic cavity", "UBERON:0035819"), ("upper limb", "UBERON:0001460"), ("left upper limb", "UBERON:8300002", "FMA:7186"), + ("left shoulder", ""), ("left brachium", ""), ("left elbow", ""), ("left antebrachium", ""), ("left hand", ""), ("left upper limb skin epidermis outer surface", "ILX:0796504"), ("right upper limb", "UBERON:8300001", "FMA:7185"), + ("right shoulder", ""), ("right brachium", ""), ("right elbow", ""), ("right antebrachium", ""), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 21143d35..142dd673 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -264,11 +264,13 @@ def generateBaseMesh(cls, region, options): armGroup = AnnotationGroup(region, get_body_term("upper limb")) # armToHandGroup = AnnotationGroup(region, ("arm to hand", "")) leftArmGroup = AnnotationGroup(region, get_body_term("left upper limb")) + leftShoulderGroup = AnnotationGroup(region, get_body_term("left shoulder")) leftBrachiumGroup = AnnotationGroup(region, get_body_term("left brachium")) leftAntebrachiumGroup = AnnotationGroup(region, get_body_term("left antebrachium")) # leftElbowGroup = AnnotationGroup(region, get_body_term("left elbow")) leftHandGroup = AnnotationGroup(region, get_body_term("left hand")) rightArmGroup = AnnotationGroup(region, get_body_term("right upper limb")) + rightShoulderGroup = AnnotationGroup(region, get_body_term("right shoulder")) rightBrachiumGroup = AnnotationGroup(region, get_body_term("right brachium")) rightAntebrachiumGroup = AnnotationGroup(region, get_body_term("right antebrachium")) # rightElbowGroup = AnnotationGroup(region, get_body_term("right elbow")) @@ -290,8 +292,8 @@ def generateBaseMesh(cls, region, options): footGroup = AnnotationGroup(region, get_body_term("foot")) annotationGroups = [bodyGroup, headGroup, neckGroup, thoraxGroup, abdomenGroup, hipGroup, - leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, - rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, + leftShoulderGroup, leftBrachiumGroup, leftAntebrachiumGroup, leftHandGroup, + rightShoulderGroup, rightBrachiumGroup, rightAntebrachiumGroup, rightHandGroup, leftLegGroup, leftUpperLegGroup, leftLowerLegGroup, leftFootGroup, rightLegGroup, rightUpperLegGroup, rightLowerLegGroup, rightFootGroup, armGroup, leftArmGroup, rightArmGroup, handGroup, @@ -314,19 +316,30 @@ def generateBaseMesh(cls, region, options): elementIdentifier += 1 left = 0 right = 1 + shoulderElementsCount = humanElementCounts['shoulderElementsCount'] brachiumElementsCount = humanElementCounts['brachiumElementsCount'] antebrachiumElementsCount = humanElementCounts['antebrachiumElementsCount'] handElementsCount = humanElementCounts['handElementsCount'] - armToHandElementsCount = brachiumElementsCount + antebrachiumElementsCount #all elbow nodes count as 1 + armToHandElementsCount = shoulderElementsCount + brachiumElementsCount + antebrachiumElementsCount armMeshGroup = armGroup.getMeshGroup(mesh) # armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup + sideShoulderGroup = leftShoulderGroup if (side == left) else rightShoulderGroup sideBrachiumGroup = leftBrachiumGroup if (side == left) else rightBrachiumGroup sideAntebrachiumGroup = leftAntebrachiumGroup if (side == left) else rightAntebrachiumGroup # sideElbowGroup = leftElbowGroup if (side == left) else rightElbowGroup sideHandGroup = leftHandGroup if (side == left) else rightHandGroup + # Setup shoulder elements + meshGroups = [bodyMeshGroup, + armMeshGroup, + sideArmGroup.getMeshGroup(mesh), sideShoulderGroup.getMeshGroup(mesh)] + for e in range(shoulderElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 # Setup brachium elements meshGroups = [bodyMeshGroup, armMeshGroup, @@ -385,7 +398,8 @@ def generateBaseMesh(cls, region, options): sideLowerLegGroup = leftLowerLegGroup if (side == left) else rightLowerLegGroup sideFootGroup = leftFootGroup if (side == left) else rightFootGroup # Hip - meshGroups = [bodyMeshGroup, legMeshGroup, hipMeshGroup] + meshGroups = [bodyMeshGroup, legMeshGroup, hipMeshGroup, + sideLegGroup.getMeshGroup(mesh), sideUpperLegGroup.getMeshGroup(mesh)] for e in range(hipElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: @@ -511,10 +525,10 @@ def generateBaseMesh(cls, region, options): shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians nonHandArmLength = armLength - handLength - shoulderElementCount = 2 - armScale = nonHandArmLength / (armToHandElementsCount - shoulderElementCount) - d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - shoulderElementCount) - d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - shoulderElementCount) + # shoulderElementsCount = 2 + armScale = nonHandArmLength / (armToHandElementsCount - shoulderElementsCount) + d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - shoulderElementsCount) + d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - shoulderElementsCount) armAngle = armAngleRadians if (side == left) else -armAngleRadians cosArmAngle = math.cos(armAngle) sinArmAngle = math.sin(armAngle) @@ -616,7 +630,7 @@ def generateBaseMesh(cls, region, options): # armStart = add(shoulderPosition,d1) # d1 = mult(armDirn, armScale) # Setting brachium coordinates - for i in range(1, brachiumElementsCount - shoulderElementCount): + for i in range(1, brachiumElementsCount): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -648,17 +662,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Diagram to calculate joint flexion - # 1 - # | - # 2 -- 3 - # 1 is brachium, 2 is the elbow, 4 is antebrachium - # we fix the position of the nodes 1 and 3, and calculate - # the position (and d1) of 2 using the sampleCubicHermiteCurvesSmooth function. - # The frame at the joint node is rotated by half the flexion angle - # Calculating initial d2 and d3 before rotation - # Necessary in case there is a non-zero twist angle - d1 = armDirn + # Elbow if twistAngle == 0.0: d2 = armSide d3 = armFront @@ -708,11 +712,12 @@ def generateBaseMesh(cls, region, options): antebrachiumDirn, antebrachiumSide, antebrachiumFront = antebrachiumDir d1 = set_magnitude(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction - for i in range(brachiumElementsCount - shoulderElementCount + 1, armToHandElementsCount - 1): + j=0 + for i in range(brachiumElementsCount + 1, armToHandElementsCount - 1): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(antebrachiumStart, mult(d1, i - (brachiumElementsCount - shoulderElementCount + 1))) + x = add(antebrachiumStart, mult(d1, j)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: @@ -743,6 +748,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + j += 1 # Hand twist options['Kinematic tree']['hand_' + side_label] = x assert handElementsCount == 1 @@ -804,7 +810,6 @@ def generateBaseMesh(cls, region, options): id13 = mult(d13, innerProportionDefault) # options['Kinematic tree']['femur_' + side_label] = x # Upper leg - # hipElementsCount = 1 for i in range(hipElementsCount-1): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) @@ -906,7 +911,7 @@ def generateBaseMesh(cls, region, options): lowerLegDirn, lowerLegSide, lowerLegFront = lowerLegDir d1 = set_magnitude(lowerLegDirn, legScale) j = 0 - for i in range(hipElementsCount+upperLegElementsCount, legToFootElementsCount): + for i in range(hipElementsCount+upperLegElementsCount, legToFootElementsCount-1): xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -925,72 +930,23 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 j+=1 # foot - # Updating frame of reference wrt rotation angle (using d2 as rotation axis) ankleFlexionRadians = ankleLeftFlexionRadians if (side == left) else ankleRightFlexionRadians - # Angle between lower leg and foot - ankleJointAngleRadians = math.pi - ankleFlexionRadians - ankleRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleFlexionRadians)) - ankleHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), (ankleFlexionRadians/2)) - # Calculating initial estimation for directions at the elbow node - ankleDirn = matrix_vector_mult(ankleHalfRotationMatrix, d1) - ankleSide = legSide - ankleFront = matrix_vector_mult(ankleHalfRotationMatrix, d3) - # Calculating directions for the lower leg - footDirn = matrix_vector_mult(ankleRotationMatrix, d1) - footSide = legSide - footFront = matrix_vector_mult(ankleRotationMatrix, d3) - # These rotation factors are used to adjust the position of the joint node relative - # to the angle of flexion, and ensures a proper transition between the two parts - rotationCoeff = 0.2 - flexionRotFactor = 1*math.sin(ankleJointAngleRadians) - jointRotFactor = 1/math.sin(ankleJointAngleRadians/2) - d13RotFactor = math.sqrt(2)*math.tan(ankleFlexionRadians/2) - jointPositions = [] - jointPositions.append(x) #1 i += 1 - radius = halfFootThickness + legBottomRadius - # radius = legbohalfFootThickness - jointAdjustDir = add( - set_magnitude(ankleFront, rotationCoeff*radius*flexionRotFactor), - set_magnitude(footDirn, rotationCoeff*radius*flexionRotFactor), - ) - jointAdjustDir = add( - set_magnitude(lowerLegDirn, 0.8*footHeight), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, 0.8*footHeight) - jointPositions.append(add(x, jointAdjustDir)) # 2 - # radius = halfFootThickness - jointAdjustDir = add( - set_magnitude(ankleDirn, rotationCoeff*radius*flexionRotFactor), - set_magnitude(lowerLegDirn, rotationCoeff*radius*flexionRotFactor), - ) - jointAdjustDir = add( - set_magnitude(lowerLegDirn, 0.8*footHeight), - jointAdjustDir - ) - jointAdjustDir = add( - set_magnitude(footDirn, footLength), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, footLength) - jointPositions.append(add(jointPositions[0], jointAdjustDir)) #3 - jointDir = [lowerLegDirn, footDirn, footDirn] - jointPositions, jointDirn = sampleCubicHermiteCurvesSmooth( - jointPositions, jointDir, 2, - derivativeMagnitudeStart=legScale, derivativeMagnitudeEnd=footLength - )[0:2] - # Set coordiantes for joint node - - x = jointPositions[1] - d1 = jointDirn[1] - # ankleFront = cross(d1, d2) + radius = (halfFootThickness+legBottomRadius)/2 + rotationCoeff = 0.15 + [x, ankleDir, footStart, footDir, ankled3_mag, ankled13_mag] =\ + getJointAndDistalFlexionFrames( + ankleFlexionRadians, x, lowerLegDir,radius, + legScale, rotationCoeff,ventralFlexion=True, distalnScale=footLength) + ankleDirn, ankleSide, ankleFront = ankleDir + footDirn, footSide, footFront = footDir + d1 = mult(footDirn, footLength) d2 = set_magnitude(ankleSide, halfFootWidth) - d3 = set_magnitude(ankleFront, radius*(jointRotFactor-(rotationCoeff*flexionRotFactor))) + d3 = set_magnitude(ankleFront, ankled3_mag) d12 = set_magnitude(ankleSide, d12_mag) d13 = add( - set_magnitude(lowerLegFront, d13_mag), - set_magnitude(d3, -radius*d13RotFactor) + set_magnitude(ankleFront, d13_mag), + set_magnitude(footDirn, ankled13_mag) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -1001,56 +957,45 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 - # Foot end node - x = jointPositions[2] - d1 = jointDirn[2] - d1 = computeCubicHermiteEndDerivative(jointPositions[1], jointDirn[1], jointPositions[2], jointDirn[2]) - d2 = set_magnitude(footSide, halfFootWidth) - d3 = set_magnitude(footFront, halfFootThickness) - d12 = set_magnitude(d2, d12_mag) - d13 = set_magnitude(d3, d13_mag) - id2 = mult(d2, innerProportionDefault) - id3 = mult(d3, innerProportionDefault) - id12 = set_magnitude(d12, d12_mag) - id13 = set_magnitude(d13, d13_mag) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) - nodeIdentifier += 1 - options['Kinematic tree']['toes_' + side_label] = x - # This positioning of the food nodes bends edge connecting the leg and the foot - # Which allows the scaffold to better capture the shape of the calcaneus - # anklePosition = add(x, set_magnitude(d1, legScale - 1*rotationFactor*footHeight)) - # fx = [ - # x, - # add(anklePosition, set_magnitude(footd1, 0)), - # add(anklePosition, set_magnitude(footd1, footLength - legBottomRadius)), - - # ] - # fd1 = [d1, set_magnitude(footd1, 0.5*footLength), set_magnitude(footd1, 0.5*footLength)] - # fd1 = smoothCubicHermiteDerivativesLine( - # fx, fd1, fixAllDirections=True, fixStartDerivative=True - # ) - # fd2 = [d2, footd2, footd2] - # fd3 = [d3, - # set_magnitude(footd3, ankleThickness + legBottomRadius), - # set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness) - # ] - # fd12 = sub(fd2[2], fd2[1]) - # fd13 = sub(fd3[2], fd3[1]) - # fid12 = mult(fd12, innerProportionDefault) - # fid13 = mult(fd13, innerProportionDefault) - # for i in range(1, 3): - # node = nodes.findNodeByIdentifier(nodeIdentifier) - # fieldcache.setNode(node) - # setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i], fd12, fd13) - # fid2 = mult(fd2[i], innerProportionDefault) - # fid3 = mult(fd3[i], innerProportionDefault) - # setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3, fid12, fid13) - # nodeIdentifier += 1 + # Foot end nodes - + d1 = mult(footDirn, footLength) + j = 0 + for i in range(footElementsCount): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = add(footStart, mult(d1, j)) + d2 = set_magnitude(footSide, halfFootWidth) + d3 = set_magnitude(footFront, halfFootThickness) + d12 = set_magnitude(d2, d12_mag) + d13 = sub(d3, set_magnitude(ankleFront, ankled3_mag)) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = set_magnitude(d12, d12_mag) + id13 = set_magnitude(d13, d13_mag) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 + j+=1 + # x = footStart + # footDirn, footSide, footFront = footDir + # d1 = mult(footDirn, footLength) + # # d1 = computeCubicHermiteEndDerivative(jointPositions[1], jointDirn[1], jointPositions[2], jointDirn[2]) + # d2 = set_magnitude(footSide, halfFootWidth) + # d3 = set_magnitude(footFront, halfFootThickness) + # d12 = set_magnitude(d2, d12_mag) + # # d13 = set_magnitude(d3, d13_mag) + # d13 = sub(d3, set_magnitude(ankleFront, ankled3_mag)) + # id2 = mult(d2, innerProportionDefault) + # id3 = mult(d3, innerProportionDefault) + # id12 = set_magnitude(d12, d12_mag) + # id13 = set_magnitude(d13, d13_mag) + # node = nodes.findNodeByIdentifier(nodeIdentifier) + # fieldcache.setNode(node) + # setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + # setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + # nodeIdentifier += 1 + options['Kinematic tree']['toes_' + side_label] = x return annotationGroups, networkMesh @classmethod @@ -1095,7 +1040,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 options["Number of elements along shoulder"] = 2 - options["Number of elements along brachium"] = 5 + options["Number of elements along brachium"] = 3 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along hip"] = 2 @@ -1116,7 +1061,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 2 options["Number of elements along thorax"] = 3 options["Number of elements along abdomen"] = 3 - options["Number of elements along brachium"] = 4 + options["Number of elements along shoulder"] = 2 + options["Number of elements along brachium"] = 3 options["Number of elements along antebrachium"] = 3 options["Number of elements along hand"] = 1 options["Number of elements along upper leg"] = 2 @@ -1130,7 +1076,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 2 options["Number of elements along thorax"] = 4 options["Number of elements along abdomen"] = 4 - options["Number of elements along brachium"] = 5 + options["Number of elements along shoulder"] = 2 + options["Number of elements along brachium"] = 3 options["Number of elements along antebrachium"] = 4 options["Number of elements along hand"] = 2 options["Number of elements along upper leg"] = 3 @@ -1153,9 +1100,11 @@ def getOrderedOptionNames(cls): "Number of elements along neck", "Number of elements along thorax", "Number of elements along abdomen", + "Number of elements along shoulder", "Number of elements along brachium", "Number of elements along antebrachium", "Number of elements along hand", + "Number of elements along hip", "Number of elements along upper leg", "Number of elements along lower leg", "Number of elements along foot", @@ -1260,6 +1209,7 @@ def generateBaseMesh(cls, region, options): elementsCountAlongNeck = options["Number of elements along neck"] elementsCountAlongThorax = options["Number of elements along thorax"] elementsCountAlongAbdomen = options["Number of elements along abdomen"] + elementsCountAlongShoulder = options["Number of elements along shoulder"] elementsCountAlongBrachium = options["Number of elements along brachium"] elementsCountAlongAntebrachium = options["Number of elements along antebrachium"] elementsCountAlongHand = options["Number of elements along hand"] @@ -1301,6 +1251,9 @@ def generateBaseMesh(cls, region, options): alongCount = elementsCountAlongAbdomen aroundCount = elementsCountAroundTorso coreBoundaryScalingMode = 2 + elif "shoulder" in name: + alongCount = elementsCountAlongShoulder + aroundCount = elementsCountAroundArm elif " brachium" in name: alongCount = elementsCountAlongBrachium aroundCount = elementsCountAroundArm @@ -1520,7 +1473,7 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, proximalDir, - frontScale, dirnScale, rotationCoeff,ventralFlexion=True): + frontScale, dirnScale, rotationCoeff, distalnScale = False, ventralFlexion=True): jointAngleRadians = math.pi - jointFlexionRadians proximalDirn, proximalSide, proximalFront = proximalDir ventral = 1 if ventralFlexion else -1 @@ -1552,25 +1505,27 @@ def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, pr # Instead it is nudged forward towards the center of the tube # To get as smooth of a line as possible between node #1 and #3 rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor + proximalScale = dirnScale + distalScale = distalnScale if (distalnScale) else dirnScale jointAdjustDir = add( set_magnitude(jointFront, ventral*rotDisplacementFactor), set_magnitude(distalDirn, rotDisplacementFactor), ) jointAdjustDir = add( - set_magnitude(proximalDirn, dirnScale), + set_magnitude(proximalDirn, proximalScale), jointAdjustDir ) - jointAdjustDir = set_magnitude(jointAdjustDir, dirnScale) + jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) nodePositions.append(add(nodePositions[-1], jointAdjustDir)) #2 jointAdjustDir = add( set_magnitude(jointDirn, rotDisplacementFactor), set_magnitude(proximalDirn, rotDisplacementFactor), ) jointAdjustDir = add( - set_magnitude(distalDirn, dirnScale), + set_magnitude(distalDirn, distalScale), jointAdjustDir ) - jointAdjustDir = set_magnitude(jointAdjustDir, dirnScale) + jointAdjustDir = set_magnitude(jointAdjustDir, distalScale) nodePositions.append(add(nodePositions[-1], jointAdjustDir)) #3 # nodeDir = [proximalDirn, jointDirn, distalDirn] # nodePositions, nodeDirn = sampleCubicHermiteCurvesSmooth( diff --git a/src/scaffoldmaker/utils/human_network_layout.py b/src/scaffoldmaker/utils/human_network_layout.py index 823602c0..596f150b 100644 --- a/src/scaffoldmaker/utils/human_network_layout.py +++ b/src/scaffoldmaker/utils/human_network_layout.py @@ -3,7 +3,8 @@ humanElementCounts = { 'headElementsCount': 3, 'neckElementsCount': 2, - 'brachiumElementsCount': 5, + 'shoulderElementsCount': 2, + 'brachiumElementsCount': 3, 'antebrachiumElementsCount': 3, 'handElementsCount': 1, 'thoraxElementsCount': 3, @@ -11,7 +12,7 @@ 'hipElementsCount': 2, 'upperLegElementsCount': 4, 'lowerLegElementsCount': 3, - 'footElementsCount': 2 + 'footElementsCount': 1 } @@ -89,7 +90,13 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): version = 2 if (i == 0) else 3 #Left is 2, right is 3 armNetworkLayout = str(neckJointNode) + '.' + str(version) + '-' nodeIdentifier += 1 + # Shoulder + armNetworkLayout, nodeIdentifier = createSegment( + humanElementCounts['shoulderElementsCount'], + armNetworkLayout, nodeIdentifier, endSegment=True) # Brachium + armNetworkLayout = armNetworkLayout + str(nodeIdentifier) + '-' + nodeIdentifier += 1 armNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['brachiumElementsCount'], armNetworkLayout, nodeIdentifier, endSegment=True) @@ -128,7 +135,7 @@ def constructNetworkLayoutStructure(humanElementCounts:dict): legNetworkLayout, nodeIdentifier = createSegment( humanElementCounts['lowerLegElementsCount'], legNetworkLayout, nodeIdentifier, endSegment=True) - # Feet + # Foot legNetworkLayout = legNetworkLayout + str(nodeIdentifier) + '-' nodeIdentifier += 1 legNetworkLayout, nodeIdentifier = createSegment( From 9f259d89505cb574c83fee513244e0b390dddc5b Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 28 Nov 2025 17:31:10 +1300 Subject: [PATCH 52/61] Initial shoulder flexion development --- .../meshtypes/meshtype_3d_wholebody2.py | 284 +++++++++++++++--- 1 file changed, 241 insertions(+), 43 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 142dd673..361a0fa6 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -58,6 +58,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Arm twist angle degrees"] = 0.0 options["Wrist thickness"] = 0.5 options["Wrist width"] = 0.7 + options["Left wrist flexion degrees"] = 0.0 + options["Right wrist flexion degrees"] = 0.0 options["Hand length"] = 2.0 options["Hand thickness"] = 0.2 options["Hand width"] = 1.0 @@ -106,6 +108,8 @@ def getOrderedOptionNames(cls): "Arm twist angle degrees", "Wrist thickness", "Wrist width", + "Left wrist flexion degrees", + "Right wrist flexion degrees", "Hand length", "Hand thickness", "Hand width", @@ -182,6 +186,8 @@ def checkOptions(cls, options): "Right shoulder flexion degrees": (-60.0, 200.0), "Left elbow flexion degrees": (0.0, 150.0), "Right elbow flexion degrees": (0.0, 150.0), + "Left wrist flexion degrees": (-30.0, 30.0), + "Right wrist flexion degrees": (-30.0, 30.0), "Left hip flexion degrees": (0.0, 150.0), "Right hip flexion degrees": (0.0, 150.0), "Left knee flexion degrees": (0.0, 140.0), @@ -220,6 +226,8 @@ def generateBaseMesh(cls, region, options): armRightAngleRadians = math.radians(options["Right shoulder abduction degrees"]) elbowLeftFlexionRadians = math.radians(options["Left elbow flexion degrees"]) elbowRightFlexionRadians = math.radians(options["Right elbow flexion degrees"]) + wristLeftFlexionRadians = math.radians(options["Left wrist flexion degrees"]) + wristRightFlexionRadians = math.radians(options["Right wrist flexion degrees"]) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) @@ -588,36 +596,45 @@ def generateBaseMesh(cls, region, options): elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) # Shoulder flexion - d2 = mult(armSide, armScale) - d3 = mult(armFront, armScale) # Updating frame of reference wrt flexion angle (using d2 as rotation axis) shoulderFlexionRadians = shoulderLeftFlexionRadians if (side == left) else shoulderRightFlexionRadians - shoulderJointAngleRadians = math.pi - shoulderFlexionRadians - shoulderRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), shoulderFlexionRadians) - shoulderHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(d2, -1), shoulderFlexionRadians/2) - armDirn = matrix_vector_mult(shoulderRotationMatrix, armDirn) - armSide = armSide - armFront = cross(armDirn, armSide) - # The d3 direction in the shoulder node is rotated by half this angle - # To ensure a better transition at this node. - shoulderDirn = armDirn - shoulderSide = armSide - shoulderFront = matrix_vector_mult(shoulderRotationMatrix, d3) - # This rotation factor is used to adjust the position of the knee node relative - # to the angle of flexion, and ensures a proper transition between the upper and lower leg - flexionRotFactor = 1.0*math.sin(shoulderFlexionRadians)*(math.sqrt(2)-1) - i = 0 + armDir = [armDirn, armSide, armFront] + i = 0 xi = i / (armToHandElementsCount - 2) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - # halfWidth = halfWidth/math.sin(shoulderJointAngleRadians/2) - shoulderPosition = add(armStart, set_magnitude(d1, 0)) - x = shoulderPosition - d1 = set_magnitude(shoulderDirn, armScale) - d2 = set_magnitude(shoulderSide, halfThickness) - d3 = set_magnitude(shoulderFront, halfWidth) - d12 = set_magnitude(shoulderSide, d12_mag) - d13 = set_magnitude(shoulderFront, d13_mag) + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + rotationCoeff = 0.25 + upperShoulderPosition = nx[1] + upperShoulderDir = sd1, sd2, sd3 + upperShoulderDir = [set_magnitude(sd, 1) for sd in upperShoulderDir] + shoulderDir, armDir = getJointFlexionFrames(shoulderFlexionRadians, upperShoulderDir, ventralFlexion=True) + shoulderDir, armDir = getJointAdbuctionFrames(armAngleRadians,shoulderDir, armDir, ventralAbduction=True) + x, shoulderd3_mag, shoulderd13_mag = getJointFlexionPosition( + shoulderFlexionRadians, upperShoulderDir, shoulderDir, + armDir, upperShoulderPosition, armStart, halfWidth, rotationCoeff, + ventralFlexion=True) + x, shoulderd2_mag, shoulderd12_mag =getJointAbductionPosition( + armAngleRadians, upperShoulderDir, shoulderDir, + armDir, upperShoulderPosition, x, halfThickness, rotationCoeff, + ventralAbduction=True) + # [x, shoulderDir, armStart, armDir, shoulderd3_mag, shoulderd13_mag] =\ + # getJointAndDistalFlexionFrames(shoulderFlexionRadians, x, upperShoulderDir,halfWidth, arcLengths[1], rotationCoeff,ventralFlexion=True) + # Set coordiantes for joint node + # x = jointPositions[1] + # x = armStart + shoulderDirn, shoulderSide, shoulderFront = shoulderDir + d1 = mult(shoulderDirn, armScale) + # shoulderFront = cross(d1, d2) + d2 = mult(shoulderSide, shoulderd2_mag) + d3 = mult(shoulderFront, shoulderd3_mag) + d12 = add( + mult(shoulderSide, d12_mag), + mult(shoulderDirn, shoulderd12_mag) + ) + d13 = add( + mult(shoulderFront, d13_mag), + mult(shoulderDirn, shoulderd13_mag) + ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) id12 = mult(d12, innerProportionDefault) @@ -627,14 +644,22 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + options['Kinematic tree']['ulna_' + side_label] = x + # antebrachiumStart = jointPositions[-1] + armDirn, armSide, armFront = armDir + d1 = set_magnitude(armDirn, armScale) + armStart = getDistalJointNodePosition( + shoulderFlexionRadians, upperShoulderDir, shoulderDir, + armDir, rotationCoeff, halfWidth, armScale, x) # armStart = add(shoulderPosition,d1) # d1 = mult(armDirn, armScale) # Setting brachium coordinates + j = 0 for i in range(1, brachiumElementsCount): xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = add(armStart, mult(d1, i)) + x = add(armStart, mult(d1, j)) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius if i == 0: @@ -662,6 +687,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + j += 1 # Elbow if twistAngle == 0.0: d2 = armSide @@ -673,8 +699,8 @@ def generateBaseMesh(cls, region, options): mult(armFront, sinTwistAngle)) d3 = add(mult(armFront, cosTwistAngle), mult(armSide, sinTwistAngle)) - armSide = d2 - armFront = d3 + armSide = d2 + armFront = d3 # Updating frame of reference wrt flexion angle (using d2 as rotation axis) elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRightFlexionRadians armDir = [armDirn, armSide, armFront] @@ -713,7 +739,7 @@ def generateBaseMesh(cls, region, options): d1 = set_magnitude(antebrachiumDirn, armScale) # Change d1 to the antebrachium direction j=0 - for i in range(brachiumElementsCount + 1, armToHandElementsCount - 1): + for i in range(brachiumElementsCount + 1, armToHandElementsCount - 2): xi = (i) / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -749,24 +775,69 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 j += 1 - # Hand twist + # Wrist flexion + if twistAngle == 0.0: + d2 = antebrachiumSide + d3 = antebrachiumFront + else: + cosTwistAngle = math.cos(twistAngle) + sinTwistAngle = math.sin(twistAngle) + d2 = sub(mult(antebrachiumSide, cosTwistAngle), + mult(antebrachiumFront, sinTwistAngle)) + d3 = add(mult(antebrachiumFront, cosTwistAngle), + mult(antebrachiumSide, sinTwistAngle)) + antebrachiumSide = d2 + antebrachiumFront = d3 + antebrachiumDir = [antebrachiumDirn, antebrachiumSide, antebrachiumFront] + wristFlexionRadians = wristLeftFlexionRadians if (side == left) else wristRightFlexionRadians + i += 1 + xi = i / (armToHandElementsCount - 2) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + rotationCoeff = 0.25 + [x, wristDir, handStart, handDir, wristd3_mag, wristd13_mag] =\ + getJointAndDistalFlexionFrames(wristFlexionRadians, x, antebrachiumDir,halfWidth, armScale, rotationCoeff,ventralFlexion=True) + # Set coordiantes for joint node + # x = jointPositions[1] + wristDirn, wristSide, wristFront = wristDir + d1 = mult(wristDirn, armScale) + # wristFront = cross(d1, d2) + d2 = mult(wristSide, halfThickness) + d3 = mult(wristFront, wristd3_mag) + d12 = mult(wristSide, d12_mag) + d13 = add( + mult(wristFront, d13_mag), + mult(wristDirn, wristd13_mag) + ) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + id12 = mult(d12, innerProportionDefault) + id13 = mult(d13, innerProportionDefault) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) + nodeIdentifier += 1 options['Kinematic tree']['hand_' + side_label] = x + handDirn, handSide, handFront = handDir + d1 = set_magnitude(handDirn, armScale) + # Hand assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - hx = add(x, mult(antebrachiumDirn, handLength)) + hx = add(x, mult(handDirn, handLength)) hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians if twistAngle >= 0.0: - hd2 = set_magnitude(d2, halfHandThickness) - hd3 = set_magnitude(d3, halfHandWidth) + hd2 = set_magnitude(handSide, halfHandThickness) + hd3 = set_magnitude(handFront, halfHandWidth) else: cosTwistAngle = math.cos(twistAngle) sinTwistAngle = math.sin(twistAngle) - hd2 = sub(mult(antebrachiumSide, halfHandThickness * cosTwistAngle), - mult(antebrachiumFront, halfHandThickness * sinTwistAngle)) - hd3 = add(mult(antebrachiumFront, halfHandWidth * cosTwistAngle), - mult(antebrachiumSide, halfHandWidth * sinTwistAngle)) + hd2 = sub(mult(handSide, halfHandThickness * cosTwistAngle), + mult(handFront, halfHandThickness * sinTwistAngle)) + hd3 = add(mult(handFront, halfHandWidth * cosTwistAngle), + mult(handSide, halfHandWidth * sinTwistAngle)) hid2 = mult(hd2, innerProportionDefault) hid3 = mult(hd3, innerProportionDefault) setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) @@ -932,8 +1003,8 @@ def generateBaseMesh(cls, region, options): # foot ankleFlexionRadians = ankleLeftFlexionRadians if (side == left) else ankleRightFlexionRadians i += 1 - radius = (halfFootThickness+legBottomRadius)/2 - rotationCoeff = 0.15 + radius = (legBottomRadius + halfFootThickness) + rotationCoeff = 0.35 [x, ankleDir, footStart, footDir, ankled3_mag, ankled13_mag] =\ getJointAndDistalFlexionFrames( ankleFlexionRadians, x, lowerLegDir,radius, @@ -958,7 +1029,6 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # Foot end nodes - d1 = mult(footDirn, footLength) j = 0 for i in range(footElementsCount): @@ -1046,7 +1116,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along hip"] = 2 options["Number of elements along upper leg"] = 3 options["Number of elements along lower leg"] = 3 - options["Number of elements along foot"] = 2 + options["Number of elements along foot"] = 1 options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 options["Number of elements around arm"] = 8 @@ -1472,6 +1542,134 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N if d13: field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) + +def getJointFlexionFrame(jointFlexionRadians, proximalDir, ventralFlexion=True): + jointAngleRadians = math.pi - jointFlexionRadians + proximalDirn, proximalSide, proximalFront = proximalDir + ventral = 1 if ventralFlexion else -1 + jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) + jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) + # Joint directions (frame is rotated by half the flexion angle) + jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) + jointSide = proximalSide + jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) + jointDir = [jointDirn, jointSide, jointFront] + return jointDir + +def getJointAdbuctionFrames(jointAbductionRadians, jointDir, distalDir, ventralAbduction=True): + jointAngleRadians = math.pi - jointAbductionRadians + jointDirn, jointSide, jointFront = jointDir + distalDirn, distalSide, distalFront = distalDir + ventral = 1 if ventralAbduction else -1 + jointRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians) + jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians/2) + # Joint directions (frame is rotated by half the abduction angle) + jointDirn = matrix_vector_mult(jointHalfRotationMatrix, jointDirn) + jointSide = matrix_vector_mult(jointHalfRotationMatrix, jointSide) + jointFront = jointFront + # Distal directions + distalDirn = matrix_vector_mult(jointRotationMatrix, distalDirn) + distalSide = matrix_vector_mult(jointRotationMatrix, distalSide) + distalFront = distalFront + jointDir = [jointDirn, jointSide, jointFront] + distalDir = [distalDirn, distalSide, distalFront] + return jointDir, distalDir + +def getJointFlexionFrames(jointFlexionRadians, proximalDir, ventralFlexion=True): + jointAngleRadians = math.pi - jointFlexionRadians + proximalDirn, proximalSide, proximalFront = proximalDir + ventral = 1 if ventralFlexion else -1 + jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) + jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) + # Joint directions (frame is rotated by half the flexion angle) + jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) + jointSide = proximalSide + jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) + # Distal directions + distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) + distalSide = proximalSide + distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) + jointDir = [jointDirn, jointSide, jointFront] + distalDir = [distalDirn, distalSide, distalFront] + return jointDir, distalDir + +def getJointFlexionPosition(jointFlexionRadians, proximalDir, + jointDir, distalDir, proximalNodePosition, + jointNodePosition, frontScale, + rotationCoeff, ventralFlexion = True): + jointAngleRadians = math.pi - jointFlexionRadians + proximalDirn, proximalSide, proximalFront = proximalDir + jointDirn, jointSide, jointFront = jointDir + distalDirn, distalSide, distalFront = distalDir + flexionRotFactor = 1*math.sin(jointAngleRadians) + jointRotFactor = 1/math.sin(jointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(jointFlexionRadians/2) + ventral = 1 if ventralFlexion else -1 + proximalScale = magnitude(sub(proximalNodePosition, jointNodePosition)) + rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor + jointAdjustDir = add( + set_magnitude(jointFront, ventral*rotDisplacementFactor), + set_magnitude(distalDirn, rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(proximalDirn, proximalScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) + jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) + jointDir + jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) + jointd13_mag = -1*ventral*frontScale*d13RotFactor + return [jointAdjustPosition, jointd3_mag, jointd13_mag] + +def getJointAbductionPosition(jointAbductionRadians, proximalDir, + jointDir, distalDir, proximalNodePosition, + jointNodePosition, sideScale, + rotationCoeff, ventralAbduction = True): + jointAngleRadians = math.pi - jointAbductionRadians + proximalDirn, proximalSide, proximalFront = proximalDir + jointDirn, jointSide, jointFront = jointDir + distalDirn, distalSide, distalFront = distalDir + AbductionRotFactor = 1*math.sin(jointAngleRadians) + jointRotFactor = 1/math.sin(jointAngleRadians/2) + d13RotFactor = math.sqrt(2)*math.tan(jointAbductionRadians/2) + ventral = 1 if ventralAbduction else -1 + proximalScale = magnitude(sub(proximalNodePosition, jointNodePosition)) + rotDisplacementFactor = rotationCoeff*sideScale*AbductionRotFactor + jointAdjustDir = add( + set_magnitude(jointSide, ventral*rotDisplacementFactor), + set_magnitude(distalDirn, rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(proximalDirn, proximalScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) + jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) + jointd2_mag = sideScale*(jointRotFactor-(rotationCoeff*AbductionRotFactor)) + jointd12_mag = -1*ventral*sideScale*d13RotFactor + return [jointAdjustPosition, jointd2_mag, jointd12_mag] + +def getDistalJointNodePosition(jointFlexionRadians, proximalDir, jointDir, distalDir, rotationCoeff, + frontScale, distalScale, jointNodePosition): + proximalDirn, proximalSide, proximalFront = proximalDir + jointDirn, jointSide, jointFront = jointDir + distalDirn, distalSide, distalFront = distalDir + jointAngleRadians = math.pi - jointFlexionRadians + flexionRotFactor = 1*math.sin(jointAngleRadians) + rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor + jointAdjustDir = add( + set_magnitude(jointDirn, rotDisplacementFactor), + set_magnitude(proximalDirn, rotDisplacementFactor), + ) + jointAdjustDir = add( + set_magnitude(distalDirn, distalScale), + jointAdjustDir + ) + jointAdjustDir = set_magnitude(jointAdjustDir, distalScale) + distalNodePosition = add(jointNodePosition, jointAdjustDir) + return distalNodePosition + def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, proximalDir, frontScale, dirnScale, rotationCoeff, distalnScale = False, ventralFlexion=True): jointAngleRadians = math.pi - jointFlexionRadians @@ -1483,7 +1681,7 @@ def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, pr jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) jointSide = proximalSide jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) - # Proximal directions + # Distal directions distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) distalSide = proximalSide distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) From 014261b236a5c2190fec117167fc983d5a967c68 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 5 Dec 2025 16:19:25 +1300 Subject: [PATCH 53/61] added arm rotation, shoulder flexion and wrist flexion/deviation --- .../meshtypes/meshtype_3d_wholebody2.py | 623 ++++++++++++------ 1 file changed, 422 insertions(+), 201 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 361a0fa6..af2aa38f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,7 +1,7 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude, axis_angle_to_rotation_matrix, matrix_vector_mult, dot +from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude, axis_angle_to_rotation_matrix, matrix_vector_mult, matrix_mult, dot, angle from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates from cmlibs.utils.zinc.finiteelement import get_maximum_node_identifier from cmlibs.zinc.element import Element @@ -46,13 +46,15 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Head width"] = 2.0 options["Neck length"] = 1.3 options["Shoulder drop"] = 1.0 - options["Shoulder width"] = 4.5 + options["Shoulder width"] = 5.0 options["Left shoulder flexion degrees"] = 0.0 options["Right shoulder flexion degrees"] = 0.0 - options["Left shoulder abduction degrees"] = 10.0 - options["Right shoulder abduction degrees"] = 10.0 - options["Left elbow flexion degrees"] = 90.0 - options["Right elbow flexion degrees"] = 110.0 + options["Left shoulder abduction degrees"] = 0.0 + options["Right shoulder abduction degrees"] = 0.0 + options["Left elbow flexion degrees"] = 0.0 + options["Right elbow flexion degrees"] = 0.0 + options["Left arm rotation degrees"] = 0.0 + options["Right arm rotation degrees"] = 0.0 options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Arm twist angle degrees"] = 0.0 @@ -60,6 +62,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Wrist width"] = 0.7 options["Left wrist flexion degrees"] = 0.0 options["Right wrist flexion degrees"] = 0.0 + options["Left wrist deviation degrees"] = 0.0 + options["Right wrist deviation degrees"] = 0.0 options["Hand length"] = 2.0 options["Hand thickness"] = 0.2 options["Hand width"] = 1.0 @@ -71,8 +75,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Pelvis width"] = 2.0 options["Left leg abduction degrees"] = 10.0 options["Right leg abduction degrees"] = 10.0 - options["Left hip flexion degrees"] = 90.0 - options["Right hip flexion degrees"] = 90.0 + options["Left hip flexion degrees"] = 0.0 + options["Right hip flexion degrees"] = 0.0 options["Leg length"] = 11.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 @@ -98,18 +102,25 @@ def getOrderedOptionNames(cls): "Shoulder drop", "Shoulder width", "Left shoulder abduction degrees", - "Right shoulder abduction degrees", "Left shoulder flexion degrees", - "Right shoulder flexion degrees", "Left elbow flexion degrees", + "Left arm rotation degrees", + "Right shoulder abduction degrees", + + "Right shoulder flexion degrees", + "Right elbow flexion degrees", + + "Right arm rotation degrees", + "Left wrist flexion degrees", + "Right wrist flexion degrees", + "Left wrist deviation degrees", + "Right wrist deviation degrees", "Arm length", "Arm top diameter", "Arm twist angle degrees", "Wrist thickness", "Wrist width", - "Left wrist flexion degrees", - "Right wrist flexion degrees", "Hand length", "Hand thickness", "Hand width", @@ -180,12 +191,14 @@ def checkOptions(cls, options): elif options[key] > 0.9: options[key] = 0.9 for key, angleRange in { - "Left shoulder abduction degrees": (-60.0, 180.0), - "Right shoulder abduction degrees": (-60.0, 180.0), + "Left shoulder abduction degrees": (-100.0, 180.0), + "Right shoulder abduction degrees": (-100.0, 180.0), "Left shoulder flexion degrees": (-60.0, 200.0), "Right shoulder flexion degrees": (-60.0, 200.0), - "Left elbow flexion degrees": (0.0, 150.0), - "Right elbow flexion degrees": (0.0, 150.0), + # "Left elbow flexion degrees": (0.0, 150.0), + # "Right elbow flexion degrees": (0.0, 150.0), + # "Left arm rotation degrees": (0.0, 150.0), + # "Right arm rotation degrees": (0.0, 150.0), "Left wrist flexion degrees": (-30.0, 30.0), "Right wrist flexion degrees": (-30.0, 30.0), "Left hip flexion degrees": (0.0, 150.0), @@ -226,8 +239,12 @@ def generateBaseMesh(cls, region, options): armRightAngleRadians = math.radians(options["Right shoulder abduction degrees"]) elbowLeftFlexionRadians = math.radians(options["Left elbow flexion degrees"]) elbowRightFlexionRadians = math.radians(options["Right elbow flexion degrees"]) + armLeftRotationRadians = math.radians(options["Left arm rotation degrees"]) + armRightRotationRadians = math.radians(options["Right arm rotation degrees"]) wristLeftFlexionRadians = math.radians(options["Left wrist flexion degrees"]) wristRightFlexionRadians = math.radians(options["Right wrist flexion degrees"]) + wristLeftAbductionRadians = math.radians(options["Left wrist deviation degrees"]) + wristRightAbductionRadians = math.radians(options["Right wrist deviation degrees"]) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] armTwistAngleRadians = math.radians(options["Arm twist angle degrees"]) @@ -527,8 +544,8 @@ def generateBaseMesh(cls, region, options): # rotate shoulder with arm, pivoting about shoulder drop below arm junction on network # this has the realistic effect of shoulders becoming narrower with higher angles # initial shoulder rotation with arm is negligible, hence: - armAngleRadians = armLeftAngleRadians if (side == left) else armRightAngleRadians - shoulderRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) + armAbductionRadians = armLeftAngleRadians if (side == left) else armRightAngleRadians + shoulderRotationFactor = 1.0 - math.cos(0.5 * armAbductionRadians) # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation shoulderLimitAngleRadians = math.asin(1.5 * shoulderDrop / halfShoulderWidth) shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians @@ -537,7 +554,7 @@ def generateBaseMesh(cls, region, options): armScale = nonHandArmLength / (armToHandElementsCount - shoulderElementsCount) d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - shoulderElementsCount) d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - shoulderElementsCount) - armAngle = armAngleRadians if (side == left) else -armAngleRadians + armAngle = armAbductionRadians if (side == left) else -armAbductionRadians cosArmAngle = math.cos(armAngle) sinArmAngle = math.sin(armAngle) armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) @@ -557,6 +574,7 @@ def generateBaseMesh(cls, region, options): sNodeIdentifiers = [] side_label = 'l' if (side == left) else 'r' options['Kinematic tree']['humerus_' + side_label] = nx[1] + # Upper shoulder nodes for i in range(2): sNodeIdentifiers.append(nodeIdentifier if (i > 0) else armJunctionNodeIdentifier) node = nodes.findNodeByIdentifier(sNodeIdentifiers[-1]) @@ -578,53 +596,48 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) - sd2_list.append([-armTopRadius * sinArmAngle, armTopRadius * cosArmAngle, 0.0]) - sd3_list.append([0.0, 0.0, armTopRadius]) - for i in range(2): - node = nodes.findNodeByIdentifier(sNodeIdentifiers[i]) - fieldcache.setNode(node) - version = 1 if (i > 0) else 2 if (side == left) else 3 - sd12 = sub(sd2_list[i + 1], sd2_list[i]) - sd13 = sub(sd3_list[i + 1], sd3_list[i]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sd12) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sd13) - sid12 = mult(sd12, innerProportionDefault) - sid13 = mult(sd13, innerProportionDefault) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sid12) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sid13) # Arm twist elementTwistAngle = ((armTwistAngleRadians if (side == left) else -armTwistAngleRadians) / (armToHandElementsCount - 3)) - # Shoulder flexion - # Updating frame of reference wrt flexion angle (using d2 as rotation axis) + # Shoulder rotation + #Reset flexion angle w.r.t. current node shoulderFlexionRadians = shoulderLeftFlexionRadians if (side == left) else shoulderRightFlexionRadians - armDir = [armDirn, armSide, armFront] + ventralFlexion = True + if shoulderFlexionRadians < 0: + ventralFlexion = False + shoulderFlexionRadians = -shoulderFlexionRadians + #Reset abdution angle w.r.t. current node + shoulderAbductionRadians = (1 if (side == left) else -1)*angle([1,0,0], sd1) - armAngle + upwardAbduction = True + if shoulderAbductionRadians < 0: + upwardAbduction = False + shoulderAbductionRadians = -shoulderAbductionRadians + # Calculate magntiudes for joint node i = 0 xi = i / (armToHandElementsCount - 2) halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - rotationCoeff = 0.25 - upperShoulderPosition = nx[1] - upperShoulderDir = sd1, sd2, sd3 - upperShoulderDir = [set_magnitude(sd, 1) for sd in upperShoulderDir] - shoulderDir, armDir = getJointFlexionFrames(shoulderFlexionRadians, upperShoulderDir, ventralFlexion=True) - shoulderDir, armDir = getJointAdbuctionFrames(armAngleRadians,shoulderDir, armDir, ventralAbduction=True) - x, shoulderd3_mag, shoulderd13_mag = getJointFlexionPosition( - shoulderFlexionRadians, upperShoulderDir, shoulderDir, - armDir, upperShoulderPosition, armStart, halfWidth, rotationCoeff, - ventralFlexion=True) - x, shoulderd2_mag, shoulderd12_mag =getJointAbductionPosition( - armAngleRadians, upperShoulderDir, shoulderDir, - armDir, upperShoulderPosition, x, halfThickness, rotationCoeff, - ventralAbduction=True) - # [x, shoulderDir, armStart, armDir, shoulderd3_mag, shoulderd13_mag] =\ - # getJointAndDistalFlexionFrames(shoulderFlexionRadians, x, upperShoulderDir,halfWidth, arcLengths[1], rotationCoeff,ventralFlexion=True) - # Set coordiantes for joint node - # x = jointPositions[1] - # x = armStart - shoulderDirn, shoulderSide, shoulderFront = shoulderDir + upperShoulderDir = [nx[1], sd1, sd2, sd3, nd1[1], 0, 0] + armRotationRadians = armLeftRotationRadians if (side == left) else armRightRotationRadians + x1 = [0,1,0] + for i in range(1, 4): + upperShoulderDir[i] = set_magnitude(upperShoulderDir[i], 1) #normalize frame + shoulderDir, armDir = getJointRotationFrames( \ + shoulderAbductionRadians, shoulderFlexionRadians, upperShoulderDir, upwardAbduction, ventralFlexion) + shoulderDir[0] = armStart + shoulderDir[4:] = [armScale, halfThickness, halfWidth] + rotationCoeff = 0.2 + x, shoulderd2_mag, shoulderd3_mag, shoulderd12_mag, shoulderd13_mag = getJointRotationPosition( + shoulderAbductionRadians, shoulderFlexionRadians, upperShoulderDir, \ + shoulderDir, armDir, rotationCoeff, upwardAbduction, ventralFlexion + ) + armRotationRadians = armLeftRotationRadians if (side == left) else armRightRotationRadians + jointAbductionMatrix = axis_angle_to_rotation_matrix(mult(shoulderDir[1], 1), armRotationRadians) + for i in range(2, 4): + shoulderDir[i] = matrix_vector_mult(jointAbductionMatrix, shoulderDir[i]) + armDir[i] = matrix_vector_mult(jointAbductionMatrix, armDir[i]) + shoulderDirn, shoulderSide, shoulderFront = shoulderDir[1:4] d1 = mult(shoulderDirn, armScale) - # shoulderFront = cross(d1, d2) d2 = mult(shoulderSide, shoulderd2_mag) d3 = mult(shoulderFront, shoulderd3_mag) d12 = add( @@ -644,15 +657,29 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 + sd2_list.append(d2) + sd3_list.append(d3) + # Adjusting d12 and 13 for the upper Shoulder node + for i in range(2): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[i]) + fieldcache.setNode(node) + version = 1 if (i > 0) else 2 if (side == left) else 3 + sd12 = sub(sd2_list[i + 1], sd2_list[i]) + sd13 = sub(sd3_list[i + 1], sd3_list[i]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sd12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sd13) + sid12 = mult(sd12, innerProportionDefault) + sid13 = mult(sd13, innerProportionDefault) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sid12) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sid13) options['Kinematic tree']['ulna_' + side_label] = x - # antebrachiumStart = jointPositions[-1] - armDirn, armSide, armFront = armDir + # Initial position for arm node + armDirn, armSide, armFront = armDir[1:4] d1 = set_magnitude(armDirn, armScale) - armStart = getDistalJointNodePosition( - shoulderFlexionRadians, upperShoulderDir, shoulderDir, - armDir, rotationCoeff, halfWidth, armScale, x) - # armStart = add(shoulderPosition,d1) - # d1 = mult(armDirn, armScale) + armStart = add(x, d1) + armDir[0] = armStart + armStart = getDistalNodePosition(shoulderAbductionRadians, shoulderFlexionRadians, + upperShoulderDir, shoulderDir, armDir, 0.2) # Setting brachium coordinates j = 0 for i in range(1, brachiumElementsCount): @@ -689,6 +716,8 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 j += 1 # Elbow + i += 1 + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i) if twistAngle == 0.0: d2 = armSide d3 = armFront @@ -703,22 +732,30 @@ def generateBaseMesh(cls, region, options): armFront = d3 # Updating frame of reference wrt flexion angle (using d2 as rotation axis) elbowFlexionRadians = elbowLeftFlexionRadians if (side == left) else elbowRightFlexionRadians - armDir = [armDirn, armSide, armFront] - i += 1 + elbowAbductionRadians = 0 + ventralFlexion = True + if shoulderFlexionRadians < 0: + ventralFlexion = False + shoulderFlexionRadians = -shoulderFlexionRadians + armDir[0] = x xi = i / (armToHandElementsCount - 2) - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius rotationCoeff = 0.25 - [x, elbowDir, antebrachiumStart, antebrachiumDir, elbowd3_mag, elbowd13_mag] =\ - getJointAndDistalFlexionFrames(elbowFlexionRadians, x, armDir,halfWidth, armScale, rotationCoeff,ventralFlexion=True) - # Set coordiantes for joint node - # x = jointPositions[1] - elbowDirn, elbowSide, elbowFront = elbowDir + elbowDir, antebrachiumDir = getJointRotationFrames( \ + 0, elbowFlexionRadians, armDir, upwardAbduction, ventralFlexion) + elbowDir[0] = add(x, d1) + elbowDir[4:] = [armScale, halfThickness, halfWidth] + x, elbowd2_mag, elbowd3_mag, elbowd12_mag, elbowd13_mag = getJointRotationPosition( + 0, elbowFlexionRadians, armDir, elbowDir, antebrachiumDir, rotationCoeff, upwardAbduction, ventralFlexion) + elbowDirn, elbowSide, elbowFront = elbowDir[1:4] d1 = mult(elbowDirn, armScale) - # elbowFront = cross(d1, d2) - d2 = mult(elbowSide, halfThickness) + d2 = mult(elbowSide, elbowd2_mag) d3 = mult(elbowFront, elbowd3_mag) - d12 = mult(elbowSide, d12_mag) + d12 = add( + mult(elbowSide, d12_mag), + mult(elbowDirn, elbowd12_mag) + ) d13 = add( mult(elbowFront, d13_mag), mult(elbowDirn, elbowd13_mag) @@ -735,9 +772,12 @@ def generateBaseMesh(cls, region, options): options['Kinematic tree']['ulna_' + side_label] = x # Antebrachium nodes starts after the elbow node # antebrachiumStart = jointPositions[-1] - antebrachiumDirn, antebrachiumSide, antebrachiumFront = antebrachiumDir + antebrachiumDirn, antebrachiumSide, antebrachiumFront = antebrachiumDir[1:4] d1 = set_magnitude(antebrachiumDirn, armScale) - # Change d1 to the antebrachium direction + antebrachiumStart = add(x, d1) + antebrachiumDir[0] = antebrachiumStart + antebrachiumStart = getDistalNodePosition(elbowAbductionRadians, elbowFlexionRadians, + armDir, elbowDir, antebrachiumDir, rotationCoeff) j=0 for i in range(brachiumElementsCount + 1, armToHandElementsCount - 2): xi = (i) / (armToHandElementsCount - 2) @@ -749,7 +789,7 @@ def generateBaseMesh(cls, region, options): if i == 0: twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians else: - twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i - 1) + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i) if twistAngle == 0.0: d2 = mult(antebrachiumSide, halfThickness) d3 = mult(antebrachiumFront, halfWidth) @@ -776,6 +816,8 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 j += 1 # Wrist flexion + i += 1 + twistAngle = -0.5 * elementTwistAngle + elementTwistAngle * (i) if twistAngle == 0.0: d2 = antebrachiumSide d3 = antebrachiumFront @@ -788,23 +830,40 @@ def generateBaseMesh(cls, region, options): mult(antebrachiumSide, sinTwistAngle)) antebrachiumSide = d2 antebrachiumFront = d3 - antebrachiumDir = [antebrachiumDirn, antebrachiumSide, antebrachiumFront] wristFlexionRadians = wristLeftFlexionRadians if (side == left) else wristRightFlexionRadians - i += 1 + wristAbductionRadians = wristLeftAbductionRadians if (side == left) else wristRightAbductionRadians + ventralFlexion = True + if wristFlexionRadians < 0: + ventralFlexion = False + wristFlexionRadians = -wristFlexionRadians + upwardAbduction = False + if wristAbductionRadians < 0: + upwardAbduction = True + wristAbductionRadians = -wristAbductionRadians + antebrachiumDir[0] = x xi = i / (armToHandElementsCount - 2) - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - rotationCoeff = 0.25 - [x, wristDir, handStart, handDir, wristd3_mag, wristd13_mag] =\ - getJointAndDistalFlexionFrames(wristFlexionRadians, x, antebrachiumDir,halfWidth, armScale, rotationCoeff,ventralFlexion=True) + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + rotationCoeff = 0.4 + wristDir, handDir = getJointRotationFrames( + wristAbductionRadians, wristFlexionRadians, antebrachiumDir, upwardAbduction, ventralFlexion + ) + wristDir[0] = add(x, d1) + wristDir[4:] = [handLength, halfThickness, halfWidth] + x, wristd2_mag, wristd3_mag, wristd12_mag, wristd13_mag = getJointRotationPosition( + wristAbductionRadians, wristFlexionRadians, antebrachiumDir, wristDir, handDir, rotationCoeff, upwardAbduction, ventralFlexion + ) # Set coordiantes for joint node # x = jointPositions[1] - wristDirn, wristSide, wristFront = wristDir - d1 = mult(wristDirn, armScale) - # wristFront = cross(d1, d2) - d2 = mult(wristSide, halfThickness) + wristDirn, wristSide, wristFront = wristDir[1:4] + handDirn, handSide, handFront = handDir[1:4] + d1 = mult(wristDirn, handLength) + d2 = mult(wristSide, wristd2_mag) d3 = mult(wristFront, wristd3_mag) - d12 = mult(wristSide, d12_mag) + d12 = add( + mult(wristSide, d12_mag), + mult(wristDirn, wristd12_mag) + ) d13 = add( mult(wristFront, d13_mag), mult(wristDirn, wristd13_mag) @@ -819,14 +878,16 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 options['Kinematic tree']['hand_' + side_label] = x - handDirn, handSide, handFront = handDir d1 = set_magnitude(handDirn, armScale) + handStart = add(x, mult(handDirn, handLength)) + handDir[0] = handStart + handStart = getDistalNodePosition(wristAbductionRadians, wristFlexionRadians, antebrachiumDir, + wristDir, handDir, rotationCoeff) # Hand assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - hx = add(x, mult(handDirn, handLength)) - hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) + hd1 = computeCubicHermiteEndDerivative(x, d1, handStart, d1) twistAngle = armTwistAngleRadians if (side == left) else -armTwistAngleRadians if twistAngle >= 0.0: hd2 = set_magnitude(handSide, halfHandThickness) @@ -840,8 +901,8 @@ def generateBaseMesh(cls, region, options): mult(handSide, halfHandWidth * sinTwistAngle)) hid2 = mult(hd2, innerProportionDefault) hid3 = mult(hd3, innerProportionDefault) - setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) - setNodeFieldParameters(innerCoordinates, fieldcache, hx, hd1, hid2, hid3) + setNodeFieldParameters(coordinates, fieldcache, handStart, hd1, hd2, hd3) + setNodeFieldParameters(innerCoordinates, fieldcache, handStart, hd1, hid2, hid3) nodeIdentifier += 1 # legs legStartX = abdomenStartX + abdomenLength + pelvisDrop @@ -1111,7 +1172,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along abdomen"] = 2 options["Number of elements along shoulder"] = 2 options["Number of elements along brachium"] = 3 - options["Number of elements along antebrachium"] = 3 + options["Number of elements along antebrachium"] = 2 options["Number of elements along hand"] = 1 options["Number of elements along hip"] = 2 options["Number of elements along upper leg"] = 3 @@ -1543,132 +1604,292 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) -def getJointFlexionFrame(jointFlexionRadians, proximalDir, ventralFlexion=True): - jointAngleRadians = math.pi - jointFlexionRadians - proximalDirn, proximalSide, proximalFront = proximalDir - ventral = 1 if ventralFlexion else -1 - jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) - jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) - # Joint directions (frame is rotated by half the flexion angle) - jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) - jointSide = proximalSide - jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) - jointDir = [jointDirn, jointSide, jointFront] - return jointDir +# def getJointFlexionFrame(jointFlexionRadians, proximalDir, ventralFlexion=True): +# jointAngleRadians = math.pi - jointFlexionRadians +# proximalDirn, proximalSide, proximalFront = proximalDir +# ventral = 1 if ventralFlexion else -1 +# jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) +# jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) +# # Joint directions (frame is rotated by half the flexion angle) +# jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) +# jointSide = proximalSide +# jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) +# jointDir = [jointDirn, jointSide, jointFront] +# return jointDir -def getJointAdbuctionFrames(jointAbductionRadians, jointDir, distalDir, ventralAbduction=True): - jointAngleRadians = math.pi - jointAbductionRadians - jointDirn, jointSide, jointFront = jointDir - distalDirn, distalSide, distalFront = distalDir - ventral = 1 if ventralAbduction else -1 - jointRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians) - jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians/2) - # Joint directions (frame is rotated by half the abduction angle) - jointDirn = matrix_vector_mult(jointHalfRotationMatrix, jointDirn) - jointSide = matrix_vector_mult(jointHalfRotationMatrix, jointSide) - jointFront = jointFront - # Distal directions - distalDirn = matrix_vector_mult(jointRotationMatrix, distalDirn) - distalSide = matrix_vector_mult(jointRotationMatrix, distalSide) - distalFront = distalFront - jointDir = [jointDirn, jointSide, jointFront] - distalDir = [distalDirn, distalSide, distalFront] - return jointDir, distalDir +# def getJointAdbuctionFrames(jointAbductionRadians, jointDir, distalDir, ventralAbduction=True): +# jointAngleRadians = math.pi - jointAbductionRadians +# jointDirn, jointSide, jointFront = jointDir +# distalDirn, distalSide, distalFront = distalDir +# ventral = 1 if ventralAbduction else -1 +# jointRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians) +# jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians/2) +# # Joint directions (frame is rotated by half the abduction angle) +# jointDirn = matrix_vector_mult(jointHalfRotationMatrix, jointDirn) +# jointSide = matrix_vector_mult(jointHalfRotationMatrix, jointSide) +# jointFront = jointFront +# # Distal directions +# distalDirn = matrix_vector_mult(jointRotationMatrix, distalDirn) +# distalSide = matrix_vector_mult(jointRotationMatrix, distalSide) +# distalFront = distalFront +# jointDir = [jointDirn, jointSide, jointFront] +# distalDir = [distalDirn, distalSide, distalFront] +# return jointDir, distalDir -def getJointFlexionFrames(jointFlexionRadians, proximalDir, ventralFlexion=True): - jointAngleRadians = math.pi - jointFlexionRadians - proximalDirn, proximalSide, proximalFront = proximalDir +def getJointRotationFrames(jointAbductionRadians, jointFlexionRadians, proximalDir, upwardAbduction=True, ventralFlexion=True): + proximalDirn, proximalSide, proximalFront = proximalDir[1:4] + ventral = 1 if upwardAbduction else -1 + jointAbductionMatrix = axis_angle_to_rotation_matrix(mult(proximalFront, -1*ventral), jointAbductionRadians) + jointHalfAbductionMatrix = axis_angle_to_rotation_matrix(mult(proximalFront, -1*ventral), jointAbductionRadians/2) ventral = 1 if ventralFlexion else -1 - jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) - jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) - # Joint directions (frame is rotated by half the flexion angle) + jointFlexionMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) + jointHalfFlexionMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) + # jointRotationMatrix = matrix_mult(jointAbductionMatrix, jointFlexionMatrix) + # jointHalfRotationMatrix = matrix_mult(jointHalfAbductionMatrix, jointHalfFlexionMatrix) + jointRotationMatrix = matrix_mult(jointFlexionMatrix,jointAbductionMatrix) + jointHalfRotationMatrix = matrix_mult(jointHalfFlexionMatrix, jointHalfAbductionMatrix) + # Joint directions (frame is rotated by half the abduction angle) jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) - jointSide = proximalSide + jointSide = matrix_vector_mult(jointHalfRotationMatrix, proximalSide) jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) # Distal directions distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) - distalSide = proximalSide + distalSide = matrix_vector_mult(jointRotationMatrix, proximalSide) distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) - jointDir = [jointDirn, jointSide, jointFront] - distalDir = [distalDirn, distalSide, distalFront] + jointDir = [0, jointDirn, jointSide, jointFront, 0, 0, 0] + distalDir = [0, distalDirn, distalSide, distalFront, 0, 0, 0] return jointDir, distalDir -def getJointFlexionPosition(jointFlexionRadians, proximalDir, - jointDir, distalDir, proximalNodePosition, - jointNodePosition, frontScale, - rotationCoeff, ventralFlexion = True): + +def getJointRotationPosition(jointAbductionRadians, jointFlexionRadians, \ + proximalDir, jointDir, distalDir, rotationCoeff, upwardAbduction=True, ventralFlexion=True): + ventral = 1 if ventralFlexion else -1 + upward = 1 if upwardAbduction else -1 + proximalNodePosition, proximalDirn, proximalSide, proximalFront = proximalDir[0:4] + jointNodePosition, jointDirn, jointSide, jointFront = jointDir[0:4] + distalNodePosition, distalDirn, distalSide, distalFront = distalDir[0:4] + # Flexion node adjustment jointAngleRadians = math.pi - jointFlexionRadians - proximalDirn, proximalSide, proximalFront = proximalDir - jointDirn, jointSide, jointFront = jointDir - distalDirn, distalSide, distalFront = distalDir - flexionRotFactor = 1*math.sin(jointAngleRadians) + frontScale = jointDir[6] + rotationRotFactor = 1*math.sin(jointAngleRadians) jointRotFactor = 1/math.sin(jointAngleRadians/2) d13RotFactor = math.sqrt(2)*math.tan(jointFlexionRadians/2) - ventral = 1 if ventralFlexion else -1 - proximalScale = magnitude(sub(proximalNodePosition, jointNodePosition)) - rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor - jointAdjustDir = add( - set_magnitude(jointFront, ventral*rotDisplacementFactor), - set_magnitude(distalDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(proximalDirn, proximalScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) - jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) - jointDir - jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) - jointd13_mag = -1*ventral*frontScale*d13RotFactor - return [jointAdjustPosition, jointd3_mag, jointd13_mag] - -def getJointAbductionPosition(jointAbductionRadians, proximalDir, - jointDir, distalDir, proximalNodePosition, - jointNodePosition, sideScale, - rotationCoeff, ventralAbduction = True): + rotDisplacementFactor = rotationCoeff*frontScale*rotationRotFactor + jointNodePosition = adjustJointNodePosition(proximalNodePosition, jointNodePosition, \ + jointFront, distalDirn, rotDisplacementFactor, ventralFlexion) + d3_mag = frontScale*(jointRotFactor-(rotationCoeff*rotationRotFactor)) + d13_mag = -1*ventral*frontScale*d13RotFactor + # Abduction node adjustment jointAngleRadians = math.pi - jointAbductionRadians - proximalDirn, proximalSide, proximalFront = proximalDir - jointDirn, jointSide, jointFront = jointDir - distalDirn, distalSide, distalFront = distalDir - AbductionRotFactor = 1*math.sin(jointAngleRadians) + sideScale = jointDir[5] + jointSide = mult(jointSide, -1) + rotationRotFactor = 1*math.sin(jointAngleRadians) jointRotFactor = 1/math.sin(jointAngleRadians/2) - d13RotFactor = math.sqrt(2)*math.tan(jointAbductionRadians/2) - ventral = 1 if ventralAbduction else -1 - proximalScale = magnitude(sub(proximalNodePosition, jointNodePosition)) - rotDisplacementFactor = rotationCoeff*sideScale*AbductionRotFactor - jointAdjustDir = add( - set_magnitude(jointSide, ventral*rotDisplacementFactor), - set_magnitude(distalDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(proximalDirn, proximalScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) - jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) - jointd2_mag = sideScale*(jointRotFactor-(rotationCoeff*AbductionRotFactor)) - jointd12_mag = -1*ventral*sideScale*d13RotFactor - return [jointAdjustPosition, jointd2_mag, jointd12_mag] + d12RotFactor = math.sqrt(2)*math.tan(jointAbductionRadians/2) + rotDisplacementFactor = rotationCoeff*sideScale*rotationRotFactor + jointNodePosition = adjustJointNodePosition(proximalNodePosition, jointNodePosition, \ + jointSide, distalDirn, rotDisplacementFactor, upwardAbduction) + d2_mag = sideScale*(jointRotFactor-(rotationCoeff*rotationRotFactor)) + d12_mag = upward*sideScale*d12RotFactor + return [jointNodePosition, d2_mag, d3_mag, d12_mag, d13_mag] -def getDistalJointNodePosition(jointFlexionRadians, proximalDir, jointDir, distalDir, rotationCoeff, - frontScale, distalScale, jointNodePosition): - proximalDirn, proximalSide, proximalFront = proximalDir - jointDirn, jointSide, jointFront = jointDir - distalDirn, distalSide, distalFront = distalDir +def getDistalNodePosition(jointAbductionRadians, jointFlexionRadians, proximalDir, jointDir, distalDir, rotationCoeff): + proximalNodePosition, proximalDirn, proximalSide, proximalFront = proximalDir[0:4] + jointNodePosition, jointDirn, jointSide, jointFront = jointDir[0:4] + distalNodePosition, distalDirn, distalSide, distalFront = distalDir[0:4] + # Flexion + jointFrontScale = jointDir[6] jointAngleRadians = math.pi - jointFlexionRadians flexionRotFactor = 1*math.sin(jointAngleRadians) - rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor + rotDisplacementFactor = rotationCoeff*jointFrontScale*flexionRotFactor + distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ + jointDirn, proximalDirn, rotDisplacementFactor) + # Abduction + jointSideScale = jointDir[5] + jointAngleRadians = math.pi - jointAbductionRadians + abductionRotFactor = 1*math.sin(jointAngleRadians) + rotDisplacementFactor = rotationCoeff*jointSideScale*abductionRotFactor + distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ + jointDirn, proximalDirn, rotDisplacementFactor) + return distalNodePosition + +def adjustJointNodePosition(proximalNodePosition, nodePosition, d1, d2, dispFactor, ventral = True): + """ + Displaces a node towards the center of the tube network, + while preserving the distance between the current node and the previous (proximal) node + + :param proximalNodePosition: Position of the node previous to nodePosition + :param nodePosition: Original position of the node to be rotated. + :param lenScale: Distance between the joint node and the proximal/distal node. + :param d1: Typically jointFront or or jointSide. + :param d2: Typically distalDirn or jointDirn. + :param dispFactor: Measure of displacement of the node away from the corner. + """ + initialDir = sub(nodePosition, proximalNodePosition) + lenScale = magnitude(initialDir) + ventral = 1 if (ventral) else -1 jointAdjustDir = add( - set_magnitude(jointDirn, rotDisplacementFactor), - set_magnitude(proximalDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(distalDirn, distalScale), - jointAdjustDir + set_magnitude(d1, ventral*dispFactor), + set_magnitude(d2, dispFactor), ) - jointAdjustDir = set_magnitude(jointAdjustDir, distalScale) - distalNodePosition = add(jointNodePosition, jointAdjustDir) - return distalNodePosition + jointAdjustDir = add(initialDir, jointAdjustDir) + jointAdjustDir = set_magnitude(jointAdjustDir, lenScale) + adjustedNodePosition = add(proximalNodePosition, jointAdjustDir) + return adjustedNodePosition +# def getJointFlexionFrames(jointFlexionRadians, proximalDir, ventralFlexion=True): +# jointAngleRadians = math.pi - jointFlexionRadians +# proximalDirn, proximalSide, proximalFront = proximalDir +# ventral = 1 if ventralFlexion else -1 +# jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) +# jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) +# # Joint directions (frame is rotated by half the flexion angle) +# jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) +# jointSide = proximalSide +# jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) +# # Distal directions +# distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) +# distalSide = proximalSide +# distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) +# jointDir = [jointDirn, jointSide, jointFront] +# distalDir = [distalDirn, distalSide, distalFront] +# return jointDir, distalDir + +# def getJointFlexionPosition(jointFlexionRadians, proximalDir, +# jointDir, distalDir, proximalNodePosition, +# jointNodePosition, frontScale, +# rotationCoeff, ventralFlexion = True): +# jointAngleRadians = math.pi - jointFlexionRadians +# proximalDirn, proximalSide, proximalFront = proximalDir +# jointDirn, jointSide, jointFront = jointDir +# jointFront = mult(jointFront, -1) if (jointFlexionRadians < 0) else jointFront +# distalDirn, distalSide, distalFront = distalDir +# flexionRotFactor = 1*math.sin(jointAngleRadians) +# jointRotFactor = 1/math.sin(jointAngleRadians/2) +# d13RotFactor = math.sqrt(2)*math.tan(jointFlexionRadians/2) +# ventral = 1 if ventralFlexion else -1 +# proximalScale = magnitude(sub(proximalNodePosition, jointNodePosition)) +# rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor +# jointAdjustDir = add( +# set_magnitude(jointFront, ventral*rotDisplacementFactor), +# set_magnitude(distalDirn, rotDisplacementFactor), +# ) +# jointAdjustDir = add( +# set_magnitude(proximalDirn, proximalScale), +# jointAdjustDir +# ) +# jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) +# jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) +# jointDir +# jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) +# jointd13_mag = -1*ventral*frontScale*d13RotFactor +# return [jointAdjustPosition, jointd3_mag, jointd13_mag] + +# def getJointAbductionPosition(jointAbductionRadians, proximalDir, +# jointDir, distalDir, proximalNodePosition, +# jointNodePosition, sideScale, proximalScale, +# rotationCoeff, ventralAbduction = True, side = 0): +# jointAngleRadians = math.pi - jointAbductionRadians +# proximalDirn, proximalSide, proximalFront = proximalDir +# jointDirn, jointSide, jointFront = jointDir +# right = -1 if (side == 0) else 1 +# jointSide = mult(jointSide, right) +# distalDirn, distalSide, distalFront = distalDir +# AbductionRotFactor = 1*math.sin(jointAngleRadians) +# jointRotFactor = 1/math.sin(jointAngleRadians/2) +# d13RotFactor = math.sqrt(2)*math.tan(jointAbductionRadians/2) +# ventral = 1 if (ventralAbduction) else -1 +# rotDisplacementFactor = rotationCoeff*sideScale*AbductionRotFactor +# jointAdjustDir = add( +# set_magnitude(jointSide, ventral*rotDisplacementFactor), +# set_magnitude(distalDirn, rotDisplacementFactor), +# ) +# jointAdjustDir = add( +# set_magnitude(proximalDirn, proximalScale), +# jointAdjustDir +# ) +# jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) +# jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) +# jointd2_mag = sideScale*(jointRotFactor-(rotationCoeff*AbductionRotFactor)) +# jointd12_mag = -1*right*ventral*sideScale*d13RotFactor +# return [jointAdjustPosition, jointd2_mag, jointd12_mag] + +# def getDistalJointNodePosition(jointFlexionRadians, proximalDir, jointDir, distalDir, rotationCoeff, +# frontScale, distalScale, jointNodePosition): +# proximalDirn, proximalSide, proximalFront = proximalDir +# jointDirn, jointSide, jointFront = jointDir +# distalDirn, distalSide, distalFront = distalDir +# jointAngleRadians = math.pi - jointFlexionRadians +# flexionRotFactor = 1*math.sin(jointAngleRadians) +# rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor +# jointAdjustDir = add( +# set_magnitude(jointDirn, rotDisplacementFactor), +# set_magnitude(proximalDirn, rotDisplacementFactor), +# ) +# jointAdjustDir = add( +# set_magnitude(distalDirn, distalScale), +# jointAdjustDir +# ) +# jointAdjustDir = set_magnitude(jointAdjustDir, distalScale) +# distalNodePosition = add(jointNodePosition, jointAdjustDir) +# return distalNodePosition + + + + +# def getJointFlexionAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff, ventralFlexion = True): +# proximalNodePosition = proximalDir[0] +# jointNodePosition = jointDir[0] +# jointFront = jointDir[3] +# jointFrontScale = jointDir[6] +# distalDirn = distalDir[1] +# jointAngleRadians = math.pi - jointRotationRadians +# abductionRotFactor = 1*math.sin(jointAngleRadians) +# rotDisplacementFactor = rotationCoeff*jointFrontScale*abductionRotFactor +# distalNodePosition = adjustJointNodePosition(proximalNodePosition, jointNodePosition, \ +# jointFront, distalDirn, rotDisplacementFactor, ventralFlexion) +# return distalNodePosition + +# def getJointAbductionAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff): +# proximalDirn = proximalDir[1] +# jointNodePosition = jointDir[0] +# jointDirn = jointDir[1] +# jointSideScale = jointDir[5] +# distalNodePosition = distalDir[0] +# jointAngleRadians = math.pi - jointRotationRadians +# abductionRotFactor = 1*math.sin(jointAngleRadians) +# rotDisplacementFactor = rotationCoeff*jointSideScale*abductionRotFactor +# distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ +# jointDirn, proximalDirn, rotDisplacementFactor) +# return distalNodePosition + +# def getDistalNodeAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff): +# proximalDirn = proximalDir[1] +# jointNodePosition = jointDir[0] +# jointDirn = jointDir[1] +# jointFrontScale = jointDir[6] +# distalNodePosition = distalDir[0] +# jointAngleRadians = math.pi - jointRotationRadians +# abductionRotFactor = 1*math.sin(jointAngleRadians) +# rotDisplacementFactor = rotationCoeff*jointFrontScale*abductionRotFactor +# distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ +# jointDirn, proximalDirn, rotDisplacementFactor) +# return distalNodePosition + +# def getDistalAbductionAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff): +# proximalDirn = proximalDir[1] +# jointNodePosition = jointDir[0] +# jointDirn = jointDir[1] +# jointSideScale = jointDir[5] +# distalNodePosition = distalDir[0] +# jointAngleRadians = math.pi - jointRotationRadians +# abductionRotFactor = 1*math.sin(jointAngleRadians) +# rotDisplacementFactor = rotationCoeff*jointSideScale*abductionRotFactor +# distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ +# jointDirn, proximalDirn, rotDisplacementFactor) +# return distalNodePosition + + + def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, proximalDir, frontScale, dirnScale, rotationCoeff, distalnScale = False, ventralFlexion=True): From 6768fde9eeb3f811aad72bdf3618dcca326defa0 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 8 Dec 2025 12:35:36 +1300 Subject: [PATCH 54/61] changed base arm abudction degrees --- .../meshtypes/meshtype_3d_wholebody2.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index af2aa38f..668bbb5a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -49,8 +49,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Shoulder width"] = 5.0 options["Left shoulder flexion degrees"] = 0.0 options["Right shoulder flexion degrees"] = 0.0 - options["Left shoulder abduction degrees"] = 0.0 - options["Right shoulder abduction degrees"] = 0.0 + options["Left shoulder abduction degrees"] = 10.0 + options["Right shoulder abduction degrees"] = 10.0 options["Left elbow flexion degrees"] = 0.0 options["Right elbow flexion degrees"] = 0.0 options["Left arm rotation degrees"] = 0.0 @@ -80,8 +80,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 11.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 - options["Left knee flexion degrees"] = 90.0 - options["Right knee flexion degrees"] = 90.0 + options["Left knee flexion degrees"] = 0.0 + options["Right knee flexion degrees"] = 0.0 options["Left ankle flexion degrees"] = 90.0 options["Right ankle flexion degrees"] = 90.0 options["Foot height"] = 1.25 @@ -844,7 +844,7 @@ def generateBaseMesh(cls, region, options): xi = i / (armToHandElementsCount - 2) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - rotationCoeff = 0.4 + rotationCoeff = 0.3 wristDir, handDir = getJointRotationFrames( wristAbductionRadians, wristFlexionRadians, antebrachiumDir, upwardAbduction, ventralFlexion ) @@ -856,7 +856,6 @@ def generateBaseMesh(cls, region, options): # Set coordiantes for joint node # x = jointPositions[1] wristDirn, wristSide, wristFront = wristDir[1:4] - handDirn, handSide, handFront = handDir[1:4] d1 = mult(wristDirn, handLength) d2 = mult(wristSide, wristd2_mag) d3 = mult(wristFront, wristd3_mag) @@ -878,6 +877,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 options['Kinematic tree']['hand_' + side_label] = x + handDirn, handSide, handFront = handDir[1:4] d1 = set_magnitude(handDirn, armScale) handStart = add(x, mult(handDirn, handLength)) handDir[0] = handStart From 15fc1656c029f682817e92bec52501a2eb836b0f Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 8 Dec 2025 13:03:49 +1300 Subject: [PATCH 55/61] Merge main branch into stickman --- docs/scaffolds/vagus.rst | 4 +- pyproject.toml | 2 +- src/scaffoldmaker/annotation/vagus_terms.py | 4 +- .../meshtypes/meshtype_1d_bifurcationtree1.py | 4 +- .../meshtypes/meshtype_3d_nerve1.py | 266 ++++++++++---- src/scaffoldmaker/meshtypes/scaffold_base.py | 6 +- src/scaffoldmaker/scaffoldpackage.py | 11 + src/scaffoldmaker/scaffolds.py | 160 ++++---- src/scaffoldmaker/utils/constructionobject.py | 18 + src/scaffoldmaker/utils/networkmesh.py | 3 +- src/scaffoldmaker/utils/octree.py | 2 +- src/scaffoldmaker/utils/read_vagus_data.py | 89 +++-- src/scaffoldmaker/utils/zinc_utils.py | 3 + tests/test_vagus.py | 341 ++++++++++++++---- 14 files changed, 653 insertions(+), 260 deletions(-) create mode 100644 src/scaffoldmaker/utils/constructionobject.py diff --git a/docs/scaffolds/vagus.rst b/docs/scaffolds/vagus.rst index 2a4976af..a302dd2b 100644 --- a/docs/scaffolds/vagus.rst +++ b/docs/scaffolds/vagus.rst @@ -101,5 +101,5 @@ annotated with standard term names and identifiers from a controlled vocabulary. Each subject-specific vagus box scaffold has the following defined groups of 3-D elements: * ``vagus centroid`` - centroid of nerve/branch, useful for later fitting centroid data in another geometric configuration such as in-body/MRI tracings. -* ``vagus epineurium`` - approximately round representation of surface of epineurium, useful for later fitting to precise contours of the epineurium boundary. -* ``vagus anterior line`` - line on anterior edge of epineurium, useful for later fitting anterior orientation points in another geometric configuration. +* ``epineurium`` - approximately round representation of surface of epineurium on nerve/branch, useful for later fitting to precise contours of the epineurium boundary. +* ``orientation anterior`` - line on anterior edge of epineurium of the nerve trunk only, useful for later fitting 'orientation anterior' points in another geometric configuration. diff --git a/pyproject.toml b/pyproject.toml index 6f2770ae..4c07fb04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "cmlibs.maths >= 0.7.1", "cmlibs.utils >= 0.12.5", "cmlibs.zinc >= 4.2.1", - "fieldfitter >= 0.5.1", + "fieldfitter >= 0.6.0", "scaffoldfitter >= 0.11.1", "scipy", "numpy" diff --git a/src/scaffoldmaker/annotation/vagus_terms.py b/src/scaffoldmaker/annotation/vagus_terms.py index 0868fd1d..27ab6fa8 100644 --- a/src/scaffoldmaker/annotation/vagus_terms.py +++ b/src/scaffoldmaker/annotation/vagus_terms.py @@ -233,8 +233,8 @@ # vagus built-in annotations ("vagus centroid", ""), - ("vagus epineurium", ""), - ("vagus anterior line", "") + ("epineurium", "ILX:0103892"), + ("orientation anterior", "") # line on the epineurium in anterior direction ] diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_bifurcationtree1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_bifurcationtree1.py index cb4d14b3..0ecd90cd 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_bifurcationtree1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_bifurcationtree1.py @@ -12,7 +12,7 @@ from cmlibs.zinc.field import Field from cmlibs.zinc.node import Node from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base - +from scaffoldmaker.utils.constructionobject import ConstructionObject class MeshType_1d_bifurcationtree1(Scaffold_base): ''' @@ -152,7 +152,7 @@ def getChildCurve(self, childIndex): return self._x, self._d1[child._parent_d1_index], self._r[child._parent_r_index], child._x, child._d1[0], child._r[0] -class BifurcationTree: +class BifurcationTree(ConstructionObject): ''' Class for generating tree of 1-D bifurcating curves and converting to Zinc model. ''' diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_nerve1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_nerve1.py index 491b8ba3..82363d74 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_nerve1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_nerve1.py @@ -3,12 +3,12 @@ from cmlibs.maths.vectorops import ( add, cross, distance, dot, magnitude, matrix_mult, matrix_inv, mult, normalize, rejection, set_magnitude, sub) +from cmlibs.utils.zinc.field import find_or_create_field_group, find_or_create_field_coordinates from cmlibs.utils.zinc.general import ChangeManager -from cmlibs.utils.zinc.field import ( - find_or_create_field_group, find_or_create_field_coordinates) from cmlibs.zinc.element import Element, Elementbasis, Elementfieldtemplate from cmlibs.zinc.field import Field, FieldFindMeshLocation, FieldGroup from cmlibs.zinc.node import Node +from cmlibs.zinc.result import RESULT_OK from scaffoldfitter.fitter import Fitter as GeometryFitter from scaffoldfitter.fitterstepfit import FitterStepFit from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm, \ @@ -16,11 +16,13 @@ from scaffoldmaker.annotation.vagus_terms import get_vagus_term, get_vagus_marker_term, \ get_left_vagus_marker_locations_list, get_right_vagus_marker_locations_list from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base +from scaffoldmaker.utils.constructionobject import ConstructionObject from scaffoldmaker.utils.eft_utils import remapEftLocalNodes, remapEftNodeValueLabel, remapEftNodeValueLabelWithNodes, \ setEftScaleFactorIds from scaffoldmaker.utils.interpolation import ( - evaluateScalarOnCurve, getCubicHermiteBasis, getCubicHermiteBasisDerivatives, getCubicHermiteArcLength, - getCubicHermiteCurvesLength, getCubicHermiteTrimmedCurvesLengths, getNearestLocationOnCurve, get_curve_from_points, + evaluateCoordinatesOnCurve, evaluateScalarOnCurve, getCubicHermiteBasis, getCubicHermiteBasisDerivatives, + getCubicHermiteArcLength, getCubicHermiteCurvature, getCubicHermiteCurvesLength, + getCubicHermiteTrimmedCurvesLengths, getNearestLocationOnCurve, get_curve_from_points, interpolateCubicHermiteDerivative, sampleCubicHermiteCurves, sampleCubicHermiteCurvesSmooth, smoothCurveSideCrossDerivatives, track_curve_side_direction) from scaffoldmaker.utils.read_vagus_data import load_vagus_data @@ -53,11 +55,12 @@ def getDefaultOptions(cls, parameterSetName="Default"): baseParameterSetName = 'Human Left Vagus 1' if (parameterSetName == 'Default') else parameterSetName options = { 'Base parameter set': baseParameterSetName, - 'Number of elements along the trunk pre-fit': 20, + 'Number of elements along the trunk pre-fit': 30, 'Number of elements along the trunk': 50, 'Trunk proportion': 1.0, 'Trunk fit number of iterations': 5, - 'Default trunk diameter mm': 3.0, + 'Default anterior direction': [0.0, 1.0, 0.0], + 'Default trunk diameter': 3.0, 'Branch diameter trunk proportion': 0.5 } return options @@ -69,13 +72,14 @@ def getOrderedOptionNames(cls): 'Number of elements along the trunk', 'Trunk proportion', 'Trunk fit number of iterations', - 'Default trunk diameter mm', + 'Default anterior direction', + 'Default trunk diameter', 'Branch diameter trunk proportion' ] @classmethod def checkOptions(cls, options): - dependentChanges = False + dependent_changes = False for key in [ 'Number of elements along the trunk', 'Number of elements along the trunk pre-fit' @@ -90,27 +94,47 @@ def checkOptions(cls, options): options[key] = 0.1 elif options[key] > 1.0: options[key] = 1.0 + for key in [ + 'Default trunk diameter' + ]: + if options[key] <= 0.0: + options[key] = 1.0 if options['Trunk fit number of iterations'] < 0: options['Trunk fit number of iterations'] = 0 - return dependentChanges + # enforce direction is 3-component non-zero vector + default_anterior_direction = options['Default anterior direction'] + if len(default_anterior_direction) == 0: + default_anterior_direction = [0.0, 1.0, 0.0] + else: + count = len(default_anterior_direction) + if count < 3: + default_anterior_direction += [0.0] * (3 - count) + elif count > 3: + default_anterior_direction = default_anterior_direction[:3] + if magnitude(default_anterior_direction) == 0.0: + default_anterior_direction = [0.0, 1.0, 0.0] + options['Default anterior direction'] = default_anterior_direction + return dependent_changes @classmethod def generateBaseMesh(cls, region, options): """ Generate the 3d mesh for a vagus with branches, incorporating 1d central line, - 2d epineurium and 3d box based on constant vagus radius. - + 2d epineurium and 3d box based on 1D hermite curve parameters with side axes. :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). - return: list of AnnotationGroup, None + return: list of AnnotationGroup, Nerve construction/metadata object """ trunk_elements_count_prefit = options['Number of elements along the trunk pre-fit'] trunk_elements_count = options['Number of elements along the trunk'] trunk_proportion = options['Trunk proportion'] trunk_fit_iterations = options['Trunk fit number of iterations'] - default_trunk_diameter_mm = options['Default trunk diameter mm'] + default_anterior_direction = options['Default anterior direction'] + default_trunk_diameter = options['Default trunk diameter'] branch_diameter_trunk_proportion = options['Branch diameter trunk proportion'] + nerve_metadata = NerveMetadata("vagus nerve") + # Zinc setup for vagus scaffold fieldmodule = region.getFieldmodule() fieldcache = fieldmodule.createFieldcache() @@ -474,9 +498,9 @@ def generateBaseMesh(cls, region, options): # fit trunk with radius and orientation only_1d_trunk = False region1d = region if only_1d_trunk else region.createRegion() - tx, td1, td2, td12, td3, td13, default_trunk_diameter = generate_trunk_1d( + tx, td1, td2, td12, td3, td13 = generate_trunk_1d( vagus_data, trunk_proportion, trunk_elements_count_prefit, trunk_elements_count, - trunk_fit_iterations, default_trunk_diameter_mm, region1d) + trunk_fit_iterations, default_anterior_direction, default_trunk_diameter, region1d, nerve_metadata) trunk_length = getCubicHermiteCurvesLength(tx, td1) trunk_mean_element_length = trunk_length / trunk_elements_count @@ -496,7 +520,7 @@ def generateBaseMesh(cls, region, options): annotation_groups.append(centroid_annotation_group) centroid_mesh_group = centroid_annotation_group.getMeshGroup(mesh1d) - epineurium_annotation_group = AnnotationGroup(region, get_vagus_term("vagus epineurium")) + epineurium_annotation_group = AnnotationGroup(region, get_vagus_term("epineurium")) annotation_groups.append(epineurium_annotation_group) epineurium_mesh_group = epineurium_annotation_group.getMeshGroup(mesh2d) @@ -591,7 +615,7 @@ def generateBaseMesh(cls, region, options): continue visited_branches_order.append(branch_name) - branch_px = [branch_node[0] for branch_node in branch_data[branch_name]] + branch_px = [branch_x[0] for branch_x in branch_data[branch_name]] branch_parent_name = branch_parent_map[branch_name] trunk_is_parent = branch_parent_name == trunk_group_name # print(branch_name, '<--', branch_parent_name) @@ -987,54 +1011,60 @@ def generateBaseMesh(cls, region, options): side_label + ' level of superior border of the clavicle on the vagus nerve' cervical_thoracic_boundary_material_coordinate = vagus_level_terms[cervical_thoracic_boundary_marker_name] - cervical_trunk_mesh_group = cervical_trunk_group.getMeshGroup(mesh3d) - thoracic_trunk_mesh_group = thoracic_trunk_group.getMeshGroup(mesh3d) - trunk_group = findAnnotationGroupByName(annotation_groups, trunk_group_name) - trunk_mesh_group = trunk_group.getMeshGroup(mesh3d) - el_iter = trunk_mesh_group.createElementiterator() - element = el_iter.next() - element_material_coordinate_span = 1.0 / trunk_elements_count - mid_element_material_coordinate = 0.5 * element_material_coordinate_span - while element.isValid(): - if mid_element_material_coordinate < cervical_thoracic_boundary_material_coordinate: - cervical_trunk_mesh_group.addElement(element) - else: - thoracic_trunk_mesh_group.addElement(element) - mid_element_material_coordinate += element_material_coordinate_span + for dimension in range(3, 0, -1): + mesh = fieldmodule.findMeshByDimension(dimension) + trunk_mesh_group = trunk_group.getMeshGroup(mesh) + cervical_trunk_mesh_group = cervical_trunk_group.getMeshGroup(mesh) + thoracic_trunk_mesh_group = thoracic_trunk_group.getMeshGroup(mesh) + el_iter = trunk_mesh_group.createElementiterator() element = el_iter.next() + while element.isValid(): + fieldcache.setMeshLocation(element, [0.5, 0.5, 0.5]) + _, material_coordinate = vagus_coordinates.evaluateReal(fieldcache, 3) + if material_coordinate[2] < cervical_thoracic_boundary_material_coordinate: + cervical_trunk_mesh_group.addElement(element) + else: + thoracic_trunk_mesh_group.addElement(element) + element = el_iter.next() - return annotation_groups, None + return annotation_groups, nerve_metadata @classmethod def defineFaceAnnotations(cls, region, options, annotationGroups): """ - Override in classes with face annotation groups. - Add face annotation groups from the highest dimension mesh. - Must have defined faces and added subelements for highest dimension groups. - + Add orientation anterior 1-D annotation group. :param region: Zinc region containing model. :param options: Dict containing options. See getDefaultOptions(). - :param annotationGroups: List of annotation groups for top-level elements. - New face annotation groups are appended to this list. + :param annotationGroups: List of annotation groups for elements created in generateBaseMesh(). + New face/line annotation groups are appended to this list. """ - - # Create 2d surface mesh groups fieldmodule = region.getFieldmodule() mesh2d = fieldmodule.findMeshByDimension(2) mesh1d = fieldmodule.findMeshByDimension(1) epineurium_annotation_group = findOrCreateAnnotationGroupForTerm( - annotationGroups, region, get_vagus_term("vagus epineurium")) + annotationGroups, region, get_vagus_term("epineurium")) epineurium_mesh_group = epineurium_annotation_group.getMeshGroup(mesh2d) vagusAnteriorLineAnnotationGroup = findOrCreateAnnotationGroupForTerm( - annotationGroups, region, get_vagus_term("vagus anterior line")) + annotationGroups, region, get_vagus_term("orientation anterior")) vagusAnteriorLineMeshGroup = vagusAnteriorLineAnnotationGroup.getMeshGroup(mesh1d) + # only want orientation on the trunk + trunk_group = None + for trunk_group_name in ["left vagus nerve", "right vagus nerve"]: + trunk_group_field = fieldmodule.findFieldByName(trunk_group_name) + if trunk_group_field.isValid(): + trunk_group = trunk_group_field.castGroup() + break + trunk_mesh_group = trunk_group.getMeshGroup(mesh2d) if trunk_group else None faceIterator = epineurium_mesh_group.createElementiterator() quadrant = 0 face = faceIterator.next() while face.isValid(): + # trunk elements are the first consecutive block + if trunk_mesh_group and not trunk_mesh_group.containsElement(face): + break if quadrant == 0: line = face.getFaceElement(4) vagusAnteriorLineMeshGroup.addElement(line) @@ -1043,7 +1073,7 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, trunk_elements_count, - trunk_fit_iterations, default_trunk_diameter_mm, region): + trunk_fit_iterations, default_anterior_direction, default_trunk_diameter, region, nerve_metadata): """ Build and fit a 1-D trunk curve to trunk data, calibrated to marker point positions. :param vagus_data: Vagus data extracted from input data region. @@ -1051,11 +1081,13 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, :param trunk_elements_count_prefit: Number of elements in pre-fit mesh to trunk data. :param trunk_elements_count: Number of elements in final 1-D mesh. :param trunk_fit_iterations: Number of iterations in main trunk fit >= 1. - :param default_trunk_diameter_mm: Diameter to use if no diameter parameters, in mm. This is scaled in magnitude by - factors of 10 until it is within the expected aspect ratio. + :param default_anterior_direction: Vector direction to use as anterior if no orientotion data is supplied. + Normalized in this function. + :param default_trunk_diameter: Diameter in final units to use if no radius parameters. :param region: Region to put the fitted 1-D geometry including marker points in. + :param nerve_metadata: Construction object to put fitting quality metadata into. :return: tx, td1, td2, td12, td3, td13 (parameters for 1-D fitted trunk geometry, left and anterior side - directions and rates of change w.r.t. d1), default_trunk_diameter (in same units as data) + directions and rates of change w.r.t. d1). """ # 1. pre-fit to range of trunk data @@ -1083,7 +1115,7 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, marker_data = [] # list from top to bottom of nerve of (name, material_coordinate, data_coordinates) for marker_term_name, material_coordinate in vagus_level_terms.items(): if marker_term_name in raw_marker_data.keys(): - data_coordinates =raw_marker_data[marker_term_name] + data_coordinates = raw_marker_data[marker_term_name] for idx, data in enumerate(marker_data): if material_coordinate < data[1]: break @@ -1117,6 +1149,10 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, (end_length - end_marker_extra_length) * material_length_per_length) # extend curves at each end, by moving end node if short extension, or adding node if large start_extra_length = start_curve_material_coordinate / material_length_per_length + if start_extra_length < 0.0: + logger.warning("Projected start material coordinate " + str(start_curve_material_coordinate) + + " is < 0; using full range of data.") + start_extra_length = 0.0 start_direction = normalize(dd1[0]) mag_d1 = magnitude(dd1[0]) start_dx = sub(dx[0], mult(start_direction, start_extra_length)) @@ -1127,6 +1163,10 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, dx.insert(0, start_dx) dd1.insert(0, mult(start_direction, 2.0 * start_extra_length - mag_d1)) end_extra_length = (trunk_proportion - end_curve_material_coordinate) / material_length_per_length + if end_extra_length < 0.0: + logger.warning("Projected end material coordinate " + str(end_curve_material_coordinate) + + " is > trunk proportion " + str(trunk_proportion) + "; using full range of data.") + end_extra_length = 0.0 end_direction = normalize(dd1[-1]) mag_d1 = magnitude(dd1[-1]) end_dx = add(dx[-1], mult(end_direction, end_extra_length)) @@ -1226,7 +1266,9 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, fit2.run() del fit2 - rms_error, max_error = fitter.getDataRMSAndMaximumProjectionError() + datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + rms_error, max_error = fitter.getDataRMSAndMaximumProjectionError(trunk_group.getNodesetGroup(datapoints)) + nerve_metadata.set_name_rms_max_error("trunk centroid fit error", rms_error, max_error) fitter.cleanup() del fitter @@ -1235,16 +1277,13 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, if pr: gradient1_penalty = 1000.0 * points_count_calibration_factor * length_calibration_factor gradient2_penalty = 1.0E+8 * points_count_calibration_factor * (length_calibration_factor ** 3) - define_and_fit_field(fit_region, "coordinates", "coordinates", "radius", - gradient1_penalty, gradient2_penalty, group_name=trunk_group_name) + rms_error, max_error = define_and_fit_field( + fit_region, "coordinates", "coordinates", "radius", + gradient1_penalty, gradient2_penalty, group_name=trunk_group_name) + nerve_metadata.set_name_rms_max_error("trunk radius fit error", rms_error, max_error) # extract fitted trunk parameters from trunk nodes (not marker points) length = getCubicHermiteCurvesLength(ex, ed1) - default_trunk_diameter = default_trunk_diameter_mm - scale_step = 10.0 # because data is frequently in 1/100 mm. - max_diameter = 0.04 * length - while (default_trunk_diameter * scale_step) < max_diameter: - default_trunk_diameter *= scale_step default_trunk_radius = 0.5 * default_trunk_diameter tx = [] td1 = [] @@ -1273,15 +1312,57 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, mean_radius = sum(pr) / len(pr) if pr else default_trunk_radius max_orientation_projection_error = 8.0 * mean_radius - # remove all datapoints from previous fits. + # remove all datapoints from previous fits datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) datapoints.destroyAllNodes() + segments_trunk_coordinates = vagus_data.get_segments_trunk_coordinates() + segments_metadata = {} + with ChangeManager(fieldmodule): + # make a real field which increases down the trunk proportional to vagus coordinates + trunk_distance = (fieldmodule.findFieldByName("cmiss_number") + + fieldmodule.createFieldComponent(fieldmodule.findFieldByName("xi"), 1)) + find_mesh_location = fieldmodule.createFieldFindMeshLocation(coordinates, coordinates, mesh1d) + find_mesh_location.setSearchMode(FieldFindMeshLocation.SEARCH_MODE_NEAREST) + host_trunk_distance = fieldmodule.createFieldEmbedded(trunk_distance, find_mesh_location) + datapoints_min_trunk_distance = fieldmodule.createFieldNodesetMinimum(host_trunk_distance, datapoints) + datapoints_max_trunk_distance = fieldmodule.createFieldNodesetMaximum(host_trunk_distance, datapoints) + fieldcache.clearLocation() + distance_to_material = trunk_proportion / trunk_elements_count + for segment_name, sx in segments_trunk_coordinates.items(): + generate_datapoints(fit_region, sx, start_data_identifier=1) + min_result, segment_min_trunk_distance = datapoints_min_trunk_distance.evaluateReal(fieldcache, 1) + max_result, segment_max_trunk_distance = datapoints_max_trunk_distance.evaluateReal(fieldcache, 1) + if (min_result == RESULT_OK) and (max_result == RESULT_OK): + segments_metadata[segment_name] = { + "minimum vagus coordinate": segment_min_trunk_distance * distance_to_material, + "maximum vagus coordinate": segment_max_trunk_distance * distance_to_material + } + else: + logger.warning("Could not calculate material coordinates range for segment " + segment_name) + datapoints.destroyAllNodes() + nerve_metadata.set_name_value("segments", segments_metadata) + # fit orientation debug_print = False orientation_dct = vagus_data.get_orientation_data() orientation_x = [] orientation_twist_angles = [] + if not orientation_dct: + # generate 'orientation anterior' points using the default_anterior_direction and default_trunk_radius + # :return Dict mapping 8 possible orientations (anterior, left, right, etc.) to list of x, y, z coordinates. + dira = normalize(default_anterior_direction) + anterior_x = [] + for e in range(trunk_elements_count): + x, d1 = evaluateCoordinatesOnCurve(tx, td1, (e, 0.5), derivative=True) + dir1 = normalize(d1) + if abs(dot(dir1, dira)) > 0.9: + continue # skip as trunk is too in-line with anterior direction + dir2 = cross(dira, dir1) + dir3 = normalize(cross(dir1, dir2)) + anterior_x.append(add(x, mult(dir3, default_trunk_radius))) + if anterior_x: + orientation_dct = {'orientation anterior': anterior_x} if orientation_dct: # convert to list in order down the trunk length = getCubicHermiteCurvesLength(tx, td1) @@ -1352,6 +1433,7 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, if not orientation_directions: logger.warning("Nerve: All orientation points ignored, using default orientation") else: + # twist angles are positive in right hand sense around direction down trunk twist_angle = 0.0 orientation_twist_angles.append(twist_angle) # top orientation point is at 0 radians direction = orientation_directions[0] @@ -1369,7 +1451,6 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, for location, direction, twist_angle, name, in zip( orientation_locations, orientation_directions, orientation_twist_angles, orientation_names): print(location, direction, twist_angle, name) - if orientation_twist_angles: data_identifier = 1 twist_angle_field_name = "twist angle" @@ -1380,8 +1461,11 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, gradient1_penalty = 100.0 * twist_points_count_calibration_factor * length_calibration_factor gradient2_penalty = 1.0E+8 * twist_points_count_calibration_factor * (length_calibration_factor ** 3) - define_and_fit_field(fit_region, "coordinates", "coordinates", twist_angle_field_name, - gradient1_penalty, gradient2_penalty, group_name=trunk_group_name) + rms_error, max_error = define_and_fit_field( + fit_region, "coordinates", "coordinates", twist_angle_field_name, + gradient1_penalty, gradient2_penalty, group_name=trunk_group_name) + nerve_metadata.set_name_rms_max_error( + "trunk twist angle fit error degrees", math.degrees(rms_error), math.degrees(max_error)) twist_angle = fieldmodule.findFieldByName(twist_angle_field_name).castFiniteElement() # extract fitted twist angle parameters from nodes ax = [] @@ -1413,11 +1497,18 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, first_direction = normalize(add(mult(dir3, math.cos(delta_twist_radians)), mult(dir2, -math.sin(delta_twist_radians)))) else: + # this case is unlikely now due to creation of anterior points above + logger.warning("Nerve: Default anterior direction is in-line with top of nerve; expect poor results.") ad1 = ax = [0.0] * (trunk_elements_count + 1) first_location = (0, 0.0) first_twist_radians = 0.0 - # default anterior direction - first_direction = [0.0, 1.0, 0.0] + dira = normalize(default_anterior_direction) + dir1 = normalize(td1[0]) + if abs(dot(dir1, dira)) > 0.999: + first_direction = dira + else: + dir2 = cross(dira, dir1) + first_direction = normalize(cross(dir1, dir2)) # compute side/anterior directions and their rates of change td2 = [] @@ -1451,10 +1542,26 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, mult(dir2, -math.sin(delta_twist_radians)))) left = normalize(cross(ante, dir1)) + # calculate centroid curvature in d2, d3 directions + curvatures = [] + for dirn in [left, ante]: + curvature = 0.0 + count = 0 + if node_index > 0: + curvature += getCubicHermiteCurvature( + tx[node_index - 1], td1[node_index - 1], x, d1, dirn, 1.0) + count += 1 + if n < trunk_elements_count: + curvature += getCubicHermiteCurvature( + x, d1, tx[node_index + 1], td1[node_index + 1], dirn, 0.0) + count += 1 + curvature /= count + curvatures.append(curvature) + d2 = set_magnitude(left, rv) - d12 = add(mult(left, rd), mult(ante, rv * ad)) + d12 = add(add(mult(left, rd), mult(ante, rv * ad)), mult(d1, -rv * curvatures[0])) d3 = set_magnitude(ante, rv) - d13 = add(mult(ante, rd), mult(left, -rv * ad)) + d13 = add(add(mult(ante, rd), mult(left, -rv * ad)), mult(d1, -rv * curvatures[1])) ix = n if forward else 0 node_indexes.insert(ix, node_index) td2.insert(ix, d2) @@ -1492,4 +1599,35 @@ def generate_trunk_1d(vagus_data, trunk_proportion, trunk_elements_count_prefit, srm = sir.createStreamresourceMemoryBuffer(buffer) region.read(sir) - return tx, td1, td2, td12, td3, td13, default_trunk_diameter + return tx, td1, td2, td12, td3, td13 + + +class NerveMetadata(ConstructionObject): + + def __init__(self, top_level_name): + """ + :param top_level_name: Unique name for top level of metadata dict + """ + self._top_level_name = top_level_name + self._metadata = {} + + def getMetadata(self): + return {self._top_level_name: self._metadata} + + def set_name_rms_max_error(self, quantity_name, rms_error, max_error): + """ + :param quantity_name: Stem name of metadata e.g. "trunk centroid fit error" + to which " max" and " rms" are added. + :param rms_error: RMS error value, scalar real or list/vector. + :param max_error: Maximum error value, scalar real or list/vector. + """ + self._metadata[quantity_name + " rms"] = rms_error + self._metadata[quantity_name + " max"] = max_error + + def set_name_value(self, quantity_name, value): + """ + Add a name value pair to the metadata. + :param quantity_name: Name of quantity to add a value for. + :param value: Any value serialisable by json.dumps(). + """ + self._metadata[quantity_name] = value diff --git a/src/scaffoldmaker/meshtypes/scaffold_base.py b/src/scaffoldmaker/meshtypes/scaffold_base.py index dab3a096..c6d0a7a8 100644 --- a/src/scaffoldmaker/meshtypes/scaffold_base.py +++ b/src/scaffoldmaker/meshtypes/scaffold_base.py @@ -104,7 +104,7 @@ def generateBaseMesh(cls, region, options): returned as the second return value. :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). - :return: list of AnnotationGroup, construction object (or None) + :return: list of AnnotationGroup, ConstructionObject-derived object (or None) """ return [], None @@ -125,8 +125,8 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): Must have defined faces and added subelements for highest dimension groups. :param region: Zinc region containing model. :param options: Dict containing options. See getDefaultOptions(). - :param annotationGroups: List of annotation groups for top-level elements. - New face annotation groups are appended to this list. + :param annotationGroups: List of annotation groups for elements created in generateBaseMesh(). + New face/line annotation groups are appended to this list. """ pass diff --git a/src/scaffoldmaker/scaffoldpackage.py b/src/scaffoldmaker/scaffoldpackage.py index 525be317..fdc887dd 100644 --- a/src/scaffoldmaker/scaffoldpackage.py +++ b/src/scaffoldmaker/scaffoldpackage.py @@ -434,3 +434,14 @@ def isUserAnnotationGroup(self, annotationGroup): :return: True if annotationGroup is user-created and editable. """ return annotationGroup in self._userAnnotationGroups + + def getMetadata(self): + """ + :return: Dict of scaffold-specific metadata, including all annotation id/name pairs. + """ + metadataDict = {} + metadataDict["annotations"] = [{"id": annotationGroup.getId(), "name": annotationGroup.getName()} + for annotationGroup in self.getAnnotationGroups()] + if self._constructionObject: + metadataDict.update(self._constructionObject.getMetadata()) + return metadataDict diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 60f33f85..c5f157ca 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -68,100 +68,90 @@ class Scaffolds(object): + _allScaffoldTypes = [ + MeshType_1d_bifurcationtree1, + MeshType_1d_uterus_network_layout1, + MeshType_1d_network_layout1, + MeshType_1d_path1, + MeshType_2d_plate1, + MeshType_2d_platehole1, + MeshType_2d_sphere1, + MeshType_2d_tube1, + MeshType_2d_tubenetwork1, + MeshType_3d_bladder1, + MeshType_3d_bladderurethra1, + MeshType_3d_bone1, + MeshType_3d_box1, + MeshType_3d_boxhole1, + MeshType_3d_boxnetwork1, + MeshType_3d_brainstem1, + MeshType_3d_cecum1, + MeshType_3d_colon1, + MeshType_3d_colonsegment1, + MeshType_3d_ellipsoid1, + MeshType_3d_esophagus1, + MeshType_3d_gastrointestinaltract1, + MeshType_3d_heart1, + MeshType_3d_heart2, + MeshType_3d_heartarterialroot1, + MeshType_3d_heartarterialvalve1, + MeshType_3d_heartatria1, + MeshType_3d_heartatria2, + MeshType_3d_heartventricles1, + MeshType_3d_heartventricles2, + MeshType_3d_heartventricles3, + MeshType_3d_heartventriclesbase1, + MeshType_3d_heartventriclesbase2, + MeshType_3d_lens1, + MeshType_3d_lung1, + MeshType_3d_lung2, + MeshType_3d_lung3, + MeshType_3d_musclefusiform1, + MeshType_3d_nerve1, + MeshType_3d_ostium1, + MeshType_3d_ostium2, + MeshType_3d_smallintestine1, + MeshType_3d_solidcylinder1, + MeshType_3d_solidsphere1, + MeshType_3d_solidsphere2, + MeshType_3d_sphereshell1, + MeshType_3d_sphereshellseptum1, + MeshType_3d_spinalnerve1, + MeshType_3d_stellate1, + MeshType_3d_stomach1, + MeshType_3d_stomachhuman1, + MeshType_3d_trigeminalnerve1, + MeshType_3d_tube1, + MeshType_3d_tubenetwork1, + MeshType_3d_tubeseptum1, + MeshType_3d_uterus1, + MeshType_3d_wholebody1, + MeshType_3d_wholebody2 + ] + _allPrivateScaffoldTypes = [ + MeshType_1d_human_body_network_layout1, + MeshType_1d_human_spinal_nerve_network_layout1, + MeshType_1d_human_trigeminal_nerve_network_layout1, + MeshType_1d_uterus_network_layout1 + ] - def __init__(self): - self._allScaffoldTypes = [ - MeshType_1d_bifurcationtree1, - MeshType_1d_network_layout1, - MeshType_1d_path1, - MeshType_2d_plate1, - MeshType_2d_platehole1, - MeshType_2d_sphere1, - MeshType_2d_tube1, - MeshType_2d_tubenetwork1, - MeshType_3d_bladder1, - MeshType_3d_bladderurethra1, - MeshType_3d_bone1, - MeshType_3d_box1, - MeshType_3d_boxhole1, - MeshType_3d_boxnetwork1, - MeshType_3d_brainstem1, - MeshType_3d_cecum1, - MeshType_3d_colon1, - MeshType_3d_colonsegment1, - MeshType_3d_ellipsoid1, - MeshType_3d_esophagus1, - MeshType_3d_gastrointestinaltract1, - MeshType_3d_heart1, - MeshType_3d_heart2, - MeshType_3d_heartarterialroot1, - MeshType_3d_heartarterialvalve1, - MeshType_3d_heartatria1, - MeshType_3d_heartatria2, - MeshType_3d_heartventricles1, - MeshType_3d_heartventricles2, - MeshType_3d_heartventricles3, - MeshType_3d_heartventriclesbase1, - MeshType_3d_heartventriclesbase2, - MeshType_3d_lens1, - MeshType_3d_lung1, - MeshType_3d_lung2, - MeshType_3d_lung3, - MeshType_3d_musclefusiform1, - MeshType_3d_nerve1, - MeshType_3d_ostium1, - MeshType_3d_ostium2, - MeshType_3d_smallintestine1, - MeshType_3d_solidcylinder1, - MeshType_3d_solidsphere1, - MeshType_3d_solidsphere2, - MeshType_3d_sphereshell1, - MeshType_3d_sphereshellseptum1, - MeshType_3d_spinalnerve1, - MeshType_3d_stellate1, - MeshType_3d_stomach1, - MeshType_3d_stomachhuman1, - MeshType_3d_trigeminalnerve1, - MeshType_3d_tube1, - MeshType_3d_tubenetwork1, - MeshType_3d_tubeseptum1, - MeshType_3d_uterus1, - MeshType_3d_wholebody1, - MeshType_3d_wholebody2, - ] - self._allPrivateScaffoldTypes = [ - MeshType_1d_human_body_network_layout1, - MeshType_1d_human_spinal_nerve_network_layout1, - MeshType_1d_human_trigeminal_nerve_network_layout1, - MeshType_1d_uterus_network_layout1 - ] - - def findScaffoldTypeByName(self, name): - for scaffoldType in self._allScaffoldTypes: + @classmethod + def findScaffoldTypeByName(cls, name): + for scaffoldType in cls._allScaffoldTypes: if scaffoldType.getName() == name: return scaffoldType - for scaffoldType in self._allPrivateScaffoldTypes: + for scaffoldType in cls._allPrivateScaffoldTypes: if scaffoldType.getName() == name: return scaffoldType return None - def getDefaultMeshType(self): - """ - Deprecated: use getDefaultScaffoldType() - """ - return self.getDefaultScaffoldType() - - def getDefaultScaffoldType(self): + @classmethod + def getDefaultScaffoldType(cls): return MeshType_3d_box1 - def getMeshTypes(self): - """ - Deprecated: use getScaffoldTypes() - """ - return self.getScaffoldTypes() - - def getScaffoldTypes(self): - return self._allScaffoldTypes + @classmethod + def getScaffoldTypes(cls): + return cls._allScaffoldTypes class Scaffolds_JSONEncoder(json.JSONEncoder): diff --git a/src/scaffoldmaker/utils/constructionobject.py b/src/scaffoldmaker/utils/constructionobject.py new file mode 100644 index 00000000..3a8e6382 --- /dev/null +++ b/src/scaffoldmaker/utils/constructionobject.py @@ -0,0 +1,18 @@ +""" +Abstract base class for objects returned as construction objects by scaffold scripts. +""" +from abc import ABC, abstractmethod + + +class ConstructionObject: + """ + Abstract base class for objects returned as construction objects by scaffold scripts. + Mainly presents method for getting metadata dict. + """ + + @abstractmethod + def getMetadata(self) -> dict: + """ + Override to get scaffold-specific metadata. + """ + return {} diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index 2723a826..efc9b546 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -8,6 +8,7 @@ from cmlibs.zinc.node import Node from cmlibs.maths.vectorops import cross, magnitude, mult, normalize, rejection, sub from scaffoldmaker.annotation.annotationgroup import AnnotationGroup +from scaffoldmaker.utils.constructionobject import ConstructionObject from scaffoldmaker.utils.interpolation import ( gaussWt4, gaussXi4, getCubicHermiteCurvesLength, interpolateCubicHermiteDerivative) from scaffoldmaker.utils.tracksurface import TrackSurface @@ -181,7 +182,7 @@ def split(self, splitNetworkNode, isPatch): return nextSegment -class NetworkMesh: +class NetworkMesh(ConstructionObject): """ Defines a 1-D network with lateral axes, and utility functions for fleshing into higher dimensional meshes. """ diff --git a/src/scaffoldmaker/utils/octree.py b/src/scaffoldmaker/utils/octree.py index 4eefa3bf..ebde76ec 100644 --- a/src/scaffoldmaker/utils/octree.py +++ b/src/scaffoldmaker/utils/octree.py @@ -89,7 +89,7 @@ def findObjectByCoordinates(self, x): def addObjectAtCoordinates(self, x, obj): ''' - Add object at coordianates to octree. + Add object at coordinates to octree. Caller must have received None result for findObjectByCoordinates() first! Assumes caller has verified x is within range of Octree. :param x: 3 coordinates in a list. diff --git a/src/scaffoldmaker/utils/read_vagus_data.py b/src/scaffoldmaker/utils/read_vagus_data.py index bf770132..174acc21 100644 --- a/src/scaffoldmaker/utils/read_vagus_data.py +++ b/src/scaffoldmaker/utils/read_vagus_data.py @@ -111,8 +111,8 @@ def __init__(self, data_region): # extract orientation data for orientation_group_name in orientation_group_names: group = fm.findFieldByName(orientation_group_name).castGroup() - nodeset = group.getNodesetGroup(nodes) - _, values = get_nodeset_field_parameters(nodeset, coordinates, [Node.VALUE_LABEL_VALUE]) + nodeset_group = group.getNodesetGroup(nodes) + _, values = get_nodeset_field_parameters(nodeset_group, coordinates, [Node.VALUE_LABEL_VALUE]) orientation_points = [value[1][0][0] for value in values] self._orientation_data[orientation_group_name] = orientation_points[:] @@ -127,30 +127,37 @@ def __init__(self, data_region): self._annotation_term_map[self._trunk_group_name] = get_vagus_term(self._trunk_group_name)[1] self._side_label = 'right' + # build list of trunk centroid data associated with segment groups ending in .exf + # exported by segmentation stitcher, but not from connections which have .exf twice + # get map from segment group_name to (nodeset_group, []) + # where [] = coordinates list to be filled from trunk groups' coordinates + self._segment_groups_info = {} + for group in group_list: + group_name = group.getName() + if (group_name[-4:] == '.exf') and (1 == group_name.count('.exf')): + self._segment_groups_info[group_name] = (group.getNodesetGroup(nodes), []) + if self._trunk_group_name: trunk_group_count = 0 + trunk_nodes = [] + trunk_coordinates = [] + trunk_radius = [] trunk_elements = [] for found_trunk_group_name in found_trunk_group_names: group = fm.findFieldByName(found_trunk_group_name).castGroup() - nodeset = group.getNodesetGroup(nodes) - meshgroup = group.getMeshGroup(mesh) - _, values = get_nodeset_field_parameters(nodeset, coordinates, [Node.VALUE_LABEL_VALUE]) - if trunk_group_count == 0: - trunk_nodes = [value[0] for value in values] - trunk_coordinates = [value[1][0] for value in values] - if radius.isValid(): - _, values = get_nodeset_field_parameters(nodeset, radius, [Node.VALUE_LABEL_VALUE]) - trunk_radius = [value[1][0][0] for value in values] - else: - trunk_nodes.extend([value[0] for value in values]) - trunk_coordinates.extend([value[1][0] for value in values]) - if radius.isValid(): - _, values = get_nodeset_field_parameters(nodeset, radius, [Node.VALUE_LABEL_VALUE]) - trunk_radius.extend([value[1][0][0] for value in values]) + nodeset_group = group.getNodesetGroup(nodes) + mesh_group = group.getMeshGroup(mesh) + coordinate_values = get_nodeset_field_parameters(nodeset_group, coordinates, + [Node.VALUE_LABEL_VALUE])[1] + trunk_nodes += [value[0] for value in coordinate_values] + trunk_coordinates += [value[1][0] for value in coordinate_values] + if radius.isValid(): + radius_values = get_nodeset_field_parameters(nodeset_group, radius, [Node.VALUE_LABEL_VALUE])[1] + trunk_radius += [value[1][0][0] for value in radius_values] # get trunk elements - if meshgroup.getSize() > 0: - element_iterator = meshgroup.createElementiterator() + if mesh_group.getSize() > 0: + element_iterator = mesh_group.createElementiterator() element = element_iterator.next() while element.isValid(): eft = element.getElementfieldtemplate(coordinates, -1) @@ -161,6 +168,14 @@ def __init__(self, data_region): trunk_group_count += 1 + # fill segment groups with coordinates of trunk nodes contained in them + if self._segment_groups_info: + for n, node_identifier in enumerate(trunk_nodes): + node = nodes.findNodeByIdentifier(node_identifier) + for segment_nodeset_group, segment_points_list in self._segment_groups_info.values(): + if segment_nodeset_group.containsNode(node): + segment_points_list.append(trunk_coordinates[n][0]) + # order trunk coordinates top to bottom in case trunk elements are available if len(trunk_elements) > 0: # build trunk graph @@ -179,11 +194,22 @@ def __init__(self, data_region): for el in trunk_graph.keys(): if len(trunk_graph[el]) <= 1: unconnected_nodes.append(el) - #print(trunk_graph) + + # choose start, not necessarily first in unconnected nodes + furthest_distance = 0 + for index_1, node_id_1 in enumerate(unconnected_nodes): + for index_2 in range(index_1, len(unconnected_nodes)): + node_id_2 = unconnected_nodes[index_2] + dist = distance(nid_coords[node_id_1], nid_coords[node_id_2]) + if dist > furthest_distance: + furthest_distance = dist + furthest_index_1 = index_1 + furthest_index_2 = index_2 + + start_index = furthest_index_1 if furthest_index_1 < furthest_index_2 else furthest_index_2 + start = unconnected_nodes[start_index] trunk_path_ids = [] - start = unconnected_nodes[0] - start_index = 0 # BFS from first to next unconnected, all connected in one long path while len(unconnected_nodes) > 0: unconnected_nodes.pop(start_index) @@ -240,12 +266,12 @@ def __init__(self, data_region): branch_nodes_data = {} for branch_name in branch_group_names: group = fm.findFieldByName(branch_name).castGroup() - nodeset = group.getNodesetGroup(nodes) - if 'xml.ex' in branch_name or nodeset.getSize() < 2: + nodeset_group = group.getNodesetGroup(nodes) + if 'xml.ex' in branch_name or nodeset_group.getSize() < 2: # xml.ex are temporary regions from segmentation stitcher that might have keywords in their names # branch should have at least two nodes to be connected to parent continue - _, values = get_nodeset_field_parameters(nodeset, coordinates, [Node.VALUE_LABEL_VALUE]) + _, values = get_nodeset_field_parameters(nodeset_group, coordinates, [Node.VALUE_LABEL_VALUE]) branch_nodes = [value[0] for value in values] branch_parameters = [value[1][0] for value in values] self._branch_coordinates_data[branch_name] = branch_parameters @@ -253,7 +279,7 @@ def __init__(self, data_region): # not used at the moment if radius.isValid(): - _, values = get_nodeset_field_parameters(nodeset, radius, [Node.VALUE_LABEL_VALUE]) + _, values = get_nodeset_field_parameters(nodeset_group, radius, [Node.VALUE_LABEL_VALUE]) branch_radius = [value[1][0][0] for value in values] if not all(value == 0.0 for value in branch_radius): self._branch_radius_data[branch_name] = branch_radius @@ -384,6 +410,17 @@ def reset_datafile_path(self): """ self._datafile_path = None + def get_segments_trunk_coordinates(self): + """ + Get coordinates of trunk nodes in each segment corresponding to each .exf file read into the + segmentations stitcher, recognized by group names ending in '.exf', but not containing '.exf' + multiple times as for connection groups. + :return: dict segment_name -> list of coordinates + """ + segments_trunk_coordinates = {} + for segment_name, rhs in self._segment_groups_info.items(): + segments_trunk_coordinates[segment_name] = rhs[1] # only the coordinates + return segments_trunk_coordinates def group_common_branches(branch_names): """ diff --git a/src/scaffoldmaker/utils/zinc_utils.py b/src/scaffoldmaker/utils/zinc_utils.py index 5d0802ed..dcf2cd03 100644 --- a/src/scaffoldmaker/utils/zinc_utils.py +++ b/src/scaffoldmaker/utils/zinc_utils.py @@ -1009,6 +1009,7 @@ def define_and_fit_field(region, coordinate_field_name, data_coordinate_field_na :param gradient1_penalty: Penalty factor for first gradient of field w.r.t. coordinates. :param gradient2_penalty: Penalty factor for second gradient of field w.r.t. coordinates. :param group_name: Optional name of group to limit fitting over. + :return: rms_error, max_error """ fitter = FieldFitter(region=region) # fitter.setDiagnosticLevel(1) @@ -1029,8 +1030,10 @@ def define_and_fit_field(region, coordinate_field_name, data_coordinate_field_na fitter.setGradient2Penalty([gradient2_penalty]) fitter.setFitField(fit_field_name, True) fitter.fitField(fit_field_name) + rms_error, max_error = fitter.getFieldDataRMSAndMaximumErrors(fit_field_name) fitter.cleanup() del fitter + return rms_error, max_error def find_first_node_conditional(nodeset, conditional_field): """ diff --git a/tests/test_vagus.py b/tests/test_vagus.py index f849b06d..ecf233de 100644 --- a/tests/test_vagus.py +++ b/tests/test_vagus.py @@ -1,21 +1,26 @@ -import os -import unittest - +from cmlibs.maths.vectorops import mult +from cmlibs.utils.zinc.field import ( + find_or_create_field_coordinates, find_or_create_field_finite_element, find_or_create_field_group, + find_or_create_field_stored_string) from cmlibs.utils.zinc.general import ChangeManager from cmlibs.utils.zinc.group import mesh_group_to_identifier_ranges, nodeset_group_to_identifier_ranges from cmlibs.zinc.context import Context +from cmlibs.zinc.element import Element, Elementbasis from cmlibs.zinc.field import Field +from cmlibs.zinc.node import Node from cmlibs.zinc.result import RESULT_OK from scaffoldmaker.annotation.annotationgroup import findAnnotationGroupByName -from scaffoldmaker.annotation.annotation_utils import annotation_term_id_to_url from scaffoldmaker.annotation.vagus_terms import vagus_branch_terms, vagus_marker_terms from scaffoldmaker.meshtypes.meshtype_3d_nerve1 import MeshType_3d_nerve1, get_left_vagus_marker_locations_list from scaffoldmaker.utils.interpolation import get_curve_from_points, getCubicHermiteCurvesLength from scaffoldmaker.utils.read_vagus_data import VagusInputData - from testutils import assertAlmostEqualList, check_annotation_term_ids +import math +import os +import unittest + here = os.path.abspath(os.path.dirname(__file__)) @@ -223,18 +228,20 @@ def test_vagus_nerve_1(self): parameterSetNames = scaffold.getParameterSetNames() self.assertEqual(parameterSetNames, ['Default', 'Human Left Vagus 1', 'Human Right Vagus 1']) options = scaffold.getDefaultOptions("Human Left Vagus 1") - self.assertEqual(len(options), 7) + self.assertEqual(len(options), 8) self.assertEqual(options.get('Base parameter set'), 'Human Left Vagus 1') - self.assertEqual(options.get('Number of elements along the trunk pre-fit'), 20) + self.assertEqual(options.get('Number of elements along the trunk pre-fit'), 30) self.assertEqual(options.get('Number of elements along the trunk'), 50) self.assertEqual(options.get('Trunk proportion'), 1.0) self.assertEqual(options.get('Trunk fit number of iterations'), 5) - self.assertEqual(options.get('Default trunk diameter mm'), 3.0) + self.assertEqual(options.get('Default anterior direction'), [0.0, 1.0, 0.0]) + self.assertEqual(options.get('Default trunk diameter'), 3.0) self.assertEqual(options.get('Branch diameter trunk proportion'), 0.5) # change options to make test fast and consistent, with minor effect on result: options['Number of elements along the trunk pre-fit'] = 10 options['Number of elements along the trunk'] = 25 options['Trunk fit number of iterations'] = 2 + options['Default trunk diameter'] = 300.0 # test with original ordered, and reordered vagus data file for i in range(2): @@ -248,48 +255,98 @@ def test_vagus_nerve_1(self): if i == 1: reorder_vagus_test_data1(self, data_region) + # create segment groups dividing the data approximately in thirds over the x-span of the trunk + data_fieldmodule = data_region.getFieldmodule() + data_nodes = data_fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + data_coordinates = data_fieldmodule.findFieldByName("coordinates") + with ChangeManager(data_fieldmodule): + data_x = data_fieldmodule.createFieldComponent(data_coordinates, 1) + conditions = [ + data_fieldmodule.createFieldLessThan(data_x, data_fieldmodule.createFieldConstant(10000.0)), + None, + data_fieldmodule.createFieldGreaterThan(data_x, data_fieldmodule.createFieldConstant(20000.0)) + ] + conditions[1] = data_fieldmodule.createFieldNot( + data_fieldmodule.createFieldOr(conditions[0], conditions[2])) + for s in range(3): + segment_group = data_fieldmodule.createFieldGroup() + segment_group.setName("segment" + str(s + 1) + ".exf") + segment_group.setManaged(True) + segment_nodeset_group = segment_group.createNodesetGroup(data_nodes) + segment_nodeset_group.addNodesConditional(conditions[s]) + del conditions + del data_x + # check annotation groups - annotation_groups = scaffold.generateMesh(region, options)[0] + annotation_groups, nerve_metadata = scaffold.generateMesh(region, options) self.assertEqual(len(annotation_groups), 20) + metadata = nerve_metadata.getMetadata()["vagus nerve"] + TOL = 1.0E-6 + expected_metadata = { + 'segments': { + 'segment1.exf': {'minimum vagus coordinate': 0.062179363163301214, + 'maximum vagus coordinate': 0.244237232602641}, + 'segment2.exf': {'minimum vagus coordinate': 0.24685186671128517, + 'maximum vagus coordinate': 0.40960681735379395}, + 'segment3.exf': {'minimum vagus coordinate': 0.41221671261995313, + 'maximum vagus coordinate': 0.5754599929406741} + }, + 'trunk centroid fit error rms': 1.6796999717877277, + 'trunk centroid fit error max': 6.004413110311745, + 'trunk radius fit error rms': 0.20126533544206293, + 'trunk radius fit error max': 1.0496575899143181, + 'trunk twist angle fit error degrees rms': 3.9094139417227405, + 'trunk twist angle fit error degrees max': 9.786303215289262} + self.assertEqual(len(metadata), len(expected_metadata)) + for key, value in metadata.items(): + expected_value = expected_metadata[key] + if key == 'segments': + self.assertEqual(len(value), len(expected_value)) + for segment_name, vagus_coordinate_range in value.items(): + expected_vagus_coordinate_range = expected_value[segment_name] + for range_key, range_value in vagus_coordinate_range.items(): + self.assertAlmostEqual(range_value, expected_vagus_coordinate_range[range_key], delta=TOL) + else: + self.assertAlmostEqual(value, expected_value, delta=TOL) # (term_id, parent_group_name, expected_elements_count, expected_start_x, expected_start_d1, expected_start_d3, # expected_surface_area, expected_volume) expected_group_info = { - "left vagus nerve": ( - "http://uri.interlex.org/base/ilx_0785628", None, 25, + 'left vagus nerve': ( + 'http://uri.interlex.org/base/ilx_0785628', None, 25, [-1269.8048516184547, -6359.977051431916, -69.78642824721726], [2163.657939271601, -1111.9771974322234, 121.45057496461462], [49.68213484225328, 258.2220400479382, 1479.1356481323735], - 248430668.23162192, - 32973643021.906002), - "left superior laryngeal nerve": ( - "http://uri.interlex.org/base/ilx_0788780", "left vagus nerve", 3, - [5923.104657597037, -4450.247919770724, -196.91175665569304], - [-1473.665051675918, 858.0807042974036, 37.61890734384076], - [29.08457359713975, 24.68196779670643, 576.3537772211523], - 9788808.200600598, - 558709236.3511268), - "left A branch of superior laryngeal nerve": ( - "http://uri.interlex.org/base/ilx_0795823", "left superior laryngeal nerve", 2, - [5105.456364262517, -1456.268405569011, 0.18793093373064806], - [-1289.581295107282, 381.4601337342457, 17.4939305617649], - [2.990626464088564, -3.646186476940329, 299.9629335060045], - 4696615.9900110625, - 236649716.13221297), - "left A thoracic cardiopulmonary branch of vagus nerve": ( - "http://uri.interlex.org/base/ilx_0794192", "left vagus nerve", 2, - [20637.123232118385, -2947.094130818923, -608.014306886659], - [99.3811573594032, -1713.8817535655435, -61.05879554434691], - [-8.860965678419234, 11.91104819082625, -348.7579634464091], - 6201808.834555441, - 328598403.66705346), - "left B thoracic cardiopulmonary branch of vagus nerve": ( - "http://uri.interlex.org/base/ilx_0794193", "left vagus nerve", 1, - [22164.37254634065, -3219.4137858081867, -620.4335804280934], - [1775.1656728388923, 1620.6261382213854, -217.236772843207], - [2.3594134224331356, 43.375834887613564, 342.871791578891], - 4658643.2842846215, - 267729615.1145657) + 249152179.8529517, + 33286242951.84727), + 'left superior laryngeal nerve': ( + 'http://uri.interlex.org/base/ilx_0788780', 'left vagus nerve', 3, + [5923.104657597034, -4450.2479197707235, -196.91175665569313], + [-1473.665051675919, 858.0807042974039, 37.618907343841], + [29.2408913560962, 24.815173194025647, 579.4389023716839], + 9798165.396244952, + 559746405.7287067), + 'left A branch of superior laryngeal nerve': ( + 'http://uri.interlex.org/base/ilx_0795823', 'left superior laryngeal nerve', 2, + [5105.456364262518, -1456.268405569011, 0.1879309337306836], + [-1289.581295107282, 381.4601337342457, 17.493930561764717], + [2.990626464939851, -3.6461864740685996, 299.96293350603094], + 4696615.99004511, + 236649716.13535246), + 'left A thoracic cardiopulmonary branch of vagus nerve': ( + 'http://uri.interlex.org/base/ilx_0794192', 'left vagus nerve', 2, + [20637.123232118392, -2947.094130818923, -608.0143068866595], + [99.38115735940329, -1713.8817535655442, -61.058795544347106], + [-8.872203312143029, 11.926532324519485, -349.21088399635704], + 6203011.915679664, + 328721624.0619874), + 'left B thoracic cardiopulmonary branch of vagus nerve': ( + 'http://uri.interlex.org/base/ilx_0794193', 'left vagus nerve', 1, + [22164.372546340644, -3219.413785808189, -620.4335804280928], + [1775.1656728388964, 1620.6261382213868, -217.23677284320627], + [2.363562419413938, 43.37866675798887, 342.92682167353087], + 4658935.705149433, + 267763437.92570886) } groups_count = len(expected_group_info) @@ -375,15 +432,14 @@ def test_vagus_nerve_1(self): xi_centre = [0.5, 0.5, 0.5] # (element_identifier, expected_d3) expected_d3_info = [ - (2, [-58.61783307229305, 252.12576421778604, 1160.1311747920795]), - (4, [-466.6104180485502, 684.1844267896083, 792.1344245659976]), - (6, [-39.96816960556015, 626.8518820786068, 194.7956076398493]), - (8, [-3.5724520665194177, 202.78915581284485, 665.5701348977416]), - (10, [-35.32843904524995, -271.90830737446174, 641.3643386269923]), - (12, [-185.7857471471036, -564.9499544682276, 246.03766692690448]), - (14, [1.024675995239022, -474.64623017953363, 117.92958351592786]), - (16, [-3.5041316672463836, -465.94920208179235, 105.11830084069737])] - + (2, [-33.391218366765855, 268.39160153845626, 1158.860985674948]), + (4, [-501.3786015366995, 675.7031254761372, 793.3595901220758]), + (6, [-33.04316735076699, 629.5192377516283, 194.49196298196927]), + (8, [-23.822365306323434, 202.80333345607843, 665.523590669333]), + (10, [-25.625279982419272, -275.14752889054415, 641.9228605835165]), + (12, [-242.75605360012025, -550.3231979114498, 242.35581137281747]), + (14, [0.06310578889011254, -474.67423296131636, 117.90792203003063]), + (16, [-3.504130626277629, -465.9492020804986, 105.11830088131188])] for element_identifier, expected_d3 in expected_d3_info: element = mesh3d.findElementByIdentifier(element_identifier) self.assertEqual(RESULT_OK, fieldcache.setMeshLocation(element, xi_centre)) @@ -404,10 +460,10 @@ def test_vagus_nerve_1(self): fieldcache.clearLocation() result, volume = volume_field.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - expected_volume = 32973643021.906002 if (coordinate_field is coordinates) else 33282940849.74868 + expected_volume = 33286242951.84727 if (coordinate_field is coordinates) else 33282940849.74868 self.assertAlmostEqual(expected_volume, volume, delta=STOL) expected_elements_count = 33 - group = fieldmodule.findFieldByName("vagus epineurium").castGroup() + group = fieldmodule.findFieldByName("epineurium").castGroup() mesh_group2d = group.getMeshGroup(mesh2d) self.assertEqual(expected_elements_count * 4, mesh_group2d.getSize()) surface_area_field = fieldmodule.createFieldMeshIntegral(one, coordinate_field, mesh_group2d) @@ -415,7 +471,7 @@ def test_vagus_nerve_1(self): fieldcache.clearLocation() result, surface_area = surface_area_field.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - expected_surface_area = 72273975.5732966 if (coordinate_field is coordinates) else 72581935.90689336 + expected_surface_area = 72452883.40392067 if (coordinate_field is coordinates) else 72585973.86409168 self.assertAlmostEqual(expected_surface_area, surface_area, delta=STOL) group = fieldmodule.findFieldByName("vagus centroid").castGroup() mesh_group1d = group.getMeshGroup(mesh1d) @@ -460,29 +516,29 @@ def test_vagus_nerve_1(self): 0.07044881379783888, 0.00014399999999999916), 'left superior laryngeal nerve': ( - [0.0004810143452341199, 0.00016215537359726758, 0.1315566022306105], - [0.012918159918245761, 0.011858038889900713, -0.005993045531008481], - [-0.004009185375038346, 0.004374444695059173, -0.00014749081697862376], - 0.0019961825738478577, - 1.9965460129518142e-06), + [0.00047730703517693016, 0.0001590104729754135, 0.13155226693210442], + [0.012766941239985318, 0.011729858195898774, -0.006056668690651825], + [-0.00403931404678569, 0.0043905839670676464, -7.155118975268882e-05], + 0.0019802618878763203, + 1.9797849471091192e-06), 'left A branch of superior laryngeal nerve': ( - [0.029038466228280567, 0.02563897682357003, 0.11742199136150967], - [-0.009398043228514652, -0.013415035881397646, -0.017342847114134856], - [-0.004251880840935028, 0.004133184586737899, -0.0009537255926423682], - 0.0016020806733303852, - 1.5300531912537515e-06), + [0.028688067705606772, 0.02535673458269107, 0.11727390888183663], + [-0.009599655303649913, -0.013266188786747662, -0.017300073369861106], + [-0.004249304133747675, 0.004150895449488073, -0.0008858783562686601], + 0.0015990563197801552, + 1.5260431704427458e-06), 'left A thoracic cardiopulmonary branch of vagus nerve': ( - [-0.00023305107492456, -5.219544526299368e-06, 0.381097023646834], - [-0.02667084814216207, 0.011012987852591622, 0.006327322474065759], - [-0.002266194165367729, -0.005551192787385297, -0.0001201586042652858], - 0.002078138926255945, - 2.1261681901588285e-06), + [-0.00023275582415062705, -5.5790425955213165e-06, 0.3810973389155537], + [-0.026617743875947883, 0.010946968854188322, 0.006187567037017488], + [-0.0022754210043383436, -0.005550105193537561, -3.3829765141102364e-05], + 0.002071680752066034, + 2.119536598394646e-06), 'left B thoracic cardiopulmonary branch of vagus nerve': ( - [0.0005132914610188526, -0.0009107494333354117, 0.4063642319952978], - [0.02359392799517729, -0.02679536894825348, 0.020864764064142793], - [0.004500416564857745, 0.0039681076697987636, 1.0469921152278516e-08], - 0.0014496559346632183, - 1.4855508165393375e-06)} + [0.0005128262687161793, -0.0009094452229105069, 0.4063493881429567], + [0.023578121466429198, -0.026751584774707175, 0.020367952991549275], + [0.004501879772565175, 0.003966530167337301, 1.7100648076362468e-05], + 0.001442332241718134, + 1.4795610548057572e-06)} XTOL = 2.0E-7 # coordinates and derivatives STOL = 1.0E-9 # surface area VTOL = 1.0E-11 # volume @@ -548,6 +604,145 @@ def test_vagus_nerve_1(self): self.assertEqual(expected_id, annotation_group.getId()) self.assertEqual(expected_mesh_size, annotation_group.getMeshGroup(mesh3d).getSize()) + def test_arc_vagus(self): + """ + Test creation of a vagus nerve scaffold following a half circle so longitudinal curvature + effects are seen. + """ + context = Context("Test") + root_region = context.getDefaultRegion() + region = root_region.createChild('vagus') + data_region = root_region.createChild('data') + generate_arc_vagus_data(data_region) + + scaffold = MeshType_3d_nerve1 + options = scaffold.getDefaultOptions('Human Left Vagus 1') + elements_count = 16 + options['Number of elements along the trunk pre-fit'] = elements_count + options['Number of elements along the trunk'] = elements_count + options['Trunk fit number of iterations'] = 2 + + annotation_groups, nerve_metadata = scaffold.generateMesh(region, options) + self.assertEqual(14, len(annotation_groups)) + fit_metadata = nerve_metadata.getMetadata()['vagus nerve'] + self.assertAlmostEqual(fit_metadata['trunk centroid fit error rms'], 0.0, delta=1.0E-4) + self.assertAlmostEqual(fit_metadata['trunk radius fit error rms'], 0.0, delta=1.0E-12) + self.assertAlmostEqual(fit_metadata['trunk twist angle fit error degrees rms'], 0.0, delta=0.002) + fieldmodule = region.getFieldmodule() + fieldcache = fieldmodule.createFieldcache() + mesh1d = fieldmodule.findMeshByDimension(1) + coordinates = fieldmodule.findFieldByName('coordinates').castFiniteElement() + self.assertTrue(coordinates.isValid()) + one = fieldmodule.createFieldConstant(1.0) + centroid_group = fieldmodule.findFieldByName('vagus centroid').castGroup() + self.assertTrue(centroid_group.isValid()) + centroid_mesh_group = centroid_group.getMeshGroup(mesh1d) + self.assertEqual(elements_count, centroid_mesh_group.getSize()) + length_field = fieldmodule.createFieldMeshIntegral(one, coordinates, centroid_mesh_group) + length_field.setNumbersOfPoints(4) + result, length = length_field.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(math.pi, length, delta=1.0E-3) + + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + node = nodes.findNodeByIdentifier((elements_count // 2) + 1) + fieldcache.setNode(node) + XTOL = 1.0E-6 + expected_parameters = [ + [1.000000940622472, -6.338102288830355e-06, 0.0], + [1.8235912738924405e-07, 0.19637019676125334, 0.0], + [-0.07071023719107788, 6.566504166275671e-08, 0.07071111904345162], + [6.81154153478958e-05, -0.01390580597932258, 6.812747932889646e-05], + [0.07071111904342112, -6.566586059468453e-08, 0.07071023719110837], + [6.814039311262761e-05, 0.013905979277005844, -6.812832897063537e-05]] + i = 0 + for value_label in [Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D2_DS1DS2, Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3]: + result, parameters = coordinates.getNodeParameters(fieldcache, -1, value_label, 1, 3) + self.assertEqual(RESULT_OK, result) + assertAlmostEqualList(self, expected_parameters[i], parameters, delta=XTOL) + i += 1 + + +def generate_arc_vagus_data(region): + """ + Generate a semicircle of vagus data for testing centroid curvature terms in vagus nerve scaffold. + The data has no branches. + :param region: Region to create vagus data in. + """ + fieldmodule = region.getFieldmodule() + with (ChangeManager(fieldmodule)): + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + mesh1d = fieldmodule.findMeshByDimension(1) + coordinates = find_or_create_field_coordinates(fieldmodule, managed=True) + radius = find_or_create_field_finite_element(fieldmodule, "radius", 1, managed=True) + + nodetemplate = nodes.createNodetemplate() + nodetemplate.defineField(coordinates) + nodetemplate.defineField(radius) + elementtemplate = mesh1d.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_LINE) + linear_basis = fieldmodule.createElementbasis(1, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) + eft = mesh1d.createElementfieldtemplate(linear_basis) + elementtemplate.defineField(coordinates, -1, eft) + elementtemplate.defineField(radius, -1, eft) + + trunk_group = find_or_create_field_group(fieldmodule, "left vagus X nerve trunk", managed=True) + trunk_nodes = trunk_group.createNodesetGroup(nodes) + trunk_mesh1d = trunk_group.createMeshGroup(mesh1d) + + half_elements_count = 100 + elements_count = 2 * half_elements_count + fieldcache = fieldmodule.createFieldcache() + r = 0.05 + for n in range(elements_count + 1): + node = trunk_nodes.createNode(n + 1, nodetemplate) + fieldcache.setNode(node) + theta = 0.5 * math.pi * (n - half_elements_count) / half_elements_count + x = [math.cos(theta), math.sin(theta), 0.0] + coordinates.assignReal(fieldcache, x) + radius.assignReal(fieldcache, r) + if n > 0: + element = trunk_mesh1d.createElement(n, elementtemplate) + element.setNodesByIdentifier(eft, [n, n + 1]) + + anterior_group = find_or_create_field_group(fieldmodule, "orientation anterior", managed=True) + anterior_nodes = anterior_group.createNodesetGroup(nodes) + node_identifier = elements_count + 2 + anterior_points_count = 20 + anterior_angle = math.pi / 4.0 + anterior_r = 1.0 + math.cos(math.pi / 4.0) * 1.5 * r + anterior_z = math.sin(anterior_angle) * 1.5 * r + for n in range(anterior_points_count): + node = anterior_nodes.createNode(node_identifier, nodetemplate) + fieldcache.setNode(node) + theta = math.pi * (-0.5 + (n + 0.5) / anterior_points_count) + x = [anterior_r * math.cos(theta), anterior_r * math.sin(theta), anterior_z] + coordinates.assignReal(fieldcache, x) + radius.assignReal(fieldcache, 0.5 * r) + node_identifier += 1 + + # add level marker data + + datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + marker_group = find_or_create_field_group(fieldmodule, "marker", managed=True) + marker_datapoints = marker_group.createNodesetGroup(datapoints) + marker_name = find_or_create_field_stored_string(fieldmodule, "marker_name", managed=True) + nodetemplate_marker = datapoints.createNodetemplate() + nodetemplate_marker.defineField(coordinates) + nodetemplate_marker.defineField(marker_name) + + data_identifier = 1 + marker_info = get_left_vagus_marker_locations_list() + for name, material_coordinate3 in marker_info.items(): + node = marker_datapoints.createNode(data_identifier, nodetemplate_marker) + fieldcache.setNode(node) + theta = math.pi * (-0.5 + material_coordinate3) + x = [math.cos(theta), math.sin(theta), 0.0] + coordinates.assignReal(fieldcache, x) + marker_name.assignString(fieldcache, name) + data_identifier += 1 + if __name__ == "__main__": unittest.main() From 3c60262505f972d8d63de834ee78de860e0ea776 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 8 Dec 2025 14:10:52 +1300 Subject: [PATCH 56/61] Updated wrist directions --- .../meshtypes/meshtype_3d_wholebody2.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 668bbb5a..9cbc5efa 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -62,8 +62,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Wrist width"] = 0.7 options["Left wrist flexion degrees"] = 0.0 options["Right wrist flexion degrees"] = 0.0 - options["Left wrist deviation degrees"] = 0.0 - options["Right wrist deviation degrees"] = 0.0 + options["Left wrist deviation degrees"] = 90.0 + options["Right wrist deviation degrees"] = 90.0 options["Hand length"] = 2.0 options["Hand thickness"] = 0.2 options["Hand width"] = 1.0 @@ -830,8 +830,8 @@ def generateBaseMesh(cls, region, options): mult(antebrachiumSide, sinTwistAngle)) antebrachiumSide = d2 antebrachiumFront = d3 - wristFlexionRadians = wristLeftFlexionRadians if (side == left) else wristRightFlexionRadians - wristAbductionRadians = wristLeftAbductionRadians if (side == left) else wristRightAbductionRadians + wristFlexionRadians = wristLeftFlexionRadians if (side == left) else -wristRightFlexionRadians + wristAbductionRadians = wristLeftAbductionRadians if (side == left) else -wristRightAbductionRadians ventralFlexion = True if wristFlexionRadians < 0: ventralFlexion = False @@ -844,19 +844,19 @@ def generateBaseMesh(cls, region, options): xi = i / (armToHandElementsCount - 2) halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - rotationCoeff = 0.3 + rotationCoeff = 0.6 wristDir, handDir = getJointRotationFrames( wristAbductionRadians, wristFlexionRadians, antebrachiumDir, upwardAbduction, ventralFlexion ) wristDir[0] = add(x, d1) - wristDir[4:] = [handLength, halfThickness, halfWidth] + wristDir[4:] = [armScale, halfThickness, halfWidth] x, wristd2_mag, wristd3_mag, wristd12_mag, wristd13_mag = getJointRotationPosition( wristAbductionRadians, wristFlexionRadians, antebrachiumDir, wristDir, handDir, rotationCoeff, upwardAbduction, ventralFlexion ) # Set coordiantes for joint node # x = jointPositions[1] wristDirn, wristSide, wristFront = wristDir[1:4] - d1 = mult(wristDirn, handLength) + d1 = mult(wristDirn, armScale) d2 = mult(wristSide, wristd2_mag) d3 = mult(wristFront, wristd3_mag) d12 = add( From f120fbfa54d04ce0888706b8f17f03bd1399addc Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Mon, 8 Dec 2025 16:24:54 +1300 Subject: [PATCH 57/61] Propagated changes to joint rotation to leg and foot --- .../meshtypes/meshtype_3d_wholebody2.py | 150 ++++++++++-------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 9cbc5efa..797dc7f8 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -62,8 +62,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Wrist width"] = 0.7 options["Left wrist flexion degrees"] = 0.0 options["Right wrist flexion degrees"] = 0.0 - options["Left wrist deviation degrees"] = 90.0 - options["Right wrist deviation degrees"] = 90.0 + options["Left wrist deviation degrees"] = 0.0 + options["Right wrist deviation degrees"] = 0.0 options["Hand length"] = 2.0 options["Hand thickness"] = 0.2 options["Hand width"] = 1.0 @@ -102,16 +102,13 @@ def getOrderedOptionNames(cls): "Shoulder drop", "Shoulder width", "Left shoulder abduction degrees", - "Left shoulder flexion degrees", - "Left elbow flexion degrees", - "Left arm rotation degrees", "Right shoulder abduction degrees", - + "Left shoulder flexion degrees", "Right shoulder flexion degrees", - - "Right elbow flexion degrees", - + "Left arm rotation degrees", "Right arm rotation degrees", + "Left elbow flexion degrees", + "Right elbow flexion degrees", "Left wrist flexion degrees", "Right wrist flexion degrees", "Left wrist deviation degrees", @@ -195,12 +192,12 @@ def checkOptions(cls, options): "Right shoulder abduction degrees": (-100.0, 180.0), "Left shoulder flexion degrees": (-60.0, 200.0), "Right shoulder flexion degrees": (-60.0, 200.0), - # "Left elbow flexion degrees": (0.0, 150.0), - # "Right elbow flexion degrees": (0.0, 150.0), - # "Left arm rotation degrees": (0.0, 150.0), - # "Right arm rotation degrees": (0.0, 150.0), - "Left wrist flexion degrees": (-30.0, 30.0), - "Right wrist flexion degrees": (-30.0, 30.0), + "Left elbow flexion degrees": (0.0, 120.0), + "Right elbow flexion degrees": (0.0, 120.0), + "Left arm rotation degrees": (-90.0, 90.0), + "Right arm rotation degrees": (-90.0, 90.0), + # "Left wrist flexion degrees": (-30.0, 30.0), + # "Right wrist flexion degrees": (-30.0, 30.0), "Left hip flexion degrees": (0.0, 150.0), "Right hip flexion degrees": (0.0, 150.0), "Left knee flexion degrees": (0.0, 140.0), @@ -830,21 +827,21 @@ def generateBaseMesh(cls, region, options): mult(antebrachiumSide, sinTwistAngle)) antebrachiumSide = d2 antebrachiumFront = d3 - wristFlexionRadians = wristLeftFlexionRadians if (side == left) else -wristRightFlexionRadians + wristFlexionRadians = wristLeftFlexionRadians if (side == left) else wristRightFlexionRadians wristAbductionRadians = wristLeftAbductionRadians if (side == left) else -wristRightAbductionRadians ventralFlexion = True if wristFlexionRadians < 0: ventralFlexion = False wristFlexionRadians = -wristFlexionRadians - upwardAbduction = False + upwardAbduction = True if wristAbductionRadians < 0: - upwardAbduction = True + upwardAbduction = False wristAbductionRadians = -wristAbductionRadians antebrachiumDir[0] = x xi = i / (armToHandElementsCount - 2) - halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius - halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius - rotationCoeff = 0.6 + halfThickness = halfWristThickness + halfWidth = halfWristWidth + rotationCoeff = 0.3 wristDir, handDir = getJointRotationFrames( wristAbductionRadians, wristFlexionRadians, antebrachiumDir, upwardAbduction, ventralFlexion ) @@ -853,19 +850,18 @@ def generateBaseMesh(cls, region, options): x, wristd2_mag, wristd3_mag, wristd12_mag, wristd13_mag = getJointRotationPosition( wristAbductionRadians, wristFlexionRadians, antebrachiumDir, wristDir, handDir, rotationCoeff, upwardAbduction, ventralFlexion ) - # Set coordiantes for joint node - # x = jointPositions[1] wristDirn, wristSide, wristFront = wristDir[1:4] - d1 = mult(wristDirn, armScale) + handDirn, handSide, handFront = handDir[1:4] + d1 = mult(handDirn, armScale) d2 = mult(wristSide, wristd2_mag) d3 = mult(wristFront, wristd3_mag) d12 = add( mult(wristSide, d12_mag), - mult(wristDirn, wristd12_mag) + mult(handDirn, wristd12_mag) ) d13 = add( mult(wristFront, d13_mag), - mult(wristDirn, wristd13_mag) + mult(handDirn, wristd13_mag) ) id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) @@ -877,12 +873,10 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 options['Kinematic tree']['hand_' + side_label] = x - handDirn, handSide, handFront = handDir[1:4] + d1 = set_magnitude(handDirn, armScale) handStart = add(x, mult(handDirn, handLength)) handDir[0] = handStart - handStart = getDistalNodePosition(wristAbductionRadians, wristFlexionRadians, antebrachiumDir, - wristDir, handDir, rotationCoeff) # Hand assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) @@ -962,16 +956,25 @@ def generateBaseMesh(cls, region, options): # Frontal hip flexion # Updating frame of reference wrt flexion angle (using d2 as rotation axis) hipFlexionRadians = hipLeftFlexionRadians if (side == left) else hipRightFlexionRadians + ventralFlexion = True + if hipFlexionRadians < 0: + ventralFlexion = False + hipFlexionRadians = -hipFlexionRadians i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - legDir = [legDirn, legSide, legFront] + legDir = [x, legDirn, legSide, legFront, legScale, 0, 0] rotationCoeff = 0.18 - [x, hipDir, upperLegStart, upperLegDir, hipd3_mag, hipd13_mag] =\ - getJointAndDistalFlexionFrames(hipFlexionRadians, x, legDir,radius, legScale, rotationCoeff,ventralFlexion=True) - hipDirn, hipSide, hipFront = hipDir + hipDir, upperLegDir = getJointRotationFrames( \ + 0, hipFlexionRadians, legDir, True, ventralFlexion) + hipDir[0] = add(x, d1) + hipDir[4:] = [legScale, radius, radius] + x, hipd2_mag, hipd3_mag, hipd12_mag, hipd13_mag = getJointRotationPosition( + 0, hipFlexionRadians, legDir, hipDir, upperLegDir, rotationCoeff + ) + hipDirn, hipSide, hipFront = hipDir[1:4] d1 = mult(hipDirn, legScale) - d2 = mult(hipSide, radius) + d2 = mult(hipSide, hipd2_mag) d3 = mult(hipFront, 0.9*hipd3_mag) d12 = mult(hipSide, d12_mag) d13 = add( @@ -988,8 +991,12 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 options['Kinematic tree']['femur_' + side_label] = x - upperLegDirn, upperLegSide, upperLegFront = upperLegDir + upperLegDirn, upperLegSide, upperLegFront = upperLegDir[1:4] d1 = set_magnitude(upperLegDirn, armScale) + upperLegStart = add(x, d1) + upperLegDir[0] = upperLegStart + upperLegStart = getDistalNodePosition(0, hipFlexionRadians, + legDir, hipDir, upperLegDir, rotationCoeff) # Rest of upper leg j = 0 for i in range(hipElementsCount, hipElementsCount+upperLegElementsCount-1): @@ -1012,17 +1019,25 @@ def generateBaseMesh(cls, region, options): j += 1 # knee kneeFlexionRadians = kneeLeftFlexionRadians if (side == left) else kneeRightFlexionRadians + ventralFlexion = False + if shoulderFlexionRadians < 0: + ventralFlexion = True + kneeFlexionRadians = -kneeFlexionRadians # # Set coordiantes for joint node i += 1 xi = i / legToFootElementsCount radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - upperLegDir = [upperLegDirn, upperLegSide, upperLegFront] rotationCoeff = 0.25 - [x, kneeDir, lowerLegStart, lowerLegDir, kneed3_mag, kneed13_mag] =\ - getJointAndDistalFlexionFrames(kneeFlexionRadians, x, upperLegDir,radius, legScale, rotationCoeff,ventralFlexion=False) - kneeDirn, kneeSide, kneeFront = kneeDir + kneeDir, lowerLegDir = getJointRotationFrames( + 0, kneeFlexionRadians, upperLegDir, True, ventralFlexion + ) + kneeDir[0] = add(x, d1) + kneeDir[4:] = [legScale, radius, radius] + x, kneed2_mag, kneed3_mag, kneed12_mag, kneed13_mag = getJointRotationPosition( + 0, kneeFlexionRadians, upperLegDir, kneeDir, lowerLegDir, rotationCoeff, True, ventralFlexion) + kneeDirn, kneeSide, kneeFront = kneeDir[1:4] d1 = mult(kneeDirn, legScale) - d2 = mult(kneeSide, radius) + d2 = mult(kneeSide, kneed2_mag) d3 = mult(kneeFront, kneed3_mag) d12 = mult(kneeSide, d12_mag) d13 = add( @@ -1040,8 +1055,12 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 options['Kinematic tree']['tibia_' + side_label] = x # Lower leg - lowerLegDirn, lowerLegSide, lowerLegFront = lowerLegDir + lowerLegDirn, lowerLegSide, lowerLegFront = lowerLegDir[1:4] d1 = set_magnitude(lowerLegDirn, legScale) + lowerLegStart = add(x, d1) + lowerLegDir[0] = lowerLegStart + lowerLegStart = getDistalNodePosition(0, kneeFlexionRadians, + upperLegDir, kneeDir, lowerLegDir, rotationCoeff) j = 0 for i in range(hipElementsCount+upperLegElementsCount, legToFootElementsCount-1): xi = i / legToFootElementsCount @@ -1063,17 +1082,25 @@ def generateBaseMesh(cls, region, options): j+=1 # foot ankleFlexionRadians = ankleLeftFlexionRadians if (side == left) else ankleRightFlexionRadians + ventralFlexion = True + if ankleFlexionRadians < 0: + ventralFlexion = False + ankleFlexionRadians = -ankleFlexionRadians i += 1 - radius = (legBottomRadius + halfFootThickness) - rotationCoeff = 0.35 - [x, ankleDir, footStart, footDir, ankled3_mag, ankled13_mag] =\ - getJointAndDistalFlexionFrames( - ankleFlexionRadians, x, lowerLegDir,radius, - legScale, rotationCoeff,ventralFlexion=True, distalnScale=footLength) - ankleDirn, ankleSide, ankleFront = ankleDir - footDirn, footSide, footFront = footDir - d1 = mult(footDirn, footLength) - d2 = set_magnitude(ankleSide, halfFootWidth) + radius = math.sqrt(2)*legBottomRadius + rotationCoeff = 0.4 + ankleDir, footDir = getJointRotationFrames( + 0, ankleFlexionRadians, lowerLegDir, True, ventralFlexion + ) + ankleDir[0] = add(x, d1) + ankleDir[4:] = [legScale, radius, radius] + [x, ankled2_mag, ankled3_mag, ankled12_mag, ankled13_mag] = getJointRotationPosition( + 0, ankleFlexionRadians, lowerLegDir, ankleDir, footDir, rotationCoeff, True, ventralFlexion + ) + ankleDirn, ankleSide, ankleFront = ankleDir[1:4] + footDirn, footSide, footFront = footDir[1:4] + d1 = mult(footDirn, legScale) + d2 = set_magnitude(ankleSide, ankled2_mag) d3 = set_magnitude(ankleFront, ankled3_mag) d12 = set_magnitude(ankleSide, d12_mag) d13 = add( @@ -1091,6 +1118,8 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # Foot end nodes d1 = mult(footDirn, footLength) + footStart = add(x, d1) + footDir[0] = footStart j = 0 for i in range(footElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) @@ -1108,24 +1137,6 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 j+=1 - # x = footStart - # footDirn, footSide, footFront = footDir - # d1 = mult(footDirn, footLength) - # # d1 = computeCubicHermiteEndDerivative(jointPositions[1], jointDirn[1], jointPositions[2], jointDirn[2]) - # d2 = set_magnitude(footSide, halfFootWidth) - # d3 = set_magnitude(footFront, halfFootThickness) - # d12 = set_magnitude(d2, d12_mag) - # # d13 = set_magnitude(d3, d13_mag) - # d13 = sub(d3, set_magnitude(ankleFront, ankled3_mag)) - # id2 = mult(d2, innerProportionDefault) - # id3 = mult(d3, innerProportionDefault) - # id12 = set_magnitude(d12, d12_mag) - # id13 = set_magnitude(d13, d13_mag) - # node = nodes.findNodeByIdentifier(nodeIdentifier) - # fieldcache.setNode(node) - # setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) - # setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) - # nodeIdentifier += 1 options['Kinematic tree']['toes_' + side_label] = x return annotationGroups, networkMesh @@ -1453,6 +1464,7 @@ def generateBaseMesh(cls, region, options): abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) # Kinematic tree markers + fieldmodule = region.getFieldmodule() nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) node_identifier = max(1, get_maximum_node_identifier(nodes) + 1) coordinates = find_or_create_field_coordinates(fieldmodule) From a3bf2d7b6f2b72c3f2fd3b8d392217d4717667af Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 12 Dec 2025 11:43:38 +1300 Subject: [PATCH 58/61] Updated tests --- tests/test_wholebody2.py | 77 +++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index d97e33ac..21edc3ef 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -34,15 +34,18 @@ def test_wholebody2_core(self): parameterSetNames = scaffold.getParameterSetNames() self.assertEqual(parameterSetNames, ["Default", "Human 1 Coarse", "Human 1 Medium", "Human 1 Fine"]) options = scaffold.getDefaultOptions("Human 1 Coarse") - self.assertEqual(19, len(options)) + self.assertEqual(23, len(options)) self.assertEqual(2, options["Number of elements along head"]) self.assertEqual(1, options["Number of elements along neck"]) self.assertEqual(2, options["Number of elements along thorax"]) self.assertEqual(2, options["Number of elements along abdomen"]) - self.assertEqual(5, options["Number of elements along arm to hand"]) + self.assertEqual(3, options["Number of elements along brachium"]) + self.assertEqual(2, options["Number of elements along antebrachium"]) self.assertEqual(1, options["Number of elements along hand"]) - self.assertEqual(4, options["Number of elements along leg to foot"]) - self.assertEqual(2, options["Number of elements along foot"]) + self.assertEqual(2, options["Number of elements along hip"]) + self.assertEqual(3, options["Number of elements along upper leg"]) + self.assertEqual(3, options["Number of elements along lower leg"]) + self.assertEqual(1, options["Number of elements along foot"]) self.assertEqual(12, options["Number of elements around head"]) self.assertEqual(12, options["Number of elements around torso"]) self.assertEqual(8, options["Number of elements around arm"]) @@ -57,18 +60,18 @@ def test_wholebody2_core(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(32, len(annotationGroups)) + self.assertEqual(58, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(704, mesh3d.getSize()) + self.assertEqual(904, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(2306, mesh2d.getSize()) + self.assertEqual(2946, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(2533, mesh1d.getSize()) + self.assertEqual(3223, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(932, nodes.getSize()) + self.assertEqual(1195, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -77,8 +80,8 @@ def test_wholebody2_core(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.0, -3.650833433150559, -1.25], tol) - assertAlmostEqualList(self, maximums, [20.48332368641937, 3.650833433150559, 2.15], tol) + assertAlmostEqualList(self, minimums, [0.0, -3.936660189011623, -1.25], tol) + assertAlmostEqualList(self, maximums, [19.725896216328984, 3.936660189011623, 2.354767205067949], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -96,17 +99,17 @@ def test_wholebody2_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 98.41184838749453, delta=tol) - self.assertAlmostEqual(surfaceArea, 228.9017722286146, delta=tol) + self.assertAlmostEqual(volume, 93.46891042325106, delta=tol) + self.assertAlmostEqual(surfaceArea, 224.0370925379395, delta=tol) # check some annotation groups: expectedSizes3d = { - "abdominal cavity": (40, 10.074522362520469), - "core": (428, 45.535080392793354), - "head": (64, 6.909618374858558), - "thoracic cavity": (40, 6.974341918899202), - "shell": (276, 52.878054197629744) + "abdominal cavity": (40, 9.250133376720713), + "core": (548, 42.72181578233019), + "head": (64, 6.909618374858556), + "thoracic cavity": (40, 6.858627377211818), + "shell": (356, 50.74698672407125) } for name in expectedSizes3d: term = get_body_term(name) @@ -122,14 +125,14 @@ def test_wholebody2_core(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "abdominal cavity boundary surface": (64, 27.428203203724674), + "abdominal cavity boundary surface": (64, 25.710221456528195), "diaphragm": (20, 3.0778936559347208), - "left upper limb skin epidermis outer surface": (68, 22.433540462588258), - "left lower limb skin epidermis outer surface": (68, 55.24949927903832), - "right upper limb skin epidermis outer surface": (68, 22.433540462588258), - "right lower limb skin epidermis outer surface": (68, 55.24949927903832), - "skin epidermis outer surface": (388, 228.9017722286146), - "thoracic cavity boundary surface": (64, 20.2627556651649) + "left upper limb skin epidermis outer surface": (56, 18.880989688740083), + "left lower limb skin epidermis outer surface": (64, 48.27786366921722), + "right upper limb skin epidermis outer surface": (56, 18.880989688740083), + "right lower limb skin epidermis outer surface": (64, 48.27786366921722), + "skin epidermis outer surface": (468, 224.0370925379395), + "thoracic cavity boundary surface": (64, 19.93080798484531) } for name in expectedSizes2d: term = get_body_term(name) @@ -145,7 +148,7 @@ def test_wholebody2_core(self): self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) expectedSizes1d = { - "spinal cord": (8, 10.85369421002332) + "spinal cord": (8, 10.154987441354445) } for name in expectedSizes1d: term = get_body_term(name) @@ -172,18 +175,18 @@ def test_wholebody2_tube(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(24, len(annotationGroups)) + self.assertEqual(50, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(276, mesh3d.getSize()) + self.assertEqual(356, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(1126, mesh2d.getSize()) + self.assertEqual(1446, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(1443, mesh1d.getSize()) + self.assertEqual(1843, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(590, nodes.getSize()) + self.assertEqual(763, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -192,8 +195,8 @@ def test_wholebody2_tube(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.0, -3.650833433150559, -1.25], tol) - assertAlmostEqualList(self, maximums, [20.48332368641937, 3.650833433150559, 2.15], tol) + assertAlmostEqualList(self, minimums, [0.0, -3.936660189011623, -1.25], tol) + assertAlmostEqualList(self, maximums, [19.21001874192839, 3.936660189011623, 2.3012811728821925], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -220,13 +223,13 @@ def test_wholebody2_tube(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 52.87781598884186, delta=tol) - self.assertAlmostEqual(outerSurfaceArea, 224.9456647093771, delta=tol) - self.assertAlmostEqual(innerSurfaceArea, 155.4114878443358, delta=tol) + self.assertAlmostEqual(volume, 49.68674237062851, delta=tol) + self.assertAlmostEqual(outerSurfaceArea, 214.61377709797668, delta=tol) + self.assertAlmostEqual(innerSurfaceArea, 147.4303598620535, delta=tol) # check some annotationGroups: expectedSizes2d = { - "skin epidermis outer surface": (320, 228.11749988635236) + "skin epidermis outer surface": (400, 217.78561227495206) } for name in expectedSizes2d: term = get_body_term(name) From fc0a7ae708f28294e007cadf646c8dec67e2caec Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 12 Dec 2025 11:44:11 +1300 Subject: [PATCH 59/61] Added temporary kinematic markers --- src/scaffoldmaker/annotation/body_terms.py | 38 +++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 1121ac8d..03c32809 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -55,7 +55,34 @@ ("thoracic cavity", "UBERON:0002224"), ("thoracic cavity boundary surface", "ILX:0796508"), ("thorax", "ILX:0742178"), - ("ventral", "") + ("ventral", ""), + # kinematic tree markers + ('pelvis', ""), + ('femur_r', ""), + ('tibia_r', ""), + ('talus_r', ""), + ('calcn_r', ""), + ('toes_r', ""), + ('femur_l', ""), + ('tibia_l', ""), + ('talus_l', ""), + ('calcn_l', ""), + ('toes_l', ""), + ('lumbar_body', ""), + ('thorax_top', ""), + ('head_marker', ""), + ('scapula_r', ""), + ('humerus_r', ""), + ('ulna_r', ""), + ('radius_r', ""), + ('hand_r', ""), + ('scapula_l', ""), + ('humerus_l', ""), + ('ulna_l', ""), + ('radius_l', ""), + ('hand_l', ""), + + ] def get_body_term(name : str): @@ -68,3 +95,12 @@ def get_body_term(name : str): if name in term: return ( term[0], term[1] ) raise NameError("Body annotation term '" + name + "' not found.") + +def marker_name_in_terms(name: str): + """ + Check if term exists in approved marker terms + """ + for term in body_terms: + if name in term: + return True + return False From 097538c1728b4a48598b36cbb87d1735a6286170 Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 12 Dec 2025 11:45:39 +1300 Subject: [PATCH 60/61] Added interactive functions to align to markers --- .../meshtypes/meshtype_3d_wholebody2.py | 344 ++++-------------- 1 file changed, 80 insertions(+), 264 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 797dc7f8..a8e55578 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -2,13 +2,17 @@ Generates a 3D body coordinates using tube network mesh. """ from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub, magnitude, axis_angle_to_rotation_matrix, matrix_vector_mult, matrix_mult, dot, angle -from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates +from cmlibs.utils.zinc.general import ChangeManager +from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates, find_or_create_field_group, find_or_create_field_stored_string, find_or_create_field_finite_element from cmlibs.utils.zinc.finiteelement import get_maximum_node_identifier +from cmlibs.utils.zinc.region import copy_fitting_data from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node +from scaffoldfitter.fitter import Fitter as GeometryFitter +from scaffoldfitter.fitterstepalign import FitterStepAlign from scaffoldmaker.annotation.annotationgroup import ( AnnotationGroup, findOrCreateAnnotationGroupForTerm, getAnnotationGroupForTerm, evaluateAnnotationMarkerNearestMeshLocation) -from scaffoldmaker.annotation.body_terms import get_body_term +from scaffoldmaker.annotation.body_terms import get_body_term, marker_name_in_terms from scaffoldmaker.meshtypes.meshtype_1d_network_layout1 import MeshType_1d_network_layout1 from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base from scaffoldmaker.scaffoldpackage import ScaffoldPackage @@ -18,9 +22,11 @@ from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import BodyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData from scaffoldmaker.utils.human_network_layout import constructNetworkLayoutStructure, humanElementCounts +from scaffoldmaker.utils.zinc_utils import generate_datapoints, find_or_create_field_zero_fibres # from scaffoldmaker.utils.human_network_layout import import math - +import logging +logger = logging.getLogger(__name__) class MeshType_1d_human_body_network_layout1(MeshType_1d_network_layout1): """ @@ -90,6 +96,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Foot width"] = 1.0 options["Inner proportion default"] = 0.7 options["Inner proportion head"] = 0.35 + options['scale'] = [1,1,1] return options @classmethod @@ -992,7 +999,7 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 options['Kinematic tree']['femur_' + side_label] = x upperLegDirn, upperLegSide, upperLegFront = upperLegDir[1:4] - d1 = set_magnitude(upperLegDirn, armScale) + d1 = set_magnitude(upperLegDirn, legScale) upperLegStart = add(x, d1) upperLegDir[0] = upperLegStart upperLegStart = getDistalNodePosition(0, hipFlexionRadians, @@ -1138,6 +1145,19 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 j+=1 options['Kinematic tree']['toes_' + side_label] = x + + # Kinematic tree markers + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + node_identifier = max(1, get_maximum_node_identifier(nodes) + 1) + coordinates = find_or_create_field_coordinates(fieldmodule) + stickman_markers = options['Kinematic tree'] + for marker_name, marker_position in stickman_markers.items(): + marker_group = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, (marker_name, ""), isMarker=True + ) + marker_group.createMarkerNode( + node_identifier, coordinates, marker_position + ) return annotationGroups, networkMesh @classmethod @@ -1150,8 +1170,63 @@ def getInteractiveFunctions(cls): if interactiveFunction[0] == "Edit structure...": interactiveFunctions.remove(interactiveFunction) break + interactiveFunctions = interactiveFunctions + [\ + ("Align to body markers", + {"Load transformation parameters into Settings": True}, + lambda region, options, networkMesh, functionOptions, editGroupName: + cls.alignNetworkLayoutToMarkers(region, options, networkMesh, functionOptions, editGroupName) + ), + ] return interactiveFunctions + @classmethod + def alignNetworkLayoutToMarkers(cls, region, options, networkMesh, functionOptions, editGroupName): + + # Load the data into the current region + data_region = region.getParent().findChildByName('data') + if not data_region.isValid(): + logger.warning('Missing input data') + return None + data_fieldmodule = data_region.getFieldmodule() + with ChangeManager(data_fieldmodule): + copy_fitting_data(region, data_region) + del data_region + # Setup fitting routine + fieldmodule = region.getFieldmodule() + fitter = GeometryFitter(region=region) + fitter.getInitialFitterStepConfig() + # Manually run the _loadModel routines + fitter._discoverModelCoordinatesField() + fitter._discoverModelFitGroup() + # + zero_fibres = find_or_create_field_zero_fibres(fieldmodule) + fitter.setFibreField(zero_fibres) + del zero_fibres + # + fitter._discoverFlattenGroup() + fitter.defineCommonMeshFields() + # Manually run the _loadData routines + fitter._discoverDataCoordinatesField() + fitter._discoverMarkerGroup() + # Stuff + fitter.defineCommonMeshFields() + fitter.defineDataProjectionFields() + fitter.initializeFit() + # Call Align step + fit1 = FitterStepAlign() + fitter.addFitterStep(fit1) + fit1.setAlignMarkers(True) + fit1._doAutoAlign() + # Pass the graphical transformation settings into options + options['rotation'] = fit1._rotation + options['scale'] = fit1._scale + options['translation'] = fit1._translation + del fit1 + return True, False + + + + class MeshType_3d_wholebody2(Scaffold_base): """ @@ -1445,9 +1520,8 @@ def generateBaseMesh(cls, region, options): isShowTrimSurfaces=options["Show trim surfaces"]) tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() - + fieldmodule = region.getFieldmodule() if isCore: - fieldmodule = region.getFieldmodule() mesh = fieldmodule.findMeshByDimension(meshDimension) thoraxGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thorax")) abdomenGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdomen")) @@ -1464,7 +1538,6 @@ def generateBaseMesh(cls, region, options): abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) # Kinematic tree markers - fieldmodule = region.getFieldmodule() nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) node_identifier = max(1, get_maximum_node_identifier(nodes) + 1) coordinates = find_or_create_field_coordinates(fieldmodule) @@ -1616,38 +1689,6 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) -# def getJointFlexionFrame(jointFlexionRadians, proximalDir, ventralFlexion=True): -# jointAngleRadians = math.pi - jointFlexionRadians -# proximalDirn, proximalSide, proximalFront = proximalDir -# ventral = 1 if ventralFlexion else -1 -# jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) -# jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) -# # Joint directions (frame is rotated by half the flexion angle) -# jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) -# jointSide = proximalSide -# jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) -# jointDir = [jointDirn, jointSide, jointFront] -# return jointDir - -# def getJointAdbuctionFrames(jointAbductionRadians, jointDir, distalDir, ventralAbduction=True): -# jointAngleRadians = math.pi - jointAbductionRadians -# jointDirn, jointSide, jointFront = jointDir -# distalDirn, distalSide, distalFront = distalDir -# ventral = 1 if ventralAbduction else -1 -# jointRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians) -# jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(jointFront, -1*ventral), jointAbductionRadians/2) -# # Joint directions (frame is rotated by half the abduction angle) -# jointDirn = matrix_vector_mult(jointHalfRotationMatrix, jointDirn) -# jointSide = matrix_vector_mult(jointHalfRotationMatrix, jointSide) -# jointFront = jointFront -# # Distal directions -# distalDirn = matrix_vector_mult(jointRotationMatrix, distalDirn) -# distalSide = matrix_vector_mult(jointRotationMatrix, distalSide) -# distalFront = distalFront -# jointDir = [jointDirn, jointSide, jointFront] -# distalDir = [distalDirn, distalSide, distalFront] -# return jointDir, distalDir - def getJointRotationFrames(jointAbductionRadians, jointFlexionRadians, proximalDir, upwardAbduction=True, ventralFlexion=True): proximalDirn, proximalSide, proximalFront = proximalDir[1:4] ventral = 1 if upwardAbduction else -1 @@ -1656,8 +1697,6 @@ def getJointRotationFrames(jointAbductionRadians, jointFlexionRadians, proximalD ventral = 1 if ventralFlexion else -1 jointFlexionMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) jointHalfFlexionMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) - # jointRotationMatrix = matrix_mult(jointAbductionMatrix, jointFlexionMatrix) - # jointHalfRotationMatrix = matrix_mult(jointHalfAbductionMatrix, jointHalfFlexionMatrix) jointRotationMatrix = matrix_mult(jointFlexionMatrix,jointAbductionMatrix) jointHalfRotationMatrix = matrix_mult(jointHalfFlexionMatrix, jointHalfAbductionMatrix) # Joint directions (frame is rotated by half the abduction angle) @@ -1748,226 +1787,3 @@ def adjustJointNodePosition(proximalNodePosition, nodePosition, d1, d2, dispFact jointAdjustDir = set_magnitude(jointAdjustDir, lenScale) adjustedNodePosition = add(proximalNodePosition, jointAdjustDir) return adjustedNodePosition -# def getJointFlexionFrames(jointFlexionRadians, proximalDir, ventralFlexion=True): -# jointAngleRadians = math.pi - jointFlexionRadians -# proximalDirn, proximalSide, proximalFront = proximalDir -# ventral = 1 if ventralFlexion else -1 -# jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) -# jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) -# # Joint directions (frame is rotated by half the flexion angle) -# jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) -# jointSide = proximalSide -# jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) -# # Distal directions -# distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) -# distalSide = proximalSide -# distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) -# jointDir = [jointDirn, jointSide, jointFront] -# distalDir = [distalDirn, distalSide, distalFront] -# return jointDir, distalDir - -# def getJointFlexionPosition(jointFlexionRadians, proximalDir, -# jointDir, distalDir, proximalNodePosition, -# jointNodePosition, frontScale, -# rotationCoeff, ventralFlexion = True): -# jointAngleRadians = math.pi - jointFlexionRadians -# proximalDirn, proximalSide, proximalFront = proximalDir -# jointDirn, jointSide, jointFront = jointDir -# jointFront = mult(jointFront, -1) if (jointFlexionRadians < 0) else jointFront -# distalDirn, distalSide, distalFront = distalDir -# flexionRotFactor = 1*math.sin(jointAngleRadians) -# jointRotFactor = 1/math.sin(jointAngleRadians/2) -# d13RotFactor = math.sqrt(2)*math.tan(jointFlexionRadians/2) -# ventral = 1 if ventralFlexion else -1 -# proximalScale = magnitude(sub(proximalNodePosition, jointNodePosition)) -# rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor -# jointAdjustDir = add( -# set_magnitude(jointFront, ventral*rotDisplacementFactor), -# set_magnitude(distalDirn, rotDisplacementFactor), -# ) -# jointAdjustDir = add( -# set_magnitude(proximalDirn, proximalScale), -# jointAdjustDir -# ) -# jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) -# jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) -# jointDir -# jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) -# jointd13_mag = -1*ventral*frontScale*d13RotFactor -# return [jointAdjustPosition, jointd3_mag, jointd13_mag] - -# def getJointAbductionPosition(jointAbductionRadians, proximalDir, -# jointDir, distalDir, proximalNodePosition, -# jointNodePosition, sideScale, proximalScale, -# rotationCoeff, ventralAbduction = True, side = 0): -# jointAngleRadians = math.pi - jointAbductionRadians -# proximalDirn, proximalSide, proximalFront = proximalDir -# jointDirn, jointSide, jointFront = jointDir -# right = -1 if (side == 0) else 1 -# jointSide = mult(jointSide, right) -# distalDirn, distalSide, distalFront = distalDir -# AbductionRotFactor = 1*math.sin(jointAngleRadians) -# jointRotFactor = 1/math.sin(jointAngleRadians/2) -# d13RotFactor = math.sqrt(2)*math.tan(jointAbductionRadians/2) -# ventral = 1 if (ventralAbduction) else -1 -# rotDisplacementFactor = rotationCoeff*sideScale*AbductionRotFactor -# jointAdjustDir = add( -# set_magnitude(jointSide, ventral*rotDisplacementFactor), -# set_magnitude(distalDirn, rotDisplacementFactor), -# ) -# jointAdjustDir = add( -# set_magnitude(proximalDirn, proximalScale), -# jointAdjustDir -# ) -# jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) -# jointAdjustPosition = add(proximalNodePosition, jointAdjustDir) -# jointd2_mag = sideScale*(jointRotFactor-(rotationCoeff*AbductionRotFactor)) -# jointd12_mag = -1*right*ventral*sideScale*d13RotFactor -# return [jointAdjustPosition, jointd2_mag, jointd12_mag] - -# def getDistalJointNodePosition(jointFlexionRadians, proximalDir, jointDir, distalDir, rotationCoeff, -# frontScale, distalScale, jointNodePosition): -# proximalDirn, proximalSide, proximalFront = proximalDir -# jointDirn, jointSide, jointFront = jointDir -# distalDirn, distalSide, distalFront = distalDir -# jointAngleRadians = math.pi - jointFlexionRadians -# flexionRotFactor = 1*math.sin(jointAngleRadians) -# rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor -# jointAdjustDir = add( -# set_magnitude(jointDirn, rotDisplacementFactor), -# set_magnitude(proximalDirn, rotDisplacementFactor), -# ) -# jointAdjustDir = add( -# set_magnitude(distalDirn, distalScale), -# jointAdjustDir -# ) -# jointAdjustDir = set_magnitude(jointAdjustDir, distalScale) -# distalNodePosition = add(jointNodePosition, jointAdjustDir) -# return distalNodePosition - - - - -# def getJointFlexionAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff, ventralFlexion = True): -# proximalNodePosition = proximalDir[0] -# jointNodePosition = jointDir[0] -# jointFront = jointDir[3] -# jointFrontScale = jointDir[6] -# distalDirn = distalDir[1] -# jointAngleRadians = math.pi - jointRotationRadians -# abductionRotFactor = 1*math.sin(jointAngleRadians) -# rotDisplacementFactor = rotationCoeff*jointFrontScale*abductionRotFactor -# distalNodePosition = adjustJointNodePosition(proximalNodePosition, jointNodePosition, \ -# jointFront, distalDirn, rotDisplacementFactor, ventralFlexion) -# return distalNodePosition - -# def getJointAbductionAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff): -# proximalDirn = proximalDir[1] -# jointNodePosition = jointDir[0] -# jointDirn = jointDir[1] -# jointSideScale = jointDir[5] -# distalNodePosition = distalDir[0] -# jointAngleRadians = math.pi - jointRotationRadians -# abductionRotFactor = 1*math.sin(jointAngleRadians) -# rotDisplacementFactor = rotationCoeff*jointSideScale*abductionRotFactor -# distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ -# jointDirn, proximalDirn, rotDisplacementFactor) -# return distalNodePosition - -# def getDistalNodeAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff): -# proximalDirn = proximalDir[1] -# jointNodePosition = jointDir[0] -# jointDirn = jointDir[1] -# jointFrontScale = jointDir[6] -# distalNodePosition = distalDir[0] -# jointAngleRadians = math.pi - jointRotationRadians -# abductionRotFactor = 1*math.sin(jointAngleRadians) -# rotDisplacementFactor = rotationCoeff*jointFrontScale*abductionRotFactor -# distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ -# jointDirn, proximalDirn, rotDisplacementFactor) -# return distalNodePosition - -# def getDistalAbductionAdjustment(jointRotationRadians, proximalDir, jointDir, distalDir, rotationCoeff): -# proximalDirn = proximalDir[1] -# jointNodePosition = jointDir[0] -# jointDirn = jointDir[1] -# jointSideScale = jointDir[5] -# distalNodePosition = distalDir[0] -# jointAngleRadians = math.pi - jointRotationRadians -# abductionRotFactor = 1*math.sin(jointAngleRadians) -# rotDisplacementFactor = rotationCoeff*jointSideScale*abductionRotFactor -# distalNodePosition = adjustJointNodePosition(jointNodePosition, distalNodePosition, \ -# jointDirn, proximalDirn, rotDisplacementFactor) -# return distalNodePosition - - - - -def getJointAndDistalFlexionFrames(jointFlexionRadians, proximalNodePosition, proximalDir, - frontScale, dirnScale, rotationCoeff, distalnScale = False, ventralFlexion=True): - jointAngleRadians = math.pi - jointFlexionRadians - proximalDirn, proximalSide, proximalFront = proximalDir - ventral = 1 if ventralFlexion else -1 - jointRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians) - jointHalfRotationMatrix = axis_angle_to_rotation_matrix(mult(proximalSide, -1*ventral), jointFlexionRadians/2) - # Joint directions (frame is rotated by half the flexion angle) - jointDirn = matrix_vector_mult(jointHalfRotationMatrix, proximalDirn) - jointSide = proximalSide - jointFront = matrix_vector_mult(jointHalfRotationMatrix, proximalFront) - # Distal directions - distalDirn = matrix_vector_mult(jointRotationMatrix, proximalDirn) - distalSide = proximalSide - distalFront = matrix_vector_mult(jointRotationMatrix, proximalFront) - # These rotation factors are used to adjust the position of the joint node relative - # to the angle of flexion, and ensures a proper transition between the two parts - flexionRotFactor = 1*math.sin(jointAngleRadians) - jointRotFactor = 1/math.sin(jointAngleRadians/2) - d13RotFactor = math.sqrt(2)*math.tan(jointFlexionRadians/2) - # Diagram to calculate position of joint node with flexion - # 1 - # | - # 2 -- 3 - # 1 is the proximal node, 2 is the joint node, 3 is distal node - # we fix the position of the nodes 1 and 3, and calculate - # the position of 2 using the sampleCubicHermiteCurvesSmooth function. - nodePositions = [] - nodePositions.append(proximalNodePosition) #1 - # The joint node is not set directly at the corner - # Instead it is nudged forward towards the center of the tube - # To get as smooth of a line as possible between node #1 and #3 - rotDisplacementFactor = rotationCoeff*frontScale*flexionRotFactor - proximalScale = dirnScale - distalScale = distalnScale if (distalnScale) else dirnScale - jointAdjustDir = add( - set_magnitude(jointFront, ventral*rotDisplacementFactor), - set_magnitude(distalDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(proximalDirn, proximalScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, proximalScale) - nodePositions.append(add(nodePositions[-1], jointAdjustDir)) #2 - jointAdjustDir = add( - set_magnitude(jointDirn, rotDisplacementFactor), - set_magnitude(proximalDirn, rotDisplacementFactor), - ) - jointAdjustDir = add( - set_magnitude(distalDirn, distalScale), - jointAdjustDir - ) - jointAdjustDir = set_magnitude(jointAdjustDir, distalScale) - nodePositions.append(add(nodePositions[-1], jointAdjustDir)) #3 - # nodeDir = [proximalDirn, jointDirn, distalDirn] - # nodePositions, nodeDirn = sampleCubicHermiteCurvesSmooth( - # nodePositions, jo, 2, - # derivativeMagnitudeStart=dirnScale, derivativeMagnitudeEnd=dirnScale - # )[0:2] - jointNodePosition = nodePositions[1] - jointDir = [jointDirn, jointSide, jointFront] - distalNodePosition = nodePositions[2] - distalDir = [distalDirn, distalSide, distalFront] - # Magnitudes - jointd3_mag = frontScale*(jointRotFactor-(rotationCoeff*flexionRotFactor)) - jointd13_mag = -1*ventral*frontScale*d13RotFactor - return [jointNodePosition, jointDir, distalNodePosition, distalDir, jointd3_mag, jointd13_mag] \ No newline at end of file From 6ead23a64c9adcca2fc84404d1e5d4c89d3ab13b Mon Sep 17 00:00:00 2001 From: 0Huitzil Date: Fri, 12 Dec 2025 11:52:12 +1300 Subject: [PATCH 61/61] Updated test w/o solid core --- tests/test_wholebody2.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index 21edc3ef..45017ceb 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -196,7 +196,7 @@ def test_wholebody2_tube(self): minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 assertAlmostEqualList(self, minimums, [0.0, -3.936660189011623, -1.25], tol) - assertAlmostEqualList(self, maximums, [19.21001874192839, 3.936660189011623, 2.3012811728821925], tol) + assertAlmostEqualList(self, maximums, [19.725896216328984, 3.936660189011623, 2.354767205067949], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -223,13 +223,13 @@ def test_wholebody2_tube(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 49.68674237062851, delta=tol) - self.assertAlmostEqual(outerSurfaceArea, 214.61377709797668, delta=tol) - self.assertAlmostEqual(innerSurfaceArea, 147.4303598620535, delta=tol) + self.assertAlmostEqual(volume, 50.747766334772045, delta=tol) + self.assertAlmostEqual(outerSurfaceArea, 220.08098501870208, delta=tol) + self.assertAlmostEqual(innerSurfaceArea, 151.26319776665184, delta=tol) # check some annotationGroups: expectedSizes2d = { - "skin epidermis outer surface": (400, 217.78561227495206) + "skin epidermis outer surface": (400, 223.25282019567732) } for name in expectedSizes2d: term = get_body_term(name)