libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

ssimulacra.txt (16032B)


      1 // Copyright (c) the JPEG XL Project Authors. All rights reserved.
      2 //
      3 // Use of this source code is governed by a BSD-style
      4 // license that can be found in the LICENSE file.
      5 
      6 /*
      7     SSIMULACRA - Structural SIMilarity Unveiling Local And Compression Related Artifacts
      8 
      9     Cloudinary's variant of DSSIM, based on Philipp Klaus Krause's adaptation of Rabah Mehdi's SSIM implementation,
     10     using ideas from Kornel Lesinski's DSSIM implementation as well as several new ideas.
     11 
     12 
     13 
     14 
     15     Changes compared to Krause's SSIM implementation:
     16     - Use C++ OpenCV API
     17     - Convert sRGB to linear RGB and then to L*a*b*, to get a perceptually more accurate color space
     18     - Multi-scale (6 scales)
     19     - Extra penalty for specific kinds of artifacts:
     20         - local artifacts
     21         - grid-like artifacts (blockiness)
     22         - introducing edges where the original is smooth (blockiness / color banding / ringing / mosquito noise)
     23 
     24     Known limitations:
     25     - Color profiles are ignored; input images are assumed to be sRGB.
     26     - Both input images need to have the same number of channels (Grayscale / RGB / RGBA)
     27 */
     28 
     29 /*
     30     This DSSIM program has been created by Philipp Klaus Krause based on
     31     Rabah Mehdi's C++ implementation of SSIM (http://mehdi.rabah.free.fr/SSIM).
     32     Originally it has been created for the VMV '09 paper
     33     "ftc - floating precision texture compression" by Philipp Klaus Krause.
     34 
     35     The latest version of this program can probably be found somewhere at
     36     http://www.colecovision.eu.
     37 
     38     It can be compiled using g++ -I/usr/include/opencv -lcv -lhighgui dssim.cpp
     39     Make sure OpenCV is installed (e.g. for Debian/ubuntu: apt-get install
     40     libcv-dev libhighgui-dev).
     41 
     42     DSSIM is described in
     43     "Structural Similarity-Based Object Tracking in Video Sequences" by Loza et al.
     44     however setting all Ci to 0 as proposed there results in numerical instabilities.
     45     Thus this implementation used the Ci from the SSIM implementation.
     46     SSIM is described in
     47     "Image quality assessment: from error visibility to structural similarity" by Wang et al.
     48 */
     49 
     50 /*
     51     Copyright (c) 2005, Rabah Mehdi <mehdi.rabah@gmail.com>
     52 
     53     Feel free to use it as you want and to drop me a mail
     54     if it has been useful to you. Please let me know if you enhance it.
     55     I'm not responsible if this program destroy your life & blablabla :)
     56 
     57     Copyright (c) 2009, Philipp Klaus Krause <philipp@colecovision.eu>
     58 
     59     Permission to use, copy, modify, and/or distribute this software for any
     60     purpose with or without fee is hereby granted, provided that the above
     61     copyright notice and this permission notice appear in all copies.
     62 
     63     THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     64     WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     65     MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     66     ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     67     WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     68     ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     69     OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     70 */
     71 
     72 #include <cv.hpp>
     73 #include <highgui.h>
     74 #include <stdio.h>
     75 #include <set>
     76 
     77 // comment this in to produce debug images that show the differences at each scale
     78 #define DEBUG_IMAGES 1
     79 using namespace std;
     80 using namespace cv;
     81 
     82 // All of the constants below are more or less arbitrary.
     83 // Some amount of tweaking/calibration was done, but there is certainly room for improvement.
     84 
     85 // SSIM constants. Original C2 was 0.0009, but a smaller value seems to work slightly better.
     86 const double C1 = 0.0001, C2 = 0.0004;
     87 
     88 // Weight of each scale. Somewhat arbitrary.
     89 // These are based on the values used in IW-SSIM and Kornel's DSSIM.
     90 // It seems weird to give so little weight to the full-size scale, but then again,
     91 // differences in more zoomed-out scales have more visual impact.
     92 // Anyway, these weights seem to work.
     93 // Added one more scale compared to IW-SSIM and Kornel's DSSIM.
     94 // Weights for chroma are modified to give more weight to larger scales (similar to Kornel's subsampled chroma)
     95 const float scale_weights[4][6] = {
     96     // 1:1   1:2     1:4     1:8     1:16    1:32
     97     {0.0448, 0.2856, 0.3001, 0.2363, 0.1333, 0.1  },
     98     {0.015,  0.0448, 0.2856, 0.3001, 0.3363, 0.25 },
     99     {0.015,  0.0448, 0.2856, 0.3001, 0.3363, 0.25 },
    100     {0.0448, 0.2856, 0.3001, 0.2363, 0.1333, 0.1  },
    101     };
    102 
    103 // higher value means more importance to chroma (weights above are multiplied by this factor for chroma and alpha)
    104 const double chroma_weight = 0.2;
    105 
    106 // Weights for the worst-case (minimum) score at each scale.
    107 // Higher value means more importance to worst artifacts, lower value means more importance to average artifacts.
    108 const float mscale_weights[4][6] = {
    109     // 1:4   1:8     1:16    1:32   1:64   1:128
    110     {0.2,    0.3,    0.25,   0.2,   0.12,  0.05},
    111     {0.01,   0.05,   0.2,    0.3,   0.35,  0.35},
    112     {0.01,   0.05,   0.2,    0.3,   0.35,  0.35},
    113     {0.2,    0.3,    0.25,   0.2,   0.12,  0.05},
    114     };
    115 
    116 
    117 // higher value means more importance to worst local artifacts
    118 const double min_weight[4] = {0.1,0.005,0.005,0.005};
    119 
    120 // higher value means more importance to artifact-edges (edges where original is smooth)
    121 const double extra_edges_weight[4] = {1.5, 0.1, 0.1, 0.5};
    122 
    123 // higher value means more importance to grid-like artifacts (blockiness)
    124 const double worst_grid_weight[2][4] = 
    125     { {1.0, 0.1, 0.1, 0.5},             // on ssim heatmap
    126       {1.0, 0.1, 0.1, 0.5} };           // on extra_edges heatmap
    127 
    128 
    129 // Convert linear RGB to L*a*b* (all in 0..1 range)
    130 inline void rgb2lab(Vec3f &p) __attribute__ ((hot));
    131 inline void rgb2lab(Vec3f &p) {
    132     const float epsilon = 0.00885645167903563081f;
    133     const float s = 0.13793103448275862068f;
    134     const float k = 7.78703703703703703703f;
    135 
    136     // D65 adjustment included
    137     float fx = (p[2] * 0.43393624408206207259f + p[1] * 0.37619779063650710152f + p[0] * .18983429773803261441f) ;
    138     float fy = (p[2] * 0.2126729f + p[1] * 0.7151522f + p[0] * 0.0721750f);
    139     float fz = (p[2] * 0.01775381083562901744f + p[1] * 0.10945087235996326905f + p[0] * 0.87263921028466483011f) ;
    140 
    141     float X = (fx > epsilon) ? powf(fx,1.0f/3.0f) - s : k * fx;
    142     float Y = (fy > epsilon) ? powf(fy,1.0f/3.0f) - s : k * fy;
    143     float Z = (fz > epsilon) ? powf(fz,1.0f/3.0f) - s : k * fz;
    144 
    145     p[0] = Y * 1.16f;
    146     p[1] = (0.39181818181818181818f + 2.27272727272727272727f * (X - Y));
    147     p[2] = (0.49045454545454545454f + 0.90909090909090909090f * (Y - Z));
    148 }
    149 
    150 
    151 int main(int argc, char** argv) {
    152 
    153     if(argc!=3) {
    154         fprintf(stderr, "Usage: %s orig_image distorted_image\n", argv[0]);
    155         fprintf(stderr, "Returns a value between 0 (images are identical) and 1 (images are very different)\n");
    156         fprintf(stderr, "If the value is above 0.1 (or so), the distortion is likely to be perceptible / annoying.\n");
    157         fprintf(stderr, "If the value is below 0.01 (or so), the distortion is likely to be imperceptible.\n");
    158         return(-1);
    159     }
    160 
    161     Scalar sC1 = {C1,C1,C1,C1}, sC2 = {C2,C2,C2,C2};
    162 
    163     Mat img1, img2, img1_img2, img1_temp, img2_temp, img1_sq, img2_sq, mu1, mu2, mu1_sq, mu2_sq, mu1_mu2, sigma1_sq, sigma2_sq, sigma12, ssim_map;
    164 
    165     // read and validate input images
    166 
    167     img1_temp = imread(argv[1],-1);
    168     img2_temp = imread(argv[2],-1);
    169 
    170     int nChan = img1_temp.channels();
    171     if (nChan != img2_temp.channels()) {
    172         fprintf(stderr, "Image file %s has %i channels, while\n", argv[1], nChan);
    173         fprintf(stderr, "image file %s has %i channels. Can't compare.\n", argv[2], img2_temp.channels());
    174         return -1;
    175     }
    176     if (img1_temp.size() != img2_temp.size()) {
    177         fprintf(stderr,  "Image dimensions have to be identical.\n");
    178         return -1;
    179     }
    180     if (img1_temp.cols < 8 || img1_temp.rows < 8) {
    181         fprintf(stderr,  "Image is too small; need at least 8 rows and columns.\n");
    182         return -1;
    183     }
    184     int pixels = img1_temp.rows * img1_temp.cols;
    185     if (nChan == 4) {
    186         // blend to a gray background to have a fair comparison of semi-transparent RGB values
    187         for( int i=0 ; i < pixels; i++ ) {
    188             Vec4b & p = img1_temp.at<Vec4b>(i);
    189             p[0] = (p[3]*p[0] + (255-p[3])*128 ) / 255;
    190             p[1] = (p[3]*p[1] + (255-p[3])*128 ) / 255;
    191             p[2] = (p[3]*p[2] + (255-p[3])*128 ) / 255;
    192         }
    193         for( int i=0 ; i < pixels; i++ ) {
    194             Vec4b & p = img2_temp.at<Vec4b>(i);
    195             p[0] = (p[3]*p[0] + (255-p[3])*128 ) / 255;
    196             p[1] = (p[3]*p[1] + (255-p[3])*128 ) / 255;
    197             p[2] = (p[3]*p[2] + (255-p[3])*128 ) / 255;
    198         }
    199     }
    200 
    201 
    202     if (nChan > 1) {
    203     // Create lookup table to convert 8-bit sRGB to linear RGB
    204     Mat sRGB_gamma_LUT(1, 256, CV_32FC1);
    205     for (int i = 0; i < 256; i++) {
    206         float c = i / 255.0;
    207         sRGB_gamma_LUT.at<float>(i) = (c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4));
    208     }
    209 
    210     // Convert from sRGB to linear RGB
    211     LUT(img1_temp, sRGB_gamma_LUT, img1);
    212     LUT(img2_temp, sRGB_gamma_LUT, img2);
    213     } else {
    214         img1 = Mat(img1_temp.rows, img1_temp.cols, CV_32FC1);
    215         img2 = Mat(img1_temp.rows, img1_temp.cols, CV_32FC1);
    216     }
    217 
    218     // Convert from linear RGB to Lab in a 0..1 range
    219     if (nChan == 3) {
    220       for( int i=0 ; i < pixels; i++ ) rgb2lab(img1.at<Vec3f>(i));
    221       for( int i=0 ; i < pixels; i++ ) rgb2lab(img2.at<Vec3f>(i));
    222     } else if (nChan == 4) {
    223       for( int i=0 ; i < pixels; i++ ) { Vec3f p = {img1.at<Vec4f>(i)[0],img1.at<Vec4f>(i)[1],img1.at<Vec4f>(i)[2]}; rgb2lab(p); img1.at<Vec4f>(i)[0] = p[0]; img1.at<Vec4f>(i)[1] = p[1]; img1.at<Vec4f>(i)[2] = p[2];}
    224       for( int i=0 ; i < pixels; i++ ) { Vec3f p = {img2.at<Vec4f>(i)[0],img2.at<Vec4f>(i)[1],img2.at<Vec4f>(i)[2]}; rgb2lab(p); img2.at<Vec4f>(i)[0] = p[0]; img2.at<Vec4f>(i)[1] = p[1]; img2.at<Vec4f>(i)[2] = p[2];}
    225     } else if (nChan == 1) {
    226       for( int i=0 ; i < pixels; i++ ) { img1.at<float>(i) = img1_temp.at<uchar>(i)/255.0;}
    227       for( int i=0 ; i < pixels; i++ ) { img2.at<float>(i) = img2_temp.at<uchar>(i)/255.0;}
    228     } else {
    229         fprintf(stderr, "Can only deal with Grayscale, RGB or RGBA input.\n");
    230         return(-1);
    231     }
    232 
    233 
    234     double dssim=0, dssim_max=0;
    235 
    236     for (int scale = 0; scale < 6; scale++) {
    237 
    238       if (img1.cols < 8 || img1.rows < 8) break;
    239       if (scale) {
    240         // scale down 50% in each iteration.
    241         resize(img1, img1, Size(), 0.5, 0.5, INTER_AREA);
    242         resize(img2, img2, Size(), 0.5, 0.5, INTER_AREA);
    243       }
    244 
    245       // Standard SSIM computation
    246       cv::pow( img1, 2, img1_sq );
    247       cv::pow( img2, 2, img2_sq );
    248 
    249       multiply( img1, img2, img1_img2, 1 );
    250 
    251       GaussianBlur(img1, mu1, Size(11,11), 1.5);
    252       GaussianBlur(img2, mu2, Size(11,11), 1.5);
    253 
    254       cv::pow( mu1, 2, mu1_sq );
    255       cv::pow( mu2, 2, mu2_sq );
    256       multiply( mu1, mu2, mu1_mu2, 1 );
    257 
    258       GaussianBlur(img1_sq, sigma1_sq, Size(11,11), 1.5);
    259       addWeighted( sigma1_sq, 1, mu1_sq, -1, 0, sigma1_sq );
    260 
    261       GaussianBlur(img2_sq, sigma2_sq, Size(11,11), 1.5);
    262       addWeighted( sigma2_sq, 1, mu2_sq, -1, 0, sigma2_sq );
    263 
    264       GaussianBlur(img1_img2, sigma12, Size(11,11), 1.5);
    265       addWeighted( sigma12, 1, mu1_mu2, -1, 0, sigma12 );
    266 
    267       ssim_map = ((2*mu1_mu2 + sC1).mul(2*sigma12 + sC2))/((mu1_sq + mu2_sq + sC1).mul(sigma1_sq + sigma2_sq + sC2));
    268 
    269 
    270       // optional: write a nice debug image that shows the problematic areas
    271 #ifdef DEBUG_IMAGES
    272       Mat ssim_image;
    273       ssim_map.convertTo(ssim_image,CV_8UC3,255);
    274         for( int i=0 ; i < ssim_image.rows * ssim_image.cols; i++ ) {
    275             Vec3b &p = ssim_image.at<Vec3b>(i);
    276             p = {(uchar)(255-p[2]),(uchar)(255-p[0]),(uchar)(255-p[1])};
    277         }
    278       imwrite("debug-scale"+to_string(scale)+".png",ssim_image);
    279 #endif
    280 
    281 
    282       // average ssim over the entire image
    283       Scalar avg = mean( ssim_map );
    284       for(unsigned int i = 0; i < nChan; i++) {
    285         printf("avg: %i  %f\n",i,avg[i]);
    286         dssim += (i>0?chroma_weight:1.0) * avg[i] * scale_weights[i][scale];
    287         dssim_max += (i>0?chroma_weight:1.0) * scale_weights[i][scale];
    288       }
    289 
    290 //      resize(ssim_map, ssim_map, Size(), 0.5, 0.5, INTER_AREA);
    291 
    292 
    293       // the edge/blockiness penalty is only done for the fullsize images
    294       if (scale == 0) {
    295 
    296         // asymmetric: penalty for introducing edges where there are none (e.g. blockiness), no penalty for smoothing away edges
    297         Mat edgediff = max(abs(img2 - mu2) - abs(img1 - mu1), 0);   // positive if img2 has an edge where img1 is smooth
    298 
    299         // optional: write a nice debug image that shows the artifact edges
    300 #ifdef DEBUG_IMAGES
    301         Mat edgediff_image;
    302         edgediff.convertTo(edgediff_image,CV_8UC3,5000); // multiplying by more than 255 to make things easier to see
    303         for( int i=0 ; i < pixels; i++ ) {
    304             Vec3b &p = edgediff_image.at<Vec3b>(i);
    305             p = {(uchar)(p[1]+p[2]),p[0],p[0]};
    306         }
    307         imwrite("debug-edgediff.png",edgediff_image);
    308 #endif
    309 
    310         edgediff = Scalar(1.0,1.0,1.0,1.0) - edgediff;
    311 
    312         avg = mean(edgediff);
    313         for(unsigned int i = 0; i < nChan; i++) {
    314           printf("extra_edges: %i  %f\n",i,avg[i]);
    315           dssim +=  extra_edges_weight[i] * avg[i];
    316           dssim_max +=  extra_edges_weight[i];
    317         }
    318 
    319         // grid-like artifact detection
    320         // do the things below twice: once for the SSIM map, once for the artifact-edge map
    321         Mat errormap;
    322         for(int twice=0; twice < 2; twice++) {
    323           if (twice == 0) errormap = ssim_map;
    324           else errormap = edgediff;
    325 
    326           // Find the 2nd percentile worst row. If the compression uses blocks, there will be artifacts around the block edges,
    327           // so even with 32x32 blocks, the 2nd percentile will likely be one of the rows with block borders
    328           multiset<double> row_scores[4];
    329           for (int y = 0; y < errormap.rows; y++) {
    330             Mat roi = errormap(Rect(0,y,errormap.cols,1));
    331             Scalar ravg = mean(roi);
    332             for (unsigned int i = 0; i < nChan; i++) row_scores[i].insert(ravg[i]);
    333           }
    334           for(unsigned int i = 0; i < nChan; i++) {
    335             int k=0; for (const double& s : row_scores[i]) { if (k++ >= errormap.rows/50) { dssim += worst_grid_weight[twice][i] * s; 
    336           printf("grid row %s %i:  %f\n",(twice?"edgediff":"ssimmap"),i,s);
    337 
    338  break; } }
    339             dssim_max += worst_grid_weight[twice][i];
    340           }
    341           // Find the 2nd percentile worst column. Same concept as above.
    342           multiset<double> col_scores[4];
    343           for (int x = 0; x < errormap.cols; x++) {
    344             Mat roi = errormap(Rect(x,0,1,errormap.rows));
    345             Scalar cavg = mean(roi);
    346             for (unsigned int i = 0; i < nChan; i++) col_scores[i].insert(cavg[i]);
    347           }
    348           for(unsigned int i = 0; i < nChan; i++) {
    349             int k=0; for (const double& s : col_scores[i]) { if (k++ >= errormap.cols/50) { dssim += worst_grid_weight[twice][i] * s; 
    350           printf("grid col %s %i:  %f\n",(twice?"edgediff":"ssimmap"),i,s);
    351 
    352 break; } }
    353             dssim_max += worst_grid_weight[twice][i];
    354           }
    355         }
    356       }
    357 
    358       // worst ssim in a particular 4x4 block (larger blocks are considered too because of multi-scale)
    359       resize(ssim_map, ssim_map, Size(), 0.25, 0.25, INTER_AREA);
    360 //      resize(ssim_map, ssim_map, Size(), 0.5, 0.5, INTER_AREA);
    361 
    362       Mat ssim_map_c[4];
    363       split(ssim_map, ssim_map_c);
    364       for (unsigned int i=0; i < nChan; i++) {
    365         double minVal;
    366         minMaxLoc(ssim_map_c[i], &minVal);
    367           printf("worst %i:  %f\n",i,minVal);
    368         dssim += min_weight[i]  * minVal * mscale_weights[i][scale];
    369         dssim_max += min_weight[i]  * mscale_weights[i][scale];
    370       }
    371 
    372     }
    373 
    374 
    375     dssim = dssim_max / dssim - 1;
    376     if (dssim < 0) dssim = 0; // should not happen
    377     if (dssim > 1) dssim = 1; // very different images
    378 
    379     printf("%.8f\n", dssim);
    380 
    381     return(0);
    382 }