8
$\begingroup$

I'm coloring a mesh using vertex colors:

mesh = cur_obj.data    
vcol_layer = mesh.vertex_colors.new('Col') 
for ind in indices:
    vcol_layer.data[ind].colors = colors[ind]

I don't have values for all the vertices. Can I get values for these vertices by smoothing?

****** Update **********

Let me be more clear. Here is the code I'm using to color objects:

mesh = cur_obj.data
scn = bpy.context.scene
valid_verts = np.where(np.abs(verts_values) >= threshold)[0]
verts_colors = calc_colors(verts_values)
scn.objects.active = cur_obj
cur_obj.select = True
vcol_layer = mesh.vertex_colors.new('Col')
for vert in valid_verts:
    x = lookup[vert]
    for loop_ind in x[x > -1]:
        d = vcol_layer.data[loop_ind]
        d.color = verts_colors[vert]

Where the lookup table was created using the following function:

def calc_faces_verts(verts, faces, out_file):
    _faces = faces.ravel()
    faces_arg_sort = np.argsort(_faces)
    faces_sort = np.sort(_faces)
    faces_count = Counter(faces_sort)
    max_len = max([v for v in faces_count.values()])
    lookup = np.ones((verts.shape[0], max_len)) * -1
    diff = np.diff(faces_sort)
    n = 0
    for ind, (k, v) in enumerate(zip(faces_sort, faces_arg_sort)):
        lookup[k, n] = v
        n = 0 if ind < len(diff) and diff[ind] > 0 else n + 1
    np.save(out_file, lookup.astype(np.int))
    if len(_faces) != int(np.max(lookup)) + 1:
        raise Exception('Wrong values in lookup table! ' +
                        'faces ravel: {}, max looup val: {}'.format(len(_faces), int(np.max(lookup))))

This is working great when I have a value per vertice. The problem starts when this isn't the case. For example, I have an object with >100k vertices, where only ~4k have values. Currently, I'm smoothing those values on the surface outside Blender and saves the result to a numpy array (verts_values). The question is whether I can do the smoothing inside Blender (which hopefully will be also faster), without the necessity of a pre-processing step.

**** Update ****

You can find here some example files:

  1. vertices.npy - An array of vertices indices that I have values for
  2. data.npy - values for vertices.npy
  3. lh.pial.ply - The object I want to color
  4. lh.pial.npz - Same as 3, but in here the vertices and faces are stored in a npz file (faster and easier to read)

To calculate the colors, I just pick a colormap (jet for example), and do something like this:

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.cm as cmx

def array_to_color(x, color_map):
    x_min, x_max = np.min(x), np.max(x)
    cm = plt.get_cmap(color_map)
    cNorm = matplotlib.colors.Normalize(vmin=x_min, vmax=x_max)
    scalar_map = cmx.ScalarMappable(norm=cNorm, cmap=cm)
    return scalar_map.to_rgba(x)

**** Update ****

I've used both suggested solutions, and added another option to set a threshold for the values. Here is the code:

import bpy
import numpy as np
from collections import defaultdict


def color_vertices_rgb(obj, vert_colors, fixed_vertices, vert_links, verts_to_loop, loops_num=25):
    """Merges the colors assigned to the FIXED vertices across the rest of the mesh"""

    obj.update_from_editmode()

    mesh = obj.data
    scn = bpy.context.scene

    # check if our mesh already has Vertex Colors, and if not add some... (first we need to make sure it's the active object)
    scn.objects.active = obj
    obj.select = True
    mesh.vertex_colors.new()

    print("Captured.")
    for ind in range(loops_num):
        new_colors = {}

        for vert in obj.data.vertices:
            vertidx = vert.index
            if vertidx not in fixed_vertices:
                new_colors[vertidx] = average_color(vert_colors, vert_links, vertidx)

        # Copy new colors back to vert_colors
        for vert in obj.data.vertices:
            vertidx = vert.index
            if vertidx not in fixed_vertices:
                vert_colors[vertidx] = new_colors[vertidx]

    print("Storing colors...")
    for vert in obj.data.vertices:
        vertidx = vert.index
        set_vertex_color(obj, vertidx, vert_colors[vertidx], verts_to_loop)


def set_vertex_color(obj, vertid, color, verts_to_loop):
    for loop_index in verts_to_loop[vertid]:
        obj.data.vertex_colors[0].data[loop_index].color = color


def make_verts_to_loop_lookup(obj):
    verts_to_loop_verts = defaultdict(list)
    for li, l in enumerate(obj.data.loops):
        verts_to_loop_verts[l.vertex_index].append(li)
    return verts_to_loop_verts


def average_color(vert_colors, vert_links, vertid):
    r = 0.0
    g = 0.0
    b = 0.0
    num_verts = 0

    for linkedvertid in vert_links[vertid]:
        col = vert_colors[linkedvertid]
        r = r + col[0]
        g = g + col[1]
        b = b + col[2]
        num_verts += 1
    return (r / num_verts, g / num_verts, b / num_verts)


def get_linked_verts(obj, vertid):
    verts = []
    for edge in obj.data.edges:
        if edge.vertices[0] == vertid:
            verts.append(edge.vertices[1])
        elif edge.vertices[1] == vertid:
            verts.append(edge.vertices[0])

    return verts


def create_linked_verts_lookup(obj):
    verts_lookup = defaultdict(list)
    for ind, edge in enumerate(obj.data.edges):
        verts_lookup[edge.vertices[0]].append(edge.vertices[1])
        verts_lookup[edge.vertices[1]].append(edge.vertices[0])
    return verts_lookup


def get_vertices_colors(obj, fixed_vertices_set, fixed_vertices_data, default_color=(1, 1, 1)):
    fixed_vertices_colors = calc_colors(fixed_vertices_data)
    vert_colors = {}
    fixed_verts_ind = 0
    for vert in obj.data.vertices:
        vert_ind = vert.index
        if vert_ind in fixed_vertices_set:
            vert_colors[vert_ind] = fixed_vertices_colors[fixed_verts_ind]
            fixed_verts_ind += 1
        else:
            vert_colors[vert_ind] = default_color
    return vert_colors


def calc_colors(data):
    # todo: Calc the colors according to the data
    colors = None
    return colors

def get_fixed_vertices(fixed_vertices_data, threshold=0):
    fixed_vertices = np.load('vertices.npy')
    fixed_vertices_set = set()
    for ind, fix_vert in enumerate(fixed_vertices):
        if abs(fixed_vertices_data[ind]) > threshold:
            fixed_vertices_set.add(fix_vert)
    return fixed_vertices_set


threshold = 1
default_color = (1, 1, 1)
loops_num = 250
object_name = 'my_object'
obj = bpy.data.objects[object_name]
fixed_vertices_data = np.load('data.npy')
fixed_vertices_set = get_fixed_vertices(fixed_vertices_data, threshold)
linked_verts_lookup = create_linked_verts_lookup(obj)
verts_to_loop_verts = make_verts_to_loop_lookup(obj)
vertices_colors = get_vertices_colors(obj, fixed_vertices_set, fixed_vertices_data, default_color)
color_vertices_rgb(obj, vertices_colors, fixed_vertices_set, linked_verts_lookup, verts_to_loop_verts, loops_num)

The calc_colors function implementation is out of this scope, so I left it empty. Outside Blender I've created colormap numpy array (cm: 256x3) from matplotlib, and I'm using this function to calculate the colors:

def calc_colors(vert_values, data, cm):
    data_min, data_max = np.min(data), np.max(data)
    colors_ratio = 256 / (data_max - data_min)
    colors_indices = ((np.array(vert_values) - data_min) * colors_ratio).astype(int)
    verts_colors = cm[colors_indices]
    return verts_colors

Thanks for your help! I wish I could split the bounty...

$\endgroup$
7
  • $\begingroup$ What do you mean by "smoothing"? Is it that you have a harsh transition between colored and uncolored areas and you want a more gradual transition? $\endgroup$
    – TLousky
    Apr 21, 2017 at 16:48
  • $\begingroup$ I think the question is very unclear for the following reasons: 1. As said @TLousky what do you mean by smoothing? 2. calc_faces_verts does not refer to any bpy types: what are the parameters? 3. By which way is it possible to not "have a value per vertice"? $\endgroup$
    – lemon
    Apr 21, 2017 at 17:26
  • $\begingroup$ In smoothing, I mean smoothing the colors for vertices (or faces) without values, so all the faces in the mesh will be colored, something like 2D interpolation. calc_faces_verts is being called offline (outside Blender). I'm sending the verts and faces of a given mesh, which I, reading from a ply file. $\endgroup$
    – Noam Peled
    Apr 21, 2017 at 17:29
  • 1
    $\begingroup$ OK, it's a bit more clear, I think. But other questions: you are referring to an outside of Blender process. In this process (for an unknown reason?) some vertices have no colors (we don't know why and how, and also we don't know how these colors are chosen, but ok). From that, it seems you can identify colored vertices and non colored. The calculation could be a barycentric attribution considering the edge lengths connecting each non colored vertex to its (colored) neighbors. Is that the kind of thing you are looking for? $\endgroup$
    – lemon
    Apr 21, 2017 at 17:51
  • $\begingroup$ Yes, exactly, barycentric interpolation looks promising! If you are interested, the mesh is a brain's hemisphere, and I'm doing source reconstruction from EEG/MEG sensors to the cortex. To make this process faster, I'm calculating the values only for ~4k vertices, and now I want to interpolate the values for the rest of the vertices. The colors are defined using a colormap. $\endgroup$
    – Noam Peled
    Apr 21, 2017 at 20:03

3 Answers 3

5
$\begingroup$

I was developing a script for a similar purpose and it should be applicable to your situation.

animated

The script is as follows :

import bpy

def color_vertices_rgb(obj):
    """Merges the colors assigned to the FIXED vertices across the rest of the mesh"""

    obj.update_from_editmode()

    mesh = obj.data 
    scn = bpy.context.scene

    #check if our mesh already has Vertex Colors, and if not add some... (first we need to make sure it's the active object)
    scn.objects.active = obj
    obj.select = True
    mesh.vertex_colors.new()
    vert_cols = mesh.vertex_colors[0]

    fixed_vertices = []
    vert_colors = {}
    vert_links = {}

    print("Capturing data...")
    for vert in obj.data.vertices:
        print(str(vert.index))
        vertidx = vert.index
        if  vertex_in_group(obj,vertidx, "FIXED"):
            fixed_vertices.append(vertidx)
            vert_colors[str(vertidx)] = get_vertex_color(obj, vertidx)
        else:
            vert_colors[str(vertidx)] = (0.5,0.5,0.5)

        vert_links[str(vertidx)] = get_linked_verts(obj, vertidx)

    print("Captured.")
    for p in range(1,250):
        print("Pass "+str(p))
        new_colors = {}

        for vert in obj.data.vertices:
            vertidx = vert.index

            color = average_color(vert_colors, vert_links, vertidx)
            red = color[0]
            green = color[1]
            blue = color[2]

            if vertidx in fixed_vertices:
                red = vert_colors[str(vertidx)][0]
                green = vert_colors[str(vertidx)][1]
                blue = vert_colors[str(vertidx)][2]

            new_colors[str(vertidx)] = (red,green,blue)

        #Copy new colors back to vert_colors 
        for vert in obj.data.vertices:
            vertidx = vert.index
            vert_colors[str(vertidx)] = new_colors[str(vertidx)]

    print("Storing colors...")
    for vert in obj.data.vertices:
        vertidx = vert.index

        set_vertex_color(obj, vertidx, vert_colors[str(vertidx)])

def vertex_in_group(obj, vertidx, group):
    """Looks for the named vertex 'group' on the specified 'obj'
       and returns true or false depending on whether the specified
       'vert' is in that 'group'"""

    groupindex = obj.vertex_groups[group].index

    for v in obj.data.vertices:
        if v.index == vertidx:
            for g in v.groups:
                if g.group == groupindex:
                    return True
    return False

def get_vertex_color(obj, vertid):
    for loop in obj.data.loops:
        if loop.vertex_index == vertid:
            return obj.data.vertex_colors[0].data[loop.index].color
    return (0.0,0.0,0.0)

def set_vertex_color(obj, vertid, color):
    for loop in obj.data.loops:
        if loop.vertex_index == vertid:
            obj.data.vertex_colors[0].data[loop.index].color = color

def average_color(vert_colors, vert_links, vertid):

    r = 0.0
    g = 0.0
    b = 0.0
    num_verts = 0

    for linkedvertid in vert_links[str(vertid)]:
        col = vert_colors[str(linkedvertid)]
        r = r + col[0]
        g = g + col[1]
        b = b + col[2]
        num_verts += 1
    return (r/num_verts, g/num_verts, b/num_verts)

def get_linked_verts(obj, vertid):
    verts = []
    for edge in obj.data.edges:
        if edge.vertices[0] == vertid:
            verts.append(edge.vertices[1])
        elif edge.vertices[1] == vertid:
            verts.append(edge.vertices[0])

    return verts

#Invoke it
color_vertices_rgb(bpy.context.scene.objects["Icosphere"])

The script processes each vertex in turn and sets its color to the average of its neighbours. The result of this is to smear the colors over the surface. Putting a vertex in a vertex group named 'FIXED' causes that vertex to be excluded from the process so that its color contributes to its neighbours but it not itself updated. This means that those 'FIXED' vertices remain unchanged and their colors are spread over the rest of the mesh.

For example, starting with an Icosphere I placed 3 vertices in the FIXED vertex group as shown :

vertex group panel vertices

I manually painted those vertices with specific colors.

vertex colors

Running the script produced the following result :

final colors

Hopefully this will help with your situation - place your vertices with 'known' values in the FIXED vertex group and ensure they are set to their initial colors and run the script, generating a new set of vertex colors.

Note that the last line of the script needs to be amended to run the function on the relevant object (in my case this was named 'Icosphere'). Also, if you need more or less 'passes' to get a smooth result you could amend the 'Range' line ('for p in range(1,xxx)') to something other than '250'. More passes will take longer. Less passes will spread the colors over a shorter range.

NOTE : The average is currently calculated based purely on the connected vertices - rather than taking each edge length into account. This should be fine if your mesh is fairly evenly distributed. However, if you need shorter edge links to have more influence on its neighbours then it should be a simple matter of enhancing the 'average_color(...)' function to retrieve the edge length between two vertices to vary each neighbour's influence on the resultant color.

Blend file attached


EDIT : The above script does not work on newer versions of Blender (eg, 2.79.6) due to the Vertex Colors now apparently including the alpha channel. I've developed an add-on that provides this functionality available on a menu option in the Paint menu. See this answer and download the add-on from Vertex Color Blend Add-on

$\endgroup$
2
+50
$\begingroup$

Here is a solution considering the performance issue.

enter image description here

For test purpose I've used 3 meshes with 10 smoothing iterations using lengths barycentric smoothing (considering the lengths is an option and it is faster if you don't use them):

  • happy.ply, 540k vertices: 50s
  • bunny.ply, original, 36k vertices: 3s
  • bunny.ply, decimated, 8k vertices: 0.6s

(I'm using a gamer laptop PC)

enter image description here

enter image description here

The program is calculating things following these steps:

  • Precalculation of a lookup table from each vertex to its corresponding loop vertices
  • Precalculation of a lookup table from each vertex to its neighbors via its edges
  • Precalculation of the lengths weighting ratios
  • First calculation of the base colors: as we don't know how you're obtaining these colors, I've chosen to use the colors of the vertex colors. By default it is white (considered as not colored), so non white vertices are considered to be initially colored
  • Smoothing: the smoothing is done by edges direct proximity (so the more vertices, the less the propagation is done, in absolute distance point of view: this is visible in the above image)
  • Affectation of the calculated colors to the vertex colors

Here is the commented script, including the options description (it applies on the selected mesh considering the vertex colors exist as created before):

import bpy
import time
from collections import defaultdict
from mathutils import Color, Vector

#Create a look up table that creates the correspondance between each vertex and its corresponding faces vertices
def MakeVertsToLoopVerts( obj ):
    vertsToLoopVerts = defaultdict( list )
    for li, l in enumerate( obj.data.loops ):
        vertsToLoopVerts[l.vertex_index].append( li )
    return vertsToLoopVerts

#Create a lookup table which allows to follow edges connected vertices
def MakeVertPaths( verts, edges ):
    result = defaultdict( list )
    #Add the possible paths via edges
    for e in edges:
        result[e.vertices[0]].append(e.vertices[1])
        result[e.vertices[1]].append(e.vertices[0])
    return result

#Create a lookup table wich contains the lengths for each vertex to its neighbors
#If not, length may be calculated for each smoothing iteration
def MakeVertPathLength( obj, vertPaths, useLength ):
    if useLength:
        result = defaultdict( list )
        vertices = obj.data.vertices
        for v, path in vertPaths.items():
            vCo = vertices[v].co
            lengths = [(vCo - vertices[n].co).length for n in path]
            total = sum( lengths )
            amount = len(lengths)
            if total == 0:
                lengths = [(amount - 1) * (1 / amount) for l in lengths]
            else:
                lengths = [(1 - l / total) for l in lengths]
            result[v] = lengths
        return result
    return {}

#Determinate if a vertex loop set corresponds to a non neutral color and averages the color including not neutral
def AvgInitialColor( vColors, vColIndices, neutralColor, baseColor ):
    nonNeutral = any( vColors.data[li].color != neutralColor for li in vColIndices )
    if nonNeutral:
        return True, sum( [vColors.data[li].color for li in vColIndices], Color() ) / len(vColIndices)
    else:
        return False, baseColor

#Determinate if a vertex loop set corresponds to a non neutral color and averages the color which is not neutral
def AvgInitialColorNonNeutral( vColors, vColIndices, neutralColor, baseColor ):
    nonNeutral = [li for li in vColIndices if vColors.data[li].color != neutralColor]
    if nonNeutral:
        return True, sum( [vColors.data[li].color for li in nonNeutral], Color() ) / len(nonNeutral)
    else:
        return False, baseColor

#Calculate base colors for both initially colored vertices and not colored verticles
def AvgColorForNonNeutral( vColors, vertsToLoopVerts, neutralColor, baseColor, useNeutral ):
    isColored = {}
    colors = {}
    if useNeutral:
        for v, lvs in vertsToLoopVerts.items():
            isColored[v], colors[v] = AvgInitialColor( vColors, lvs, neutralColor, baseColor )
    else:
        for v, lvs in vertsToLoopVerts.items():
            isColored[v], colors[v] = AvgInitialColorNonNeutral( vColors, lvs, neutralColor, baseColor )

    return isColored, colors

#Average the vertex color with its neighbors colors
def AvgColor( v, colors, vertPaths ):
    linked = vertPaths[v]
    return sum( (colors[i] for i in linked), colors[v] ) / (1 + len(linked))

#Average the vertex color with its neighbors colors and using lengths ponderation
def AvgColorUsingLength( v, colors, vertPaths, vertPathLengths ):
    linked = vertPaths[v]
    lengths = vertPathLengths[v]
    return sum( (l*colors[i] for i, l in zip( linked, lengths )), colors[v] ) / len(linked)

#An iteration of smoothing
def AvgColorsOneIteration( isColored, colors, vertPaths, avgInitialColors, useLength, vertPathLengths ):
    newColors = {}

    for v, info in colors.items():
        #If initially colored and option is not to average it
        if isColored[v] and not avgInitialColors:
            newColors[v] = colors[v]
        elif useLength:
            newColors[v] = AvgColorUsingLength( v, colors, vertPaths, vertPathLengths )
        else:
            newColors[v] = AvgColor( v, colors, vertPaths )

    return newColors

#Make n iterations of smoothing
def AvgColorsIterations( iterations, isColored, colors, vertPaths, avgInitialColors, useLength, vertPathLengths ):
    for i in range( iterations ):
        colors = AvgColorsOneIteration( isColored, colors, vertPaths, avgInitialColors, useLength, vertPathLengths )
    return colors

#Set the calculated colors to the vertex colors
def SetToVertexColors( obj, vColors, colors ):
    for l in obj.data.loops:
        vColors.data[l.index].color = colors[l.vertex_index]

print( '--------------' )

startTime = time.time()

obj = bpy.context.object #The object we want to use (selected one)
vColors = obj.data.vertex_colors['Col'] #The vertex colors (create via the UI before)
neutralColor = Color( (1,1,1) ) #Neutral color indicates if a vertex is colored or not. A vertex is not colored if its initial color is this color (convention)
baseColor = Color( (0,0,0) ) #The base color to set for non colored vertices
useNeutral = False #Indicates if we keep neutral color for the eventual loop indices that are not colored for a colored vertex
useLength = True #Indicates if the lengths between vertices is to take into accound, if not simple average
avgInitialColors = True #Indicates if the initial colors are smoothed or not
iterations = 10 #Number of smoothing iterations

#Precalculation: get a lookup table that associates each vertex to its corresponding per face vertex index
vertsToLoopVerts = MakeVertsToLoopVerts( obj )

print( 'elapse0', time.time() - startTime )

#Precalculation: get a lookup table that contains for each vertex its neighbors considerings edges
vertPaths = MakeVertPaths( obj.data.vertices, obj.data.edges )

print( 'elapse1', time.time() - startTime )

#Precalculation: lengths ponderations
vertPathLengths = MakeVertPathLength( obj, vertPaths, useLength )

print( 'elapse2', time.time() - startTime )

#First color calculation: determinates if each vertex is initially colored and its starting color
isColored, colors = AvgColorForNonNeutral( vColors, vertsToLoopVerts, neutralColor, baseColor, useNeutral )

print( 'elapse3', time.time() - startTime )

#Smoothing iterations
colors = AvgColorsIterations( iterations, isColored, colors, vertPaths, avgInitialColors, useLength, vertPathLengths )

print( 'elapse4', time.time() - startTime )

#Writes the calculated colors to the vertex colors
SetToVertexColors( obj, vColors, colors )

print( 'elapse5', time.time() - startTime )

The file provided here does not include the big meshes because it was over 170MB and giantcowfilms allows at max 30MB:

edit: the updated question contains data file that I'm unable to use (load the npy files crashes Blender).

But from the above code, simply replace:

isColored, colors = AvgColorForNonNeutral( vColors, vertsToLoopVerts, neutralColor, baseColor, useNeutral )

By

isColored, colors = GetInitiallyColored( obj, baseColor )

where isColored is a dictionary vertIndex/Boolean indicating for each vertex if it is initially colored (from your data) and colors is a dictionary vertIndex/color for each vertex based on your data if the vertex belongs to it or set to 'baseColor' if not.

$\endgroup$
2
  • $\begingroup$ Thanks! You solution look very promising. My only problem with your solution is that I the vertices with the initial colors have many colors, and can't be defined as "neurtral" only by their colors. My initial data is a list of vertices, and a list of values which I transfer to colors according to the colormap, and the min and max of these vertices data. So I can only identify those vertices by their indices. $\endgroup$
    – Noam Peled
    Apr 26, 2017 at 21:21
  • $\begingroup$ @NoamPeled, yes I guessed that kind of thing. But could you give me a formalized input for that (so, I'll replace the needed parts of the code using it)? $\endgroup$
    – lemon
    Apr 27, 2017 at 5:32
1
$\begingroup$

'vertex_colors' is a per face data. It is related to what is called 'loop' in the internal data.

'Per face' means that a vertex is represented one time per face it contributes to in this kind of loop data.

The base loop is 'in obj.data.loops'.

For instance:

[(li, lv.vertex_index) for li, lv in enumerate(obj.data.loops)]

will give the following result which is a list of pairs '(vertex loop index, vertex index)'

[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 7), (6, 6), (7, 5), (8, 0), (9, 4), (10, 5), (11, 1), (12, 1), (13, 5), (14, 6), (15, 2), (16, 2), (17, 6), (18, 7), (19, 3), (20, 4), (21, 0), (22, 3), (23, 7)]

The previous list is obtained from a simple cube. So we have 8 vertex indices and 24 (6 faces x 4 vertices per face) loop vertex indices.

You can index:

obj.data.loops[vertex loop index]

and

obj.data.vertices[vertex index]

So for your program, you can:

mesh = cur_obj.data    

vcol_layer = mesh.vertex_colors.new('Col') 

for li, lv in enumerate(obj.data.loops):
    vcol_layer.data[li].color = colors[lv.vertex_index]

This main loops in obj.data.loops also contains information about per face vertex normals.

Vertex colors is a loop.

UV or texture layers are loops too.

$\endgroup$
1
  • 2
    $\begingroup$ Considering the update/edit of the question, this answer means 'nothing'... but it may contain information that interest others... so I don't delete it. $\endgroup$
    – lemon
    Apr 21, 2017 at 16:36

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .