Files
q3DMASC/confusionmatrix.cpp
T

305 lines
9.6 KiB
C++
Raw Normal View History

2022-12-14 11:37:29 +01:00
#include "confusionmatrix.h"
#include "ui_confusionmatrix.h"
2022-12-16 16:13:15 +01:00
#include <CCConst.h>
2022-12-14 11:37:29 +01:00
#include <iterator>
#include <set>
#include <algorithm>
#include <QBrush>
#include <QFile>
#include <QTextStream>
#include <iostream>
#include <ccLog.h>
2022-12-16 16:13:15 +01:00
2023-05-29 12:45:02 +02:00
#include <ccLog.h>
QColor getColor(double value, double r1, double g1, double b1)
{
double r0 = 255;
double g0 = 255;
double b0 = 255;
if (value < 0.05)
value = 0.05;
if (value > 0.95)
value = 0.95;
2023-05-29 12:45:02 +02:00
int r = int((r1 - r0) * value + r0);
int g = int ((g1 - g0) * value + g0);
int b = int ((b1 - b0) * value + b0);
// ccLog::Warning("value " + QString::number(value) + " (" + QString::number(r) + ", " + QString::number(g) + ", " + QString::number(b) + ")");
2023-05-29 12:45:02 +02:00
return QColor(r, g, b);
}
ConfusionMatrix::ConfusionMatrix(const std::vector<ScalarType> &actual, const std::vector<ScalarType> &predicted, QWidget *parent) :
2022-12-14 11:37:29 +01:00
QWidget(parent),
ui(new Ui::ConfusionMatrix)
{
ui->setupUi(this);
2023-01-18 23:57:59 +01:00
this->setWindowFlag(Qt::WindowStaysOnTopHint);
2023-01-17 16:43:57 +01:00
compute(actual, predicted);
2023-01-18 23:57:59 +01:00
this->show();
2023-05-29 12:45:02 +02:00
this->ui->tableWidget->resizeColumnsToContents();
this->ui->tableWidget->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents);
QSize tableSize = this->ui->tableWidget->sizeHint();
QSize labelSize = this->ui->label->sizeHint();
QSize widgetSize = QSize(tableSize.width() + 20, tableSize.height() + 50);
2023-05-29 12:45:02 +02:00
this->setMinimumSize(widgetSize);
2022-12-14 11:37:29 +01:00
}
ConfusionMatrix::~ConfusionMatrix()
{
delete ui;
}
2023-05-29 12:45:02 +02:00
void ConfusionMatrix::computePrecisionRecallF1Score(cv::Mat& matrix, cv::Mat& precisionRecallF1Score, cv::Mat& vec_TP_FN)
2022-12-14 11:37:29 +01:00
{
int nbClasses = matrix.rows;
2022-12-16 16:13:15 +01:00
// compute precision
for (int predictedIdx = 0; predictedIdx < nbClasses; predictedIdx++)
2022-12-16 16:13:15 +01:00
{
float TP = 0;
float FP = 0;
for (int realIdx = 0; realIdx < nbClasses; realIdx++)
2022-12-16 16:13:15 +01:00
{
if (realIdx == predictedIdx)
TP = matrix.at<int>(realIdx, realIdx);
2022-12-16 16:13:15 +01:00
else
FP += matrix.at<int>(realIdx, predictedIdx);
2022-12-16 16:13:15 +01:00
}
float TP_FP = TP + FP;
if (TP_FP == 0)
precisionRecallF1Score.at<float>(predictedIdx, PRECISION) = CCCoreLib::NAN_VALUE;
2022-12-16 16:13:15 +01:00
else
precisionRecallF1Score.at<float>(predictedIdx, PRECISION) = TP / TP_FP;
2022-12-16 16:13:15 +01:00
}
2022-12-16 16:13:15 +01:00
// compute recall
for (int realIdx = 0; realIdx < nbClasses; realIdx++)
2022-12-16 16:13:15 +01:00
{
float TP = 0;
float FN = 0;
for (int predictedIdx = 0; predictedIdx< nbClasses; predictedIdx++)
2022-12-16 16:13:15 +01:00
{
if (realIdx == predictedIdx)
TP = matrix.at<int>(realIdx, realIdx);
2022-12-16 16:13:15 +01:00
else
FN += matrix.at<int>(realIdx, predictedIdx);
2022-12-16 16:13:15 +01:00
}
float TP_FN = TP + FN;
if (TP_FN == 0)
precisionRecallF1Score.at<float>(realIdx, RECALL) = CCCoreLib::NAN_VALUE;
else
precisionRecallF1Score.at<float>(realIdx, RECALL) = TP / TP_FN;
2023-05-29 12:45:02 +02:00
vec_TP_FN.at<int>(realIdx, 0) = TP_FN;
2022-12-16 16:13:15 +01:00
}
// compute F1-score
for (int realIdx = 0; realIdx < nbClasses; realIdx++)
{
float den = precisionRecallF1Score.at<float>(realIdx, PRECISION)
+ precisionRecallF1Score.at<float>(realIdx, RECALL);
if (den == 0)
precisionRecallF1Score.at<float>(realIdx, F1_SCORE) = CCCoreLib::NAN_VALUE;
else
precisionRecallF1Score.at<float>(realIdx, F1_SCORE) =
2
* precisionRecallF1Score.at<float>(realIdx, PRECISION)
* precisionRecallF1Score.at<float>(realIdx, RECALL)
/ den;
}
2022-12-14 11:37:29 +01:00
}
float ConfusionMatrix::computeOverallAccuracy(cv::Mat& matrix)
2022-12-14 11:37:29 +01:00
{
int nbClasses = matrix.rows;
float totalTrue = 0;
float totalFalse = 0;
2023-03-22 23:45:19 +01:00
m_overallAccuracy = 0.0;
for (int realIdx = 0; realIdx < nbClasses; realIdx++)
2022-12-14 11:37:29 +01:00
{
for (int predictedIdx = 0; predictedIdx< nbClasses; predictedIdx++)
2022-12-14 11:37:29 +01:00
{
if (realIdx == predictedIdx)
totalTrue += matrix.at<int>(realIdx, realIdx);
2022-12-14 11:37:29 +01:00
else
totalFalse += matrix.at<int>(realIdx, predictedIdx);
2022-12-14 11:37:29 +01:00
}
}
if ((totalTrue + totalFalse) != 0)
2023-03-22 23:45:19 +01:00
m_overallAccuracy = totalTrue / (totalTrue + totalFalse);
else
2023-03-22 23:45:19 +01:00
m_overallAccuracy = CCCoreLib::NAN_VALUE;
2023-03-22 23:45:19 +01:00
return m_overallAccuracy;
}
void ConfusionMatrix::compute(const std::vector<ScalarType>& actual, const std::vector<ScalarType>& predicted)
{
int idxActual;
int idxPredicted;
int actualClass;
int predictedClass;
// get the set of classes with the contents of the actual classes
std::set<ScalarType> classes(actual.begin(), actual.end());
int nbClasses = classes.size();
cv::Mat confusionMatrix(nbClasses, nbClasses, CV_32S, cv::Scalar(0));
cv::Mat precisionRecallF1Score(nbClasses, 3, CV_32F, cv::Scalar(0));
2023-05-29 12:45:02 +02:00
cv::Mat vec_TP_FN(nbClasses, 1, CV_32S, cv::Scalar(0));
// fill the confusion matrix
for (int i = 0; i < actual.size(); i++)
{
actualClass = actual.at(i);
idxActual = std::distance(classes.begin(), classes.find(actualClass));
predictedClass = predicted.at(i);
idxPredicted = std::distance(classes.begin(), classes.find(predictedClass));
confusionMatrix.at<int>(idxActual, idxPredicted)++;
}
// compute precision recall F1-score
2023-05-29 12:45:02 +02:00
computePrecisionRecallF1Score(confusionMatrix, precisionRecallF1Score, vec_TP_FN);
float overallAccuracy = computeOverallAccuracy(confusionMatrix);
// display the overall accuracy
this->ui->label_overallAccuracy->setText(QString::number(overallAccuracy, 'g', 2));
2022-12-14 11:37:29 +01:00
std::set<ScalarType>::iterator itB = classes.begin();
std::set<ScalarType>::iterator itE = classes.end();
class_numbers.assign(itB, itE);
2022-12-14 11:37:29 +01:00
// BUILD THE QTABLEWIDGET
this->ui->tableWidget->setColumnCount(2+ nbClasses + 3); // +2 for titles, +3 for precision / recall / F1-score
this->ui->tableWidget->setRowCount(2 + nbClasses);
// create a font for the table widgets
QFont font;
2022-12-14 11:37:29 +01:00
font.setBold(true);
QTableWidgetItem *newItem = nullptr;
// set the row and column names
this->ui->tableWidget->setSpan(0, 0, 2, 2); // empty area
this->ui->tableWidget->setSpan(0, 2, 1, nbClasses); // 'Predicted' header
this->ui->tableWidget->setSpan(2, 0, nbClasses, 1); // 'Actual' header
this->ui->tableWidget->setSpan(0, 2 + nbClasses, 1, 3); // empty area
// Predicted
newItem = new QTableWidgetItem("Predicted");
2022-12-14 11:37:29 +01:00
newItem->setFont(font);
newItem->setBackground(Qt::lightGray);
newItem->setTextAlignment(Qt::AlignCenter);
2022-12-14 11:37:29 +01:00
this->ui->tableWidget->setItem(0, 2, newItem);
2023-05-29 12:45:02 +02:00
// Real
newItem = new QTableWidgetItem("Real");
2022-12-14 11:37:29 +01:00
newItem->setFont(font);
newItem->setBackground(Qt::lightGray);
newItem->setTextAlignment(Qt::AlignCenter);
2022-12-14 11:37:29 +01:00
this->ui->tableWidget->setItem(2, 0, newItem);
// add precision / recall / F1-score headers
newItem = new QTableWidgetItem("Precision");
newItem->setToolTip("TP / (TP + FP)");
newItem->setFont(font);
this->ui->tableWidget->setItem(1, 2 + nbClasses + PRECISION, newItem);
newItem = new QTableWidgetItem("Recall");
newItem->setToolTip("TP / (TP + FN)");
newItem->setFont(font);
this->ui->tableWidget->setItem(1, 2 + nbClasses + RECALL, newItem);
newItem = new QTableWidgetItem("F1-score");
newItem->setToolTip("Harmonic mean of precision and recall (the closer to 1 the better)\n2 x precision x recall / (precision + recall)");
newItem->setFont(font);
this->ui->tableWidget->setItem(1, 2 + nbClasses + F1_SCORE, newItem);
// add column names and row names
for (int idx = 0; idx < class_numbers.size(); idx++)
2022-12-14 11:37:29 +01:00
{
QString str = QString::number(class_numbers[idx]);
2022-12-14 11:37:29 +01:00
newItem = new QTableWidgetItem(str);
newItem->setFont(font);
this->ui->tableWidget->setItem(1, 2 + idx, newItem);
newItem = new QTableWidgetItem(str);
newItem->setFont(font);
this->ui->tableWidget->setItem(2 + idx, 1, newItem);
}
// FILL THE QTABLEWIDGET
// add the confusion matrix values
for (int row = 0; row < nbClasses; row++)
for (int column = 0; column < nbClasses; column++)
{
2023-05-29 12:45:02 +02:00
double val = confusionMatrix.at<int>(row, column);
QTableWidgetItem *newItem = new QTableWidgetItem(QString::number(val));
if (row == column)
{
2023-05-29 12:45:02 +02:00
newItem->setBackground(getColor(val / vec_TP_FN.at<int>(row, 0), 0, 128, 255));
}
else
newItem->setBackground(getColor(val / vec_TP_FN.at<int>(row, 0), 200, 50, 50));
this->ui->tableWidget->setItem(2 + row, + 2 + column, newItem);
}
// set precision / recall / F1-score values
for (int realIdx=0; realIdx < nbClasses; realIdx++)
{
newItem = new QTableWidgetItem(QString::number(precisionRecallF1Score.at<float>(realIdx, PRECISION), 'g', 2));
this->ui->tableWidget->setItem(2 + realIdx, 2 + nbClasses + PRECISION, newItem);
newItem = new QTableWidgetItem(QString::number(precisionRecallF1Score.at<float>(realIdx, RECALL), 'g', 2));
this->ui->tableWidget->setItem(2 + realIdx, 2 + nbClasses + RECALL, newItem);
newItem = new QTableWidgetItem(QString::number(precisionRecallF1Score.at<float>(realIdx, F1_SCORE), 'g', 2));
this->ui->tableWidget->setItem(2 + realIdx, 2 + nbClasses + F1_SCORE, newItem);
}
2022-12-14 11:37:29 +01:00
}
2023-03-22 23:45:19 +01:00
void ConfusionMatrix::setSessionRun(QString session, int run)
{
QString label;
label = session + " / " + QString::number(run);
this->ui->label_sessionRun->setText(label);
}
bool ConfusionMatrix::save(QString filePath)
{
std::unique_ptr<QFile> file(new QFile(filePath));
QTextStream stream;
if(!file->open(QIODevice::WriteOnly | QIODevice::Text))
{
ccLog::Error("impossible to open file: " + filePath);
return false;
}
if (file && file->isOpen())
{
stream.setDevice(file.get());
stream << "# columns: predicted classes\n# rows: actual classes\n";
stream << "# last three colums: precision / recall / F1-score\n";
for (auto class_number : class_numbers)
{
stream << class_number << " ";
}
stream << Qt::endl;
for (int row = 0; row < confusionMatrix.rows; row++)
{
stream << class_numbers.at(row) << " ";
for (int col = 0; col < confusionMatrix.cols; col++)
{
stream << confusionMatrix.at<int>(row, col) << " ";
}
stream << precisionRecallF1Score.at<float>(row, PRECISION) << " ";
stream << precisionRecallF1Score.at<float>(row, RECALL) << " ";
stream << precisionRecallF1Score.at<float>(row, F1_SCORE) << Qt::endl;
}
file->close();
return true;
}
else
return false;
}