How to measure the sum of the length of all edges of an object?

Hi Everyone,

Please help me to measure the sum of the length of all edges of an object.
Eventually, I would like to have a file (e.g. csv), in which the sum of the length of all edges are listed for all objects in the blender file.

We do neuroscience research. We reconstructed several neurons. We want to measure the changes of the cross-sectional areas and surface of neuronal processes.

Using NeuroMorph plugin we created hundreds of cross-sections in the neuronal processes and we have the cross-sectional areas. This is great.

However we also need the changes in the surface area of the neuronal processes. The cross-sectional areas are very simple objects created by the NeuroMorph plugin.

They are a ring of vertices connected by edges. This edge-ring is the perimeter of the flat cross-sectional area. This edge-ring shows the local perimeter of the neuronal process.

If there would be a way to print out the sum of the length of all edges of these named object into a file, that would solve our problem.

Is there any way? How should we start.
Thank you very much in advance.

Best,
GeeN

This isn’t elegant but at least you could call it a start. There’s already a built in method for measuring edge length so we just need to do that for each edge and sum them together.

https://docs.blender.org/api/current/bmesh.types.html#bmesh.types.BMEdge.calc_length

# ##### BEGIN GPL LICENSE BLOCK #####
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,
#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####

bl_info = {
    "name": "Measure Tools",
    "author": "Chris Kohl",
    "version": (0, 0, 1),
    "blender": (2, 80, 0),
    "location": "View3D > Properties",
    "description": "Track previous selection when new selection made",
    "wiki_url": "https://blenderartists.org/t/how-to-measure-the-sum-of-the-length-of-all-edges-of-an-object/1224932/2",
    "tracker_url": "",
    "category": ""
}

import bpy
import bmesh
import os
from bpy.types import Panel

class KO_OT_Measure_Perimeters(bpy.types.Operator):
    bl_idname = "ko.measure_perimeters"
    bl_label = "Write perimeters to file"
    bl_description = "Measure perimeters of selected edge loops and write them to a text file"
    bl_options = {"UNDO"}

    @classmethod
    def poll(cls, context):
        return context.selected_objects is not None

    def execute(self, context):
        # Must apply scale transform otherwise the measurement will be wrong.
        # However, you could probably calculate the scale delta(?) and adjust the numbers as needed
        # without forcibly applying scale (If you were smarter than me)
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)

        context = bpy.context
        sel = context.selected_objects

        result = ""

        # Measurement has to be done in edit mode.
        bpy.ops.object.mode_set(mode='EDIT')

        for obj in sel:
            me = obj.data
            bm = bmesh.from_edit_mesh(me)
            
            object_edges = [e for e in bm.edges]
            
            perimeter_length = 0.0
            for e in object_edges:
                if len(e.link_faces) < 2:
                    # Measure length of e with calc_length
                    perimeter_length = perimeter_length + e.calc_length()
                elif len(e.link_faces) > 1:
                    bpy.ops.object.mode_set(mode='OBJECT')
                    self.report({'ERROR'}, 'ERROR: Connected faces detected in selection.  Only works with stand-alone loops of edges or single unconnected faces and n-gons.')
                    return {'CANCELLED'}
            print(str(obj.name) + " length is: " + str(perimeter_length))

            result = result + (str(obj.name) + "," + str(perimeter_length) + "\n")
        
        # Write result to file
        # Ensure all folders of the path exist. Make the directory if it doesn't exist.
        path = "C:/YOURFOLDERPATH/"
        os.makedirs(path, exist_ok=True)

        # Write data out (THIS WILL OVERWRITE THE FILE IF IT ALREADY EXISTS)
        with open(path + "YOURFILENAME.txt", "w") as file:
            file.write(result)
            file.close()

        # Finally return back to object mode
        bpy.ops.object.mode_set(mode='OBJECT')
        return {'FINISHED'}

class PanelMeasureTools(Panel):
    bl_idname = "VIEW3D_PT_measure_tools"
    bl_label = "Measure Tools"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = 'Measure Tools'

    def draw(self, context):
        layout = self.layout
        col = layout.column()
        col.label(text="Measure Perimeters:")
        col.operator("ko.measure_perimeters", text="Make it so")

def register():
    bpy.utils.register_class(KO_OT_Measure_Perimeters)
    bpy.utils.register_class(PanelMeasureTools)

def unregister():
    bpy.utils.unregister_class(PanelMeasureTools)
    del bpy.types.Scene.ko_measure_tools_props

if __name__ == "__main__":
    register()

Before running the script, set your file name and file path on line 85 and 81 respectively.

This adds a panel and a button into the n-panel. Select your slices (in Object mode, not Edit Mode) and then click it.

Limitations:

  • It seems to me that the calc_length method will always return length in meters, regardless of the units or scale being used in the scene? I’m not familiar enough with how units and scale work to conclusively say if there is a more proper way to do this.
  • You have to open the scene and select all the objects you want to measure (in Object mode, not Edit Mode) rather than, say, having it grab everything automatically. Could be easily improved in a v 0.0.2.
  • The file name and file path are hard-coded. I tried to add a text entry field for setting file path but I kept getting problems registering the functions so for now this is the state of things.
  • It forcibly applies the object scale (because measurement will be wrong if the object isn’t at 1.0 scale unless you can calculate the difference, which I’m not smart enough to do)
  • If the file already exists it will overwrite that file and there will be no warning.
  • Not really a limitation so much as a deliberate choice: Only works on objects that are solely composed of floating loops of edges, e.g. like the Circle primitive type, or on objects that are unconnected single faces/n-gons that are not connected to other faces. Measuring edge length on a normal object wouldn’t produce a real result.

Edit (2020-05-09 23:27):

Version 0.0.3 is much better. See post 9 and 10 for changes.

Measure_Tools_v_0_0_3.py (10.1 KB)

1 Like

Dear Chris,

This is amazing! It works as it is.
The limitations are not big deal. Certainly I will have to open all the 77 files I have and apply the script and I will have to multiply all data in the text file with my scale, but this is a very little price because otherwise I would have not been able to do it at all.
Thank you very much.
Can I acknowledge your scripting work in the scientific paper that we will publish in about a year from now? It would be something like: “We thank Chris Kohl for the Blender script that measured the perimeters of the dendritic cross-sections”

Thank you for your quick and perfect reply,
GeeN

Sure, go ahead. You’re welcome. :slight_smile:

Regarding the unit scale (again), someone mentioned that internally Blender uses “Blender units” which are basically just meters anyway (I checked by measuring the length of the default 4-sided plane object which has side lengths of 2m and the result was 8.0 which supports the idea that it’s returning meters even if it’s technically “Blender units”).

OK thanks. Actually I have a reference object in all files, so it was easy to figure out that I only needed to multiply all results with 0.006 and it gives me micrometers in real life. Thank you.

Dear Chris,

Thanks again for your help. You have been really generous with your time, and I know it is ridiculous to ask anything more, but I still have one problem. Your script work fine and it turned out to be very useful that you built in a fail safe that notifies if “Connected faces detected in selection.”

However it has also turned out that the original 3rd party VMTK script that created the cross-sections actually makes quite some error that I have to repair manually. This is fine. My only problem is that it takes a lot of hunting time until I find the particular faulty object where connected faces are present.

Would there be any way to select these problematic faces automatically? I have sometimes several hundreds of perimeters and it takes a lot of time to find the few that are not good.

I can imagine that if the script runs into error, it would deselect all objects and would select only the one or all objects with error. I do not mind if the script only selects them one at a time either.

If it is too much to ask than never mind, your help was already extremely useful anyway.

Thanks,
GeeN

I don’t mind. This is fun. I’m going to bed right now but I’ll work on it after I wake up.

Could you attach an example of 1 good cross-section and 1 bad cross-section? (I probably don’t need hundreds of examples. :stuck_out_tongue: )

Also:

  • Are there any other meshes in your scenes aside from the cross-sections and the scale reference object?
  • Does the reference object always have the same name?
  • Are the cross-sections always named “cross-section_xxxx” ?

Dear Chris,

Thanks a lot for trying.
I attached a very small segment with several good and 1 bad cross-section.
Only cross-section_0285 has a problematic inner island.

  • Are there any other meshes in your scenes aside from the cross-sections and the scale reference object?
    NO
  • Does the reference object always have the same name?
    YES
  • Are the cross-sections always named “cross-section_xxxx” ?
    YES

Here is the file:
Base_file_perimeter.blend (927.1 KB)

Thank you,
GeeN

Okay here we go. Mucho big improvements. Like, a whole buttload of them. Give it a whirl.

Install the attached .py file as an Add-on. Edit > Preferences > Add-ons tab > Install > browse to .py file and accept. Don’t forget to enable it after installing. No need to copy/paste and run from the Script Editor.

  • No more hard-coded file names or file paths! party.gif
  • By default the script will output a .csv file in the same directory as the currently opened .blend file using the file’s name. e.g. If you open ‘filename.blend’ and run the script it will save ‘filename.csv’ in the same folder.
  • User can uncheck those checkboxes and manually supply an output directory or output file name if they wish. (fragile? May not work with more than one period in a file name; may or may not work with special characters or accents; only tested on Windows 10 in English)
  • Script will now process every mesh in the opened file automatically (I haven’t yet added a filter to match “cross-section_xxxx” so it just grabs everything). You don’t have to select meshes anymore. Just open the file and press the button.
  • Script will ignore the scale reference object if the user supplies the name. Script defaults to “SCALE 4.2x6.3” (without the quotes) if a name is not manually provided.
  • Any mesh that possibly has an error will now be flagged in the console output and left selected at the end of the operation.
  • Meshes with possible errors WILL NOT be added to the .csv output. All the good meshes will still be written to the .csv file.

I see in the example file you attached that cross section 285 has a tiny hole in it, which is why the error happens. Is there a scenario where there is a real (valid) hole in a cross section? Like a cavity or void inside? Or are the slices supposed to always be one outer perimeter with a solid inside? If there are cases where a real hole is supposed to exist then the script in its current state will probably error on them every time (false positive error), which I would want to fix for you (a simple fix, in theory).

Measure_Tools_v_0_0_2.py (10.0 KB)

1 Like

Slight improvement:

  • Any potential error objects now enter Local View at the end. This is equal to activating View > Local View > Toggle Local View from the viewport menu (which is also how you exit the Local View). That should make it even easier to deal with any error objects because only the possible error objects will be visible at the end. :smiley:

Measure_Tools_v_0_0_3.py (10.1 KB)

One observation: The unit scale on your example file is 0.006* which means a plane with side lengths of 2m reports a side length of 333.3333435058594 when the script runs, but I assume that’s why you mentioned earlier multiplying by 0.006? I could just have the script automatically multiply the results before writing to the .csv file if you’d like. It could be another text input field in the panel above the button if you ever need to change your scaling factor.

*actually it’s 0.006000000052154064 according to bpy.data.scenes[0].unit_settings.scale_length

Measure_Tools_v_0_0_4.py (10.3 KB)

1 Like

Dear Chris,

OMG, you saved me man.
Mucho big improvements.indeed!

Sorry for not giving you feedback immediately. In the last couple of days we used your script like 10 hours per day. We had to finish a project because we had a deadline.
The add-on worked perfectly, it saved us a lot of time.
Everything worked as described. Going into local view was a nice touch, previously I used it anyway with shortcut.
Your help was highly appreciated. Really thank you.
I hope in some shape or form your addon will be useful for others as well.

Thank you,
GeeN

1 Like