From 6da67a32e56c101b9334d2c6f33bd5238d082330 Mon Sep 17 00:00:00 2001 From: Samuel Fadel Date: Fri, 24 Jul 2015 13:01:16 -0300 Subject: Color mapping in Scatterplot and initial measures. - Scatterplot: can now map any floating point data to colors; - Scatterplot: somewhat optimized geometry/material updates; - Removed anything related to labels where it was not necessary; - Added observers to implement distortion (via measures) visualization; - Added skeleton implementations of NP and silhouette. --- dist.cpp | 4 +- distortionobserver.cpp | 27 ++++++++++ distortionobserver.h | 29 ++++++++++ interactionhandler.cpp | 5 +- interactionhandler.h | 6 +-- main.cpp | 22 +++++--- measures.cpp | 17 ++++++ mp.h | 15 +++++- npdistortion.cpp | 14 +++++ npdistortion.h | 18 +++++++ pm.pro | 5 ++ scatterplot.cpp | 142 ++++++++++++++++++++++++++++++++----------------- scatterplot.h | 23 +++++--- 13 files changed, 255 insertions(+), 72 deletions(-) create mode 100644 distortionobserver.cpp create mode 100644 distortionobserver.h create mode 100644 measures.cpp create mode 100644 npdistortion.cpp create mode 100644 npdistortion.h diff --git a/dist.cpp b/dist.cpp index aaa2167..a5620ad 100644 --- a/dist.cpp +++ b/dist.cpp @@ -5,14 +5,14 @@ double mp::euclidean(const arma::rowvec &x1, const arma::rowvec &x2) return arma::norm(x1 - x2, 2); } -arma::mat mp::dist(const arma::mat &X, double (*distCalc)(const arma::rowvec &, const arma::rowvec &)) +arma::mat mp::dist(const arma::mat &X, mp::DistFunc dfunc) { arma::uword n = X.n_rows; arma::mat D(n, n, arma::fill::zeros); for (arma::uword i = 0; i < n; i++) for (arma::uword j = 0; j < i; j++) { - D(i, j) = distCalc(X.row(i), X.row(j)); + D(i, j) = dfunc(X.row(i), X.row(j)); D(j, i) = D(i, j); } diff --git a/distortionobserver.cpp b/distortionobserver.cpp new file mode 100644 index 0000000..a6c5ba4 --- /dev/null +++ b/distortionobserver.cpp @@ -0,0 +1,27 @@ +#include "distortionobserver.h" + +#include "mp.h" + +DistortionObserver::DistortionObserver(const arma::mat &X, + const arma::uvec &sampleIndices) + : m_X(X) + , m_sampleIndices(sampleIndices) +{ + m_distX = mp::dist(m_X); +} + +DistortionObserver::~DistortionObserver() +{ +} + +void DistortionObserver::setMap(const arma::mat &Y) +{ + arma::vec measures = measureFunc(m_distX, mp::dist(Y)); + + if (m_Y.n_elem != 0) { + emit mapChanged(measures - m_measures); + } else { + m_Y = Y; + m_measures = measures; + } +} diff --git a/distortionobserver.h b/distortionobserver.h new file mode 100644 index 0000000..9141d25 --- /dev/null +++ b/distortionobserver.h @@ -0,0 +1,29 @@ +#ifndef DISTORTIONOBSERVER_H +#define DISTORTIONOBSERVER_H + +#include +#include + +class DistortionObserver : public QObject +{ + Q_OBJECT +public: + DistortionObserver(const arma::mat &X, const arma::uvec &sampleIndices); + virtual ~DistortionObserver(); + +signals: + void mapChanged(const arma::vec &distortion); + +public slots: + void setMap(const arma::mat &Y); + +protected: + virtual arma::vec measureFunc(const arma::mat &distA, const arma::mat &distB) = 0; + +private: + arma::mat m_X, m_Y, m_distX; + arma::uvec m_sampleIndices; + arma::vec m_measures; +}; + +#endif // DISTORTIONOBSERVER_H diff --git a/interactionhandler.cpp b/interactionhandler.cpp index 50d0653..b79fdd8 100644 --- a/interactionhandler.cpp +++ b/interactionhandler.cpp @@ -3,15 +3,12 @@ #include "mp.h" InteractionHandler::InteractionHandler(const arma::mat &X, - const arma::vec &labels, const arma::uvec &sampleIndices) : m_X(X) - , m_Y(X.n_rows, 3) - , m_labels(labels) + , m_Y(X.n_rows, 2) , m_sampleIndices(sampleIndices) , m_technique(TECHNIQUE_LAMP) { - m_Y.col(2) = m_labels; } void InteractionHandler::setSubsample(const arma::mat &Ys) diff --git a/interactionhandler.h b/interactionhandler.h index 5468dab..0104d65 100644 --- a/interactionhandler.h +++ b/interactionhandler.h @@ -1,10 +1,9 @@ #ifndef INTERACTIONHANDLER_H #define INTERACTIONHANDLER_H +#include #include -#include "scatterplot.h" - class InteractionHandler : public QObject { Q_OBJECT @@ -15,7 +14,7 @@ public: TECHNIQUE_LSP }; - InteractionHandler(const arma::mat &X, const arma::vec &labels, const arma::uvec &sampleIndices); + InteractionHandler(const arma::mat &X, const arma::uvec &sampleIndices); signals: void subsampleChanged(const arma::mat &Y); @@ -25,7 +24,6 @@ public slots: private: arma::mat m_X, m_Y; - arma::vec m_labels; arma::uvec m_sampleIndices; InteractiveTechnique m_technique; }; diff --git a/main.cpp b/main.cpp index 84579b5..70499e7 100644 --- a/main.cpp +++ b/main.cpp @@ -8,6 +8,8 @@ #include "mp.h" #include "scatterplot.h" #include "interactionhandler.h" +#include "distortionobserver.h" +#include "npdistortion.h" int main(int argc, char **argv) { @@ -35,21 +37,27 @@ int main(int argc, char **argv) arma::uvec sampleIndices = arma::randi(subsampleSize, arma::distr_param(0, n-1)); arma::mat Ys(subsampleSize, 2, arma::fill::randn); Ys = mp::forceScheme(mp::dist(X.rows(sampleIndices)), Ys); - arma::mat subsampleData(subsampleSize, 3); - subsampleData.cols(0, 1) = Ys; - subsampleData.col(2) = labels(sampleIndices); Scatterplot *subsamplePlot = engine.rootObjects()[0]->findChild("subsamplePlot"); subsamplePlot->setAcceptedMouseButtons(Qt::LeftButton | Qt::MiddleButton | Qt::RightButton); - subsamplePlot->setData(subsampleData); + subsamplePlot->setXY(Ys); + subsamplePlot->setColorData(labels(sampleIndices)); Scatterplot *plot = engine.rootObjects()[0]->findChild("plot"); // connect both plots through interaction handler - InteractionHandler interactionHandler(X, labels, sampleIndices); - QObject::connect(subsamplePlot, SIGNAL(dataChanged(const arma::mat &)), + InteractionHandler interactionHandler(X, sampleIndices); + QObject::connect(subsamplePlot, SIGNAL(xyChanged(const arma::mat &)), &interactionHandler, SLOT(setSubsample(const arma::mat &))); QObject::connect(&interactionHandler, SIGNAL(subsampleChanged(const arma::mat &)), - plot, SLOT(setData(const arma::mat &))); + plot, SLOT(setXY(const arma::mat &))); + + // TODO: works; though it needs measures implementation + // std::unique_ptr distortionObs(new NPDistortion(X, sampleIndices)); + // QObject::connect(&interactionHandler, SIGNAL(subsampleChanged(const arma::mat &)), + // distortionObs.get(), SLOT(setMap(const arma::mat &))); + // QObject::connect(distortionObs.get(), SIGNAL(mapChanged(const arma::vec &)), + // plot, SLOT(setColorData(const arma::vec &))); + interactionHandler.setSubsample(Ys); return app.exec(); diff --git a/measures.cpp b/measures.cpp new file mode 100644 index 0000000..fe4077a --- /dev/null +++ b/measures.cpp @@ -0,0 +1,17 @@ +#include "mp.h" + +arma::vec mp::neighborhoodPreservation(const arma::mat &distA, + const arma::mat &distB, + int k) +{ + // TODO + return arma::vec(distA.n_rows, arma::fill::zeros); +} + +arma::vec mp::silhouette(const arma::mat &distA, + const arma::mat &distB, + const arma::vec &labels) +{ + // TODO + return arma::vec(distA.n_rows, arma::fill::zeros); +} diff --git a/mp.h b/mp.h index 7d6b535..a9ddd38 100644 --- a/mp.h +++ b/mp.h @@ -1,10 +1,21 @@ +#ifndef MP_H +#define MP_H + #include namespace mp { +// --- Distance-related +typedef double (*DistFunc)(const arma::rowvec &, const arma::rowvec &); double euclidean(const arma::rowvec &x1, const arma::rowvec &x2); -arma::mat dist(const arma::mat &X, double (*distCalc)(const arma::rowvec &, const arma::rowvec &) = euclidean); +arma::mat dist(const arma::mat &X, DistFunc dfunc = euclidean); + +// --- Evaluation measures +typedef arma::vec (*MeasureFunc)(const arma::mat &distA, const arma::mat &distB); +arma::vec neighborhoodPreservation(const arma::mat &distA, const arma::mat &distB, int k = 10); +arma::vec silhouette(const arma::mat &distA, const arma::mat &distB, const arma::vec &labels); +// --- Techniques arma::mat lamp(const arma::mat &X, const arma::uvec &sampleIndices, const arma::mat &Ys); void lamp(const arma::mat &X, const arma::uvec &sampleIndices, const arma::mat &Ys, arma::mat &Y); @@ -14,3 +25,5 @@ arma::mat tSNE(const arma::mat &X, arma::uword k = 2, double perplexity = 30, ar void tSNE(const arma::mat &X, arma::mat &Y, double perplexity = 30, arma::uword nIter = 1000); } // namespace mp + +#endif // MP_H diff --git a/npdistortion.cpp b/npdistortion.cpp new file mode 100644 index 0000000..71db85b --- /dev/null +++ b/npdistortion.cpp @@ -0,0 +1,14 @@ +#include "npdistortion.h" + +#include "mp.h" + +NPDistortion::NPDistortion(const arma::mat &X, const arma::uvec &sampleIndices, int k) + : DistortionObserver(X, sampleIndices) + , m_k(k) +{ +} + +arma::vec NPDistortion::measureFunc(const arma::mat &distA, const arma::mat &distB) +{ + return mp::neighborhoodPreservation(distA, distB, m_k); +} diff --git a/npdistortion.h b/npdistortion.h new file mode 100644 index 0000000..a84f63a --- /dev/null +++ b/npdistortion.h @@ -0,0 +1,18 @@ +#ifndef NPDISTORTION_H +#define NPDISTORTION_H + +#include "distortionobserver.h" + +class NPDistortion : public DistortionObserver +{ +public: + NPDistortion(const arma::mat &X, const arma::uvec &sampleIndices, int k = 10); + +protected: + arma::vec measureFunc(const arma::mat &distA, const arma::mat &distB); + +private: + int m_k; +}; + +#endif // NPDISTORTION_H diff --git a/pm.pro b/pm.pro index 3ca2aaa..8856347 100644 --- a/pm.pro +++ b/pm.pro @@ -5,14 +5,19 @@ QMAKE_LIBS += -larmadillo -fopenmp HEADERS += colorscale.h \ scatterplot.h \ interactionhandler.h \ + distortionobserver.h \ + npdistortion.h \ mp.h SOURCES += main.cpp \ colorscale.cpp \ scatterplot.cpp \ interactionhandler.cpp \ + distortionobserver.cpp \ + npdistortion.cpp \ lamp.cpp \ forceScheme.cpp \ tsne.cpp \ + measures.cpp \ dist.cpp RESOURCES += pm.qrc diff --git a/scatterplot.cpp b/scatterplot.cpp index 0c2fbc1..47066fa 100644 --- a/scatterplot.cpp +++ b/scatterplot.cpp @@ -11,6 +11,8 @@ static const float PI = 3.1415f; Scatterplot::Scatterplot(QQuickItem *parent) : QQuickItem(parent) + , m_shouldUpdateGeometry(false) + , m_shouldUpdateMaterials(false) , m_colorScale{ QColor("#1f77b4"), QColor("#ff7f0e"), @@ -27,24 +29,53 @@ Scatterplot::Scatterplot(QQuickItem *parent) setFlag(QQuickItem::ItemHasContents); } -Scatterplot::~Scatterplot() +void Scatterplot::setColorScale(const ColorScale &colorScale) { + m_colorScale = colorScale; + + if (m_colorData.n_elem > 0) { + m_colorScale.setExtents(m_colorData.min(), m_colorData.max()); + updateMaterials(); + } } -void Scatterplot::setData(const arma::mat &data) +void Scatterplot::setXY(const arma::mat &xy) { - if (data.n_cols != 3) + if (xy.n_cols != 2) return; - m_data = data; - m_xmin = data.col(0).min(); - m_xmax = data.col(0).max(); - m_ymin = data.col(1).min(); - m_ymax = data.col(1).max(); - - m_colorScale.setExtents(m_data.col(2).min(), m_data.col(2).max()); + m_xy = xy; + m_xmin = xy.col(0).min(); + m_xmax = xy.col(0).max(); + m_ymin = xy.col(1).min(); + m_ymax = xy.col(1).max(); m_selectedGlyphs.clear(); + emit xyChanged(m_xy); + + updateGeometry(); +} + +void Scatterplot::setColorData(const arma::vec &colorData) +{ + if (colorData.n_elem != m_xy.n_rows) + return; + + m_colorData = colorData; + m_colorScale.setExtents(m_colorData.min(), m_colorData.max()); + emit colorDataChanged(m_colorData); + + updateMaterials(); +} +void Scatterplot::updateGeometry() +{ + m_shouldUpdateGeometry = true; + update(); +} + +void Scatterplot::updateMaterials() +{ + m_shouldUpdateMaterials = true; update(); } @@ -84,11 +115,12 @@ inline float Scatterplot::fromDataYToScreenY(float y) return PADDING + (y - m_ymin) / (m_ymax - m_ymin) * (height() - 2*PADDING); } -QSGNode *Scatterplot::newGlyphNodeTree() { +QSGNode *Scatterplot::createGlyphNodeTree() +{ QSGNode *node = new QSGNode; int vertexCount = calculateCircleVertexCount(GLYPH_SIZE / 2); - for (arma::uword i = 0; i < m_data.n_rows; i++) { + for (arma::uword i = 0; i < m_xy.n_rows; i++) { QSGGeometryNode *glyphNode = new QSGGeometryNode; QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), vertexCount); @@ -97,7 +129,7 @@ QSGNode *Scatterplot::newGlyphNodeTree() { glyphNode->setFlag(QSGNode::OwnsGeometry); QSGFlatColorMaterial *material = new QSGFlatColorMaterial; - material->setColor(m_colorScale.color(m_data(i, 2))); + material->setColor(QColor()); glyphNode->setMaterial(material); glyphNode->setFlag(QSGNode::OwnsMaterial); @@ -112,45 +144,56 @@ QSGNode *Scatterplot::newGlyphNodeTree() { QSGNode *Scatterplot::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) { - if (m_data.n_rows < 1) + if (m_xy.n_rows < 1) return 0; - qreal x, y, xt, yt, moveTranslationF; + qreal x, y, tx, ty, moveTranslationF; QSGNode *root = 0; if (!oldNode) { root = new QSGNode; - root->appendChildNode(newGlyphNodeTree()); - } else { + root->appendChildNode(createGlyphNodeTree()); + } else root = oldNode; - } - if (m_currentState != INTERACTION_MOVING) - xt = yt = 0; - else { - xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); - yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); - } + if (m_currentState == INTERACTION_MOVING) { + tx = m_dragCurrentPos.x() - m_dragOriginPos.x(); + ty = m_dragCurrentPos.y() - m_dragOriginPos.y(); + } else + tx = ty = 0; QSGNode *node = root->firstChild()->firstChild(); - for (arma::uword i = 0; i < m_data.n_rows; i++) { - arma::rowvec row = m_data.row(i); + for (arma::uword i = 0; i < m_xy.n_rows; i++) { + arma::rowvec row = m_xy.row(i); bool isSelected = m_selectedGlyphs.contains(i); QSGOpacityNode *glyphOpacityNode = static_cast(node); glyphOpacityNode->setOpacity(isSelected ? GLYPH_OPACITY_SELECTED : GLYPH_OPACITY); QSGGeometryNode *glyphNode = static_cast(node->firstChild()); - QSGGeometry *geometry = glyphNode->geometry(); - moveTranslationF = isSelected ? 1.0 : 0.0; - x = fromDataXToScreenX(row[0]) + xt * moveTranslationF; - y = fromDataYToScreenY(row[1]) + yt * moveTranslationF; - updateCircleGeometry(geometry, GLYPH_SIZE, x, y); - glyphNode->markDirty(QSGNode::DirtyGeometry); + if (m_shouldUpdateGeometry) { + QSGGeometry *geometry = glyphNode->geometry(); + moveTranslationF = isSelected ? 1.0 : 0.0; + x = fromDataXToScreenX(row[0]) + tx * moveTranslationF; + y = fromDataYToScreenY(row[1]) + ty * moveTranslationF; + updateCircleGeometry(geometry, GLYPH_SIZE, x, y); + glyphNode->markDirty(QSGNode::DirtyGeometry); + } + if (m_shouldUpdateMaterials) { + QSGFlatColorMaterial *material = static_cast(glyphNode->material()); + material->setColor(m_colorScale.color(m_colorData[i])); + glyphNode->setMaterial(material); + glyphNode->markDirty(QSGNode::DirtyMaterial); + } node = node->nextSibling(); } + if (m_shouldUpdateGeometry) + m_shouldUpdateGeometry = false; + if (m_shouldUpdateMaterials) + m_shouldUpdateMaterials = false; + // Draw selection if (m_currentState == INTERACTION_SELECTING) { QSGSimpleRectNode *selectionNode = 0; @@ -200,8 +243,8 @@ void Scatterplot::mouseMoveEvent(QMouseEvent *event) break; case INTERACTION_MOVING: m_dragCurrentPos = event->localPos(); - updateData(); - update(); + applyManipulation(); + updateGeometry(); m_dragOriginPos = m_dragCurrentPos; break; case INTERACTION_NONE: @@ -225,7 +268,7 @@ void Scatterplot::mouseReleaseEvent(QMouseEvent *event) case INTERACTION_MOVING: m_currentState = INTERACTION_SELECTED; - update(); + updateGeometry(); break; case INTERACTION_NONE: case INTERACTION_SELECTED: @@ -242,8 +285,8 @@ bool Scatterplot::selectGlyphs(bool mergeSelection) QRectF selectionRect(m_dragOriginPos, m_dragCurrentPos); bool anySelected = false; - for (arma::uword i = 0; i < m_data.n_rows; i++) { - arma::rowvec row = m_data.row(i); + for (arma::uword i = 0; i < m_xy.n_rows; i++) { + arma::rowvec row = m_xy.row(i); x = fromDataXToScreenX(row[0]); y = fromDataYToScreenY(row[1]); @@ -257,20 +300,23 @@ bool Scatterplot::selectGlyphs(bool mergeSelection) return anySelected; } -void Scatterplot::updateData() +void Scatterplot::applyManipulation() { - float xt = m_dragCurrentPos.x() - m_dragOriginPos.x(); - float yt = m_dragCurrentPos.y() - m_dragOriginPos.y(); + float tx = m_dragCurrentPos.x() - m_dragOriginPos.x(); + float ty = m_dragCurrentPos.y() - m_dragOriginPos.y(); + + tx /= (width() - PADDING); + ty /= (height() - PADDING); + + float x_extent = m_xmax - m_xmin; + float y_extent = m_ymax - m_ymin; - xt /= (width() - PADDING); - yt /= (height() - PADDING); for (auto it = m_selectedGlyphs.cbegin(); it != m_selectedGlyphs.cend(); it++) { - arma::rowvec row = m_data.row(*it); - row[0] = ((row[0] - m_xmin) / (m_xmax - m_xmin) + xt) * (m_xmax - m_xmin) + m_xmin; - row[1] = ((row[1] - m_ymin) / (m_ymax - m_ymin) + yt) * (m_ymax - m_ymin) + m_ymin; - m_data.row(*it) = row; + arma::rowvec row = m_xy.row(*it); + row[0] = ((row[0] - m_xmin) / x_extent + tx) * x_extent + m_xmin; + row[1] = ((row[1] - m_ymin) / y_extent + ty) * y_extent + m_ymin; + m_xy.row(*it) = row; } - // does not send last column (labels) - emit dataChanged(m_data.cols(0, m_data.n_cols - 2)); + emit xyChanged(m_xy); } diff --git a/scatterplot.h b/scatterplot.h index a155daf..a942217 100644 --- a/scatterplot.h +++ b/scatterplot.h @@ -11,13 +11,16 @@ class Scatterplot : public QQuickItem Q_OBJECT public: Scatterplot(QQuickItem *parent = 0); - ~Scatterplot(); + + void setColorScale(const ColorScale &colorScale); signals: - void dataChanged(const arma::mat &data); + void xyChanged(const arma::mat &XY); + void colorDataChanged(const arma::vec &colorData); public slots: - void setData(const arma::mat &data); + void setXY(const arma::mat &xy); + void setColorData(const arma::vec &colorData); protected: QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *); @@ -26,16 +29,24 @@ protected: void mouseReleaseEvent(QMouseEvent *event); private: - QSGNode *newGlyphNodeTree(); + QSGNode *createGlyphNodeTree(); bool selectGlyphs(bool mergeSelection); - void updateData(); float fromDataXToScreenX(float x); float fromDataYToScreenY(float y); - arma::mat m_data; + void applyManipulation(); + + void updateGeometry(); + void updateMaterials(); + + arma::mat m_xy; float m_xmin, m_xmax, m_ymin, m_ymax; + bool m_shouldUpdateGeometry, m_shouldUpdateMaterials; + + arma::vec m_colorData; + ColorScale m_colorScale; enum InteractionState { -- cgit v1.2.3