<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - binary</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/binary.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2020-09-17T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Plotting Binary File Data with Matplotlib in PyQt6 — Load binary files and plot them interactively using Matplotlib and PyQt6</title><link href="https://www.pythonguis.com/faq/plotting-with-matplotlib-tutorial-adding-file-option/" rel="alternate"/><published>2020-09-17T09:00:00+00:00</published><updated>2020-09-17T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2020-09-17:/faq/plotting-with-matplotlib-tutorial-adding-file-option/</id><summary type="html">I got the Matplotlib plotting example working in my PyQt app, and now I'd like to plot data from a binary file. How do I read a binary file into an array for plotting &amp;mdash; letting the user choose the file location, bytes per sample (1, 2, 3, or 4), and endianness &amp;mdash; then plot the sample values on the Y axis and sample numbers on the X axis?</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;I got the Matplotlib plotting example working in my PyQt app, and now I'd like to plot data from a binary file. How do I read a binary file into an array for plotting &amp;mdash; letting the user choose the file location, bytes per sample (1, 2, 3, or 4), and endianness &amp;mdash; then plot the sample values on the Y axis and sample numbers on the X axis?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Reading binary data and plotting it is a common task in signal processing, embedded development, and data analysis. In this tutorial, we'll build a small PyQt6 application that lets the user select a binary file, configure how the bytes should be interpreted, and then plot the resulting waveform using Matplotlib.&lt;/p&gt;
&lt;h2 id="reading-binary-data-into-a-numpy-array"&gt;Reading binary data into a NumPy array&lt;/h2&gt;
&lt;p&gt;Before we wire anything into a GUI, let's look at how binary file reading works in Python. A binary file is just a sequence of raw bytes. To interpret those bytes as numbers, you need to know two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Bytes per sample&lt;/strong&gt; &amp;mdash; how many bytes make up a single data point (1, 2, 3, or 4).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Endianness&lt;/strong&gt; &amp;mdash; whether the most significant byte (MSB) comes first (&lt;em&gt;big-endian&lt;/em&gt;) or last (&lt;em&gt;little-endian&lt;/em&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Python's &lt;code&gt;struct&lt;/code&gt; module can unpack bytes into integers, but for large files NumPy is far more convenient. NumPy's &lt;code&gt;numpy.frombuffer&lt;/code&gt; function reads raw bytes and returns an array of numbers, which is exactly what Matplotlib needs for plotting.&lt;/p&gt;
&lt;p&gt;Here's a minimal example of reading a binary file into a NumPy array:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import numpy as np

def read_binary_file(filepath, bytes_per_sample, endianness="little"):
    with open(filepath, "rb") as f:
        raw = f.read()

    # Build a NumPy dtype string based on endianness and byte width
    endian_char = "&amp;lt;" if endianness == "little" else "&amp;gt;"
    type_map = {1: "u1", 2: "u2", 4: "u4"}

    if bytes_per_sample in type_map:
        dtype = np.dtype(f"{endian_char}{type_map[bytes_per_sample]}")
        data = np.frombuffer(raw, dtype=dtype)
    elif bytes_per_sample == 3:
        # NumPy doesn't have a native 3-byte integer type,
        # so we handle this manually.
        data = read_3byte_samples(raw, endianness)
    else:
        raise ValueError("bytes_per_sample must be 1, 2, 3, or 4")

    return data
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;NumPy dtype strings use &lt;code&gt;&amp;lt;&lt;/code&gt; for little-endian and &lt;code&gt;&amp;gt;&lt;/code&gt; for big-endian. The &lt;code&gt;u&lt;/code&gt; means unsigned integer, and the digit is the number of bytes. So &lt;code&gt;&amp;lt;u2&lt;/code&gt; means "little-endian unsigned 2-byte integer."&lt;/p&gt;
&lt;h3&gt;Handling 3-byte samples&lt;/h3&gt;
&lt;p&gt;NumPy doesn't have a built-in 3-byte integer type, so we need to pad each 3-byte chunk to 4 bytes and then interpret the result. Here's a function that does that:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;def read_3byte_samples(raw, endianness="little"):
    # Trim any trailing bytes that don't make a complete sample
    num_samples = len(raw) // 3
    trimmed = raw[:num_samples * 3]

    output = np.zeros(num_samples, dtype=np.uint32)
    for i in range(num_samples):
        three_bytes = trimmed[i * 3 : i * 3 + 3]
        if endianness == "little":
            padded = three_bytes + b'\x00'
        else:
            padded = b'\x00' + three_bytes
        output[i] = int.from_bytes(padded, byteorder=endianness)

    return output
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This loops through the raw bytes three at a time, pads each chunk to four bytes, and converts it to an integer. For large files you could optimize this further, but this approach is clear and works well for most use cases.&lt;/p&gt;
&lt;h2 id="embedding-matplotlib-in-pyqt6"&gt;Embedding Matplotlib in PyQt6&lt;/h2&gt;
&lt;p&gt;To plot inside a PyQt6 window, we use &lt;code&gt;FigureCanvasQTAgg&lt;/code&gt; from the &lt;code&gt;matplotlib.backends&lt;/code&gt; module. This gives us a Qt widget that contains a Matplotlib figure. If you haven't done this before, see our full guide to &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-plotting-matplotlib/"&gt;Plotting with Matplotlib in PyQt6&lt;/a&gt; for a detailed introduction. The setup is straightforward &amp;mdash; you create a &lt;code&gt;Figure&lt;/code&gt;, wrap it in a canvas widget, and add it to your layout like any other widget.&lt;/p&gt;
&lt;p&gt;Let's start building the application step by step.&lt;/p&gt;
&lt;h3&gt;Setting up the canvas&lt;/h3&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=8, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super().__init__(fig)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This small class creates a Matplotlib figure with a single set of axes. We'll use &lt;code&gt;self.axes&lt;/code&gt; to draw our plots.&lt;/p&gt;
&lt;h2 id="building-the-main-window"&gt;Building the main window&lt;/h2&gt;
&lt;p&gt;The main window needs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;toolbar&lt;/strong&gt; for Matplotlib's built-in zoom/pan controls.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;canvas&lt;/strong&gt; where the plot is drawn.&lt;/li&gt;
&lt;li&gt;Controls to let the user choose a file, set bytes per sample, and pick endianness.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Plot&lt;/strong&gt; button to trigger the plotting.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're new to building PyQt6 applications, you may want to start with &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-creating-your-first-window/"&gt;Creating your first PyQt6 window&lt;/a&gt; before continuing.&lt;/p&gt;
&lt;p&gt;Here's the complete application:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import numpy as np

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QComboBox, QLabel, QFileDialog, QLineEdit,
)

from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


def read_3byte_samples(raw, endianness="little"):
    """Read raw bytes as 3-byte unsigned integers."""
    num_samples = len(raw) // 3
    trimmed = raw[:num_samples * 3]

    output = np.zeros(num_samples, dtype=np.uint32)
    for i in range(num_samples):
        three_bytes = trimmed[i * 3 : i * 3 + 3]
        if endianness == "little":
            padded = three_bytes + b"\x00"
        else:
            padded = b"\x00" + three_bytes
        output[i] = int.from_bytes(padded, byteorder=endianness)

    return output


def read_binary_file(filepath, bytes_per_sample, endianness="little"):
    """Read a binary file and return data as a NumPy array."""
    with open(filepath, "rb") as f:
        raw = f.read()

    endian_char = "&amp;lt;" if endianness == "little" else "&amp;gt;"
    type_map = {1: "u1", 2: "u2", 4: "u4"}

    if bytes_per_sample in type_map:
        dtype = np.dtype(f"{endian_char}{type_map[bytes_per_sample]}")
        # Trim any incomplete trailing bytes
        usable = len(raw) - (len(raw) % bytes_per_sample)
        data = np.frombuffer(raw[:usable], dtype=dtype)
    elif bytes_per_sample == 3:
        data = read_3byte_samples(raw, endianness)
    else:
        raise ValueError("bytes_per_sample must be 1, 2, 3, or 4")

    return data


class MplCanvas(FigureCanvas):
    def __init__(self, parent=None, width=8, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super().__init__(fig)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Binary File Plotter")

        # Matplotlib canvas and toolbar
        self.canvas = MplCanvas(self, width=8, height=4, dpi=100)
        toolbar = NavigationToolbar(self.canvas, self)

        # File selection
        self.file_path_edit = QLineEdit()
        self.file_path_edit.setPlaceholderText("No file selected")
        self.file_path_edit.setReadOnly(True)

        browse_button = QPushButton("Browse&amp;hellip;")
        browse_button.clicked.connect(self.browse_file)

        file_layout = QHBoxLayout()
        file_layout.addWidget(QLabel("File:"))
        file_layout.addWidget(self.file_path_edit)
        file_layout.addWidget(browse_button)

        # Bytes per sample
        self.bytes_combo = QComboBox()
        self.bytes_combo.addItems(["1", "2", "3", "4"])
        self.bytes_combo.setCurrentText("2")

        # Endianness
        self.endian_combo = QComboBox()
        self.endian_combo.addItems(["little", "big"])

        options_layout = QHBoxLayout()
        options_layout.addWidget(QLabel("Bytes per sample:"))
        options_layout.addWidget(self.bytes_combo)
        options_layout.addWidget(QLabel("Endianness:"))
        options_layout.addWidget(self.endian_combo)
        options_layout.addStretch()

        # Plot button
        plot_button = QPushButton("Plot")
        plot_button.clicked.connect(self.plot_data)
        options_layout.addWidget(plot_button)

        # Main layout
        layout = QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(self.canvas)
        layout.addLayout(file_layout)
        layout.addLayout(options_layout)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.filepath = None

    def browse_file(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "Open Binary File", "", "Binary Files (*.bin);;All Files (*)"
        )
        if path:
            self.filepath = path
            self.file_path_edit.setText(path)

    def plot_data(self):
        if not self.filepath:
            return

        bytes_per_sample = int(self.bytes_combo.currentText())
        endianness = self.endian_combo.currentText()

        data = read_binary_file(self.filepath, bytes_per_sample, endianness)

        # X axis is simply the sample index
        x = np.arange(len(data))

        self.canvas.axes.clear()
        self.canvas.axes.plot(x, data)
        self.canvas.axes.set_xlabel("Sample Number")
        self.canvas.axes.set_ylabel("Value")
        self.canvas.axes.set_title(f"{len(data)} samples from {self.filepath}")
        self.canvas.draw()


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Let's walk through what this does.&lt;/p&gt;
&lt;h3&gt;The file selection row&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;QFileDialog.getOpenFileName&lt;/code&gt; method opens a native file picker. The filter &lt;code&gt;"Binary Files (*.bin);;All Files (*)"&lt;/code&gt; shows &lt;code&gt;.bin&lt;/code&gt; files by default but lets the user switch to all files. Once a file is selected, its path is stored in &lt;code&gt;self.filepath&lt;/code&gt; and displayed in the read-only &lt;code&gt;QLineEdit&lt;/code&gt;. For more on using dialogs in your PyQt6 applications, see our &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-dialogs/"&gt;PyQt6 Dialogs tutorial&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The options row&lt;/h3&gt;
&lt;p&gt;Two &lt;code&gt;QComboBox&lt;/code&gt; widgets let the user choose the bytes per sample (1, 2, 3, or 4) and the endianness (little or big). Little-endian is the default because it's the most common byte order on modern PCs and many embedded systems.&lt;/p&gt;
&lt;h3&gt;The Plot button&lt;/h3&gt;
&lt;p&gt;When clicked, &lt;code&gt;plot_data&lt;/code&gt; reads the selected file using our &lt;code&gt;read_binary_file&lt;/code&gt; function, generates an array of sample indices for the X axis, clears the previous plot, and draws the new data. Calling &lt;code&gt;self.canvas.draw()&lt;/code&gt; at the end tells Matplotlib to refresh the display.&lt;/p&gt;
&lt;h2 id="creating-a-test-file"&gt;Creating a test file&lt;/h2&gt;
&lt;p&gt;If you don't have a binary file handy, you can generate one with this short script:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import numpy as np

# Generate a sine wave: 1000 samples, 16-bit unsigned, little-endian
num_samples = 1000
t = np.linspace(0, 4 * np.pi, num_samples)
values = ((np.sin(t) + 1) / 2 * 65535).astype(np.uint16)

values.tofile("sample.bin")
print(f"Wrote {num_samples} samples to sample.bin")
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Run this first, then open the generated &lt;code&gt;sample.bin&lt;/code&gt; in the plotter with &lt;strong&gt;2 bytes per sample&lt;/strong&gt; and &lt;strong&gt;little-endian&lt;/strong&gt; selected. You should see a smooth sine wave.&lt;/p&gt;
&lt;h2 id="signed-vs-unsigned-data"&gt;Signed vs. unsigned data&lt;/h2&gt;
&lt;p&gt;The example above reads all values as unsigned integers. If your binary data contains signed values (for example, audio samples centered around zero), you can change the dtype from unsigned to signed by replacing &lt;code&gt;u&lt;/code&gt; with &lt;code&gt;i&lt;/code&gt; in the type map:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;type_map = {1: "i1", 2: "i2", 4: "i4"}
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;You could add a third combo box to the UI to let the user toggle between signed and unsigned interpretation. The wiring would be the same &amp;mdash; just pass the choice into &lt;code&gt;read_binary_file&lt;/code&gt; and select the right dtype prefix.&lt;/p&gt;
&lt;p&gt;Once you're ready to distribute your finished application, take a look at &lt;a href="https://www.pythonguis.com/tutorials/packaging-pyqt6-applications-windows-pyinstaller/"&gt;Packaging PyQt6 applications with PyInstaller on Windows&lt;/a&gt; for a step-by-step guide to creating standalone executables.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="matplotlib"/><category term="pyqt6"/><category term="plotting"/><category term="binary"/><category term="files"/><category term="numpy"/><category term="data-science"/><category term="python"/><category term="qt"/><category term="qt6"/><category term="pyqt6-data-science"/></entry></feed>