# Cycles node editor - Map gray scale value map to extremes

I have a value map that is black and white, but it covers a very small range of that spectrum, having the brightest value at 90% grey, and the darkest somewhere around 80% grey. What I need to do is stretch these values, using nodes, from fully black to fully white, well maintaining their relative brightness. For example, if it spans between 80-90% grey, something at 81% grey would become 10% grey and something 85% grey would be 50% grey, etc.

If possible I would like to do this entirely proceduraly, so that any entered map would work.

• Are the max and min values known? Or does the setup need to figure those out dynamically? Jul 26 '15 at 21:42
• @ajwood dynamically Jul 26 '15 at 21:46
• Somewhat related to finding min and max values automatically: blender.stackexchange.com/q/3775/599. If you already know the min and max values, a color ramp node will work, but you'll have to set the stop positions manually
– gandalf3
Jul 26 '15 at 23:20
• It might be very challenging to find the min/max automatically since each node/shader only has access to the pixel it's working on, not the entire texture. Jul 27 '15 at 19:59
• Where does your value map come from? Might it be possible to wrap the source somehow, forcing it to advertise its min/max? Jul 31 '15 at 12:46

The only solution and a major workaround that would work is:

• Create a frame change handler that grabs bpy.data.textures and for each texture determines the min and max value in it (by iterating over all pixels). This would be slow. The data should be outputted to some scene custom prop array for example.

• Drive a math cycles nodes with those custom prop array items. Everytime you switch texture you would need to re-target to appropriate array item (index). With this the rest of shader is simple.

With only nodes it's impossible because they operate per-sample. You would have to use as many nodes as are pixels in the texture and compare them (you can sample any pixel from texture with mapping vectors).

It would be possible if the min/max values could be encoded into the image (for example as grey values in 2 exact corners). The shader then would crop the texture by some extend and wouldn't use the border of the image with encoded data.

With OSL it's definitely doable but the texture would have to be part of that shader, it wouldn't work with RGB input socket (reason as above). Instead a path to the texture could be an input for such shader.

• Sadly, my texture is based of a cords input. If I were to put it on disk I may as well do it in the compositor. Jul 31 '15 at 18:29
• @GiantCowFilms I see - so you have a large texture, and with coords input you are only using a little part of it and you need this part to be always normalized? I first thought there are multiple textures and you use the whole surface of each. Am I assuming this right? Aug 4 '15 at 8:27
• @GiantCowFilms if so this changes things for the script or OSL shader. Also if the image is not on disk the data could be grabbed from bpy.data.textures an the input would be the name. Aug 5 '15 at 16:20

You need to make these steps:

1. Create a Map Range node
2. Create python scripts to get the maximum and minimum values
3. Add drivers to execute the scripts dinamically

Create a Map Range node

You can use a node group like this:

This node group is similar to another node available for compositing.

It works plugging your input value into the Value socket (the value you describe in your problem) and setting up the four sockets below that. In your problem the ToMin value (To minimum) should be 0.0 and the ToMax value (To Maximum) should be 1.0. The FromMin (From Minimum) and FromMax (From Maximum) values depends on you input.

Create python scripts

You have to create a script with this code:

import bpy

# create an index to be used in a dictionary
def idx_to_co(idx, width):
r = int(idx / width)
c = idx % width
return r, c

# create a dict from a list
def px_list_to_dict(px_list, width):
px_dict = {}
for idx, px in enumerate(px_list):
px_dict[idx_to_co(idx, width)] = px

return px_dict

# return the maximum color value in a Black_and_White image
def max_color_bw_image(image_name):
# access image from blend file
img = bpy.data.images[image_name]

# initial max_value to compare with the image
max_value  = 0.0

# tuples!
img_width = img.size[0]
#img_height = img.size[1]

# slowest part is the transfer from pixel array to list
pxs = tuple(img.pixels)
#r,g,b,a = pxs[::4], pxs[1::4], pxs[2::4], pxs[3::4]
r,g,b = pxs[::4], pxs[1::4], pxs[2::4]
#px_list = zip(r,g,b,a)
px_list = zip(r,g,b)

# create dictionary
px_dict = px_list_to_dict(px_list, img_width)

# iterate over the dictionary and get the maximum value
for key, value in px_dict.items():
# evaluate only r color coordinate since the image is BW
max_value = max(max_value,value[0])

return max_value

# return the minimum color value in a Black_and_White image
def min_color_bw_image(image_name):
# access image from blend file
img = bpy.data.images[image_name]

# initial min_value to compare with the image
min_value  = 1.0

# tuples!
img_width = img.size[0]
#img_height = img.size[1]

# slowest part is the transfer from pixel array to list
pxs = tuple(img.pixels)
r,g,b = pxs[::4], pxs[1::4], pxs[2::4]
px_list = zip(r,g,b)

# create dictionary
px_dict = px_list_to_dict(px_list, img_width)

# iterate over the dictionary and get the maximum value
for key, value in px_dict.items():
# evaluate only r color coordinate since the image is BW
min_value = min(min_value,value[0])

return min_value

bpy.app.driver_namespace['min_color_bw_image'] = min_color_bw_image
bpy.app.driver_namespace['max_color_bw_image'] = max_color_bw_image


Thanks to Blender scripting blog for explaining how to work with images files.

Add drivers to execute the scripts dinamically

Finally you have to add a driver to the From Min parameter (min_color_bw_image function) an another driver to the From Max parameter (max_color_bw_image function) of the Map Range node.

Note: You have to use drivers of Scripted Expression kind and type the name of the datablock image. You can not use driver variables for strings. For example, you can write "max_color_bw_image function('my_image_datablock_name')" in the Expression textbox of the driver.

That is not the topic of this question. I have created an article in my blog about how to create a python script and add it to a driver in a node.

These are the resutls:

No Map Range used:

Map Range used:

• And exactly how do you set those FromMax and FromMin values dynamically as OP asked? Jul 27 '15 at 10:30
• Interesting, but figuring out those min and max values is half the problem ;D. Jul 27 '15 at 14:23
• Note that this can also be done with a color ramp node, without the need for an extra node group
– gandalf3
Jul 27 '15 at 18:45
• I think that it is not possible to set the Max and Min values only through nodes. You can determine the max and min values if you evaluate you map, take the min and max values and then you use them in your node set up. There are not nodes to collect the values of your pixels before rendering. Besides you map is a texture in cycles and it is evalulating at render time, depending on how it is applied to you mesh. Jul 28 '15 at 13:15
• With OSL language you will have the same problems. The only way may be creating two drivers with python and connecting them to the FromMax and FromMin sockets in my node group. Jul 28 '15 at 13:22

Since in cycles nodes there isn't really a way to deal with an image as a whole, only on individual pixels this is technically impossible to do perfectly. But I have come up with a very ugly method of doing it imperfectly, i.e. not sampling all the pixels of the image.

First I made two group nodes, one which returns the maximum of two inputs, the other returns the minimum.

Here is the max node:

The min node is identical except that the math node is set to Less Than.

Now comes the ugly part. What I did was to duplicate the image texture node several times. Each node is using a different vector so the more nodes you use the more places you sample the image. The colors from each image texture node are sent through a progressive series of min and max nodes so the min and max nodes at the end will have the minimum and maximum of all the pixels sampled.

This example samples 11 pixels along a diagonal of the image and gives the minimum and maximum of each. The more duplicates you make the more accurate the result.

Here is the .blend file:

The image I am using is CC-0 from Pixabay.

Run the image, in this case a pointiness input for Suzanne through the following node setup. Keep in mind, the invert value was arbitrarily chosen but works quite well for almost any image. Out of the 'Normalizes' frame comes the image that is quite close to being valued from 0-1. If you further want a custom range placed on those values, run the output of that frame into the Custom Range frame. In the second frame, simply enter the minimum and maximum values you want as the range for your image. Run the output where ever you'd like.

• Would you mind explaining how those nodes work, as well as any short comings? What is your strategy? Oct 20 '16 at 15:19