Added post roll to timelapse

You can now define an amount of seconds that should be added to the rendered timelapse (so, since the current fps setting is 25 frames per seconds, 25 * the configured amount of post roll seconds images will need to be captured).

Timed timelapses add this to the actual run time of the timelapse capturing process (so if you configure 1s post roll, a timed timelapse will run 25s longer than the print)

Z-triggered timelapses just create one last capture from the webcam and use this image again and again (so 25 times for a post roll of 1s).

Implements #384
This commit is contained in:
Gina Häußge 2014-03-05 20:52:25 +01:00
parent 27322a971f
commit f580acaf70
7 changed files with 94 additions and 17 deletions

View file

@ -23,12 +23,13 @@ from octoprint.server.api import api
def getTimelapseData():
timelapse = octoprint.timelapse.current
type = "off"
config = {"type": "off"}
if timelapse is not None and isinstance(timelapse, octoprint.timelapse.ZTimelapse):
config["type"] = "zchange"
config["postRoll"] = timelapse.postRoll()
elif timelapse is not None and isinstance(timelapse, octoprint.timelapse.TimedTimelapse):
config["type"] = "timed"
config["postRoll"] = timelapse.postRoll()
config.update({
"interval": timelapse.interval()
})
@ -64,9 +65,17 @@ def setTimelapseConfig():
if "type" in request.values:
config = {
"type": request.values["type"],
"postRoll": 0,
"options": {}
}
if "postRoll" in request.values:
try:
config["postRoll"] = int(request.values["postRoll"])
except ValueError:
pass
if "interval" in request.values:
interval = 10
try:

View file

@ -52,7 +52,8 @@ default_settings = {
"flipV": False,
"timelapse": {
"type": "off",
"options": {}
"options": {},
"postRoll": 0
}
},
"gcodeViewer": {

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,7 @@ function TimelapseViewModel(loginStateViewModel) {
self.timelapseType = ko.observable(undefined);
self.timelapseTimedInterval = ko.observable(undefined);
self.timelapsePostRoll = ko.observable(undefined);
self.persist = ko.observable(false);
self.isDirty = ko.observable(false);
@ -17,6 +18,9 @@ function TimelapseViewModel(loginStateViewModel) {
self.isReady = ko.observable(undefined);
self.isLoading = ko.observable(undefined);
self.timelapseTypeSelected = ko.computed(function() {
return ("off" != self.timelapseType());
});
self.intervalInputEnabled = ko.computed(function() {
return ("timed" == self.timelapseType());
});
@ -122,6 +126,7 @@ function TimelapseViewModel(loginStateViewModel) {
self.save = function(data, event) {
var data = {
"type": self.timelapseType(),
"postRoll": self.timelapsePostRoll(),
"save": self.persist()
}

View file

@ -333,7 +333,8 @@ table {
width: 50px;
}
#temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support, #webcam_timelapse_interval {
#temp_newTemp, #temp_newBedTemp, #speed_innerWall, #speed_outerWall, #speed_fill, #speed_support,
#webcam_timelapse_interval, #webcam_timelapse_postRoll {
text-align: right;
}

View file

@ -533,7 +533,13 @@
<option value="timed">Timed</option>
</select>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled()">
<label for="webcam_timelapse_postRoll">Timelapse post roll (in rendered seconds)</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_postRoll" data-bind="value: timelapsePostRoll, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser() && timelapseTypeSelected()">
<span class="add-on">sec</span>
</div>
<div id="webcam_timelapse_timedsettings" data-bind="visible: intervalInputEnabled">
<label for="webcam_timelapse_interval">Interval</label>
<div class="input-append">
<input type="text" class="input-mini" id="webcam_timelapse_interval" data-bind="value: timelapseTimedInterval, valueUpdate: 'afterkeydown', enable: isOperational() && !isPrinting() && loginState.isUser()">

View file

@ -12,6 +12,7 @@ import subprocess
import fnmatch
import datetime
import sys
import shutil
import octoprint.util as util
@ -72,15 +73,16 @@ def configureTimelapse(config=None, persist=False):
current.unload()
type = config["type"]
postRoll = config["postRoll"]
if type is None or "off" == type:
current = None
elif "zchange" == type:
current = ZTimelapse()
current = ZTimelapse(postRoll=postRoll)
elif "timed" == type:
interval = 10
if "options" in config and "interval" in config["options"]:
interval = config["options"]["interval"]
current = TimedTimelapse(interval)
current = TimedTimelapse(postRoll=postRoll, interval=interval)
notifyCallbacks(current)
@ -90,16 +92,22 @@ def configureTimelapse(config=None, persist=False):
class Timelapse(object):
def __init__(self):
def __init__(self, postRoll=0):
self._logger = logging.getLogger(__name__)
self._imageNumber = None
self._inTimelapse = False
self._gcodeFile = None
self._postRoll = postRoll
self._postRollStart = None
self._onPostRollDone = None
self._captureDir = settings().getBaseFolder("timelapse_tmp")
self._movieDir = settings().getBaseFolder("timelapse")
self._snapshotUrl = settings().get(["webcam", "snapshot"])
self._fps = 25
self._renderThread = None
self._captureMutex = threading.Lock()
@ -111,6 +119,9 @@ class Timelapse(object):
for (event, callback) in self.eventSubscriptions():
eventManager().subscribe(event, callback)
def postRoll(self):
return self._postRoll
def unload(self):
if self._inTimelapse:
self.stopTimelapse(doCreateMovie=False)
@ -175,13 +186,36 @@ class Timelapse(object):
def stopTimelapse(self, doCreateMovie=True, success=True):
self._logger.debug("Stopping timelapse")
if doCreateMovie:
self._inTimelapse = False
def resetImageNumber():
self._imageNumber = None
def createMovie():
self._renderThread = threading.Thread(target=self._createMovie, kwargs={"success": success})
self._renderThread.daemon = True
self._renderThread.start()
self._imageNumber = None
self._inTimelapse = False
def resetAndCreate():
resetImageNumber()
createMovie()
if self._postRoll > 0:
self._postRollStart = time.time()
if doCreateMovie:
self._onPostRollDone = resetAndCreate
else:
self._onPostRollDone = resetImageNumber
self.processPostRoll()
else:
self._postRollStart = None
if doCreateMovie:
resetAndCreate()
else:
resetImageNumber()
def processPostRoll(self):
pass
def captureImage(self):
if self._captureDir is None:
@ -195,6 +229,7 @@ class Timelapse(object):
captureThread = threading.Thread(target=self._captureWorker, kwargs={"filename": filename})
captureThread.daemon = True
captureThread.start()
return filename
def _captureWorker(self, filename):
eventManager().fire(Events.CAPTURE_START, {"file": filename});
@ -217,7 +252,7 @@ class Timelapse(object):
# prepare ffmpeg command
command = [
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', '25', '-y', '-b:v', bitrate,
ffmpeg, '-i', input, '-vcodec', 'mpeg2video', '-pix_fmt', 'yuv420p', '-r', str(self._fps), '-y', '-b:v', bitrate,
'-f', 'vob']
filters = []
@ -275,8 +310,8 @@ class Timelapse(object):
class ZTimelapse(Timelapse):
def __init__(self):
Timelapse.__init__(self)
def __init__(self, postRoll=0):
Timelapse.__init__(self, postRoll=postRoll)
self._logger.debug("ZTimelapse initialized")
def eventSubscriptions(self):
@ -289,13 +324,29 @@ class ZTimelapse(Timelapse):
"type": "zchange"
}
def processPostRoll(self):
Timelapse.processPostRoll(self)
filename = os.path.join(self._captureDir, "tmp_%05d.jpg" % self._imageNumber)
self._imageNumber += 1
with self._captureMutex:
self._captureWorker(filename)
for i in range(self._postRoll * self._fps):
newFile = os.path.join(self._captureDir, "tmp_%05d.jpg" % (self._imageNumber))
self._imageNumber += 1
shutil.copyfile(filename, newFile)
if self._onPostRollDone is not None:
self._onPostRollDone()
def _onZChange(self, event, payload):
self.captureImage()
class TimedTimelapse(Timelapse):
def __init__(self, interval=1):
Timelapse.__init__(self)
def __init__(self, postRoll=0, interval=1):
Timelapse.__init__(self, postRoll=postRoll)
self._interval = interval
if self._interval < 1:
self._interval = 1 # force minimum interval of 1s
@ -328,6 +379,10 @@ class TimedTimelapse(Timelapse):
def _timerWorker(self):
self._logger.debug("Starting timer for interval based timelapse")
while self._inTimelapse:
while self._inTimelapse or (self._postRollStart and time.time() - self._postRollStart <= self._postRoll * self._fps):
self.captureImage()
time.sleep(self._interval)
if self._postRollStart is not None and self._onPostRollDone is not None:
self._onPostRollDone()
self._postRollStart = None