Skip to content

Commit e3107a8

Browse files
committed
Add mean-square error (MSE) to exrmetrics
Signed-off-by: Pierre-Anthony Lemieux <pal@palemieux.com>
1 parent f19c773 commit e3107a8

6 files changed

Lines changed: 318 additions & 23 deletions

File tree

src/bin/exrmetrics/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-License-Identifier: BSD-3-Clause
22
# Copyright (c) Contributors to the OpenEXR Project.
33

4-
add_openexr_bin_program(exrmetrics SOURCES main.cpp exrmetrics.cpp)
4+
add_openexr_bin_program(exrmetrics SOURCES main.cpp exrmetrics.cpp mseutils.cpp)

src/bin/exrmetrics/exrmetrics.cpp

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//
66

77
#include "exrmetrics.h"
8+
#include "mseutils.h"
89

910
#include "ImfChannelList.h"
1011
#include "ImfDeepFrameBuffer.h"
@@ -23,15 +24,20 @@
2324
#include "ImfTiledMisc.h"
2425
#include "ImfTiledOutputPart.h"
2526

27+
#include <Imath/half.h>
28+
2629
#include <chrono>
30+
#include <cmath>
2731
#include <ctime>
32+
#include <limits>
2833
#include <list>
2934
#include <stdexcept>
3035
#include <vector>
3136
#include <sys/stat.h>
3237

3338
using namespace OPENEXR_IMF_NAMESPACE;
3439
using IMATH_NAMESPACE::Box2i;
40+
using IMATH_NAMESPACE::half;
3541

3642
using std::cerr;
3743
using namespace std::chrono;
@@ -988,7 +994,8 @@ exrmetrics (
988994
bool write,
989995
bool reread,
990996
PixelMode pixelMode,
991-
bool verbose)
997+
bool verbose,
998+
bool computeMSE)
992999
{
9931000

9941001
if (verbose)
@@ -1171,11 +1178,122 @@ exrmetrics (
11711178
else { metrics.outputFileSize = fileSize; }
11721179
}
11731180

1181+
//
1182+
// compute MSE vs. re-read data
1183+
//
1184+
if (computeMSE && write && reread)
1185+
{
1186+
for (size_t p = 0; p < parts.size (); ++p)
1187+
{
1188+
metrics.stats[p].mseCount = 0;
1189+
metrics.stats[p].mse = std::numeric_limits<double>::quiet_NaN ();
1190+
string partType = outHeaders[p].type ();
1191+
if (partType != SCANLINEIMAGE && partType != TILEDIMAGE) continue;
1192+
1193+
// skip parts with mixed channel types or with no channels
1194+
{
1195+
auto chBegin = outHeaders[p].channels ().begin ();
1196+
auto chEnd = outHeaders[p].channels ().end ();
1197+
if (chBegin == chEnd) continue;
1198+
PixelType firstType = chBegin.channel ().type;
1199+
bool allSameType = true;
1200+
for (auto i = chBegin; i != chEnd; ++i)
1201+
{
1202+
if (i.channel ().type != firstType)
1203+
{
1204+
allSameType = false;
1205+
break;
1206+
}
1207+
}
1208+
if (!allSameType) continue;
1209+
}
1210+
1211+
Box2i dw = outHeaders[p].dataWindow ();
1212+
uint64_t width = dw.max.x + 1 - dw.min.x;
1213+
uint64_t height = dw.max.y + 1 - dw.min.y;
1214+
1215+
double sumSq = 0.0;
1216+
uint64_t count = 0;
1217+
int channelIndex = 0;
1218+
1219+
for (ChannelList::ConstIterator i =
1220+
outHeaders[p].channels ().begin ();
1221+
i != outHeaders[p].channels ().end ();
1222+
++i, ++channelIndex)
1223+
{
1224+
if (i.channel ().type != HALF && i.channel ().type != FLOAT &&
1225+
i.channel ().type != UINT)
1226+
continue;
1227+
1228+
uint64_t pixelsInChannel =
1229+
(width / i.channel ().xSampling) *
1230+
(height / i.channel ().ySampling);
1231+
1232+
const char* origData = nullptr;
1233+
const char* rereadData = nullptr;
1234+
1235+
if (partType == SCANLINEIMAGE)
1236+
{
1237+
origData =
1238+
parts[p].readBuf.scanlinePixelData[channelIndex].data ();
1239+
rereadData =
1240+
parts[p].rereadBuf.scanlinePixelData[channelIndex].data ();
1241+
}
1242+
else
1243+
{
1244+
if (parts[p].readBuf.tilePixelData.empty () ||
1245+
parts[p].rereadBuf.tilePixelData.empty ())
1246+
continue;
1247+
origData =
1248+
parts[p].readBuf.tilePixelData[0][channelIndex].data ();
1249+
rereadData =
1250+
parts[p].rereadBuf.tilePixelData[0][channelIndex].data ();
1251+
}
1252+
1253+
if (i.channel ().type == HALF)
1254+
{
1255+
metrics.stats[p].mseKind = MSE_LOG_HALF;
1256+
accumMSE (
1257+
reinterpret_cast<const half*> (origData),
1258+
reinterpret_cast<const half*> (rereadData),
1259+
pixelsInChannel,
1260+
sumSq,
1261+
count);
1262+
}
1263+
else if (i.channel ().type == FLOAT)
1264+
{
1265+
metrics.stats[p].mseKind = MSE_LOG_FLOAT;
1266+
accumMSE (
1267+
reinterpret_cast<const float*> (origData),
1268+
reinterpret_cast<const float*> (rereadData),
1269+
pixelsInChannel,
1270+
sumSq,
1271+
count);
1272+
}
1273+
else
1274+
{
1275+
metrics.stats[p].mseKind = MSE_LOG_INT;
1276+
accumMSE (
1277+
reinterpret_cast<const unsigned int*> (origData),
1278+
reinterpret_cast<const unsigned int*> (rereadData),
1279+
pixelsInChannel,
1280+
sumSq,
1281+
count);
1282+
}
1283+
}
1284+
1285+
metrics.stats[p].mseCount = count;
1286+
metrics.stats[p].mse =
1287+
count > 0 ? sumSq / count
1288+
: std::numeric_limits<double>::quiet_NaN ();
1289+
}
1290+
}
1291+
11741292
//
11751293
// sum across all parts
11761294
//
11771295

1178-
metrics.totalStats = metrics.stats[0];
1296+
metrics.totalStats = metrics.stats[0];
11791297
for (size_t i = 1; i < metrics.stats.size (); ++i)
11801298
{
11811299
accumulate (metrics.totalStats.readPerf, metrics.stats[i].readPerf);

src/bin/exrmetrics/exrmetrics.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include "ImfCompression.h"
1717

18+
#include <limits>
1819
#include <stdint.h>
1920

2021
#include <vector>
@@ -43,6 +44,14 @@ struct partSizeData
4344
std::string partType = "";
4445
};
4546

47+
enum MSEKind
48+
{
49+
MSE_NONE,
50+
MSE_LOG_HALF,
51+
MSE_LOG_FLOAT,
52+
MSE_LOG_INT
53+
};
54+
4655
struct partStats
4756
{
4857
std::vector<double>
@@ -57,6 +66,10 @@ struct partStats
5766
rereadPerf; // for deep, times reading the sample count, otherwise times reading the entire data
5867
uint64_t sizeOnDisk = 0; // record compressed size of part on disk.
5968

69+
MSEKind mseKind = MSE_NONE;
70+
double mse = std::numeric_limits<double>::quiet_NaN ();
71+
uint64_t mseCount = 0;
72+
6073
partSizeData sizeData;
6174
};
6275

@@ -78,6 +91,7 @@ fileMetrics exrmetrics (
7891
bool write,
7992
bool reread,
8093
PixelMode pixelMode,
81-
bool verbose);
94+
bool verbose,
95+
bool computeMSE = false);
8296

8397
#endif

src/bin/exrmetrics/main.cpp

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <list>
2121
#include <vector>
2222

23+
#include <cmath>
2324
#include <math.h>
2425
#include <stdlib.h>
2526
#include <string.h>
@@ -91,6 +92,11 @@ usageMessage (ostream& stream, const char* program_name, bool verbose = false)
9192
" --csv print output in csv mode. If passes>1, show median timing\n"
9293
" default is JSON mode\n"
9394
" --passes num write and re-read file num times (default 1)\n"
95+
" --mse compute MSE per part, comparing original vs. re-read after compression.\n"
96+
" Parts must have uniform channel types. Half and float channels use\n"
97+
" signed log-space MSE (reported as \"log_mse\"), skipping non-finite\n"
98+
" original samples; non-finite re-read samples produce NaN. Uint\n"
99+
" channels use linear MSE (reported as \"mse\").\n"
94100
"\n"
95101
" -h, --help print this message\n"
96102
" -v output progress messages\n"
@@ -121,6 +127,7 @@ struct options
121127
bool outputPartSizeOnDisk = false;
122128
bool verbose = false;
123129
bool csv = false;
130+
bool computeMSE = false;
124131
std::vector<PixelMode> pixelModes;
125132
std::vector<OPENEXR_IMF_NAMESPACE::Compression> compressions;
126133

@@ -259,7 +266,8 @@ jsonStats (
259266
int timing,
260267
bool partSize,
261268
bool raw,
262-
bool stats)
269+
bool stats,
270+
bool computeMSE)
263271
{
264272

265273
static const char* lastFileName = nullptr;
@@ -386,25 +394,37 @@ jsonStats (
386394
printPartStats (
387395
out, run.metrics.totalStats, " ", timing,false, raw, stats);
388396
}
389-
if (timing && run.metrics.stats.size () > 1)
397+
if (timing && run.metrics.stats.size () > 1 || computeMSE)
390398
{
391399
out << ",\n";
392400
out << " \"parts\":\n";
393401
out << " [\n";
394-
//first print total statistics, then print all part data, unless there's only one part
395402
for (size_t part = 0; part < run.metrics.stats.size (); ++part)
396403
{
397404
out << " {\n";
398-
out << " \"part\": " << part << ",\n";
399-
400-
printPartStats (
401-
out,
402-
run.metrics.stats[part],
403-
" ",
404-
timing,
405-
partSize,
406-
raw,
407-
stats);
405+
out << " \"part\": " << part;
406+
if (computeMSE)
407+
{
408+
out << ",\n";
409+
const char* mseKey =
410+
run.metrics.stats[part].mseKind != MSE_LOG_INT
411+
? "log_mse"
412+
: "mse";
413+
out << " \"" << mseKey
414+
<< "\": " << run.metrics.stats[part].mse;
415+
}
416+
if (timing)
417+
{
418+
out << ",\n";
419+
printPartStats (
420+
out,
421+
run.metrics.stats[part],
422+
" ",
423+
timing,
424+
partSize,
425+
raw,
426+
stats);
427+
}
408428
out << "\n }";
409429
if (part < run.metrics.stats.size () - 1) { out << ','; }
410430
out << endl;
@@ -425,7 +445,7 @@ jsonStats (
425445
}
426446

427447
void
428-
csvStats (ostream& out, list<runData>& data, bool outputSizeData, int timing)
448+
csvStats (ostream& out, list<runData>& data, bool outputSizeData, int timing, bool computeMSE)
429449
{
430450
out << "file name";
431451
if (outputSizeData)
@@ -434,6 +454,7 @@ csvStats (ostream& out, list<runData>& data, bool outputSizeData, int timing)
434454
}
435455
out << ",compression,pixel mode";
436456
if (outputSizeData) { out << ",output size"; }
457+
if (computeMSE) { out << ",mse"; }
437458
if (timing & options::TIME_READ)
438459
{
439460
out << ",count read time";
@@ -470,6 +491,23 @@ csvStats (ostream& out, list<runData>& data, bool outputSizeData, int timing)
470491
out << ',' << compName << ',' << modeName (run.mode);
471492

472493
if (outputSizeData) { out << ',' << run.metrics.outputFileSize; }
494+
if (computeMSE)
495+
out << ',';
496+
for (size_t p = 0; p < run.metrics.stats.size (); ++p)
497+
{
498+
if (p > 0) { out << '|'; }
499+
switch (run.metrics.stats[p].mseKind) {
500+
case MSE_LOG_INT:
501+
out << "mse:" << run.metrics.stats[p].mse;
502+
break;
503+
case MSE_LOG_HALF:
504+
case MSE_LOG_FLOAT:
505+
out << "log_mse:" << run.metrics.stats[p].mse;
506+
break;
507+
default:
508+
out << "---";
509+
}
510+
}
473511
if (timing & options::TIME_READ)
474512
{
475513
if (run.metrics.totalStats.sizeData.isDeep)
@@ -545,10 +583,13 @@ main (int argc, char** argv)
545583
opts.level,
546584
opts.passes,
547585
opts.outFile || opts.outputSizeData ||
548-
opts.timing & options::TIME_WRITE,
549-
opts.timing & options::TIME_REREAD,
586+
opts.timing & options::TIME_WRITE ||
587+
opts.computeMSE,
588+
opts.timing & options::TIME_REREAD ||
589+
opts.computeMSE,
550590
mode,
551-
opts.verbose);
591+
opts.verbose,
592+
opts.computeMSE);
552593
data.push_back (d);
553594
}
554595
}
@@ -571,12 +612,12 @@ main (int argc, char** argv)
571612

572613
if (opts.csv)
573614
{
574-
csvStats (cout, data, opts.outputSizeData, opts.timing);
615+
csvStats (cout, data, opts.outputSizeData, opts.timing, opts.computeMSE);
575616
}
576617
else
577618
{
578619
jsonStats (
579-
cout, data, opts.outputSizeData, opts.timing,showPartSizeOnDisk, true, true);
620+
cout, data, opts.outputSizeData, opts.timing, showPartSizeOnDisk, true, true, opts.computeMSE);
580621
}
581622
}
582623

@@ -905,6 +946,10 @@ options::parse (int argc, char* argv[])
905946
}
906947

907948
outputPartSizeOnDisk = true;
949+
}
950+
else if (!strcmp (argv[i], "--mse"))
951+
{
952+
computeMSE = true;
908953
i += 1;
909954
}
910955
else if (!strcmp (argv[i], "-i"))

0 commit comments

Comments
 (0)