예제 #1
0
 def getCameraAltitudeHeight(self, main_scene_xml_file_prifix, pos_x,
                             pos_z):
     currdir = os.path.split(os.path.realpath(__file__))[0]
     sys.path.append(currdir + '/bin/rt/' + current_rt_program +
                     '/python/2.7/')
     os.environ[
         'PATH'] = currdir + '/bin/rt/' + current_rt_program + os.pathsep + os.environ[
             'PATH']
     import mitsuba
     from mitsuba.core import Vector, Point, Ray, Thread
     from mitsuba.render import SceneHandler
     import platform
     scenepath = session.get_scenefile_path()
     fileResolver = Thread.getThread().getFileResolver()
     logger = Thread.getThread().getLogger()
     logger.clearAppenders()
     scenepath = scenepath.encode('utf-8')
     fileResolver.appendPath(scenepath)
     filepath = os.path.join(session.get_scenefile_path(),
                             main_scene_xml_file_prifix +
                             terr_scene_file).encode("utf-8")
     scene = SceneHandler.loadScene(fileResolver.resolve(filepath))
     scene.configure()
     scene.initialize()
     ray = Ray()
     ray.setOrigin(Point(pos_x, 999999999, pos_z))
     ray.setDirection(Vector(0, -1, 0))
     its = scene.rayIntersect(ray)
     if not its is None:
         return its.p[1]
     else:
         print("Camera is outside of the scene, do not use relative height")
         return 0
예제 #2
0
def redirect_logger(write_function=print, log_level=EInfo, tqdm_progressbar=None):
    """
    Redirect Mitsuba's Logger output to a custom function (so it can be used with e.g. tqdm).
    Additionally, can be used to control the log level
    :param write_function: A function like print() or tqdm.write() that is used to write log messages and progess bars
    :param log_level: The Mitsuba log level (mitsuba.EError, ...)
    :param tqdm_progressbar: Optionally, pass a tqdm progress bar. The Mitsuba rendering bar will be set as that bar's description
    :return:
    """
    class RedirectedAppender(Appender):
        def __init__(self, write_function, tqdm_progressbar):
            self.write_function = write_function
            self.tqdm_progressbar = tqdm_progressbar
            super().__init__()

        def append(self, log_level, message):
            self.write_function(message)

        def logProgress(self, progress, name, formatted, eta):
            if self.tqdm_progressbar is not None:
                self.tqdm_progressbar.set_description_str(formatted.replace('\r', ''), refresh=True)
            else:
                self.write_function(f"\r{formatted}", end='')

    logger = Thread.getThread().getLogger()
    logger.clearAppenders()
    logger.addAppender(RedirectedAppender(write_function, tqdm_progressbar))
    logger.setLogLevel(log_level)
    def initializeMitsuba(self):
        # Start up the scheduling system with one worker per local core
        self.scheduler = Scheduler.getInstance()
        for i in range(0, multiprocessing.cpu_count()):
            self.scheduler.registerWorker(LocalWorker(i, 'wrk%i' % i))

        self.scheduler.start()
        # Create a queue for tracking render jobs
        self.queue = RenderQueue()
        # Get a reference to the plugin manager
        self.pmgr = PluginManager.getInstance()
        # Process Mitsuba log and progress messages within Python

        class CustomAppender(Appender):
            def append(self2, logLevel, message):
                print(message)

            def logProgress(self2, progress, name, formatted, eta):
                # Asynchronously notify the main thread
                self.renderProgress.emit(progress)

        logger = Thread.getThread().getLogger()
        logger.setLogLevel(EWarn) # Display warning & error messages
        logger.clearAppenders()
        logger.addAppender(CustomAppender())

        def closeEvent(self, e):
            self.job.cancel()
            self.queue.join()
            self.scheduler.stop()
예제 #4
0
    def getScene(self):
        if (project_dir == "null"):
            print("No simulation.")
            return

        fileResolver = Thread.getThread().getFileResolver()
        logger = Thread.getThread().getLogger()
        logger.clearAppenders()
        scenepath = session.get_scenefile_path_according_to_basedir(
            project_dir)
        fileResolver.appendPath(str(scenepath))
        main_xml = str(os.path.join(scenepath, main_scene_xml_file))
        if not os.path.exists(main_xml):
            print("Simulation not generated.")
            return
        scene = SceneHandler.loadScene(main_xml)
        scene.configure()
        scene.initialize()
        return LessScene(scene, self.XSize, self.YSize)
예제 #5
0
    def __initialize_mitsuba_setting(self):
        self.plgr = PluginManager.getInstance()
        self.output_dir = self.scene.output_dir

        mitsuba_module_path = os.path.dirname(inspect.getfile(MitsubaRenderer))
        self.file_resolver = Thread.getThread().getFileResolver()
        self.file_resolver.appendPath(
            os.path.join(mitsuba_module_path, "xml_files/"))
        self.file_resolver.appendPath(
            os.path.join(mitsuba_module_path, "textures/"))
        self.file_resolver.appendPath(
            os.path.join(mitsuba_module_path, "shapes/"))

        self.mitsuba_scene = Scene()
예제 #6
0
    def getTerrainBoundingAABB(self, main_scene_xml_file_prifix):
        # get bounding sphere
        currdir = os.path.split(os.path.realpath(__file__))[0]
        import platform
        if platform.system() == "Windows":
            sys.path.append(
                os.path.join(currdir, "bin", "rt", current_rt_program,
                             "python", "3.6"))
        else:
            sys.path.append(
                os.path.join(currdir, "bin", "rt", current_rt_program,
                             "python", "3.5"))

        os.environ['PATH'] = os.path.join(
            currdir, "bin", "rt",
            current_rt_program) + os.pathsep + os.environ['PATH']
        import mitsuba
        from mitsuba.core import Vector, Point, Ray, Thread
        from mitsuba.render import SceneHandler
        import platform
        scenepath = session.get_scenefile_path()
        # if "Windows" in platform.system():
        #     scenepath = str(scenepath.replace('\\', '\\\\'))
        # 得到高程信息 通过光线跟踪的方法精确得到高程信息
        fileResolver = Thread.getThread().getFileResolver()
        logger = Thread.getThread().getLogger()
        logger.clearAppenders()
        scenepath = scenepath.encode('utf-8')
        fileResolver.appendPath(scenepath)
        filepath = os.path.join(session.get_scenefile_path(),
                                main_scene_xml_file_prifix +
                                terr_scene_file).encode("utf-8")
        scene = SceneHandler.loadScene(fileResolver.resolve(filepath))
        scene.configure()
        scene.initialize()
        return scene.getKDTree().getAABB()
예제 #7
0
def do_simulation_multi_spectral_py():
    currdir = os.path.split(os.path.realpath(__file__))[0]
    sys.path.append(currdir + '/bin/rt/' + current_rt_program + '/python/2.7/')
    os.environ['PATH'] = currdir + '/bin/rt/' + current_rt_program + os.pathsep + os.environ['PATH']
    import mitsuba
    from mitsuba.core import Vector, Point, Ray, Thread, Scheduler, LocalWorker, PluginManager, Transform
    from mitsuba.render import SceneHandler
    from mitsuba.render import RenderQueue, RenderJob
    from mitsuba.render import Scene
    import multiprocessing

    scheduler = Scheduler.getInstance()
    for i in range(0, multiprocessing.cpu_count()):
        scheduler.registerWorker(LocalWorker(i, 'wrk%i' % i))
    scheduler.start()

    cfgfile = session.get_config_file()
    f = open(cfgfile, 'r')
    cfg = json.load(f)
    distFileName = spectral_img_prefix + "_VZ=" + str(cfg["observation"]["obs_zenith"]) + \
                   "_VA=" + str(cfg["observation"]["obs_azimuth"])
    distFile = os.path.join(session.get_output_dir(), distFileName).encode("utf-8")
    scene_file_path = os.path.join(session.get_scenefile_path(), main_scene_xml_file).encode("utf-8")

    scene_path = session.get_scenefile_path().encode("utf-8")
    fileResolver = Thread.getThread().getFileResolver()
    fileResolver.appendPath(scene_path)
    scene = SceneHandler.loadScene(fileResolver.resolve(scene_file_path))
    scene.setDestinationFile(distFile)
    scene.configure()
    scene.initialize()
    queue = RenderQueue()
    sceneResID = scheduler.registerResource(scene)
    job = RenderJob(('Simulation Job '+distFileName).encode("utf-8"), scene, queue, sceneResID)
    job.start()
    queue.waitLeft(0)
    queue.join()

    if output_format not in ("npy", "NPY") and os.path.exists(distFile + ".npy"):
        data = np.load(distFile + ".npy")
        bandlist = cfg["sensor"]["bands"].split(",")
        RasterHelper.saveToHdr_no_transform(data, distFile, bandlist, output_format)
        os.remove(distFile + ".npy")
    log("INFO: Finished")
예제 #8
0
파일: Viewer3d.py 프로젝트: jianboqi/lessrt
    def forest_generate_according_tree_pos_file_for3d(config_file_path):
        import mitsuba
        from mitsuba.core import Vector, Point, Ray, Thread
        from mitsuba.render import SceneHandler
        from mitsuba.render import RenderQueue, RenderJob
        from mitsuba.render import Scene
        from mitsuba.render import Intersection

        f = open(config_file_path, 'r')
        cfg = json.load(f)
        tree_pos = combine_file_path(session.get_input_dir(),
                                     cfg["scene"]["forest"]["tree_pos_file"])
        if cfg["scene"]["forest"]["tree_pos_file"] == "" or (
                not os.path.exists(tree_pos)):
            return

        # 读取地形数据 计算每个树的高程
        if cfg["scene"]["terrain"]["terrain_type"] != "PLANE" and cfg["scene"][
                "terrain"]["terrain_type"] == "RASTER":
            demfile = combine_file_path(session.get_input_dir(),
                                        cfg["scene"]["terrain"]["terr_file"])
            img_w, img_h, dem_arr = RasterHelper.read_dem_as_array(demfile)
            dem_arr = dem_arr - dem_arr.min()

        scenepath = session.get_scenefile_path()
        if "Windows" in platform.system():
            scenepath = str(scenepath.replace('\\', '\\\\'))
        # 得到高程信息 通过光线跟踪的方法精确得到高程信息
        fileResolver = Thread.getThread().getFileResolver()
        logger = Thread.getThread().getLogger()
        logger.clearAppenders()
        fileResolver.appendPath(scenepath)
        # 由于batch模式不会改变地形几何结构,因此在用地形打点计算树木的高程时,用第一个terrain文件即可,所以加上了_0_

        scene = SceneHandler.loadScene(
            fileResolver.resolve(str(terr_scene_file)))
        scene.configure()
        scene.initialize()
        tf = open(tree_pos)

        objectPosFile = open(
            os.path.join(session.get_input_dir(), "object_pos_3dView.txt"),
            'w')

        # 创建一个虚拟根节点,最后再删除
        for line in tf:
            arr = line.replace("\n", "").split(" ")
            objectName = arr[0]
            x = float(arr[1])
            y = float(arr[2])
            xScale = cfg["scene"]["terrain"]["extent_width"]
            zScale = cfg["scene"]["terrain"]["extent_height"]
            treeX = 0.5 * xScale - x
            treeZ = 0.5 * zScale - y
            if cfg["scene"]["terrain"]["terrain_type"] != "PLANE":
                ray = Ray()
                ray.setOrigin(Point(treeX, 9999, treeZ))
                ray.setDirection(Vector(0, -1, 0))
                its = scene.rayIntersect(ray)
                if not its is None:
                    linestr = objectName + " " + str(treeX) + " " + str(
                        its.p[1]) + " " + str(treeZ) + "\n"
                else:
                    # log("warning: precise height not found.")
                    if cfg["scene"]["terrain"]["terrain_type"] == "RASTER":
                        im_r = int((y / float(zScale)) * img_h)
                        im_c = int((x / float(xScale)) * img_w)
                        if im_r >= img_h:
                            im_r = img_h - 1
                        if im_c >= img_w:
                            im_c = img_w - 1
                        linestr = objectName + " " + str(treeX) + " " + str(
                            dem_arr[im_r][im_c]) + " " + str(treeZ) + "\n"
                    else:
                        linestr = objectName + " " + str(treeX) + " " + str(
                            0) + " " + str(treeZ) + "\n"
            else:
                linestr = objectName + " " + str(treeX) + " " + str(
                    0) + " " + str(treeZ) + "\n"
            objectPosFile.write(linestr)
        objectPosFile.close()
예제 #9
0
    def forest_generate_according_tree_pos_file(config_file_path,
                                                forest_file_name,
                                                linestart,
                                                lineend,
                                                forest_prifix=""):
        import mitsuba
        from mitsuba.core import Vector, Point, Ray, Thread
        from mitsuba.render import SceneHandler
        from mitsuba.render import RenderQueue, RenderJob
        from mitsuba.render import Scene
        from mitsuba.render import Intersection

        f = open(config_file_path, 'r')
        cfg = json.load(f)
        tree_pos = combine_file_path(session.get_input_dir(),
                                     cfg["scene"]["forest"]["tree_pos_file"])
        if cfg["scene"]["forest"]["tree_pos_file"] == "" or (
                not os.path.exists(tree_pos)):
            return
        # 保存场景中树的位置 forest*.xml
        # f = open(os.path.join(session.get_scenefile_path(),forest_file_name),'w')
        f = codecs.open(
            os.path.join(session.get_scenefile_path(), forest_file_name), "w",
            "utf-8-sig")
        doc = minidom.Document()
        root = doc.createElement("scene")
        doc.appendChild(root)
        root.setAttribute("version", "0.5.0")

        #读取地形数据 计算每个树的高程
        if cfg["scene"]["terrain"]["terrain_type"] != "PLANE" and cfg["scene"][
                "terrain"]["terrain_type"] == "RASTER":
            demfile = combine_file_path(session.get_input_dir(),
                                        cfg["scene"]["terrain"]["terr_file"])
            img_w, img_h, dem_arr = RasterHelper.read_dem_as_array(demfile)
            dem_arr = dem_arr - dem_arr.min()

        #读取object boundingbox 数据
        bound_path = os.path.join(session.get_input_dir(),
                                  obj_bounding_box_file)
        if os.path.exists(bound_path):
            fobj = open(bound_path)
            bound_dict = dict()
            for line in fobj:
                arr = line.split(":")
                objName = arr[0]
                arr = list(map(lambda x: float(x), arr[1].split(" ")))
                bound_dict[objName] = [
                    arr[3] - arr[0], arr[4] - arr[1], arr[5] - arr[2]
                ]

        scenepath = session.get_scenefile_path()
        # if "Windows" in platform.system():
        #     scenepath = str(scenepath.replace('\\', '\\\\'))
        #得到高程信息 通过光线跟踪的方法精确得到高程信息
        fileResolver = Thread.getThread().getFileResolver()
        logger = Thread.getThread().getLogger()
        logger.clearAppenders()
        fileResolver.appendPath(str(scenepath))
        # 由于batch模式不会改变地形几何结构,因此在用地形打点计算树木的高程时,用第一个terrain文件即可,所以加上了_0_
        # if(forest_prifix != ""):
        #     forest_prifix = forest_prifix[0:len(forest_prifix)-1] +"_0_"
        scene = SceneHandler.loadScene(
            fileResolver.resolve(str(forest_prifix + terr_scene_file)))
        # scene = SceneHandler.loadScene(fileResolver.resolve(r"E:\Research\20-LESS\RealScene\SimProj\calLAI\Parameters\_scenefile\terrain1.xml"))
        scene.configure()
        scene.initialize()
        tf = open(tree_pos)

        hidden_objects = SceneGenerate.get_hidded_objects()

        #创建一个虚拟根节点,最后再删除
        treeIdx = 0
        for line in tf:
            if treeIdx >= linestart and treeIdx <= lineend:
                arr = line.replace("\n", "").strip().split(" ")
                objectName = arr[0]
                if objectName in hidden_objects:
                    continue

                shapenode = doc.createElement("shape")
                root.appendChild(shapenode)
                shapenode.setAttribute("type", "instance")
                refnode = doc.createElement("ref")
                shapenode.appendChild(refnode)
                refnode.setAttribute("id", objectName)
                trnode = doc.createElement("transform")
                shapenode.appendChild(trnode)
                trnode.setAttribute("name", "toWorld")
                if len(arr) == 6:  # fit with
                    scale_node = doc.createElement("scale")
                    trnode.appendChild(scale_node)
                    scale_node.setAttribute(
                        "x", str(float(arr[4]) / bound_dict[objectName][0]))
                    scale_node.setAttribute(
                        "z", str(float(arr[4]) / bound_dict[objectName][0]))
                    scale_node.setAttribute(
                        "y", str(float(arr[5]) / bound_dict[objectName][1]))

                if len(arr) == 5:  # for rotation of the tree
                    angle = arr[len(arr) - 1]
                    rotatenode = doc.createElement("rotate")
                    trnode.appendChild(rotatenode)
                    rotatenode.setAttribute("y", '1')
                    rotatenode.setAttribute("angle", angle)
                translatenode = doc.createElement("translate")
                trnode.appendChild(translatenode)
                x = float(arr[1])
                y = float(arr[2])
                z = float(arr[3])
                xScale = cfg["scene"]["terrain"]["extent_width"]
                zScale = cfg["scene"]["terrain"]["extent_height"]
                # treeX = xScale - x * (2 * xScale) / float(img_w)
                # treeZ = zScale - y * (2 * zScale) / float(img_h)
                treeX = 0.5 * xScale - x
                treeZ = 0.5 * zScale - y
                translatenode.setAttribute("x", str(treeX))
                translatenode.setAttribute("z", str(treeZ))
                if cfg["scene"]["terrain"]["terrain_type"] != "PLANE":
                    ray = Ray()
                    ray.setOrigin(Point(treeX, 9999, treeZ))
                    ray.setDirection(Vector(0, -1, 0))
                    its = scene.rayIntersect(ray)
                    if not its is None:
                        translatenode.setAttribute("y", str(its.p[1] + z))
                        # translatenode.setAttribute("y", str(z))
                    else:
                        # log("warning: precise height not found.")
                        if cfg["scene"]["terrain"]["terrain_type"] == "RASTER":
                            im_r = int((y / float(zScale)) * img_h)
                            im_c = int((x / float(xScale)) * img_w)
                            if im_r >= img_h:
                                im_r = img_h - 1
                            if im_c >= img_w:
                                im_c = img_w - 1

                            translatenode.setAttribute(
                                "y", str(dem_arr[im_r][im_c] + z))
                        else:
                            translatenode.setAttribute("y", str(z))
                else:
                    translatenode.setAttribute("y", str(z))

            treeIdx += 1

        xm = doc.toprettyxml()
        # xm = xm.replace('<?xml version="1.0" ?>', '')
        f.write(xm)
        f.close()

        log("INFO: Objects and positions generated.")
예제 #10
0
def do_simulation_multiangle_seq(seqname):
    currdir = os.path.split(os.path.realpath(__file__))[0]
    sys.path.append(currdir + '/bin/rt/' + current_rt_program + '/python/2.7/')
    os.environ['PATH'] = currdir + '/bin/rt/' + current_rt_program + os.pathsep + os.environ['PATH']
    import mitsuba
    from mitsuba.core import Vector, Point, Ray, Thread, Scheduler, LocalWorker, PluginManager, Transform
    from mitsuba.render import SceneHandler
    from mitsuba.render import RenderQueue, RenderJob
    from mitsuba.render import Scene
    import multiprocessing

    scheduler = Scheduler.getInstance()
    for i in range(0, multiprocessing.cpu_count()):
        scheduler.registerWorker(LocalWorker(i, 'wrk%i' % i))
    scheduler.start()


    scene_path = session.get_scenefile_path()
    fileResolver = Thread.getThread().getFileResolver()
    fileResolver.appendPath(str(scene_path))
    scene = SceneHandler.loadScene(fileResolver.resolve(
        str(os.path.join(session.get_scenefile_path(), main_scene_xml_file))))
    scene.configure()
    scene.initialize()
    queue = RenderQueue()
    sceneResID = scheduler.registerResource(scene)
    bsphere = scene.getKDTree().getAABB().getBSphere()
    radius = bsphere.radius
    targetx, targety, targetz = bsphere.center[0], bsphere.center[1], bsphere.center[2]
    f = open(seqname + ".conf", 'r')
    params = json.load(f)
    obs_azimuth = params['seq1']['obs_azimuth']
    obs_zenith = params['seq2']['obs_zenith']
    cfgfile = session.get_config_file()
    f = open(cfgfile, 'r')
    cfg = json.load(f)
    viewR = cfg["sensor"]["obs_R"]
    mode = cfg["sensor"]["film_type"]
    azi_arr = map(lambda x: float(x), obs_azimuth.strip().split(":")[1].split(","))
    zeni_arr = map(lambda x: float(x), obs_zenith.strip().split(":")[1].split(","))
    seq_header = multi_file_prefix + "_" + seqname
    index = 0
    for azi in azi_arr:
        for zeni in zeni_arr:
            distFile = os.path.join(session.get_output_dir(),
                                    seq_header + ("_VA_%.2f" % azi).replace(".", "_") + ("_VZ_%.2f" % zeni).replace(".", "_"))
            newScene = Scene(scene)
            pmgr = PluginManager.getInstance()
            newSensor = pmgr.createObject(scene.getSensor().getProperties())
            theta = zeni / 180.0 * math.pi
            phi = (azi - 90) / 180.0 * math.pi
            scale_x = radius
            scale_z = radius
            toWorld = Transform.lookAt(
                Point(targetx - viewR * math.sin(theta) * math.cos(phi), targety + viewR * math.cos(theta),
                      targetz - viewR * math.sin(theta) * math.sin(phi)),  # original
                Point(targetx, targety, targetz),  # target
                Vector(0, 0, 1)  # up
            ) * Transform.scale(
                Vector(scale_x, scale_z, 1)  # 视场大小
            )
            newSensor.setWorldTransform(toWorld)
            newFilm = pmgr.createObject(scene.getFilm().getProperties())
            newFilm.configure()
            newSensor.addChild(newFilm)
            newSensor.configure()
            newScene.addSensor(newSensor)
            newScene.setSensor(newSensor)
            newScene.setSampler(scene.getSampler())
            newScene.setDestinationFile(str(distFile))
            job = RenderJob('Simulation Job' + "VA_"+str(azi)+"_VZ_"+str(zeni), newScene, queue, sceneResID)
            job.start()
        queue.waitLeft(0)
        queue.join()
    # handle npy
    if mode == "spectrum" and (output_format not in ("npy", "NPY")):
        for azi in azi_arr:
            for zeni in zeni_arr:
                distFile = os.path.join(session.get_output_dir(),
                                        seq_header + ("_VA_%.2f" % azi).replace(".", "_") + ("_VZ_%.2f" % zeni).replace(
                                            ".", "_"))
                data = np.load(distFile + ".npy")
                bandlist = cfg["sensor"]["bands"].split(",")
                RasterHelper.saveToHdr_no_transform(data, distFile, bandlist, output_format)
                os.remove(distFile + ".npy")
예제 #11
0
        if sys.platform == 'linux':
            sys.setdlopenflags(oldflags)

        from mitsuba.core import (
            Scheduler, LocalWorker, Thread, Bitmap, Point2i, Vector2i, FileStream,
            PluginManager, Spectrum, InterpolatedSpectrum, BlackBodySpectrum, Vector, Point,
            Matrix4x4, Transform, AnimatedTransform,
            Appender, EInfo, EWarn, EError,
        )
        from mitsuba.render import (
            RenderQueue, RenderJob, RenderListener, Scene, SceneHandler, TriMesh
        )

        import multiprocessing

        main_thread = Thread.getThread()
        main_fresolver = main_thread.getFileResolver()
        main_logger = main_thread.getLogger()

        class CustomAppender(Appender):
            def append(self, logLevel, message):
                MtsLog(message)

            def logProgress(self, progress, name, formatted, eta):
                render_engine = MtsManager.RenderEngine

                if not render_engine.is_preview:
                    percent = progress / 100
                    render_engine.update_progress(percent)
                    render_engine.update_stats('', 'Progress: %s - ETA: %s' % ('{:.2%}'.format(percent), eta))
예제 #12
0
            Vector,
            Point,
            Matrix4x4,
            Transform,
            AnimatedTransform,
            Appender,
            EInfo,
            EWarn,
            EError,
        )
        from mitsuba.render import (RenderQueue, RenderJob, RenderListener,
                                    Scene, SceneHandler, TriMesh)

        import multiprocessing

        main_thread = Thread.getThread()
        main_fresolver = main_thread.getFileResolver()
        main_logger = main_thread.getLogger()

        class CustomAppender(Appender):
            def append(self, logLevel, message):
                MtsLog(message)

            def logProgress(self, progress, name, formatted, eta):
                render_engine = MtsManager.RenderEngine

                if not render_engine.is_preview:
                    percent = progress / 100
                    render_engine.update_progress(percent)
                    render_engine.update_stats(
                        '', 'Progress: %s - ETA: %s' %