Cinema need: place images based on filename content?

Blender is fantastic dough for Cinema!
To study movie rhythm, tension and scene composition, I strip out existing video.
Now, I have a list of JPG like this:


In each filename you can see frame number.

I’d need something to place the images at the frame position in Video Sequence Editor, with the frame difference as the duration (ex: frame_171 starts at 171 and lasts 1232 frames because next file is called frame_1403, etc), NOT a unique image sequence strip.

I understand that some steps involved in Blender are:

bpy.ops.sequencer.image_strip_add(directory="E:\\test\\", files=[{"name":"frame_171.jpg", "name":"frame_171.jpg"}, {"name":"frame_1403.jpg", "name":"frame_1403.jpg"}], show_multiview=False, frame_start=171, frame_end=3000, channel=2, use_placeholders=False)
bpy.ops.sequencer.select(extend=False, deselect_all=True, linked_handle=False, left_right='NONE', linked_time=False)
bpy.ops.sequencer.images_separate()

I don’t know how to manage the list of files, extract frame number from 2 filenames, do math operation based on 2 frame numbers, increment by one file, build the list of durations, and separate images with specific duration for each with a script.

Maybe there’s something already existing? I crawled github and this forum without luck so far. Maybe wrong keywords?

In advance, thanks Blender python guru for your help :smiley:

After you have a list of files, you can use python “re” such as,

>>> import re
>>> match = re.search("\d+", "frame_1024")
>>> if match:
...     print("frame number: %s"% match.group(0))
...     
frame number: 1024

>>>

You can do a math operation on two numbers similar to how a calculator is used:

>>> 1024 + 2048
3072

The conversion to integer from string is done like this:

>>> number = int(match.group(0))
>>> number
1024

You can get the length of a list by using the built-in “len” function

>>> arr = [1,2,3]
>>> len(arr)
3

and finally you can go through a range by index like this:

>>> for i in range(0,3):
...     print(arr[i])
...     
1
2
3

You wouldn’t use a comprehension such as “map” or “for i in frame” because you are comparing two objects by index, and that would just be extra difficulty for no gain.

Give it another try and let me know if you get stuck.

Thank you @horusscope!

  • I understand re is for regex, but how to open the list of file interactively in the first place? I won’t enter the file path in the script right?
  • Thanks for the integer, I forgot this part.
  • Then, I compute duration on the array a[i], a[i+1] for len(a), OK
  • How to place each file at frame X for duration Y? Is frame_start=171, frame_end=3000, channel=2 available out of image_strip_add?

This will be my first script in Blender so excuse my noobiness.

Here is how you can list the files in a directory with python

>>> import os
>>> rootDir = '/Users/YourUsernameHere/Blender/games/chip-1usd/'
>>> for dirName, subdirList, fileList in os.walk(rootDir):
...     print('Found directory: %s' % dirName)
...     for fname in fileList:
...         print('\t%s' % fname)
...         
Found directory: /Users/.../Blender/games/chip-1usd/
    .DS_Store
    chip-1usd-baked.png
    chip-1usd.blend
    chip-1usd.blend1
    chip-1usd.dae

>>>

or more concisely:

>>> (_, _, files) = next(os.walk(rootDir))
>>> files
['.DS_Store', 'chip-1usd-baked.png', 'chip-1usd.blend', 'chip-1usd.blend1', 'chip-1usd.dae']

>>>

I don’t know the blender VSE API, but if you get stuck enough I might delve into it just to help you out :slight_smile:

Thank you very much again!
I can’t do it right now but I’ll check and search ASAP and let you know.

Alright, one thing you can do to make this easier for us both, is do some of the steps by hand. Then, from the “info” panel, copy paste the actual function calls from blender into here.

It will tell you the lines of python to execute to get that 1 exact result.
From there, we can easily work out how to make a script produce that result.

In this case, you would be putting let’s say, 10 image frames into the VSE. Then copy/paste the info output here.

Thank you again,
These steps are to change 2 images:

bpy.context.space_data.params.sort_method = 'FILE_SORT_TIME'
bpy.context.space_data.params.sort_method = 'FILE_SORT_ALPHA'
bpy.ops.sequencer.image_strip_add(directory="E:\\test\\", files=[{"name":"frame_171.jpg", "name":"frame_171.jpg"}, {"name":"frame_1403.jpg", "name":"frame_1403.jpg"}, {"name":"frame_2058.jpg", "name":"frame_2058.jpg"}], show_multiview=False, frame_start=1, frame_end=26, channel=2)
bpy.ops.power_sequencer.mouse_trim(select_mode='CONTEXT', gap_remove=False)
bpy.ops.sequencer.images_separate()
bpy.ops.sequencer.select(extend=False, deselect_all=True, linked_handle=False, left_right='NONE', linked_time=False)
bpy.context.scene.sequence_editor.sequences_all["frame_171.002"].frame_start = 2058
bpy.ops.sequencer.select(extend=False, deselect_all=True, linked_handle=False, left_right='NONE', linked_time=False)
bpy.context.scene.sequence_editor.sequences_all["frame_171.001"].frame_start = 1403
bpy.ops.sequencer.select(extend=False, deselect_all=True, linked_handle=False, left_right='NONE', linked_time=False)
bpy.context.scene.sequence_editor.sequences_all["frame_171.jpg"].frame_start = 171
bpy.context.scene.sequence_editor.sequences_all["frame_171.jpg"].frame_final_duration = 1232
bpy.ops.sequencer.select(extend=False, deselect_all=True, linked_handle=False, left_right='NONE', linked_time=False)
bpy.context.scene.sequence_editor.sequences_all["frame_171.001"].frame_final_duration = 655

Unfortunately the names are changed to the first filename+.%d (frame171.001, etc) when done by hand.
The file sort was just for easier manual selection.

BTW (_, _, files) = next(os.walk(rootDir)) returns

Traceback (most recent call last):
  File "<blender_console>", line 1, in <module>
StopIteration

I don’t understand this error. I’m on Window$ for this.

So if I’m understanding you, then you simply need to call these two functions for each frame. Is that right? Plus lets say 1000 for the final frame or what have you…

Make sure you:

  1. import os
  2. rootDir = “/path” even if it’s “C:/Path”, the exact name isn’t the central concept.
    os.walk is doing the heavy lifting, it returns a generator. the function ‘next’ actuates it, so the restructuring assignment (path, sub-dirs, files) = os.walk(…) actually works, it’s just a shorthand.

there are about 1000 ways to solve this problem, someone like me would normally force you down the perfect path of solving it in 1 iteration, but not this time.

start by making a dictionary of the values you need to solve your problem:

import os
import re
rootDir = "your full directory path here"
(_, _, files) = next(os.walk(rootDir))
dict = { }
num = len(files)

def frame(name):
    return re.search("\d+", name).group(0)

for i in range(0,num):
    a = frame(file[i])
    b = i < num-1 ? frame(file[i+1]) : a+1000
    dict[file[i]] = b-a

import pprint
pprint.pprint(dict) # see what you get

I strongly believe that beyond that point, you will succeed without any more help.
If you do get stuck, that’s O.K., I’ll still help you :slight_smile:

Wow, fantastic, thank you @horusscope!
It works fine after renumbering filenames.

Now, I wanted to go on and tested to insert an image by console, but it failed because I found that we can’t simply call image_strip_add from the scripting context (it makes sense once you get the idea of context).
BTW:

>>> area = bpy.context.area
>>> area.type = 'SEQUENCE_EDITOR'

is fun to do in the console :smiley:

From there, it’s piece of cake to change the function call with variables and end up with:


Note that frame_end = frame_start of the next image so Blender spread images on 2 channels to prevent overlap, neat!
Simply a matter of b-1 to have everything on the same channel if needed.

final script:

import os
import re
import bpy

rootDir = 'E:/test/'
(_, _, files) = next(os.walk(rootDir))

dict = { }
num = len(files)

def frame(name):
    return re.search("\d+", name).group(0)

area = bpy.context.area
old_type = area.type
area.type = 'SEQUENCE_EDITOR'
for i in range(0,num):
    a = frame(files[i])
    a = int(a)
    if i < num-1:
        b = int(frame(files[i+1]))
    else:
        b = a+1000
    dict[files[i]] = b-a
    bpy.ops.sequencer.image_strip_add(directory=rootDir, files=[{"name":files[i]}], frame_start=a, frame_end=b, channel=2)

#import pprint
#pprint.pprint(dict) # see what you get
area.type = old_type

Maybe calling image_strip_add for all images, then resizing all images is more memory and CPU wise, but I wanted quick result. Feel free to improve and share.

I think garbage collector is not present because before script, memory is 34.7MB and after running the script with 274 files (just a tense 24min episode) it jumped to 1.7GB as soon as I start moving the playhead in the sequencer, it goes up to 5GB then lowers down to 4.33GB. JPG images are 9.87MB altogether, so there’s an issue somewhere for sure.

if i<num-1 has been an issue, I broke down the if statement to spot the error, and had to make sure it was an int in the end. Could be wrapped up again.

I learned quite a few things in this process, thank you @horusscope for this.
Could we go further to learn how to wrap it as an add-on?
And final bonus: Is there a way to export the sequencer timeline to something? Screenshot is not the best option.

I’m glad you had a good success :slight_smile:

you mean like a movie? or simply take a screencapture of what the strip looks like automatically?

Yes, making a script an add-on is a fairly simple task which mostly entails defining meta-data.
Take a look at the walkthrough, it probably answers all your questions already
https://docs.blender.org/manual/en/latest/advanced/scripting/addon_tutorial.html

If not, I have made a couple quick scripts so I can explain if your script gets broken.

1 Like

Great, I’ll have a look at the docs and keep you updated!

I need to add text annotations on each scene, image, group of shots so some export as top-down CSV would be better, taking in account the channels since images are manually spread out all over 10+ channels now.

Oh I see, you want a data output.
Writing a CSV file is a fairly straightforward process, it is just comma separated value lines.

You’ll need to first decide how you want the final data to be, then again working out how to generate it becomes a simple matter.

First, decide on the column names
Next, create a sample row
Finally, write a python snippet for each of your first sample’s values

Once you have a generator function for 1 row, you might just see the answer for yourself. You mentioned scene in addition to image, which I suppose could have been 3 or even a dozen of those frames. Therefor it is clear to me you would have some over-arching organization strategy for the information. The process of creating a linear sequence of tabular data can be more or less difficult depending how you classify the information in the creative stage.

I think of importing the CSV in OOcalc then manually merge some cells for the scenes.
So the CSV is straightforward:

  1. it must read which image: files[i]
  2. on which channel in the sequencer: ?
  3. when does it start: frame(files[i])
  4. for which duration: b-a

CSV format would be:
filename, channel, start, duration
frame_00171.jpg, 2, 171, 1232

How to get the data from the sequencer?
bpy.ops.sequencer.select_all(action='SELECT') effectively selects all strips but just throws {'FINISHED'}
Where is the data? Is it an array with all needed information?
I crawled https://docs.blender.org/api/current/bpy.ops.sequencer.html?highlight=sequencer#module-bpy.ops.sequencer without luck.

Or maybe, I could export to CSV first, tweak channel number in CSV for each image by hand, then import it into Sequencer? It’s far less visual and Blender is barely used as editor in this setting.

Both ways would be needed: CSV to and from the sequencer since you might tweak channels after import, I guess.

I should have thought about the annotations before.
You see there’re no specifications, I just follow what I feel could help visually understand cinematographic edition.
bpy is so versatile to manipulate data, but doc is most of the time hard for a beginner.

Edit: Found https://github.com/julienduroure/DynamicStamp but not realted to the sequencer

I think what you wanted was the sequencer context, not an operation
https://docs.blender.org/api/blender_python_api_current/bpy.types.Mask.html#bpy.types.Mask
active_layer_index and layers?

btw if you want to actively evaluate some context, you can also dump all the properties of it in the console

def dump(obj):
   for attr in dir(obj):
       if hasattr(obj, attr):
           print("obj.%s = %r" % (attr, getattr(obj, attr)))

>>> dump(bpy.context.sequences)

or something

I found the link for the “Mask” thing here:
https://docs.blender.org/api/blender_python_api_current/bpy.context.html#sequencer-context
under sequencer context

I don’t think I need mask layer, AFAIK.

I tried with:

bpy.ops.sequencer.select_all(action='SELECT')
dump(bbpy.context.selected_sequences)

but got Blender not responding :slightly_frowning_face:
Well I saved just before so it should be ok.
Oh, I see “bbpy” in my code. haha.

Before breaking it again, let’s start with just one selected strip.
Your script, throws multiple obj and attributes, the only mentioning sequence is:
obj.\_\_doc\_\_ = 'Built-in mutable sequence.\n\nIf no argument is given, the constructor creates a new empty list.\nThe argument must be an iterable if specified.'

How do I drill this one to see what’s inside?

https://docs.blender.org/api/blender_python_api_current/bpy.types.SequenceEditor.html#bpy.types.SequenceEditor.sequences
This seems correct

for strip in bpy.data.scenes["Scene"].sequence_editor.sequences_all:
    dump(strip)

Again, perfect!
Thank you so much @horusscope!

I get the data of each strip, it looks sorted by channel
FTR, here are useful data:

obj.name
obj.frame_start
obj.channel
obj.frame_final_duration

May I ask how do you get to bpy.data.scenes["Scene"].sequence_editor.sequences_all. Reading the doc I don’t understand how you crafted it. It’s magic to my eyes and I want to learn.
Can you break it down?

I found sequences_all from the docs, it has “References” at the bottom
in sequenceeditor references it says scene
I knew that scene is accessed from bpy.data, but if I didn’t, it also has references in its API docs page

In python, there is a useful function called dir() which will display the available method for any python object. once you have an object/varible, you can print to see what funtions/properties are available.
Untitled-1
I typically use this built-in feature of python to discover more about .bpy objects before I hit the docs, which can be sparse at times.

Thanks both, I understand I need to drill down and drill up sometimes.
To recap:

  • Drill down (what is inside/next)
    • print(dir(bpy.data.scenes[“Scene”].sequence_editor))
  • Drill up (keywords to crafted sentence)
    • search and pray for “All strips” to come out
    • you get sequences_all in types/struct (I don’t know the difference for now, both in titles, breadcrumb)
    • references to Scene.sequence_editor
    • So it must specify a scene

bpy.data.scenes["Scene"] is not directly visible in the docs from there but I understand this one is to be known by heart.

1 Like