5
$\begingroup$

I'd like to set up a command line script that renders out each frame to the desired output image type (EXR, PNG, TGA, etc.) at full resolution, but then also generates and saves a small thumbnail JPG image at a specified width (say 400px for example).

I'd like to generate this thumbnail from the command line or with a script instead of using the file output node in the compositor. Makes it easier to replicate for multiple render jobs.

Note1: I do not want to render twice. I want to render once and then scale and save out a second smaller file. I don't want to do this from inside the blender file directly, but with an external script.

Note2: I would be open to a second blender instance that renders by only using the compositor to input the large image rendered in the first instance with the new output resolution. That way it's only processing flat images and not doing any scene rendering.

$\endgroup$

4 Answers 4

6
$\begingroup$

Here's a second option, which I now believe is the best solution, as it is the most elegant and versatile that I've found so far.

The solution is essentially an extension of the Compositing nodes Scale and FileOutput option. However the critical difference is the addition of an Image node being mixed with the scaled Render Output image before being piped into the FileOutput node. The image node uses a dummy UV image created at the required Thumbnail output dimensions. When mixed with this dummy image, the FileOutput node will output the new dimensions without having to change the Scene Render Dimensions.

This option is better than my original solution because it is simpler to implement, shorter code, as well as not needing to rely on external file paths.

enter image description here

Command line usage (last argument pipes in thumbnail width):

blender -b "file.blend" -o "//out/" -P thumbnail.py -- 500

and the script thumbnail.py:

import bpy
import sys

class Thumbnail:

    tWidthArg = int(sys.argv[9]) 

    scn = bpy.context.scene
    resX = scn.render.resolution_x
    resY = scn.render.resolution_y
    thumbWidth = 400

    if tWidthArg > 0:
        thumbWidth = tWidthArg

    thumbHeight = ((thumbWidth/resX)*resY)

    #create new internal dummy image the size we want for thumbnail
    bpy.ops.image.new(name="ThumbNailDummyImage", width=thumbWidth, height=thumbHeight, color=(0.0, 0.0, 0.0, 0.0), alpha=False, generated_type='BLANK', float=False)

    #set up compositing nodes
    nTree = scn.node_tree
    Src = nTree.nodes["Render Layers"]
    Dst = nTree.nodes["Composite"]

    #image input node - use dummy uv image
    imageNode = nTree.nodes.new('CompositorNodeImage')
    imageNode.name = "INPUTIMAGE"
    imageNode.image = bpy.data.images['ThumbNailDummyImage']
    imageNode.location = (Src.location.x+300, Src.location.y-600)

    #scale node to resize thumbnail to correct size
    scaleNode = nTree.nodes.new('CompositorNodeScale')
    scaleNode.space = "ABSOLUTE"
    scaleNode.location = (imageNode.location.x, imageNode.location.y+200)
    scaleNode.inputs['X'].default_value = thumbWidth
    scaleNode.inputs['Y'].default_value = thumbHeight

    #mix the scale render output with the dummy uv image to force new output size
    mixNode = nTree.nodes.new('CompositorNodeMixRGB')
    mixNode.location = (imageNode.location.x+300, imageNode.location.y)

    #setup output filenode
    fileOutNode  = nTree.nodes.new('CompositorNodeOutputFile')
    fileOutNode.location = (mixNode.location.x+300, mixNode.location.y)
    fileOutNode.layer_slots[0].name='Thumb'
    fileOutNode.file_slots[0].path='Thumb'
    fileOutNode.format.file_format= 'JPEG'
    fileOutNode.format.color_mode = 'RGB'
    fileOutNode.base_path = '//out/'

    #connect nodes
    nTree.links.new(scaleNode.inputs[0], Src.outputs[0])
    nTree.links.new(mixNode.inputs[2], scaleNode.outputs[0])
    nTree.links.new(mixNode.inputs[1], imageNode.outputs[0])
    nTree.links.new(fileOutNode.inputs[0], mixNode.outputs[0])
$\endgroup$
3
$\begingroup$

Here is a very basic script, that adds an app handler to receive callbacks whenever a frame has been rendered, and prints the frame number and path to rendered image file to system console:

import bpy

def render_post_callback(scene):
    frame = scene.frame_current
    filepath = scene.render.frame_path()
    print("Frame: %i, Filepath: %s" % (frame, filepath))

bpy.app.handlers.render_post.append(render_post_callback)

Note: It works for the render settings output options, but not for File Output nodes. As of now, there is no equivalent of frame_path() for File Output nodes.


It's easy to retrieve the base paths (output folders) from File Output nodes:

base_paths = [node.base_path for node in bpy.context.scene.node_tree.nodes if node.type == 'OUTPUT_FILE']

You could let an external script check these paths and generate thumbnails for all rendered images, unless there is already a thumbnail (should be as easy as to name the thumbnails like the original images in a different folder and do a name check). Should work as long as there aren't any unrelated image files in the rendered images folders.

$\endgroup$
2
$\begingroup$

You could write a simple python script, and then call it from the command line. See this related post:

Another way is to use blender's python API, and run a python script in blender from the command line (see this post), e.g.:

blender --background /path/to/file.blend --python /path/to/script.py --render-anim or  

blender -b /path/to/file.blend -P /path/to/script.py -a

The render resolution can be set with bpy.context.scene.render.resolution_x and bpy.context.scene.render.resolution_y.

There are several ways you could go about creating a second thumbnail image (related post):

  • Render twice using a python script, at different resolutions.

  • Create some compositing nodes from python to scale the image in the compositor.

    For example, this:

    import bpy
    
    #define thumbnail dimensions and name:
    thumb_x = 400
    thumb_y = 400
    thumb_name = "thumbnail"
    
    # function to find nodes connected to a node
    def followLinks(node_in):
        for n_inputs in node_in.inputs:
            for node_links in n_inputs.links:
                print("going to " + node_links.from_node.name)
                return node_links.from_node
    
    scene = bpy.context.scene
    scene.use_nodes = True # make sure composite nodes are enabled
    for node in scene.node_tree.nodes:
        if node.type == 'COMPOSITE':
            last_node = followLinks(node)
            tree = bpy.context.scene.node_tree
    
            scale_node = tree.nodes.new(type="CompositorNodeScale")
            scale_node.space = 'ABSOLUTE'
            scale_node.location.x = 200
            scale_node.inputs["X"].default_value = thumb_x
            scale_node.inputs["Y"].default_value = thumb_y
    
            tree.links.new(last_node.outputs[0], scale_node.inputs[0])
    
    
            node = tree.nodes.new(type="CompositorNodeOutputFile")
            node.format.file_format = 'JPEG'
            node.location.x = 400
            node.file_slots[0].path = thumb_name
    
            tree.links.new(scale_node.outputs[0], node.inputs[0])
    
    
    bpy.ops.render.render(animation=True) # render animation
    

    Will create an absolute scale node and connect whichever node is connected to the Composite node to it. It will also create a File Output node:

    enter image description here

  • Scale using an external command line image processing program such as ImageMagick.

  • Scale using a python image manipulation library (see this post).

$\endgroup$
7
  • $\begingroup$ I don't want to render twice, and I'd rather avoid an external image processing program/library if possible. I just want to replicate the process whereby Blender takes the temporary EXR file it renders and then converts it to the output file format and saves. I want to replicate that process with a smaller file and not get hit with the cost of a second render. Surely this is possible? $\endgroup$ Jan 31, 2014 at 19:00
  • $\begingroup$ @ToddMcIntosh The best option is probably creating some compositing nodes with python (it will handle animations), it's not actually that difficult. I'll try and write something out in a bit.. $\endgroup$
    – gandalf3
    Jan 31, 2014 at 21:07
  • $\begingroup$ Yeah, the only problem with the Scale/FileOutput solution, is that it has to be in a different blend file as the scale dimensions only scale the image then overlay it on a large black background the size of the Render resolution. I'm working on a solution using the bpy.app.handlers.render_complete event to add a callback that creates a second scene with separate Render resolution. I'll post it once I get it working. $\endgroup$ Feb 1, 2014 at 0:41
  • $\begingroup$ @ToddMcIntosh Your right.. That's annoying :( I thought I found a solution, but it just outputs transparent (black since jpg doesn't support transparency) images. Here it is in case you want to look at it. $\endgroup$
    – gandalf3
    Feb 1, 2014 at 2:59
  • $\begingroup$ I found a trick that makes the Scale/FileOutput option work. You have to mix the render image with another image that's at the output dimensions you want, and then it works. I've added a new solution that does this dynamically by creating a dummy UV image. $\endgroup$ Feb 2, 2014 at 6:32
2
$\begingroup$

Ok, I've written a script that solves the question in my own specific context. The following script works when the render job is rendering one frame in the sequence per Blender instance. That works fine for me since the context I'm using it in is a render farm (Brenda with AWS) which handles each frame in the animation queue as an individual render.

CAVEAT: This does not work when trying to render the whole frame range from one instance of Blender as the Event Handler only fires after the last frame is rendered.

thumbnail.py (to be called as part of the command line job launch)

import bpy

class Thumbnail:

doneThumbRender = bpy.props.BoolProperty()
doneThumbRender = False 
lastFullFramePath = bpy.props.StringProperty()
bpy.context.scene.frame_current = bpy.context.scene.frame_start
outThumbnailWidth = bpy.props.IntProperty()

def callbackFunc(context):

    lastFullFramePath = bpy.context.scene.render.frame_path(frame=bpy.context.scene.frame_current)

    if bpy.data.scenes.find("THUMB") < 0:

        #Set up Thumbnail Scene         
        bpy.ops.scene.new()
        thumbScn = bpy.data.scenes[1]
        thumbScn.name = "THUMB"
        bpy.data.screens['Default'].scene = bpy.data.scenes['THUMB']
        bpy.data.scenes['THUMB'].render.engine = "CYCLES"

        #Setup Composite nodes to resize source image
        bpy.context.scene.use_nodes = True
        inputImageLoc = lastFullFramePath 
        Scene = bpy.context.scene
        Tree = Scene.node_tree
        Src = Tree.nodes["Render Layers"]
        Dst = Tree.nodes["Composite"]

        #Create input image node for large source image
        imageNode = Tree.nodes.new('CompositorNodeImage')
        imageNode.name = "INPUTIMAGE"

        #Scale Node
        scaleNode = Tree.nodes.new('CompositorNodeScale')
        scaleNode.space = "RENDER_SIZE"
        imageNode.image = bpy.data.images.load( inputImageLoc )
        Tree.links.new(scaleNode.inputs[0], imageNode.outputs[0])
        Tree.links.new(Dst.inputs[0], scaleNode.outputs[0])

        inputImageXSize = Scene.node_tree.nodes['INPUTIMAGE'].image.size[0]
        inputImageYSize = Scene.node_tree.nodes['INPUTIMAGE'].image.size[1]

        #Calc new render height based on new thumbnail width as aspect ratio of source image
        newRenderHeight = ((outThumbnailWidth/inputImageXSize)*inputImageYSize)

        #Set Thumb Scene Render settings
        Scene.render.resolution_percentage = 100
        Scene.render.resolution_x = outThumbnailWidth
        Scene.render.resolution_y = newRenderHeight
        Scene.render.filepath = "//out/thumb"
        Scene.render.image_settings.file_format ="JPEG"
        Scene.render.image_settings.color_mode = "RGB"
        Scene.cycles.samples = 1
        Scene.frame_start = bpy.data.scenes['Scene'].frame_current
        Scene.frame_end = Scene.frame_start 
        bpy.data.scenes['THUMB'].frame_current = Scene.frame_start

        #Check for camera in Thumb Scene and add if missing
        numCams = 0

        for ob in Scene.objects:
            if ob.type == 'CAMERA':
                bpy.context.scene.camera = ob
                numCams + numCams + 1
                break 

        if numCams==0:
            bpy.ops.object.camera_add()
            for ob in Scene.objects:
                if ob.type == 'CAMERA':
                    bpy.context.scene.camera = ob
                    break 

        #Do Thumb Render
        if Thumbnail.doneThumbRender == False:

            Scene = bpy.context.scene
            Tree = Scene.node_tree
            getImageNode = Tree.nodes["INPUTIMAGE"] 
            getImageNode.image = bpy.data.images.load(lastFullFramePath)
            bpy.ops.render.render(animation=True,scene="THUMB")
            doneThumbRender = True


if __name__ == "__main__":
    if Thumbnail.callbackFunc not in bpy.app.handlers.render_complete:
        bpy.app.handlers.render_complete.append(Thumbnail.callbackFunc) 
$\endgroup$

You must log in to answer this question.

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