Skip to content
Merged

Dev #48

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
647fd77
color and marker assignment by test
Jul 22, 2025
5123abd
csv parser compatibility with missing data items
Jul 22, 2025
110679d
integrated linestiles for plotting
Jul 23, 2025
e70dad8
removed logging
Jul 23, 2025
4e14c46
Switched to more recent Python versions
Sep 25, 2025
4d6e1ce
Switched support to python version 3.9
Sep 25, 2025
9162ab4
fixed numpy version
Sep 25, 2025
3eb35dd
skip test_gui
Sep 26, 2025
3a8c8d9
Removed coveralls dependency as currently unavailable
Sep 26, 2025
d68618c
fix qt error
Sep 26, 2025
8c376f6
Made csv parser robust against missing data items and QP column
Sep 28, 2025
cdb1efe
changed build pipeline
Sep 29, 2025
ca6a6d4
Removed gui tests
Sep 29, 2025
338768f
extended support of python versions
Sep 29, 2025
889d134
removed python 3.12 as it does not yet work properly
Sep 29, 2025
07d029c
Increased plot warning to 100 plots
Sep 29, 2025
d02cf33
prevent crash if there are missing values in BD-rate calculation
Sep 29, 2025
2a2eef8
implemented pre-selected line styles for y, u and v components
Sep 29, 2025
3cf4296
included coveralls in build pipeline
Sep 29, 2025
e7297a7
Update appveyor.yml
timclassen Sep 30, 2025
a714cc0
Update requirements.txt
timclassen Sep 30, 2025
5c00074
Update requirements.txt
timclassen Sep 30, 2025
4388252
Update installer.cfg
timclassen Sep 30, 2025
8b597d4
Update installer.cfg
timclassen Sep 30, 2025
ee67552
Update installer.cfg
timclassen Sep 30, 2025
5c82a0e
Update requirements.txt
timclassen Oct 2, 2025
1c3ef6b
Update requirements.txt
timclassen Oct 2, 2025
22ea37a
Update appveyor.yml
timclassen Oct 2, 2025
f04b77c
Update installer.cfg
timclassen Oct 2, 2025
f910f7a
Update installer.cfg
timclassen Oct 2, 2025
1ee1f91
Update installer.cfg
timclassen Oct 3, 2025
4dd3bd7
Update installer.cfg
timclassen Oct 3, 2025
ad49581
Update installer.cfg
timclassen Oct 3, 2025
5acb92b
Update installer.cfg
timclassen Oct 3, 2025
05dd824
removed scipy dependency
Oct 3, 2025
54caa3b
removed scipy dependency
Oct 3, 2025
6d41f15
removed tikzplottlib dependency
timclassen Oct 3, 2025
ea1011b
removed scipy dependency
timclassen Oct 3, 2025
69da1d2
added scipy dependency again
timclassen Oct 3, 2025
0e7cd3a
Removed tikzplotlib export button
timclassen Oct 3, 2025
4fb5373
Fixed linestyle for mos
timclassen Oct 3, 2025
c0c669a
add debug info to PlotWidget
timclassen Oct 3, 2025
94e2785
Added debugging prints
timclassen Oct 4, 2025
594238f
Implemented alternative ci plotting method
timclassen Oct 4, 2025
514ca04
Improved robustness of rate parsing
timclassen Oct 4, 2025
c3c3de1
removed unnecessary imports
timclassen Oct 5, 2025
c6a0d79
Improved robustness for sequence column parsing
timclassen Oct 11, 2025
635919c
Support single value orig plotting
timclassen Oct 16, 2025
b4be71a
UI improvements
timclassen Oct 17, 2025
cac56ad
Update version number
timclassen Oct 17, 2025
f420e88
Merge branch 'master' of https://github.com/IENT/RDPlot into dev
timclassen Oct 17, 2025
a8d64d9
Show Plot Area if more than 1 Sequence is selected
timclassen Oct 17, 2025
c250439
UI Improvements
timclassen Oct 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def get_version():

def get_install_requires():
if 'FLATPAK_INSTALL' in os.environ:
install_requires=['cycler', 'matplotlib', 'numpy',
install_requires=['cycler', 'matplotlib', 'numpy==1.26.4',
'py', 'pyparsing', 'pyqt5', 'pytest',
'python-dateutil', 'pytz', 'six',
'scipy',
Expand Down
2 changes: 1 addition & 1 deletion src/installer.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[Application]
name=rdplot
publisher=IENT
version=1.0
version=1.3.4
# How to launch the app - this calls the 'main' function from the 'myapp' package:
entry_point=rdplot.__main__:main
icon=rdplot/logo/plot512_0wd_icon.ico
Expand Down
6 changes: 3 additions & 3 deletions src/rdplot/SimulationDataItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from os import listdir
from os.path import join, abspath, isfile, isdir, basename, splitext
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QComboBox, QPushButton, QDialogButtonBox, QLabel, QCheckBox, QGroupBox, QMessageBox, QApplication
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QComboBox, QDialogButtonBox, QLabel, QCheckBox
import re


Expand Down Expand Up @@ -457,7 +457,7 @@ def create_item_list_from_directory(self, directory_path):
except SimulationDataItemError as error:
pass
# We definitely cannot accept thousands of exceptions on the command line
# print((AbstractEncLog
# print((
# "Could not create simulation data item from file '{}'"
# " due to {}"
# ).format(path, error))
Expand Down Expand Up @@ -522,7 +522,7 @@ def parse_csv_item_list(self, log_path):
continue
item_list.append(CSVLog(config, header, line))
return item_list
except:
except Exception as e:
raise SimulationDataItemError()

# Magic Methods
Expand Down
10 changes: 9 additions & 1 deletion src/rdplot/SimulationDataItemClasses/CsvLogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ def __init__(self, config, header, line):
header = header.replace("\n", "")
header = re.split(r'[,;]',header.lower())
header = list(filter(None, header))
sequence_idx = header.index("sequence")

try:
sequence_idx = header.index("sequence")
except Exception: # Sequence not found. Search for partial string
sequence_idx = 0
for i, token in enumerate(header):
if "sequence" in token:
sequence_idx = i
break

try:
qp_idx = header.index("qp")
Expand Down
2 changes: 1 addition & 1 deletion src/rdplot/SimulationDataItemClasses/DatLogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import xmltodict
from abc import abstractmethod
from xml.parsers.expat import ExpatError
from os.path import normpath, basename, sep, dirname
from os.path import normpath, dirname

from rdplot.SimulationDataItem import (AbstractSimulationDataItem, SimulationDataItemError)

Expand Down
1 change: 0 additions & 1 deletion src/rdplot/SimulationDataItemClasses/EncoderLogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,6 @@ def _parse_summary_data(self):
vals = [float(val) for val in vals] # convert to numbers

name_val_dict = dict(zip(names, vals)) # pack both together in a dict
# print(summary_type)

name_rate = 'Bitrate'
if summary_type == 'SUMMARY':
Expand Down
3 changes: 2 additions & 1 deletion src/rdplot/Widgets/MainWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ def remove(self):
self._selection_model.selectionChanged.connect(self.change_list)
self._variable_tree_selection_model.selectionChanged.connect(self.update_plot)
if len(self.selectedSimulationDataItemListModel.values()) == 0:
self.plotPreview.reset_plot_color_cycle()
self.update_plot()

def change_list(self, q_selected, q_deselected):
Expand Down Expand Up @@ -396,7 +397,7 @@ def check_labels(self):
selectionmodel.clearSelection()

# updates the plot if the plot variable is changed
def update_plot(self, force=False):
def update_plot(self, force=True):
# user-generated curves and curves loaded from files are not supposed to be mixed
user_generated_curves = False
if self.sender() == self._variable_tree_selection_model or self.sender() == self.curveListSelectionModel or force:
Expand Down
112 changes: 95 additions & 17 deletions src/rdplot/Widgets/PlotWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,22 @@ def __init__(self, ):
self.anchor_identifier = ''
self.ci_mode = 'average'

self.reset_plot_color_cycle()

self.ci_visible = False

def reset_plot_color_cycle(self):
'''
Reset the linestyles and color cycle for plotting
'''
self.color_cycle = ['r', 'b', 'y', 'k', 'c', 'm', 'g', 'r', 'b', 'y', 'k', 'c', 'm', 'g']
self.marker_cycle = ['x', 'x', 'x', 'x', 'x', 'x', 'x', 'o', 'o', 'o', 'o', 'o', 'o', 'o']
self.marker_cycle = ['x', 'o', 'v', '2', 's', 'P', '*', 'D', 'h']
self.linestyle_cycle = ["-", "--", ":", "-."]
self.plot_index = 0
self.marker_index = 0
self.plot_linestyle_index = 0
self.color_list = []
self.marker_list = []
self.linestyle_list = [
("psnr y", self.linestyle_cycle[0]), ("psnr u", self.linestyle_cycle[1]), ("psnr v", self.linestyle_cycle[2]),
("wpsnr y", self.linestyle_cycle[0]), ("wpsnr u", self.linestyle_cycle[1]), ("wpsnr v", self.linestyle_cycle[2]),
Expand All @@ -100,9 +110,6 @@ def __init__(self, ):
("mos", self.linestyle_cycle[0]),
]

self.ci_visible = False


def create_legend(self, plot_data_collection):
tmp_legend = []
for plot_data in plot_data_collection:
Expand All @@ -121,20 +128,35 @@ def create_legend(self, plot_data_collection):

return legend


def set_color(self, name):
def set_color(self, name, sequence):
color = None
for color_item in self.color_list:
if color_item[0] == name:
return (color_item[1], color_item[2])
color = color_item[1]

marker = None
for marker_item in self.marker_list:
if marker_item[0] == sequence:
marker = marker_item[1]

color = self.color_cycle[self.plot_index]
marker = self.marker_cycle[self.plot_index]
if color is None:
color = self.color_cycle[self.plot_index]

self.plot_index += 1
if(self.plot_index >= len(self.color_cycle)):
self.plot_index = 0

self.color_list.append([name, color])

if marker is None:
marker = self.marker_cycle[self.marker_index]

self.plot_index += 1
if(self.plot_index >= len(self.color_cycle)):
self.plot_index = 0
self.marker_index += 1
if(self.marker_index >= len(self.marker_cycle)):
self.marker_index = 0

self.marker_list.append([sequence, marker])

self.color_list.append([name, color, marker])
return (color, marker)

def set_linestyle(self, path):
Expand All @@ -155,7 +177,6 @@ def plot_confidence_interval(self, ax, x, y, ci, color):
for i in range(len(x)):
top = y[i] + ci[i]
bottom = y[i] - ci[i]
print(x[i], top, bottom)
ax.plot([x[i], x[i]], [top, bottom], color=color, marker="_", ms=8, solid_capstyle="butt") #, alpha=0.3)

# refreshes the figure according to new changes done
Expand All @@ -167,6 +188,14 @@ def change_plot(self, plot_data_collection, user_generated_curves=False):
temporal data
"""

try:
if(len(plot_data_collection) == 1):
self.label_2.setText(plot_data_collection[0].identifiers[0])
else:
self.label_2.setText("Plot Area")
except Exception:
self.label_2.setText("Plot Area")

# Set the anchor identifier for the first time
# if no identifier has been set so far (similar
# to the selection in BdTableModel update method)
Expand Down Expand Up @@ -229,14 +258,36 @@ def change_plot(self, plot_data_collection, user_generated_curves=False):
for plot_data in plot_data_collection:
legend.append(plot_data.identifiers[0])

# Get min and max for reference plotting
minr = 1e100
maxr = -minr
miny = 1e100
maxy = -miny
for plot_data in plot_data_collection:
if not plot_data.has_ci:
values = ((float(x), float(y)) for (x, y) in plot_data.values)
sorted_value_pairs = sorted(values, key=lambda pair: pair[0])
[xs, ys] = list(zip(*sorted_value_pairs))
else:
# A confidence interval is included in the data
values = ((float(x), float(y), float(z)) for (x, y, z) in plot_data.values)
sorted_value_pairs = sorted(values, key=lambda pair: pair[0])
[xs, ys, zs] = list(zip(*sorted_value_pairs))
if np.isnan(min(xs)) or np.isnan(max(xs)):
continue
minr = min(min(xs), minr)
maxr = max(max(xs), maxr)
miny = min(min(ys), miny)
maxy = max(max(ys), maxy)

# plot all the lines which are missing yet
plot_count = 0
for plot_data in plot_data_collection:
# Create legend from variable path and sim data items identifiers
l = legend[plot_count] #" ".join([i for i in plot_data.identifiers] + plot_data.path)

if plot_data.color == " ":
(plot_data.color, plot_data.marker) = self.set_color(plot_data.identifiers[1])
(plot_data.color, plot_data.marker) = self.set_color(plot_data.identifiers[1], plot_data.identifiers[0])
plot_data.linestyle = self.set_linestyle(plot_data.path[1])

# Convert list of pairs of strings to two sorted lists of floats
Expand All @@ -249,6 +300,10 @@ def change_plot(self, plot_data_collection, user_generated_curves=False):
sorted_value_pairs = sorted(values, key=lambda pair: pair[0])
[xs, ys] = list(zip(*sorted_value_pairs))

if len(xs) == 1 and np.isnan(xs[0]):
xs = [minr, maxr]
ys = [ys[0], ys[0]]

# plot the current plot data
curve = self.ax.plot(xs, ys, label=l, color=plot_data.color, marker=plot_data.marker, linestyle=plot_data.linestyle)

Expand All @@ -259,6 +314,11 @@ def change_plot(self, plot_data_collection, user_generated_curves=False):
sorted_value_pairs = sorted(values, key=lambda pair: pair[0])
[xs, ys, zs] = list(zip(*sorted_value_pairs))

if len(xs) == 1 and np.isnan(xs[0]):
xs = [minr, maxr]
ys = [ys[0], ys[0]]
zs = [zs[0], zs[0]]

# calculate the lower and upper boundaries of the CI
ys_low = np.subtract(ys, zs)
ys_up = np.add(ys, zs)
Expand Down Expand Up @@ -288,14 +348,32 @@ def change_plot(self, plot_data_collection, user_generated_curves=False):
self.plot_confidence_interval(self.ax, xs, ys, zs, plot_data.color)

plot_count += 1
except:
sys.stderr.write("Too many values for confidence interval. Please only add one value.")
except Exception as e:
print(e)

# Set the legend
if not(legend == ['']):
self.ax.legend(loc='lower right')
DataCursor(self.ax.get_lines())

# Specific to MOS plotting
try:
if plot_data_collection[0].label[1].lower() == "mos":
if miny >= 1 and maxy <= 5: # 5-grade scale
self.ax.set_ylim(1, 5)
elif miny >= 0 and maxy <= 10: # 11-grade scale
self.ax.set_ylim(0, 10)
elif miny >= 0 and maxy <= 100: # 101-grade scale
self.ax.set_ylim(0, 100)
elif miny >= -1 and maxy <= 1: # the other one :D
self.ax.set_ylim(-1, 1)
elif miny >= -2 and maxy <= 2: # the other one :D
self.ax.set_ylim(-2, 2)
elif miny >= -3 and maxy <= 3: # the other one :D
self.ax.set_ylim(-3, 3)
except Exception as e:
print(e)

start, end = self.ax.get_ylim()
data_range = end - start
# get ticks with decimal precision
Expand Down
1 change: 1 addition & 0 deletions src/rdplot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ def main():
sys.exit(app.exec_())

if __name__ == '__main__':
print("Starting RD-Plot...")
main()
2 changes: 0 additions & 2 deletions src/rdplot/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
##################################################################################################
from collections import deque
from os.path import sep
from os import environ
import numpy as np
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QPushButton
Expand Down Expand Up @@ -794,7 +793,6 @@ def update(self, sim_data_items, check_add_param = True):
if hasattr(sim_data_item, 'qp') and not QP_added:
self.dialog.chosen_par.addItems(['QP'])
QP_added = True
# print(sim_data_item.summary_data['encoder_config'])
value_filter = ['.yuv', '.bin', '.hevc', '.jem']
key_filter = []
for sim_class in all_log_configs.keys():
Expand Down
14 changes: 7 additions & 7 deletions src/rdplot/ui/mainWindow.ui
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,6 @@ padding: 4px;
<item>
<widget class="QComboBox" name="combo_rate_psnr"/>
</item>
<item>
<widget class="QCheckBox" name="checkBox_plot_ci">
<property name="text">
<string>Plot CI as area</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_bdplot">
<property name="text">
Expand Down Expand Up @@ -301,6 +294,13 @@ padding: 4px;
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_plot_ci">
<property name="text">
<string>Plot CI as area</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
Expand Down
14 changes: 12 additions & 2 deletions src/rdplot/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,14 @@ def run(self):
try:
sim_data_items = self._factory.create_item_list_from_path(path)
print("Parsed '{}' ".format(path))
except SimulationDataItemError:
except SimulationDataItemError as error:
self.newParsedData.emit([])
self.path_list.clear()

# print((
# "Could not create simulation data item from file '{}'"
# " due to {}"
# ).format(path, error))
return

self.newParsedData.emit(sim_data_items)
Expand Down Expand Up @@ -117,9 +122,14 @@ def run(self):
try:
sim_data_items = self._factory.create_item_list_from_path(path)
print("Parsed '{}' ".format(path))
except SimulationDataItemError:
except SimulationDataItemError as error:
self.newParsedData.emit([])
self.path_list.clear()

# print((
# "Could not create simulation data item from file '{}'"
# " due to {}"
# ).format(path, error))
return

self.newParsedData.emit(sim_data_items)
Expand Down