|
18 | 18 |
|
19 | 19 | #include <cerrno> |
20 | 20 | #include <cmath> |
| 21 | +#include <limits> |
21 | 22 | #include <cstdarg> |
22 | 23 | #include <cstdint> |
23 | 24 | #include <cstdio> |
@@ -122,6 +123,8 @@ void usage(std::FILE* out) { |
122 | 123 | " -n N element count, for decompressing a bare bitstream\n" |
123 | 124 | " --bare with -z, write the bitstream with no file header\n" |
124 | 125 | " --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" |
125 | 128 | " -h, --help this message\n" |
126 | 129 | "\n" |
127 | 130 | "Input is a raw float32 or float64 array of any dimensionality; up to\n" |
@@ -299,6 +302,7 @@ struct Options { |
299 | 302 | bool force_x = false; |
300 | 303 | bool bare = false; |
301 | 304 | bool csv = false; |
| 305 | + bool ssim = false; |
302 | 306 | }; |
303 | 307 |
|
304 | 308 | 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) { |
358 | 362 | o.bare = true; |
359 | 363 | } else if (std::strcmp(a, "--csv") == 0) { |
360 | 364 | o.csv = true; |
| 365 | + } else if (std::strcmp(a, "--ssim") == 0) { |
| 366 | + o.ssim = true; |
361 | 367 | } else if (std::strcmp(a, "-i") == 0) { |
362 | 368 | if (i + 1 >= argc) die_usage("-i needs a path"); |
363 | 369 | o.in_path = argv[++i]; |
@@ -438,6 +444,66 @@ void resolve_shape(const Options& o, uint64_t n_file, const std::string& what, |
438 | 444 | n = prod; |
439 | 445 | } |
440 | 446 |
|
| 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 | + |
441 | 507 | // Compress path, shared by report mode and -z. Report mode differs only in that |
442 | 508 | // it writes no output file. T is the element type of the input array. |
443 | 509 |
|
@@ -520,13 +586,28 @@ int run_compress(const Options& o, bool write_output) { |
520 | 586 |
|
521 | 587 | double max_err = 0.0; |
522 | 588 | size_t nonfinite = 0; |
| 589 | + double sum_sq = 0.0; |
| 590 | + double omin = (double)h_in[0]; |
| 591 | + double omax = (double)h_in[0]; |
523 | 592 | 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; |
524 | 596 | const T v = h_out[i]; |
525 | 597 | 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); |
527 | 599 | if (e > max_err) max_err = e; |
| 600 | + sum_sq += e * e; |
528 | 601 | } |
529 | 602 | 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); |
530 | 611 |
|
531 | 612 | uint64_t total_bytes = 0; |
532 | 613 | if (write_file) { |
@@ -576,6 +657,10 @@ int run_compress(const Options& o, bool write_output) { |
576 | 657 | } |
577 | 658 | std::printf(" compress = %.2f ms (%.2f GB/s)\n", c_ms, gbps(in_bytes, c_ms)); |
578 | 659 | 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); |
579 | 664 | std::printf(" max_err = %.6e (bound %.6e)\n", max_err, (double)eb_abs); |
580 | 665 | if (ok) { |
581 | 666 | std::printf(" %sPASS%s\n", green(), reset()); |
|
0 commit comments