I’ve made a small script to assist with visualizing some data. The script include()
s another script that has a module to do all my isosurface and 3D plotting using Makie, as well as all the reading in with CSV. I save all the images to file, and then use FFMPEG to compile them together into a movie. I’ll make slight changes to my visualization script inputs that get passed to the module functions, such as different isolevels and plotting parameters and re-include()
the visualization script to run it and produce my movies. Each time I do this, the amount of RAM Julia uses increases. Every single one of my processes is containerized into a function, including my main script which is inside a main()
, so there can’t even be global variable usage. I am confused why there is this “memory leak”, per-se, as currently I have to just restart Julia once I’ve run the script a certain number of times when tweaking visualizations otherwise my computer will crash. Why is this necessary?
This will be very difficult to help you debug without code (preferably a mwe).
I was hoping to avoid this as I’m worried this will be confusing, but I’ll see if I can distill it down to a minimal nonworking example as an overview:
vis.jl
using CSV, Makie, FFMPEG
module wave
export read3D, isosurface3D
function read3D(filepath,timeend)
for i=1:timeend
data[:,:,:,i] = Array(CSV.read(filepath*string(i)*".txt")
end # end for loop
return data
end # end read()
function isosurface3D(data,timeend)
for i=1:timeend
scene = volume(data[:,:,:,i],algorithm=:iso)
end # end for loop
# Save 3D image
Makie.save("/somepath/",scene)
# Make movie
FFMPEG.exe("options for movie")
end # end isosurface3D()
end # end module
analysis.jl
include("vis.jl")
using .wave
function main()
filepath = "/somepath/"
timeend = 10
data = read3D(filepath,timeend)
isosurface3D(data,timeend)
end
main()
I then run analysis.jl using:
julia> include("analysis.jl")
I think that’s the gist of it. I’m a bit puzzled as to why this would eat more memory each time.
Check the Memory Allocation section.
The reason your script is taking so much memory is because you’re continually creating Scenes, which are quite expensive (and, since you’re using GLMakie, have to be stored on the GPU as well). Instead of creating and then saving individual scenes, you can use Makie’s inbuilt animation capabilities to achieve the same thing.
First, here’s an example of your code changed to use Observables (such that you only mutate a single Scene, and don’t create multiple Scenes):
function isosurface3D(data,timeend)
scene = volume(data[:,:,:,1],algorithm=:iso)
volumeplot = scene.plots[end] # the last plot plotted to the Scene.
for i=1:timeend
volumeplot[4][] = data[:, :, :, i] # update the data of the plot
Makie.save("scene_$i.png", scene) # save each frame separately
end # end for loop
# Save 3D image
Makie.save("final.png",scene)
# Make movie
FFMPEG.exe("options for movie")
end # end isosurface3D()
Makie actually uses FFMPEG internally for animations, and we’ve built in a bunch of optimizations throughout the pipeline to make recording fast. On the latest version of AbstractPlotting.jl (Makie is the metapackage, AbstractPlotting is the bulk of the plotting code):
function isosurface3D(data,timeend)
scene = volume(data[:,:,:,1],algorithm=:iso)
volumeplot = scene.plots[end] # the last plot plotted to the Scene.
record(scene, "movie.mp4", 1:timeend; framerate = 30) do i
volumeplot[4][] = data[:, :, :, i] # update the data of the plot
end # end recording loop
# Save 3D image
Makie.save("final.png",scene)
end # end isosurface3D()
and you’re done! If you want, you can also pass sleep = false
as a keyword argument to record, to make it record as fast as possible. If you don’t pass it, the animation will be shown on the screen in real-time.
You should also take a look at http://makie.juliaplots.org/dev/animation.html and http://makie.juliaplots.org/dev/interaction.html, which have some more info about using Observables.
Let me know if you have any questions!
This is very helpful, thank you. I had looked at Makie’s animation section, but to be truthful, I was a little overwhelmed with the Node()
stuff hence me turning to stitching together frames manually. I looked through the second link you sent, but I’m afraid I’m still not fully understanding this scene.plots[end]
section. In general, I guess I am internally thinking that scene
is a variable holding the plot, so each time I call volume()
and write to scene
it is overwriting this, but in reality I suppose this is not the case. I’m assuming that the scene
object is thus then updated (the point of Observables, as far as I can tell) with plots
, but [end]
seems to indicate that there are multiple.
It might be useful to think of the Scene as a holder for plots, much like Matplotlib’s figure. scene.plots
is an array of the plots in the Scene, and scene.plots[end]
is simply the latest plot to have been plotted to the Scene (in this case, the volume plot).
You can access the arguments of a Plot using linear indexing (plot[i]
). Volumes actually have 4 arguments - an x range, y range, z range, and the 3D array of values. Since you only provided one in the call to volume
, it was expanded into 4 arguments internally, so to update the value you must update the 4th argument.
Hi @asinghvi17 ,
I’m trying to follow your advice for creating a scene and then just update it iteratively to avoid memory usage to increase until crash. I’m working with video, I would like to add a grid on each frames and extract them iteratively. Here you will find the leak I firstly followed to avoid memory leak Makie memory leak? but it seems that it’s not sufficient when using it on large video… With your advice my problem is to be able to extract from the scene the information corresponding to the image and replace it by another one… Thanks in advance for your help !
This piece of code is my MWE:
using VideoIO, FileIO
using GLMakie,AbstractPlotting, Makie
function save_png(img, i, grid)
fn=string(i)*".png"
scene = AbstractPlotting.image(rotr90(img), show_axis = false);
AbstractPlotting.linesegments!(scene, grid, color = "white", linewidth = 2, show_axis = false);
AbstractPlotting.save(fn, scene; resolution = (720,450))
close(GLMakie.global_gl_screen())
nothing
end
function ext_frames(vid_name::String, grid) ## to Add Grid and extract frames
io = VideoIO.testvideo(vid_name) ## for testing purposes
f = VideoIO.openvideo(io); ## Open the video
img = read(f); ## Take the first frame
n = 1
save_png(img,n, grid) ## Add grid and save the frame
while !eof(f) ## While the video is not finished
next_img = read!(f, img); ## Take the next frame
n = n + 1
save_png(next_img,n, grid)
end
close(f)
end
grid = Tuple{Float32,Float32}[(203.18451, 391.91782), (203.18451, 34.85404), (359.62997, 391.91782), (359.62997, 34.85404), (516.07544, 391.91782), (516.07544, 34.85404), (46.739048, 124.11998), (672.52094, 124.11998), (46.739048, 213.38593), (672.52094, 213.38593), (46.739048, 302.6519), (672.52094, 302.6519)]
ext_frames("annie_oakley",grid)
So the idea would be to keep the first part because in my real code I’m doing specific stuff there, but in the while loop I would like to change a bit the save_png function to create the scene with the image + the grid and then iteratively change the image under the grid and save it. Ideally I would like to couple this process with an encoder to recreate the video with the added grid.
Important note: I’m working with 30min videos at 30 fps so it’s a lot of frames to work with → all is about avoiding RAM usage and speed up the process of saving frames.