3

I am a VFX Teacher and currently when I want to grab images of Node Graphs to use in Lecture slides I have to make the Node Graph full screen and do a screen capture, but as you can imagine with larger scripts I have to zoom out so far that sometimes its not recognisable.

It looks to me the way Nuke's Node Graph resizes when you zoom in and out, that's its probably a Vector image of some kind under the hood. I am looking for a way either export this image so I can get Higher Res version of the whole node graph. Either as Vector, or just a higher res rasterized image.

Does anyone know if there might be a way to do this with Python? Or is there some external script that can do this?

1 Answers1

2

I have wanted this for a long time, but it took your question to get me looking into it a bit deeper.

I don't believe there is an actual vector image available under the hood (it's openGL under the hood), but I also don't see why there wouldn't be a way to automate a full-res screenshot.

I have written a script that will check the size of your node graph, set the zoom to 100% and screenshot every piece of the DAG, then stitch it automatically. It takes a few seconds because if I try to do it too fast things get mixed up due to the threading, so I had to introduce artificial pauses in between each screenshot.

You should be able to run the code below. Don't forget to set your path on the second to last line!

from PySide2 import QtWidgets, QtOpenGL, QtGui
from math import ceil
import time

 
def get_dag():
    stack = QtWidgets.QApplication.topLevelWidgets()
    while stack:
        widget = stack.pop()
        if widget.objectName() == 'DAG.1':
            for c in widget.children():
                if isinstance(c, QtOpenGL.QGLWidget):
                    return c
        stack.extend(c for c in widget.children() if c.isWidgetType())

def grab_dag(dag, path):
    dag.updateGL()  # This does some funky back and forth but function grabs the wrong thing without it
    pix = dag.grabFrameBuffer()
    pix.save(path)
    
class DagCapture(threading.Thread):
    def __init__(self, path, margins=20, ignore_right=200):
        self.path = path
        threading.Thread.__init__(self)
        self.margins = margins
        self.ignore_right = ignore_right

    def run(self):
        # Store the current dag size and zoom
        original_zoom = nuke.zoom()
        original_center = nuke.center()
        # Calculate the total size of the DAG
        min_x = min([node.xpos() for node in nuke.allNodes()]) - self.margins
        min_y = min([node.ypos() for node in nuke.allNodes()]) - self.margins
        max_x = max([node.xpos() + node.screenWidth() for node in nuke.allNodes()]) + self.margins
        max_y = max([node.ypos() + node.screenHeight() for node in nuke.allNodes()]) + self.margins

        # Get the Dag Widget
        dag = get_dag()
        if not dag:
            raise RuntimeError("Couldn't get DAG widget")

        # Check the size of the current widget, excluding the right side (because of minimap)
        capture_width = dag.width() - self.ignore_right
        capture_height = dag.height()

        # Calculate the number of tiles required to coveral all
        image_width = max_x - min_x
        image_height = max_y - min_y
        horizontal_tiles = int(ceil(image_width / float(capture_width)))
        vertical_tiles = int(ceil(image_height / float(capture_height)))
        # Create a pixmap to store the results
        pixmap = QtGui.QPixmap(image_width, image_height)
        painter = QtGui.QPainter(pixmap)
        painter.setCompositionMode(painter.CompositionMode_SourceOver)
        # Move the dag so that the top left corner is in the top left corner, screenshot, paste in the pixmap, repeat
        for xtile in range(horizontal_tiles):
            left = min_x + capture_width * xtile
            for ytile in range(vertical_tiles):
                top = min_y + capture_height * ytile
                nuke.executeInMainThread(nuke.zoom, (1, (left + (capture_width + self.ignore_right) / 2, top + capture_height / 2)))
                time.sleep(.5)
                nuke.executeInMainThread(grab_dag, (dag, self.path))
                time.sleep(.5)
                screengrab = QtGui.QImage(self.path)
                painter.drawImage(capture_width * xtile, capture_height * ytile, screengrab)
        painter.end()
        pixmap.save(self.path)
        nuke.executeInMainThread(nuke.zoom, (original_zoom, original_center))
        print "Capture Complete"

t = DagCapture("C:\\Users\\erwan\\Downloads\\test.png")
t.start()

I'm sure this could be improved upon but, hopefully, it will save you time already!

Edit: I have now improved this code a bit: https://github.com/herronelou/nuke_dag_capture

Erwan Leroy
  • 160
  • 10
  • Wow, this looks awesome. I'm just testing it now. I'm not getting any errors, it gets through to the Print "capture complete" but I'm also not getting a file output either. I changed the path where the DagCapture class gets called to my local download directory, but nothing is being generated. Will I need to install any other libraries or are the ones you are using standard with Nuke (12.1v2)? – Different Gravity Nov 18 '20 at 22:26
  • 1
    Hello. I have noticed that I do not have any sort of check to see if the save to disk is successful or not. I ran with a wrong path and it all behaved as if it was successful, but then nothing happened. I don't have time right now to change it but add a print in front of the pix.save() to see if it prints False or True – Erwan Leroy Nov 19 '20 at 22:11
  • Its returns False.... correction, it returns x4 Falses – Different Gravity Nov 20 '20 at 00:47
  • 1
    Right, so that would mean it fails to save. Double check the path, if on windows make sure you put double backslashes not single ones, make sure the directory exists (this won't create directories), and that you enter the filename with .png at the end. – Erwan Leroy Nov 20 '20 at 01:58
  • Yep, it was the double slashes that were getting me... awesome, thank you... so close! Just one more thing, the result seems to be an image of DAG that is fully zoomed out rather than zoomed in. Could this just be a number somewhere that can be inverted? – Different Gravity Nov 20 '20 at 09:03
  • It is neither zoomed in or out, it does the capture as scale 100%, which should be plenty (same as running nuke.zoom(1)). If you really need more, you could try modifying the code so that it uses a factor other than 1, in which case you'd need to adjust the tiling code a bit as well to capture the right tiles. – Erwan Leroy Nov 22 '20 at 02:43
  • 1
    I have edited the answer to include a link to GitHub where I have improved the code I originally wrote for you to add a UI and allow for custom zoom levels, margins, etc... – Erwan Leroy Dec 28 '20 at 02:55
  • Wow, this is looking great. Just downloaded from GitHub - Apologies for long delay in testing this. The UI is working and I am getting an output, but Im getting an odd result in the image. It is taking whatever my current view of the DAG is and then just repeating it in however many tiles the settings say... so I am just getting a repeating pattern of the DAG as opposed to 4 (or so) "zoomed in" quadrants that past together to make one larger image. I have tried a few different variations of settings but no quite the result Im after. Im on 12.1.v2 if that helps. – Different Gravity Feb 16 '21 at 11:31