Tuesday 12 March 2013

Plot Multi-Channel Histogram in QWT -Part 2

This is the second part in a series on how to do multi-channel plots in QWT. These kinds of plots can be done very simply in Matlab but are more challenging to implement in C++. Armed with the QWT library, we have investigated how to overlay multiple channel histograms on each other. Here I will show you how to make a plot similar to what you would do with the subplot() function in Matlab. This method simply puts multiple channels into one window in a grid form; this method is more presentable and makes it easier to compare plots. Lets get started...

Code

The most important part of the code is the PlotMatrix class which uses a QGridLayout to arrange the sub-plots into a matrix with specified number of rows and cols.The class is defined in the plotmatix.h header file and implemented in plotmatrix.cpp. In main.cpp we define and implement a MainWindow class which inherits from the PlotMatrix class, the histograms are created from an image and displayed in the MainWindow.

The Main function and others

The MainWindow class is a derived class from the PlotMatrix class. Here, we define a 2 by 3 matrix of plots in a static manner(Although, we only want to plot 3 channels). Grid lines are included in the plots for fun but this can be removed or edited as needed.
#include <QApplication>
#include <plotmatrix.h>
#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_symbol.h>
#include <qwt_legend.h>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

using namespace cv;

class MainWindow: public PlotMatrix
{
public:
   MainWindow():PlotMatrix( 2, 3 )
   {
     for ( int row = 0; row < numRows(); row++ )
     {
        for ( int col = 0; col < numColumns(); col++ )
        {
            //set default scale if any.
            setAxisScale( QwtPlot::yLeft, row, col, 0, 100 );
            setAxisScale( QwtPlot::xBottom, row, col, 0, 255 );
            QwtPlot *plt = plot( row, col );
            plt->setCanvasBackground( QColor( Qt::darkGray ) );
            //Allows the Axis to scale automatically. 
            plt->setAxisAutoScale(QwtPlot::yLeft, true);
            //Show plot grid lines just for aesthetics.
            QwtPlotGrid *grid = new QwtPlotGrid();
            grid->enableXMin( true );
            grid->setMajorPen( Qt::white, 0, Qt::DashLine);
            grid->setMinorPen( Qt::gray, 0 , Qt::DotLine );
            grid->attach( plt );
        }
     }
   }
};

void getPoints(MatND& hist, int* histSize, QPolygonF& points)
{
    points.clear();
    for( int h = 0; h < histSize[0]; ++h) {
      float bin_value = hist.at<float>(h);
      points << QPointF((float)h, bin_value);
    }
}

class Curve: public QwtPlotCurve
{
public:
    Curve( const QString &title, const QColor &colour, int penSize, 
           QPolygonF &points ):QwtPlotCurve( title )
    {
        QColor c = colour;
        c.setAlpha( 150 );//handle colour transparency
        setPen(c, penSize);
        setBrush( c );
        setSamples( points );
        setRenderHint( QwtPlotItem::RenderAntialiased );
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    if (argc < 2)
        return 1;

    //Read input image
    Mat img = imread(argv[1]);

    //Convert to grayscale
    if (img.data && img.channels() != 3)
       return 1;

    MainWindow mainWindow;
    mainWindow.resize( 600, 400 );

    int histSize[] = {256}; // number of bins
    float hranges[] = {0.0, 255.0}; // min and max pixel value
    const float* ranges[] = {hranges};
    int channels[] = {0}; // only 1 channel used

    std::vector<Mat> rgbChannels(3);
    split(img, rgbChannels);

    MatND hist;
    QPolygonF points;

    calcHist(&rgbChannels[2], 1, channels, Mat(), hist, 1, histSize,ranges);
    /*Insert the points that should be plotted on the graph in a
    Vector of QPoints or a QPolgonF */
    getPoints(hist, histSize, points);
    Curve *curve = new Curve("Red Channel", Qt::red, 2, points);
    curve->attach( mainWindow.plot(0,0) ); // Attach curve to the plot

    calcHist(&rgbChannels[1], 1, channels, Mat(), hist, 1, histSize,ranges);
    getPoints(hist, histSize, points);
    curve = new Curve("Green Channel", Qt::green, 2, points);
    curve->attach( mainWindow.plot(0,1) );

    calcHist(&rgbChannels[0], 1, channels, Mat(), hist, 1, histSize,ranges);
    getPoints(hist, histSize, points);
    curve = new Curve("Blue Channel", Qt::blue, 2, points);
    curve->attach( mainWindow.plot(0,2) );


    mainWindow.show(); //Show plot
    return a.exec();
}

PlotMatrix class

This implementation uses a QVector of pointers to QwtPlot instances to represent the matrix of subplots. It provides a function plot(row, col) which returns the pointer to the desired plot.
//PlotMatrix.h

#ifndef _PLOT_MATRIX_H_
#define _PLOT_MATRIX_H_

#include <qframe.h>
#include <qwt_plot.h>

class PlotMatrix: public QFrame
{
    Q_OBJECT

public:
    PlotMatrix( int rows, int columns, QWidget * parent = NULL );
    virtual ~PlotMatrix();

    int numRows() const;
    int numColumns() const;

    QwtPlot* plot( int row, int column );
    const QwtPlot* plot( int row, int column ) const;

    void setAxisScale(int axisId, int row, int col,
        double min, double max, double step = 0 );

private:
    QVector<QwtPlot *> plotWidgets;
};

#endif

//PlotMatrix.cpp
//This is based on the PlotMatrix implementation in the Qwt Samples. 
#include <qlayout.h>
#include <qpen.h>
#include <qwt_plot.h>
#include <qwt_scale_widget.h>
#include <qwt_scale_draw.h>
#include "plotmatrix.h"

PlotMatrix::PlotMatrix( int numRows, int numColumns, QWidget *parent ):
    QFrame( parent )
{
    plotWidgets.resize( numRows * numColumns );

    QGridLayout *layout = new QGridLayout( this );
    for ( int row = 0; row < numRows; row++ )
    {
        for ( int col = 0; col < numColumns; col++ )
        {
            QwtPlot *plot = new QwtPlot( this );
            layout->addWidget( plot, row, col );
            plotWidgets[row * numColumns + col] = plot;
        }
    }
}

PlotMatrix::~PlotMatrix()
{
    foreach(QwtPlot* p, plotWidgets)
        delete p;
}

int PlotMatrix::numRows() const
{
    const QGridLayout *l = qobject_cast<const QGridLayout *>( layout() );
    if ( l )
        return l->rowCount();

    return 0;
}

int PlotMatrix::numColumns() const
{
    const QGridLayout *l = qobject_cast<const QGridLayout *>( layout() );
    if ( l )
        return l->columnCount();
    return 0;
}

QwtPlot* PlotMatrix::plot( int row, int column )
{
    const int index = row * numColumns() + column;
    if ( index < plotWidgets.size() )
        return plotWidgets[index];

    return NULL;
}

const QwtPlot* PlotMatrix::plot( int row, int column ) const
{
    const int index = row * numColumns() + column;
    if ( index <plotWidgets.size() )
        return plotWidgets[index];

    return NULL;
}

void PlotMatrix::setAxisScale( int axis, int row, int col,
    double min, double max, double step )
{

    QwtPlot *plt = plot( row, col );
    if ( plt )
    {
        plt->setAxisScale( axis, min, max, step );
        plt->updateAxes();
    }
}

Result

Conclusion

The subplot() is one of the most useful matlab functions if you plot several graphs or images. Now you can have something similar in QWT although not as simple. It is also possible to plot multi-channel histograms in a 3 dimensional plot, we leave this for another time. Happy Coding!.

Saturday 2 March 2013

Plot Multi-Channel Histogram in QWT -Part 1

Here I will show you how to plot a multi-channel/colour histogram using QWT. I generate the histograms for the different channels in a RGB image using OpenCV, then I plot the histogram of each channel overlaid on each other on the same axis. This method is suitable for visualization but not very suitable for peak/valley histogram analysis as the overlaid colours would merge making it difficult to see what peaks/valleys belong to which channel.
The set-up for this tutorial is similar to the one in my previous tutorial, only this time I also include the OpenCV library. Again, we can just dive straight into the code.

Code

#include <QApplication>
#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_symbol.h>
#include <qwt_legend.h>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>

using namespace cv;

void getPoints(MatND& hist, int* histSize, QPolygonF& points)
{
    for( int h = 0; h < histSize[0]; ++h) {
      float bin_value = hist.at<float>(h);
      points << QPointF((float)h, bin_value);
    }
}


class Curve: public QwtPlotCurve
{
public:
    Curve( const QString &title ):
        QwtPlotCurve( title )
    {
        setRenderHint( QwtPlotItem::RenderAntialiased );
    }

    void setColour(const QColor &color, int penSize)
    {
        QColor c = color;
        c.setAlpha( 150 );
        setPen(c, penSize);
        setBrush( c );
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    if (argc < 2)
        return 1;

    //Read input image
    Mat img = cv::imread(argv[1]);

    //Convert to grayscale
    if (img.data && img.channels() != 3)
       return 1;

    QwtPlot plot; //Create plot widget
    plot.setTitle( "Plot Demo" ); //Name the plot
    plot.setCanvasBackground(Qt::white ); //Set the Background colour
    plot.setAxisScale(QwtPlot::xBottom,0,255); //Scale the x-axis
    plot.insertLegend(new QwtLegend()); //Insert a legend

    int histSize[] = {256}; // number of bins
    float hranges[] = {0.0, 255.0}; // min and max pixel value
    const float* ranges[] = {hranges};
    int channels[] = {0}; // only 1 channel used

    std::vector<cv::Mat> rgbChannels(3);
    split(img, rgbChannels);

    MatND hist;
    QPolygonF points;

    calcHist(&rgbChannels[2], 1, channels, cv::Mat(), hist, 1, histSize,ranges);
    Curve *curve = new Curve("Red Channel");
    curve->setColour(Qt::red , 2);//Set colour and thickness for drawn curve.
    /*Insert the points that should be plotted on the graph in a
    Vector of QPoints or a QPolgonF */

    getPoints(hist, histSize, points);
    curve->setZ( curve->z() - 1 );
    curve->setSamples(points); //pass points to be drawn on the curve
    curve->attach( &plot ); // Attach curve to the plot


    calcHist(&rgbChannels[1], 1, channels, cv::Mat(), hist, 1, histSize,ranges);
    curve = new Curve("Green Channel");
    curve->setColour(Qt::green , 2);
    points.clear();
    getPoints(hist, histSize, points);
    curve->setZ( curve->z() - 2 );
    curve->setSamples( points );
    curve->attach( &plot );


    calcHist(&rgbChannels[0], 1, channels, cv::Mat(), hist, 1, histSize,ranges);
    curve = new Curve("Blue Channel");
    curve->setColour(Qt::blue, 2);
    points.clear();
    getPoints(hist, histSize, points);
    curve->setZ( curve->z() - 3 );
    curve->setSamples( points );
    curve->attach( &plot );

    plot.resize( 600, 400 ); //Resize the plot
    plot.show(); //Show plot
    return a.exec();

}

Result

Figure 1: A) The input image B) The resulting plot 
Note: It is possible to remove the coloured area under the curves and reduce the plot to a simple line plot. This would make each channels' peaks and valleys more visible. This could be done by commenting out line:36 in the code. The result is shown below.
Figure 2: The result with the setBrush() property turned off.

Conclusion

That concludes this part of the tutorial, it is also possible to plot each histogram in its separate axis (in a matrix format) similar to what using the subplot() function in matlab does. This would be seen in our next tutorial. Happy Coding!.