# Generate thumbnail image in addition to main image? (command line render)

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.

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.

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


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.

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
for n_inputs in node_in.inputs:

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':
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

node = tree.nodes.new(type="CompositorNodeOutputFile")
node.format.file_format = 'JPEG'
node.location.x = 400
node.file_slots[0].path = thumb_name

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:

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

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

• 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? Jan 31, 2014 at 19:00
• @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..
– gandalf3
Jan 31, 2014 at 21:07
• 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. Feb 1, 2014 at 0:41
• @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.
– gandalf3
Feb 1, 2014 at 2:59
• 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. Feb 2, 2014 at 6:32

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"

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:
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"]