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 }