13

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.

GiantCowFilms
  • 18,710
  • 10
  • 76
  • 138
  • Are the max and min values known? Or does the setup need to figure those out dynamically? – ajwood Jul 26 '15 at 21:42
  • @ajwood dynamically – GiantCowFilms Jul 26 '15 at 21:46
  • 4
    Somewhat related to finding min and max values automatically: http://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
  • @gandalf3 all of those solutions rely on the normalize node... not available for texture nodes. –  Jul 27 '15 at 16:31
  • I'm pretty sure that this is impossible in material nodes without some ridiculous hacks, unless maybe something can be done with OSL – gandalf3 Jul 27 '15 at 18:47
  • 4
    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. – Mike Pan Jul 27 '15 at 19:59
  • 1
    Where does your value map come from? Might it be possible to wrap the source somehow, forcing it to advertise its min/max? – ajwood Jul 31 '15 at 12:46
  • @GiantCowFilms Is the map generated or does it come from a texture or from a gif? – WorldSEnder Jul 31 '15 at 21:39
  • @WorldSEnder It comes form texture nodes. – GiantCowFilms Aug 01 '15 at 00:16
  • @GiantCowFilms When you say 'texture nodes' are you referring to the generated textures like Voronoi, Musgrave etc...? In my experience, these textures output full value range gradients by default, How are you limiting the value range? More specifically, is this associated with the input or output of your texture node (or none of the above)? Maybe there is some useful data to employ from your means of generating this limited value range, which could be used for iteration to identify the min/max values. Might be nothing, but do you have any images you could post? Thanks. Good question! – YoeyYutch Aug 03 '15 at 22:28
  • @YoeyYutch In this case I am using the Normal out put of the geometry node. I would like to map the values from 0 to 1 without editing my geometry. – GiantCowFilms Aug 03 '15 at 23:32
  • i think this means a complete image/texture with separated RGB values check to get the min/max values, i don't think it's possible (unless you use an incredibly complex node setup) without scripting – Bithur Aug 04 '15 at 02:09
  • @GiantCowFilms Normal output from geometry will always be normalized. Do you refer to only single channel/axis? Then that could need normalizing indeed. And that could be done with processing the geometry, so no need to analyze some image. – Jaroslav Jerryno Novotny Aug 05 '15 at 16:33

4 Answers4

7

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.

Jaroslav Jerryno Novotny
  • 51,077
  • 7
  • 129
  • 218
  • 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. – GiantCowFilms 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? – Jaroslav Jerryno Novotny Aug 04 '15 at 08: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. – Jaroslav Jerryno Novotny Aug 05 '15 at 16:20
5

(EDITED ANSWER: ADDING DRIVER)

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:

enter image description here

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.

enter image description here

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:

enter image description here

Map Range used:

enter image description here

  • 4
    And exactly how do you set those FromMax and FromMin values dynamically as OP asked? – Jaroslav Jerryno Novotny Jul 27 '15 at 10:30
  • 1
    Interesting, but figuring out those min and max values is half the problem ;D. – GiantCowFilms 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. – Elbrujodelatribu Jul 28 '15 at 13:15
  • 1
    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. – Elbrujodelatribu Jul 28 '15 at 13:22
  • Will this work with any input, not just image textures, because it looks like the python script is only ready to handle images from disk, when there is also procedural data to account for? – GiantCowFilms Aug 05 '15 at 15:59
  • The script will work with the name of a datablock image. In my example the name of the datablock is the same that the name of the file. That name is given by blender when I opened the image in the Image Texture Node. Besides the script is optimized to work with images. Other simple scripts will take a long time to get the maximum and minimum values. This one is fast enough. You need to provide the name when you set up the driver. You can create another driver with another image datablock name and the same python function, but not with other kind of datablock. – Elbrujodelatribu Aug 05 '15 at 16:05
2

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:
enter image description here
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.

enter image description here

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.

PGmath
  • 25,106
  • 17
  • 103
  • 199
0

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.enter image description here