Interactive GUI toolkit for robotics visualization - Python & C++, runs on desktop and web

Hi everyone,

I’d like to share Dear ImGui Bundle, an open-source framework for building interactive GUI applications in Python and C++. It wraps Dear ImGui with 23 integrated libraries (plotting, image inspection, node editors, 3D gizmos, etc.) and runs on desktop, mobile, and web.

I’m a solo developer and have been working hard on this for 4 years. I am new here, but I thought it might be useful for robotics developers.

It provides:

Real-time visualization

  • ImPlot and ImPlot3D for sensor data, trajectories, live plots at 60fps (or even 120fps)
  • ImmVision for camera feed inspection with zoom, pan, pixel values, and colormaps
  • All GPU-accelerated (OpenGL/Metal/Vulkan)

Interactive parameter tuning

  • Immediate mode means your UI code is just a few lines of Python or C++
  • Sliders, knobs, toggles, color pickers - all update in real time
  • No callbacks, no widget trees, no framework boilerplate

Cross-platform deployment

  • Same code runs on Linux, macOS, Windows
  • Python apps can run in the browser via Pyodide (useful for sharing dashboards without requiring install)
  • C++ apps compile to WebAssembly via Emscripten

Example: live camera + Laplacian filter with colormaps in 54 lines

import cv2
import numpy as np
from imgui_bundle import imgui, immvision, immapp


class AppState:
    def __init__(self):
        self.cap = cv2.VideoCapture(0)
        self.image = None
        self.filtered = None
        self.blur_sigma = 2.0
        # ImmVision params
        # For the camera image
        self.params_image = immvision.ImageParams()
        self.params_image.image_display_size = (400, 0)
        self.params_image.zoom_key = "cam"
        # For the filtered image (synced zoom via zoom_key)
        self.params_filter = immvision.ImageParams()
        self.params_filter.image_display_size = (400, 0)
        self.params_filter.zoom_key = "cam"
        self.params_filter.show_options_panel = True


def gui(s: AppState):
    # grab
    has_image, frame = s.cap.read()
    if has_image:
        s.image = cv2.resize(frame, (640, 480))
        gray = cv2.cvtColor(s.image, cv2.COLOR_BGR2GRAY)
        gray_f = gray.astype(np.float64) / 255.0
        blurred = cv2.GaussianBlur(gray_f, (0, 0), s.blur_sigma)
        s.filtered = cv2.Laplacian(blurred, cv2.CV_64F, ksize=5)

    # Refresh images only if needed
    s.params_image.refresh_image = has_image
    s.params_filter.refresh_image = has_image

    if s.image is not None:
        immvision.image("Camera", s.image, s.params_image)
        imgui.same_line()
        immvision.image("Filtered", s.filtered, s.params_filter)

    # Controls
    _, s.blur_sigma = imgui.slider_float("Blur", s.blur_sigma, 0.5, 10.0)


state = AppState()
immvision.use_bgr_color_order()
immapp.run(lambda: gui(state), window_size=(1200, 550), window_title="Camera Filter", fps_idle=0)

The filtered image is float64 - click “Options” to try different colormaps (Heat, Jet, Viridis…). Both views are zoom-linked: pan one, the other follows.

Try it:

Install: pip install imgui-bundle

Adoption:
The framework is used in several research projects, including CVPR 2024 papers (4K4D), Newton Physics, and moderngl. The Python bindings are auto-generated with litgen, so they stay in sync with upstream Dear ImGui.

Happy to answer any questions or discuss how it could fit into ROS workflows.

Best,
Pascal

4 Likes

Looks pretty cool, and I’m always all for making UI easier to use and less boilerplate :rocket:

Why did you choose ImGui over a traditional GUI framework like Qt / Qt Quick?
I understand the use in games for simple overlays, but it seems like most of this would already be much easier in QML, or am I missing something?

Thanks for the feedback! (and sorry for the late reply)

How does it compare to more traditionnal frameworks like Qt & Qt Quick

This is an interesting question.

Using the “Immediate Gui” paradigm leads to code that is much shorter, and readable: the application state is not split and synchronized between ui elements and application logic.

This is very adapted for cases where you want to iterate very quickly. Thanks to the plethora of libraries (plotting, image display, etc), it is easy to build analysis tools with this.

If you want to build apps for final users, where you need a very polished user experience, a traditional frameworks is better adapted (although the look of ImGui can be themed quite a lot).


Comparison with other frameworks

As an example, you may want to take a look at this page, which is summarized below.

This app:

can be written in 9 lines with ImGui

from imgui_bundle import imgui, hello_imgui

selected_idx = 0
items = ["Apple", "Banana", "Cherry"]

def gui():
    global selected_idx
    imgui.text("Choose a fruit:")
    _, selected_idx = imgui.list_box("##fruits", selected_idx, items)
    imgui.text(f"You selected: {items[selected_idx]}")

hello_imgui.run(gui, window_title="Fruit Picker")

With Qt the code would use about 24 lines (and be a little less readable IMHO)

from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QListWidget

items = ["Apple", "Banana", "Cherry"]

class FruitPicker(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.label = QLabel("Choose a fruit:")
        self.list_widget = QListWidget()
        self.list_widget.addItems(items)
        self.result_label = QLabel(f"You selected: {items[0]}")
        self.list_widget.currentRowChanged.connect(self.on_selection_changed)
        layout.addWidget(self.label)
        layout.addWidget(self.list_widget)
        layout.addWidget(self.result_label)
        self.setLayout(layout)

    def on_selection_changed(self, index):
        self.result_label.setText(f"You selected: {items[index]}")

app = QApplication([])
window = FruitPicker()
window.show()
app.exec()

With Qt Quick, it would use 2 files, like so

main.py

 from __future__ import annotations
from typing import Final

import sys
from PySide6.QtCore import QObject, Property, Signal, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine


ITEMS: Final[list[str]] = ["Apple", "Banana", "Cherry"]


class FruitBackend(QObject):
    selectedIndexChanged = Signal()

    def __init__(self) -> None:
        super().__init__()
        self._selected_index = 0

    def get_selected_index(self) -> int:
        return self._selected_index

    def set_selected_index(self, value: int) -> None:
        if value == self._selected_index:
            return
        self._selected_index = value
        self.selectedIndexChanged.emit()

    selectedIndex = Property(
        int,
        get_selected_index,
        set_selected_index,
        notify=selectedIndexChanged,
    )

    @Slot(result=str)
    def selected_fruit(self) -> str:
        return ITEMS[self._selected_index]


def main() -> int:
    app = QGuiApplication(sys.argv)

    engine = QQmlApplicationEngine()
    backend = FruitBackend()

    engine.rootContext().setContextProperty("backend", backend)
    engine.rootContext().setContextProperty("fruitItems", ITEMS)
    engine.load("Main.qml")

    if not engine.rootObjects():
        return 1
    return app.exec()


if __name__ == "__main__":
    raise SystemExit(main())

Main.yml

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    visible: true
    width: 300
    height: 220
    title: "Fruit Picker"

    Column {
        anchors.centerIn: parent
        spacing: 12

        Label {
            text: "Choose a fruit:"
        }

        ListView {
            width: 160
            height: 100
            model: fruitItems
            currentIndex: backend.selectedIndex

            delegate: ItemDelegate {
                width: parent.width
                text: modelData
                highlighted: ListView.isCurrentItem
                onClicked: backend.selectedIndex = index
            }
        }

        Label {
            text: "You selected: " + backend.selected_fruit()
        }
    }
}

QML also doesn’t require you to split UI and application logic.

Your example could be simplified to:

import QtQuick
import QtQuick.Controls

ApplicationWindow {
    id: root
    title: "Fruit Picker"
    property int index: 0
    property var items: ["Apple", "Banana", "Cherry"]

    Column {
        anchors.fill: parent

        Label { text: "Choose a fruit:" }

        ListView {
            width: 160
            height: 100
            model: root.items
            currentIndex: root.index

            delegate: ItemDelegate {
                width: parent.width
                text: modelData
                highlighted: ListView.isCurrentItem
                onClicked: root.index = index
            }
        }

        Label {
            text: "You selected: " + root.items[root.index]
        }
    }
}

You can even connect it to ROS in QML to publish your selection directly.

import QtQuick
import QtQuick.Controls
import Ros2

ApplicationWindow {
    id: root
    title: "Fruit Picker"
    property int index: 0
    property var items: ["Apple", "Banana", "Cherry"]
    property var publisher: Ros2.createPublisher("/fruit", "std_msg/msg/String", 10)
    Component.onCompleted: {
      Ros2.init("fruit_picker_node");
    }
    onIndexChanged: publisher.publish({ data: root.items[root.index] })

    Column {
        anchors.fill: parent

        Label { text: "Choose a fruit:" }

        ListView {
            width: 160
            height: 100
            model: root.items
            currentIndex: root.index

            delegate: ItemDelegate {
                width: parent.width
                text: modelData
                highlighted: ListView.isCurrentItem
                onClicked: root.index = index
            }
        }

        Label {
            text: "You selected: " + root.items[root.index]
        }
    }
}

Your ImGui example is quite short, though, so if you only need a small form input and are working with Python anyway, I can see the value.