Commit 546b3b47e049601250c8f6b0822c1998f4609465

Authored by Ben Klein
2 parents 3fc8ee22 abe33c8e

Merge pull request #395 from biometrics/plotting

Plotting customization
docs/docs/api_docs/c_api/functions.md
... ... @@ -595,14 +595,15 @@ In order of their output, the figures are:
595 595 1. Metadata table
596 596 2. Receiver Operating Characteristic (ROC)
597 597 3. Detection Error Tradeoff (DET)
598   -4. Score Distribution (SD) histogram
599   -5. True Accept Rate Bar Chart (BC)
600   -6. Cumulative Match Characteristic (CMC)
601   -7. Error Rate (ERR) curve
  598 +4. Identification Error Tradeoff (IET)
  599 +5. Cumulative Match Characteristic (CMC)
  600 +6. Score Distribution (SD) histogram
  601 +7. True Accept Rate Bar Chart (BC)
  602 +8. Error Rate (ERR) curve
602 603  
603 604 Two files will be created:
604 605 * **destination.R** which is the auto-generated R script used to render the figures.
605   - * **destination.pdf** which has all of the figures in one file multi-page file.
  606 + * **destination.pdf** which has all of the figures in one file multi-page file (note that **destination%1d.png** will output each figure to a separate file).
606 607  
607 608 OpenBR uses file and folder names to automatically determine the plot legend.
608 609 For example, let's consider the case where three algorithms (<tt>A</tt>, <tt>B</tt>, & <tt>C</tt>) were each evaluated on two datasets (<tt>Y</tt> & <tt>Z</tt>).
... ... @@ -611,6 +612,37 @@ The &#39;&lt;tt&gt;_&lt;/tt&gt;&#39; character plays a special role in determining the legend title(
611 612 In this case, <tt>A</tt>, <tt>B</tt>, & <tt>C</tt> will be identified as different values of type <tt>Algorithm</tt>, and each will be assigned its own color; <tt>Y</tt> & <tt>Z</tt> will be identified as different values of type Dataset, and each will be assigned its own line style.
612 613 Matches around the EER will be displayed if the matches parameter is set in [br_eval](#br_eval).
613 614  
  615 +It is possible to customize some aspects of your plots using the [File](../cpp_api/file/file.md) key/value metadata convention; possible keys are described below.
  616 +
  617 +
  618 +Key | Value | Description
  619 +--- | ---- | -----------
  620 +smooth | [QString] | The file pivot to average across evaluation splits. Typically "Dataset" if using the folder name from above.
  621 +ncol | int | Number of columns in plot legends.
  622 +confidence | float | Confidence interval calculated for smooth, defaults to `0.95`
  623 +metadata | bool | Optional plot metadata, defaults to `true`
  624 +csv | bool | Optional output metadata tables to csv, defaults to `false`
  625 +\*Options | [QStringList] | Key/value list of options for a specific plot. Plots include "roc", "det", "iet", "cmc"
  626 +
  627 +Specific plot options are described below:
  628 +
  629 +Key | Value | Description
  630 +--- | ---- | -----------
  631 +title | [QString] | Plot title
  632 +size | float | Line width
  633 +legendPosition | [QPointF] | Legend coordinates on plot, ex. legendPosition=(X,Y)
  634 +textSize | float | Size of text for title, legend and axes
  635 +xTitle/yTitle | [QString] | Title for x/y axis
  636 +xLog/yLog | bool | Plot log scale for x/y axis
  637 +xLimits/yLimits | [QPointF] | Set x/y axis limits, ex. xLimits=(lower,upper)
  638 +xLabels/yLabels | [QString] | Labels for ticks on x/y axis, ex. xLabeles=percent or xLabels=c(1,5,10)
  639 +xBreaks/yBreaks | [QString] | Specify breaks/ticks on x/y axis, ex. xBreaks=pretty_breaks(n=10) or xBreaks=c(1,5,10)
  640 +
  641 +If specifying plot options it is a good idea to wrap the destination file in single quotes to avoid parsing errors.
  642 +The example below plots plots the six br_eval results in the Algorithm_Dataset folder described above, sets the number of legend columns and specifies some options for the CMC plot.
  643 +
  644 +`br -plot Algorithm_Dataset/* 'destination.pdf[ncol=3,cmcOptions=[xLog=false,xLimits=(1,20),xBreaks=pretty_breaks(n=10),xTitle=Ranks 1 through 20]]'`
  645 +
614 646 This function requires a current [R][R] installation with the following packages:
615 647  
616 648 install.packages(c("ggplot2", "gplots", "reshape", "scales", "jpg", "png"))
... ... @@ -1395,3 +1427,6 @@ Close a provided [Gallery](../cpp_api/gallery/gallery.md).
1395 1427 <!-- Links -->
1396 1428 [R]: http://www.r-project.org/ "R"
1397 1429 [QRegExp]: http://doc.qt.io/qt-5/QRegExp.html "QRegExp"
  1430 +[QString]: http://doc.qt.io/qt-5/QString.html "QString"
  1431 +[QStringList]: http://doc.qt.io/qt-5/QStringList.html "QStringList"
  1432 +[QPointF]: http://doc.qt.io/qt-5/QPointF.html "QPointF"
... ...
openbr/core/plot.cpp
... ... @@ -42,20 +42,6 @@ static QString getScale(const QString &amp;mode, const QString &amp;title, int vals)
42 42 // Custom sorting method to ensure datasets are ordered nicely
43 43 static bool sortFiles(const QString &fileA, const QString &fileB)
44 44 {
45   - QFileInfo fileInfoA(fileA);
46   - QFileInfo fileInfoB(fileB);
47   -
48   - if (fileInfoA.fileName().contains("Good")) return true;
49   - if (fileInfoB.fileName().contains("Good")) return false;
50   - if (fileInfoA.fileName().contains("Bad")) return true;
51   - if (fileInfoB.fileName().contains("Bad")) return false;
52   - if (fileInfoA.fileName().contains("Ugly")) return true;
53   - if (fileInfoB.fileName().contains("Ugly")) return false;
54   - if (fileInfoA.fileName().contains("MEDS")) return true;
55   - if (fileInfoB.fileName().contains("MEDS")) return false;
56   - if (fileInfoA.fileName().contains("PCSO")) return true;
57   - if (fileInfoB.fileName().contains("PCSO")) return false;
58   -
59 45 return fileA < fileB;
60 46 }
61 47  
... ... @@ -65,7 +51,8 @@ struct RPlot
65 51 QFile file;
66 52 QStringList pivotHeaders;
67 53 QVector< QSet<QString> > pivotItems;
68   - float confidence;
  54 + float confidence; // confidence interval for plotting across splits
  55 + int ncol; // Number of columns for plot legends
69 56  
70 57 bool flip;
71 58  
... ... @@ -124,6 +111,7 @@ struct RPlot
124 111 }
125 112 file.write("data <- rbind(data, tmp)\n");
126 113 }
  114 +
127 115 for (int i=0; i<pivotItems.size(); i++) {
128 116 const int size = pivotItems[i].size();
129 117 if (size > major.size) {
... ... @@ -133,13 +121,10 @@ struct RPlot
133 121 minor = Pivot(i, size, pivotHeaders[i]);
134 122 }
135 123 }
  124 +
136 125 const QString &smooth = destination.get<QString>("smooth", "");
137   - if (destination.contains(QString("confidence"))) {
138   - const QString &CI = destination.get<QString>("confidence");
139   - confidence = !CI.isEmpty() ? CI.toFloat()/100.0 : 0.95;
140   - } else {
141   - confidence = 0.95;
142   - }
  126 + confidence = destination.get<float>("confidence", 95) / 100.0;
  127 +
143 128 major.smooth = !smooth.isEmpty() && (major.header == smooth) && (major.size > 1);
144 129 minor.smooth = !smooth.isEmpty() && (minor.header == smooth) && (minor.size > 1);
145 130 if (major.smooth) major.size = 1;
... ... @@ -147,6 +132,7 @@ struct RPlot
147 132 if (major.size < minor.size)
148 133 std::swap(major, minor);
149 134  
  135 + ncol = destination.get<int>("ncol", major.size > 1 ? major.size : (minor.header.isEmpty() ? major.size : minor.size));
150 136 flip = minor.header == "Algorithm";
151 137 // Format data
152 138 if (isEvalFormat)
... ... @@ -187,7 +173,7 @@ struct RPlot
187 173 "TS$Y <- as.character(TS$Y)\n"
188 174 "CMC$Y <- as.numeric(as.character(CMC$Y))\n"
189 175 "\n"
190   - "if (%1) {\n\tsummarySE <- function(data=NULL, measurevar, groupvars=NULL, na.rm=FALSE, conf.interval=%7, .drop=TRUE) {\n\t\t"
  176 + "if (%1) {\n\tsummarySE <- function(data=NULL, measurevar, groupvars=NULL, na.rm=FALSE, conf.interval=%3, .drop=TRUE) {\n\t\t"
191 177 "require(plyr)\n\n\t\tlength2 <- function (x, na.rm=FALSE) {\n\t\t\tif (na.rm) sum(!is.na(x))\n\t\t\telse length(x)"
192 178 "\n\t\t}\n\n\t\tdatac <- ddply(data, groupvars, .drop=.drop, .fun = function(xx, col) {\n\t\t\t"
193 179 "c(N=length2(xx[[col]], na.rm=na.rm), mean=mean(xx[[col]], na.rm=na.rm), sd=sd(xx[[col]], na.rm=na.rm))\n\t\t\t},"
... ... @@ -197,6 +183,7 @@ struct RPlot
197 183 "datac$lower <- if(datac[, measurevar] - datac$ci > 0) (datac[, measurevar] - datac$ci) else 0\n\n\t\treturn(datac)\n\t}\n\t"
198 184 "DET <- summarySE(DET, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t"
199 185 "IET <- summarySE(IET, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t"
  186 + "CMC <- summarySE(CMC, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t"
200 187 "ERR <- summarySE(ERR, measurevar=\"X\", groupvars=c(\"Error\", \"%2\", \"Y\"))\n\t"
201 188 "TF <- summarySE(TF, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t"
202 189 "FT <- summarySE(FT, measurevar=\"Y\", groupvars=c(\"%2\", \"X\"))\n\t"
... ... @@ -204,82 +191,112 @@ struct RPlot
204 191 "# Code to format FAR values\n"
205 192 "far_names <- list('0.001'=\"FAR = 0.1%\", '0.01'=\"FAR = 1%\")\n"
206 193 "far_labeller <- function(variable,value) { return(far_names[as.character(value)]) }\n"
207   - "\n"
208   - "# Code to format TAR@FAR table\n"
209   - "algs <- unique(%6)\n"
210   - "algs <- algs[!duplicated(algs)]\n"
211   - "mat <- matrix(%3,nrow=6,ncol=length(algs),byrow=FALSE)\n"
212   - "colnames(mat) <- algs \n"
213   - "rownames(mat) <- c(\"FAR = 1e-06\", \"FAR = 1e-05\", \"FAR = 1e-04\", \"FAR = 1e-03\", \"FAR = 1e-02\", \"FAR = 1e-01\")\n"
214   - "FTtable <- as.table(mat)\n"
215   - "\n"
216   - "# Code to format FAR@TAR table\n"
217   - "mat <- matrix(%4,nrow=6,ncol=length(algs),byrow=FALSE)\n"
218   - "colnames(mat) <- algs \n"
219   - "rownames(mat) <- c(\"TAR = 0.40\", \"TAR = 0.50\", \"TAR = 0.65\", \"TAR = 0.75\", \"TAR = 0.85\", \"TAR = 0.95\")\n"
220   - "F_at_Ttable <- as.table(mat)\n"
221   - "\n"
222   - "# Code to format CMC Table\n"
223   - "mat <- matrix(%5,nrow=6,ncol=length(algs),byrow=FALSE)\n"
224   - "colnames(mat) <- algs \n"
225   - "rownames(mat) <- c(\" Rank 1\", \"Rank 5\", \"Rank 10\", \"Rank 20\", \"Rank 50\", \"Rank 100\")\n"
226   - "CMCtable <- as.table(mat)\n"
227   - "\n"
228   - "# Code to format Template Size Table\n"
229   - "if (nrow(TS) != 0) {\n\t"
230   - "mat <- matrix(TS$Y,nrow=1,ncol=length(algs),byrow=FALSE)\n\t"
231   - "colnames(mat) <- algs\n\t"
232   - "rownames(mat) <- c(\"Template Size (bytes):\")\n\t"
233   - "TStable <- as.table(mat)\n}\n").arg(((major.smooth || minor.smooth) ? "TRUE" : "FALSE"), major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header),
234   - (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(TF$Y, 3)), round(TF$ci, 3), sep=\"\\u00b1\")" : "TF$Y",
235   - (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(FT$Y, 3)), round(FT$ci, 3), sep=\"\\u00b1\")" : "FT$Y",
236   - (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(CT$Y, 3)), round(CT$ci, 3), sep=\"\\u00b1\")" : "CT$Y",
237   - (major.size > 1 && minor.size > 1) && !(major.smooth || minor.smooth) ? QString("paste(TF$%1, TF$%2, sep=\"_\")").arg(major.header, minor.header) : QString("TF$%1").arg(major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header)),
238   - QString::number(confidence))));
  194 + "\n").arg((major.smooth || minor.smooth) ? "TRUE" : "FALSE",
  195 + major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header),
  196 + QString::number(confidence))));
239 197  
240 198 // Open output device
241 199 file.write(qPrintable(QString("\n"
242 200 "# Open output device\n"
243   - "%1(\"%2.%1\")\n").arg(suffix, basename)));
244   -
245   - // Write metadata table
246   - if ((suffix == "pdf") && isEvalFormat) {
247   - file.write("\n"
248   - "# Write metadata table\n");
249   - QString textplot = "MT <- as.data.frame(Metadata[c(1,2,3,4,5),])\n"
250   - "par(mfrow=c(4,1))\n"
251   - "plot.new()\n"
252   - "print(title(paste(\"%1 - %2\",date(),sep=\"\\n\")))\n"
253   - "mat <- matrix(MT$X[c(1,2)],ncol=2)\n"
254   - "colnames(mat) <- c(\"Gallery\", \"Probe\")\n"
255   - "imageTable <- as.table(mat)\n"
256   - "print(textplot(imageTable,show.rownames=FALSE))\n"
257   - "print(title(\"Images\"))\n"
258   - "mat <- matrix(MT$X[c(3,4,5)],ncol=3)\n"
259   - "colnames(mat) <- c(\"Genuine\", \"Impostor\", \"Ignored\")\n"
260   - "matchTable <- as.table(mat)\n"
261   - "print(textplot(matchTable,show.rownames=FALSE))\n"
262   - "print(title(\"Matches\"))\n"
263   - "plot.new()\n"
264   - "print(title(\"Gallery * Probe = Genuine + Impostor + Ignored\"))\n"
265   - "plot.new()\n"
266   - "print(textplot(FTtable))\n"
267   - "print(title(\"Table of True Accept Rates at various False Accept Rates\"))\n"
268   - "print(textplot(F_at_Ttable))\n"
269   - "print(title(\"Table of False Accept Rates at various True Accept Rates\"))\n"
270   - "print(textplot(CMCtable))\n"
271   - "print(title(\"Table of retrieval rate at various ranks\"))\n"
272   - "if (nrow(TS) != 0) {\n\t"
273   - "print(textplot(TStable, cex=1.15))\n\t"
274   - "print(title(\"Template Size by Algorithm\"))\n}\n";
275   - file.write(qPrintable(textplot.arg(PRODUCT_NAME, PRODUCT_VERSION)));
276   - }
  201 + "%1(\"%2.%1\"%3)\n").arg(suffix, basename, suffix != "pdf" ? ", width=800, height=800" : "")));
277 202  
278 203 // Write figures
279 204 file.write("\n"
280 205 "# Write figures\n");
281 206 }
282 207  
  208 + void plotMetadata(bool csv)
  209 + {
  210 + file.write(qPrintable(QString("# Code to format TAR@FAR table\n"
  211 + "algs <- unique(%4)\n"
  212 + "algs <- algs[!duplicated(algs)]\n"
  213 + "mat <- matrix(%1,nrow=6,ncol=length(algs),byrow=FALSE)\n"
  214 + "colnames(mat) <- algs \n"
  215 + "rownames(mat) <- c(\"FAR = 1e-06\", \"FAR = 1e-05\", \"FAR = 1e-04\", \"FAR = 1e-03\", \"FAR = 1e-02\", \"FAR = 1e-01\")\n"
  216 + "TFtable <- as.table(mat)\n"
  217 + "\n"
  218 + "# Code to format FAR@TAR table\n"
  219 + "mat <- matrix(%2,nrow=6,ncol=length(algs),byrow=FALSE)\n"
  220 + "colnames(mat) <- algs \n"
  221 + "rownames(mat) <- c(\"TAR = 0.40\", \"TAR = 0.50\", \"TAR = 0.65\", \"TAR = 0.75\", \"TAR = 0.85\", \"TAR = 0.95\")\n"
  222 + "FTtable <- as.table(mat)\n"
  223 + "\n"
  224 + "# Code to format CMC Table\n"
  225 + "mat <- matrix(%3,nrow=6,ncol=length(algs),byrow=FALSE)\n"
  226 + "colnames(mat) <- algs \n"
  227 + "rownames(mat) <- c(\"Rank 1\", \"Rank 5\", \"Rank 10\", \"Rank 20\", \"Rank 50\", \"Rank 100\")\n"
  228 + "CMCtable <- as.table(mat)\n"
  229 + "\n"
  230 + "# Code to format Template Size Table\n"
  231 + "if (nrow(TS) != 0) {\n\t"
  232 + "mat <- matrix(TS$Y,nrow=1,ncol=length(algs),byrow=FALSE)\n\t"
  233 + "colnames(mat) <- algs\n\t"
  234 + "rownames(mat) <- c(\"Template Size (bytes):\")\n\t"
  235 + "TStable <- as.table(mat)\n}"
  236 + "\n").arg((major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(TF$Y, 3)), round(TF$ci, 3), sep=\"\\u00b1\")" : "TF$Y",
  237 + (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(FT$Y, 3)), round(FT$ci, 3), sep=\"\\u00b1\")" : "FT$Y",
  238 + (major.smooth || minor.smooth) && confidence != 0 ? "paste(as.character(round(CT$Y, 3)), round(CT$ci, 3), sep=\"\\u00b1\")" : "CT$Y",
  239 + (major.size > 1 && minor.size > 1) && !(major.smooth || minor.smooth) ? QString("paste(TF$%1, TF$%2, sep=\"_\")").arg(major.header, minor.header)
  240 + : QString("TF$%1").arg(major.size > 1 ? major.header : (minor.header.isEmpty() ? major.header : minor.header)))));
  241 +
  242 + file.write("\n# Write metadata table\n");
  243 + QString textplot = "MT <- as.data.frame(Metadata[c(1,2,3,4,5),])\n"
  244 + "par(mfrow=c(4,1))\n"
  245 + "plot.new()\n"
  246 + "print(title(paste(\"%1 - %2\",date(),sep=\"\\n\")))\n"
  247 + "mat <- matrix(MT$X[c(1,2)],ncol=2)\n"
  248 + "colnames(mat) <- c(\"Gallery\", \"Probe\")\n"
  249 + "imageTable <- as.table(mat)\n"
  250 + "print(textplot(imageTable,show.rownames=FALSE))\n"
  251 + "print(title(\"Images\"))\n"
  252 + "mat <- matrix(MT$X[c(3,4,5)],ncol=3)\n"
  253 + "colnames(mat) <- c(\"Genuine\", \"Impostor\", \"Ignored\")\n"
  254 + "matchTable <- as.table(mat)\n"
  255 + "print(textplot(matchTable,show.rownames=FALSE))\n"
  256 + "print(title(\"Matches\"))\n"
  257 + "plot.new()\n"
  258 + "print(title(\"Gallery * Probe = Genuine + Impostor + Ignored\"))\n";
  259 + file.write(qPrintable(textplot.arg(PRODUCT_NAME, PRODUCT_VERSION)));
  260 +
  261 + if (csv)
  262 + textplot = QString("write.csv(TFtable,file=\"%1_TF.csv\")\n"
  263 + "write.csv(FTtable,file=\"%1_FT.csv\")\n"
  264 + "write.csv(CMCtable,file=\"%1_CMC.csv\")\n\n").arg(basename);
  265 + else
  266 + textplot = "plot.new()\n"
  267 + "print(textplot(TFtable))\n"
  268 + "print(title(\"Table of True Accept Rates at various False Accept Rates\"))\n"
  269 + "print(textplot(FTtable))\n"
  270 + "print(title(\"Table of False Accept Rates at various True Accept Rates\"))\n"
  271 + "print(textplot(CMCtable))\n"
  272 + "print(title(\"Table of retrieval rate at various ranks\"))\n"
  273 + "if (nrow(TS) != 0) {\n\t"
  274 + "print(textplot(TStable, cex=1.15))\n\t"
  275 + "print(title(\"Template Size by Algorithm\"))\n}\n\n";
  276 + file.write(qPrintable(textplot));
  277 + }
  278 +
  279 + void qplot(QString geom, QString data, bool flipY, File opts)
  280 + {
  281 + file.write(qPrintable(QString("qplot(X, %1, data=%2, geom=\"%3\", main=\"%4\"").arg(flipY ? "1-Y" : "Y", data, geom, opts.get<QString>("title", "")) +
  282 + (opts.contains("size") ? QString(", size=I(%1)").arg(opts.get<QString>("size")) : QString()) +
  283 + (major.size > 1 ? QString(", colour=factor(%1)").arg(major.header) : QString()) +
  284 + (minor.size > 1 ? QString(", linetype=factor(%1)").arg(minor.header) : QString()) +
  285 + (QString(", xlab=\"%1\", ylab=\"%2\") + theme_minimal()").arg(opts.get<QString>("xTitle"), opts.get<QString>("yTitle"))) +
  286 + ((major.smooth || minor.smooth) && confidence != 0 && data != "CMC" ? QString(" + geom_errorbar(data=%1[seq(1, NROW(%1), by = 29),], aes(x=X, ymin=%2), width=0.1, alpha=I(1/2))").arg(data, flipY ? "(1-lower), ymax=(1-upper)" : "lower, ymax=upper") : QString()) +
  287 + (major.size > 1 ? getScale("colour", major.header, major.size) : QString()) +
  288 + (minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(minor.header) : QString()) +
  289 + (opts.getBool("xLog") ? QString(" + scale_x_log10(labels=%1, breaks=%2) + annotation_logticks(sides=\"b\")").arg(opts.get<QString>("xLabels", "trans_format(\"log10\", math_format())"), opts.get<QString>("xBreaks", "waiver()"))
  290 + : QString(" + scale_x_continuous(labels=%1, breaks=%2)").arg(opts.get<QString>("xLabels", "percent"), opts.get<QString>("xBreaks", "pretty_breaks(n=10)"))) +
  291 + (opts.getBool("yLog") ? QString(" + scale_y_log10(labels=%1, breaks=%2) + annotation_logticks(sides=\"l\")").arg(opts.get<QString>("yLabels", "trans_format(\"log10\", math_format())"), opts.get<QString>("yBreaks", "waiver()"))
  292 + : QString(" + scale_y_continuous(labels=%1, breaks=%2)").arg(opts.get<QString>("yLabels", "percent"), opts.get<QString>("yBreaks", "pretty_breaks(n=10)"))) +
  293 + (opts.contains("xLimits") ? QString(" + xlim%1").arg(QtUtils::toString(opts.get<QPointF>("xLimits", QPointF()))) : QString()) +
  294 + (opts.contains("yLimits") ? QString(" + ylim%1").arg(QtUtils::toString(opts.get<QPointF>("yLimits", QPointF()))) : QString()) +
  295 + QString(" + theme(legend.title = element_text(size = %1), legend.text = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1),").arg(QString::number(opts.get<float>("textSize",12))) +
  296 + QString(" legend.position=%1, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"))").arg(opts.contains("legendPosition") ? "c"+QtUtils::toString(opts.get<QPointF>("legendPosition")) : "'bottom'") +
  297 + QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(ncol)));
  298 + }
  299 +
283 300 bool finalize(bool show = false)
284 301 {
285 302 file.write("dev.off()\n");
... ... @@ -297,66 +314,31 @@ bool Plot(const QStringList &amp;files, const File &amp;destination, bool show)
297 314 {
298 315 qDebug("Plotting %d file(s) to %s", files.size(), qPrintable(destination));
299 316  
300   - const bool minimalist = destination.getBool("minimalist");
301   - const bool uncertainty = destination.get<bool>("uncertainty");
  317 + RPlot p(files, destination);
302 318  
303 319 // Use a br::file for simple storage of plot options
304   - File cmcOpts;
305   - const QStringList cmcOptions = destination.get<QStringList>("cmcOptions", QStringList());
306   - foreach (const QString& option, cmcOptions) {
307   - QStringList words = QtUtils::parse(option, '=');
308   - QtUtils::checkArgsSize(words[0],words,1,2);
309   - cmcOpts.set(words[0],words[1]);
310   - }
311   -
312   - File rocOpts;
313   - const QStringList rocOptions = destination.get<QStringList>("rocOptions", QStringList());
314   - foreach (const QString& option, rocOptions) {
315   - QStringList words = QtUtils::parse(option, '=');
316   - QtUtils::checkArgsSize(words[0],words,1,2);
317   - rocOpts.set(words[0],words[1]);
  320 + QMap<QString,File> optMap;
  321 + optMap.insert("rocOptions", File(QString("[xTitle=False Accept Rate,yTitle=True Accept Rate,xLog=true,yLog=false]")));
  322 + optMap.insert("detOptions", File(QString("[xTitle=False Accept Rate,yTitle=False Reject Rate,xLog=true,yLog=true]")));
  323 + optMap.insert("ietOptions", File(QString("[xTitle=False Positive Identification Rate (FPIR),yTitle=False Negative Identification Rate (FNIR),xLog=true,yLog=true]")));
  324 + optMap.insert("cmcOptions", File(QString("[xTitle=Rank,yTitle=Retrieval Rate,xLog=true,yLog=false,size=1,xLabels=c(1,5,10,50,100),xBreaks=c(1,5,10,50,100)]")));
  325 +
  326 + foreach (const QString &key, optMap.keys()) {
  327 + const QStringList options = destination.get<QStringList>(key, QStringList());
  328 + foreach (const QString &option, options) {
  329 + QStringList words = QtUtils::parse(option, '=');
  330 + QtUtils::checkArgsSize(words[0], words, 1, 2);
  331 + optMap[key].set(words[0], words[1]);
  332 + }
318 333 }
319 334  
320   -
321   - RPlot p(files, destination);
322   -
323   - p.file.write(qPrintable(QString("qplot(X, 1-Y, data=DET, geom=\"line\", main=\"%1\"").arg(rocOpts.get<QString>("title",QString())) +
324   - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) +
325   - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) +
326   - QString(", xlab=\"False Accept Rate\", ylab=\"True Accept Rate\") + theme_minimal()") +
327   - ((p.major.smooth || p.minor.smooth) && p.confidence != 0 ? " + geom_errorbar(data=DET[seq(1, NROW(DET), by = 29),], aes(x=X, ymin=(1-lower), ymax=(1-upper)), width=0.1, alpha=I(1/2))" : QString()) +
328   - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) +
329   - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) +
330   - QString(" + scale_x_log10(labels=trans_format(\"log10\", math_format()))") +
331   - (rocOpts.contains("yLimits") ? QString(" + scale_y_continuous(labels=percent) + coord_cartesian(ylim=%1)").arg("c"+QtUtils::toString(rocOpts.get<QPointF>("yLimits",QPointF()))) : QString(" + scale_y_continuous(labels=percent)")) +
332   - QString(" + annotation_logticks(sides=\"b\")") +
333   - QString(" + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1),"
334   - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(rocOpts.get<float>("textSize",12)), rocOpts.contains("legendPosition") ? "c"+QtUtils::toString(rocOpts.get<QPointF>("legendPosition")) : "'bottom'") +
335   - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size)))));
336   -
337   - p.file.write(qPrintable(QString("qplot(X, Y, data=DET, geom=\"line\"") +
338   - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) +
339   - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) +
340   - QString(", xlab=\"False Accept Rate\", ylab=\"False Reject Rate\") + geom_abline(alpha=0.5, colour=\"grey\", linetype=\"dashed\") + theme_minimal()") +
341   - ((p.major.smooth || p.minor.smooth) && p.confidence != 0 ? " + geom_errorbar(data=DET[seq(1, NROW(DET), by = 29),], aes(x=X, ymin=lower, ymax=upper), width=0.1, alpha=I(1/2))" : QString()) +
342   - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) +
343   - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) +
344   - QString(" + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1),"
345   - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(rocOpts.get<float>("textSize",12)), rocOpts.contains("legendPosition") ? "c"+QtUtils::toString(rocOpts.get<QPointF>("legendPosition")) : "'bottom'") +
346   - QString(" + scale_x_log10(labels=trans_format(\"log10\", math_format())) + scale_y_log10(labels=trans_format(\"log10\", math_format())) + annotation_logticks()") +
347   - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size)))));
348   -
349   - p.file.write(qPrintable(QString("qplot(X, Y, data=IET, geom=\"line\"") +
350   - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) +
351   - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) +
352   - QString(", xlab=\"False Positive Identification Rate (FPIR)\", ylab=\"False Negative Identification Rate (FNIR)\") + theme_minimal()") +
353   - ((p.major.smooth || p.minor.smooth) && p.confidence != 0 ? " + geom_errorbar(data=IET[seq(1, NROW(IET), by = 29),], aes(x=X, ymin=lower, ymax=upper), width=0.1, alpha=I(1/2))" : QString()) +
354   - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) +
355   - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) +
356   - QString(" + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1),"
357   - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(rocOpts.get<float>("textSize",12)), rocOpts.contains("legendPosition") ? "c"+QtUtils::toString(rocOpts.get<QPointF>("legendPosition")) : "'bottom'") +
358   - QString(" + scale_x_log10(labels=trans_format(\"log10\", math_format())) + scale_y_log10(labels=trans_format(\"log10\", math_format())) + annotation_logticks()") +
359   - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size)))));
  335 + // optional plot metadata and accuracy tables
  336 + if (destination.getBool("metadata", true))
  337 + p.plotMetadata(destination.getBool("csv", false));
  338 + p.qplot("line", "DET", true, optMap["rocOptions"]);
  339 + p.qplot("line", "DET", false, optMap["detOptions"]);
  340 + p.qplot("line", "IET", false, optMap["ietOptions"]);
  341 + p.qplot("line", "CMC", false, optMap["cmcOptions"]);
360 342  
361 343 p.file.write(qPrintable(QString("qplot(X, data=SD, geom=\"histogram\", fill=Y, position=\"identity\", alpha=I(1/2)") +
362 344 QString(", xlab=\"Score\", ylab=\"Frequency\"") +
... ... @@ -364,17 +346,6 @@ bool Plot(const QStringList &amp;files, const File &amp;destination, bool show)
364 346 (p.major.size > 1 ? (p.minor.size > 1 ? QString(" + facet_grid(%2 ~ %1, scales=\"free\")").arg((p.flip ? p.major.header : p.minor.header), (p.flip ? p.minor.header : p.major.header)) : QString(" + facet_wrap(~ %1, scales = \"free\")").arg(p.major.header)) : QString()) +
365 347 QString(" + theme(aspect.ratio=1)\n\n")));
366 348  
367   - p.file.write(qPrintable(QString("ggplot(CMC, aes(x=X, y=Y%1%2)) + ggtitle(\"%3\") + xlab(\"Rank\") + ylab(\"Retrieval Rate\")").arg(p.major.size > 1 ? QString(" ,colour=factor(%1)").arg(p.major.header) : QString(), p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString(), cmcOpts.get<QString>("title",QString())) +
368   - QString(((p.major.smooth || p.minor.smooth) ? (!uncertainty ? " + stat_summary(geom=\"line\", fun.y=mean, size=%1)" : " + stat_summary(geom=\"line\", fun.y=min, aes(linetype=\"Min/Max\"), size=%1) + stat_summary(geom=\"line\", "
369   - "fun.y=max, aes(linetype=\"Min/Max\"), size=%1) + stat_summary(geom=\"line\", fun.y=mean, aes(linetype=\"Mean\"), size=%1) + scale_linetype_manual(\"Legend\", values=c(\"Mean\"=1, \"Min/Max\"=2))") : " + geom_line(size=%1)")).arg(QString::number(cmcOpts.get<float>("thickness",1))) +
370   - (minimalist ? "" : " + scale_x_log10(labels=c(1,5,10,50,100), breaks=c(1,5,10,50,100)) + annotation_logticks(sides=\"b\")") +
371   - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) +
372   - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) +
373   - (cmcOpts.contains("yLimits") ? QString(" + scale_y_continuous(labels=percent) + coord_cartesian(ylim=%1)").arg("c"+QtUtils::toString(cmcOpts.get<QPointF>("yLimits",QPointF()))) : QString(" + scale_y_continuous(labels=percent)")) +
374   - QString(" + theme_minimal() + theme(legend.title = element_text(size = %1), plot.title = element_text(size = %1), axis.text = element_text(size = %1), axis.title.x = element_text(size = %1), axis.title.y = element_text(size = %1),"
375   - " legend.position=%2, legend.background = element_rect(fill = 'white'), panel.grid.major = element_line(colour = \"gray\"), panel.grid.minor = element_line(colour = \"gray\", linetype = \"dashed\"), legend.text = element_text(size = %1))").arg(QString::number(cmcOpts.get<float>("textSize",12)), cmcOpts.contains("legendPosition") ? "c"+QtUtils::toString(cmcOpts.get<QPointF>("legendPosition")) : "'bottom'") +
376   - QString(" + guides(col=guide_legend(ncol=%1))\n\n").arg(destination.get<int>("ncol", p.major.size > 1 ? p.major.size : (p.minor.header.isEmpty() ? p.major.size : p.minor.size)))));
377   -
378 349 p.file.write(qPrintable(QString("qplot(factor(%1)%2, data=BC, %3").arg(p.major.smooth ? (p.minor.header.isEmpty() ? "Algorithm" : p.minor.header) : p.major.header, (p.major.smooth || p.minor.smooth) ? ", Y" : "", (p.major.smooth || p.minor.smooth) ? "geom=\"boxplot\"" : "geom=\"bar\", position=\"dodge\", weight=Y") +
379 350 (p.major.size > 1 ? QString(", fill=factor(%1)").arg(p.major.header) : QString()) +
380 351 QString(", xlab=\"False Accept Rate\", ylab=\"True Accept Rate\") + theme_minimal()") +
... ... @@ -451,6 +422,20 @@ bool PlotDetection(const QStringList &amp;files, const File &amp;destination, bool show)
451 422 qDebug("Plotting %d detection file(s) to %s", files.size(), qPrintable(destination));
452 423 RPlot p(files, destination, false);
453 424  
  425 + // Use a br::file for simple storage of plot options
  426 + QMap<QString,File> optMap;
  427 + optMap.insert("rocOptions", File(QString("[xTitle=False Accepts Per Image,yTitle=True Accept Rate,xLog=true,yLog=false]")));
  428 + optMap.insert("prOptions", File(QString("[xTitle=False Accept Rate,yTitle=False Reject Rate,xLog=true,yLog=true]")));
  429 +
  430 + foreach (const QString &key, optMap.keys()) {
  431 + const QStringList options = destination.get<QStringList>(key, QStringList());
  432 + foreach (const QString &option, options) {
  433 + QStringList words = QtUtils::parse(option, '=');
  434 + QtUtils::checkArgsSize(words[0], words, 1, 2);
  435 + optMap[key].set(words[0], words[1]);
  436 + }
  437 + }
  438 +
454 439 p.file.write("# Split data into individual plots\n"
455 440 "plot_index = which(names(data)==\"Plot\")\n"
456 441 "DiscreteROC <- data[grep(\"DiscreteROC\",data$Plot),-c(1)]\n"
... ... @@ -466,23 +451,15 @@ bool PlotDetection(const QStringList &amp;files, const File &amp;destination, bool show)
466 451 if (filesHaveSinglePoint(files))
467 452 plotType = QString("point");
468 453  
469   - foreach (const QString &type, QStringList() << "Discrete" << "Continuous")
470   - p.file.write(qPrintable(QString("qplot(X, Y, data=%1ROC%2").arg(type, (p.major.smooth || p.minor.smooth) ? ", geom=\"smooth\", method=loess, level=0.99" : QString(", geom=\"%1\"").arg(plotType)) +
471   - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) +
472   - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) +
473   - QString(", xlab=\"False Accepts Per Image\", ylab=\"True Accept Rate\") + theme_minimal()") +
474   - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) +
475   - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) +
476   - QString(" + scale_x_log10() + scale_y_continuous(labels=percent, limits=c(0,1)) + annotation_logticks(sides=\"b\") + ggtitle(\"%1\") + theme(legend.position=\"bottom\")\n\n").arg(type)));
477   -
478   - foreach (const QString &type, QStringList() << "Discrete" << "Continuous")
479   - p.file.write(qPrintable(QString("qplot(X, Y, data=%1PR%2").arg(type, (p.major.smooth || p.minor.smooth) ? ", geom=\"smooth\", method=loess, level=0.99" : QString(", geom=\"%1\"").arg(plotType)) +
480   - (p.major.size > 1 ? QString(", colour=factor(%1)").arg(p.major.header) : QString()) +
481   - (p.minor.size > 1 ? QString(", linetype=factor(%1)").arg(p.minor.header) : QString()) +
482   - QString(", xlab=\"Recall\", ylab=\"Precision\") + theme_minimal()") +
483   - (p.major.size > 1 ? getScale("colour", p.major.header, p.major.size) : QString()) +
484   - (p.minor.size > 1 ? QString(" + scale_linetype_discrete(\"%1\")").arg(p.minor.header) : QString()) +
485   - QString(" + scale_x_continuous(limits=c(0,1)) + scale_y_continuous(labels=percent, limits=c(0,1)) + ggtitle(\"%1\") + theme(legend.position=\"bottom\")\n\n").arg(type)));
  454 + foreach (const QString &type, QStringList() << "Discrete" << "Continuous") {
  455 + optMap["rocOptions"].set("title", type);
  456 + p.qplot(plotType, type + "ROC", false, optMap["rocOptions"]);
  457 + }
  458 +
  459 + foreach (const QString &type, QStringList() << "Discrete" << "Continuous") {
  460 + optMap["prOptions"].set("title", type);
  461 + p.qplot(plotType, type + "PR", false, optMap["prOptions"]);
  462 + }
486 463  
487 464 p.file.write(qPrintable(QString("qplot(X, data=Overlap, geom=\"histogram\", position=\"identity\", xlab=\"Overlap\", ylab=\"Frequency\")") +
488 465 QString(" + theme_minimal() + scale_x_continuous(minor_breaks=NULL) + scale_y_continuous(minor_breaks=NULL) + theme(axis.text.y=element_blank(), axis.ticks=element_blank(), axis.text.x=element_text(angle=-90, hjust=0))") +
... ...