Skip to content

Commit 2f3ecdf

Browse files
CLI: PSNR, NRMSE, and opt-in windowed SSIM
The round-trip report now always includes PSNR and NRMSE (double accumulation alongside the existing error scan), and --ssim adds the windowed structural similarity: 7-point windows, stride 2 per axis, window-local luminance range, averaged over windows, for float32 and float64 and any recorded shape. Verified against independent implementations on Miranda at rel 1e-3: numpy reproduces PSNR 71.39 dB and NRMSE 2.693698e-04 to every digit, and the SSIM of 0.900335 is identical in all six decimals to the value computed by cuSZp's own computeSSIM on the same data, so quality numbers are directly comparable across tools.
1 parent 8e726fa commit 2f3ecdf

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ fsz -t f64 -i data.d64 -eb rel 1e-3 any mode on float64 data
122122
| `--bare` | with `-z`: write the raw bitstream without the 56-byte header. Decompressing a bare stream needs `-n` (or `-d`) and `-eb abs`. |
123123
| `-n N` | element count, for decompressing bare streams. |
124124
| `--csv` | additionally print a one-line machine-readable record. |
125+
| `--ssim` | additionally compute the windowed structural similarity of the reconstruction (slower; uses the `-d` shape). PSNR and NRMSE are always reported. |
125126

126127
Exit codes: `0` success (error check passed), `1` bound violated, `2` usage or
127128
file errors.

tools/fsz.cu

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#include <cerrno>
2020
#include <cmath>
21+
#include <limits>
2122
#include <cstdarg>
2223
#include <cstdint>
2324
#include <cstdio>
@@ -122,6 +123,8 @@ void usage(std::FILE* out) {
122123
" -n N element count, for decompressing a bare bitstream\n"
123124
" --bare with -z, write the bitstream with no file header\n"
124125
" --csv also print one machine-readable csv line\n"
126+
" --ssim also compute the windowed structural similarity\n"
127+
" of the reconstruction (slower; uses -d shape)\n"
125128
" -h, --help this message\n"
126129
"\n"
127130
"Input is a raw float32 or float64 array of any dimensionality; up to\n"
@@ -299,6 +302,7 @@ struct Options {
299302
bool force_x = false;
300303
bool bare = false;
301304
bool csv = false;
305+
bool ssim = false;
302306
};
303307

304308
void parse_dim_list(int argc, char** argv, int& i, std::vector<uint64_t>& dims) {
@@ -358,6 +362,8 @@ Options parse_args(int argc, char** argv) {
358362
o.bare = true;
359363
} else if (std::strcmp(a, "--csv") == 0) {
360364
o.csv = true;
365+
} else if (std::strcmp(a, "--ssim") == 0) {
366+
o.ssim = true;
361367
} else if (std::strcmp(a, "-i") == 0) {
362368
if (i + 1 >= argc) die_usage("-i needs a path");
363369
o.in_path = argv[++i];
@@ -438,6 +444,66 @@ void resolve_shape(const Options& o, uint64_t n_file, const std::string& what,
438444
n = prod;
439445
}
440446

447+
// Windowed structural similarity over the recorded shape: 7-point windows
448+
// with stride 2 per axis, the window-local range of the original data as the
449+
// luminance scale, and the mean over all windows. This matches the windowed
450+
// convention used by the common compression quality tools, so values are
451+
// directly comparable.
452+
template <class T>
453+
double windowed_ssim(const std::vector<T>& ori, const std::vector<T>& dec,
454+
const uint64_t dims[3], uint32_t ndims) {
455+
size_t s2 = 1, s1 = 1, s0 = 1;
456+
if (ndims >= 3) { s2 = dims[0]; s1 = dims[1]; s0 = dims[2]; }
457+
else if (ndims == 2) { s1 = dims[0]; s0 = dims[1]; }
458+
else { s0 = dims[0]; }
459+
const size_t w2 = (s2 > 1) ? 7 : 1;
460+
const size_t w1 = (s1 > 1) ? 7 : 1;
461+
const size_t w0 = 7;
462+
if (w0 > s0 || w1 > s1 || w2 > s2)
463+
die("--ssim needs every recorded dimension to hold at least 7 values");
464+
double sum = 0.0;
465+
size_t nw = 0;
466+
const double np = (double)(w0 * w1 * w2);
467+
for (size_t o2 = 0; o2 + w2 <= s2; o2 += 2) {
468+
for (size_t o1 = 0; o1 + w1 <= s1; o1 += 2) {
469+
for (size_t o0 = 0; o0 + w0 <= s0; o0 += 2) {
470+
double xmin = (double)ori[o0 + s0 * (o1 + s1 * o2)];
471+
double xmax = xmin, xs = 0.0, ys = 0.0;
472+
for (size_t i2 = 0; i2 < w2; ++i2)
473+
for (size_t i1 = 0; i1 < w1; ++i1)
474+
for (size_t i0 = 0; i0 < w0; ++i0) {
475+
const size_t idx =
476+
(o0 + i0) + s0 * ((o1 + i1) + s1 * (o2 + i2));
477+
const double x = (double)ori[idx];
478+
const double y = (double)dec[idx];
479+
if (x < xmin) xmin = x;
480+
if (x > xmax) xmax = x;
481+
xs += x; ys += y;
482+
}
483+
const double xm = xs / np, ym = ys / np;
484+
double vx = 0.0, vy = 0.0, vxy = 0.0;
485+
for (size_t i2 = 0; i2 < w2; ++i2)
486+
for (size_t i1 = 0; i1 < w1; ++i1)
487+
for (size_t i0 = 0; i0 < w0; ++i0) {
488+
const size_t idx =
489+
(o0 + i0) + s0 * ((o1 + i1) + s1 * (o2 + i2));
490+
const double dx = (double)ori[idx] - xm;
491+
const double dy = (double)dec[idx] - ym;
492+
vx += dx * dx; vy += dy * dy; vxy += dx * dy;
493+
}
494+
vx /= np; vy /= np; vxy /= np;
495+
const double L = xmax - xmin;
496+
const double c1 = (L == 0.0) ? 0.01 * 0.01 : 0.01 * 0.01 * L * L;
497+
const double c2 = (L == 0.0) ? 0.03 * 0.03 : 0.03 * 0.03 * L * L;
498+
sum += ((2.0 * xm * ym + c1) * (2.0 * vxy + c2))
499+
/ ((xm * xm + ym * ym + c1) * (vx + vy + c2));
500+
++nw;
501+
}
502+
}
503+
}
504+
return sum / (double)nw;
505+
}
506+
441507
// Compress path, shared by report mode and -z. Report mode differs only in that
442508
// it writes no output file. T is the element type of the input array.
443509

@@ -520,13 +586,28 @@ int run_compress(const Options& o, bool write_output) {
520586

521587
double max_err = 0.0;
522588
size_t nonfinite = 0;
589+
double sum_sq = 0.0;
590+
double omin = (double)h_in[0];
591+
double omax = (double)h_in[0];
523592
for (size_t i = 0; i < n; ++i) {
593+
const double x = (double)h_in[i];
594+
if (x < omin) omin = x;
595+
if (x > omax) omax = x;
524596
const T v = h_out[i];
525597
if (!std::isfinite(v)) { ++nonfinite; continue; }
526-
const double e = std::fabs((double)h_in[i] - (double)v);
598+
const double e = std::fabs(x - (double)v);
527599
if (e > max_err) max_err = e;
600+
sum_sq += e * e;
528601
}
529602
const bool ok = (nonfinite == 0) && (max_err <= 1.01 * (double)eb_abs);
603+
const double range = omax - omin;
604+
const double rmse = std::sqrt(sum_sq / (double)n);
605+
const double psnr = (range > 0.0 && rmse > 0.0)
606+
? 20.0 * std::log10(range / rmse)
607+
: std::numeric_limits<double>::infinity();
608+
const double nrmse = (range > 0.0) ? rmse / range : 0.0;
609+
double ssim = -1.0;
610+
if (o.ssim) ssim = windowed_ssim(h_in, h_out, dims, ndims);
530611

531612
uint64_t total_bytes = 0;
532613
if (write_file) {
@@ -576,6 +657,10 @@ int run_compress(const Options& o, bool write_output) {
576657
}
577658
std::printf(" compress = %.2f ms (%.2f GB/s)\n", c_ms, gbps(in_bytes, c_ms));
578659
std::printf(" decompress = %.2f ms (%.2f GB/s)\n", d_ms, gbps(in_bytes, d_ms));
660+
std::printf(" PSNR = %.2f dB\n", psnr);
661+
std::printf(" NRMSE = %.6e\n", nrmse);
662+
if (o.ssim)
663+
std::printf(" SSIM = %.6f\n", ssim);
579664
std::printf(" max_err = %.6e (bound %.6e)\n", max_err, (double)eb_abs);
580665
if (ok) {
581666
std::printf(" %sPASS%s\n", green(), reset());

0 commit comments

Comments
 (0)