ios/SpectrumKit/SpectrumKitInstrumentationTestsHelpers/FSPSSIMCalculator.m (130 lines of code) (raw):

// Copyright (c) Facebook, Inc. and its affiliates. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. #import "FSPSSIMCalculator.h" #define FSPImageSimilaritySSIMWindowSize 8 #define FSPImageSimilaritySSIMWindowArea (FSPImageSimilaritySSIMWindowSize * FSPImageSimilaritySSIMWindowSize) static const NSUInteger FSPImageSimilaritySSIMWindowStep = 4; static const NSUInteger FSPImageSimilaritySSIMChannelsCount = 4; static const CGFloat FSPImageSimilaritySSIMK1 = 0.01f; static const CGFloat FSPImageSimilaritySSIMK2 = 0.03f; static const CGFloat FSPImageSimilaritySSIMMaxValue = 256.0f; static const CGFloat FSPImageSimilaritySSIMC1 = FSPImageSimilaritySSIMK1 * FSPImageSimilaritySSIMMaxValue * FSPImageSimilaritySSIMK1 * FSPImageSimilaritySSIMMaxValue; static const CGFloat FSPImageSimilaritySSIMC2 = FSPImageSimilaritySSIMK2 * FSPImageSimilaritySSIMMaxValue * FSPImageSimilaritySSIMK2 * FSPImageSimilaritySSIMMaxValue; #pragma mark - SSIM Helpers static CGFloat FSPComputeAverageForChannels(int const *const channels) { NSUInteger average = 0; for (NSUInteger i = 0; i < FSPImageSimilaritySSIMWindowArea; ++i) { average += channels[i]; } return (CGFloat)average / (CGFloat)FSPImageSimilaritySSIMWindowArea; } static CGFloat FSPComputeVarianceForChannels(int const *const channels, const CGFloat average) { CGFloat variance = 0.0f; for (NSUInteger i = 0; i < FSPImageSimilaritySSIMWindowArea; ++i) { variance += (channels[i] - average) * (channels[i] - average); } return variance / (CGFloat)FSPImageSimilaritySSIMWindowArea; } static CGFloat FSPComputeCovarianceForChannels(const int *const lhsChannels, const int *const rhsChannels, const CGFloat lhsAverage, const CGFloat rhsAverage) { CGFloat covariance = 0.0f; for (NSUInteger i = 0; i < FSPImageSimilaritySSIMWindowArea; ++i) { const CGFloat lhsDelta = lhsChannels[i] - lhsAverage; const CGFloat rhsDelta = rhsChannels[i] - rhsAverage; covariance += lhsDelta * rhsDelta; } return covariance / (CGFloat)FSPImageSimilaritySSIMWindowArea; } static CGFloat FSPComputeSSIMForChannels(const int *const lhsChannels, const int *const rhsChannels) { const CGFloat lhsAverage = FSPComputeAverageForChannels(lhsChannels); const CGFloat lhsVariance = FSPComputeVarianceForChannels(lhsChannels, lhsAverage); const CGFloat rhsAverage = FSPComputeAverageForChannels(rhsChannels); const CGFloat rhsVariance = FSPComputeVarianceForChannels(rhsChannels, rhsAverage); const CGFloat covariance = FSPComputeCovarianceForChannels(lhsChannels, rhsChannels, lhsAverage, rhsAverage); const CGFloat t1 = 2 * lhsAverage * rhsAverage + FSPImageSimilaritySSIMC1; const CGFloat t2 = 2 * covariance + FSPImageSimilaritySSIMC2; const CGFloat t3 = (lhsAverage * lhsAverage) + (rhsAverage * rhsAverage) + FSPImageSimilaritySSIMC1; const CGFloat t4 = lhsVariance + rhsVariance + FSPImageSimilaritySSIMC2; return (t1 * t2) / (t3 * t4); } #pragma mark - FSPImageData typedef struct { uint8_t const* bytes; CGContextRef context; } FSPImageData; static FSPImageData FSPMakeImageDataFromImage(CGImageRef image) { // Inspired from https://developer.apple.com/library/content/qa/qa1509/_index.html const size_t imageHeight = CGImageGetHeight(image); const size_t imageWidth = CGImageGetWidth(image); // Declare the number of bytes per row. Each pixel in the bitmap in this // example is represented by 4 bytes; 8 bits each of red, green, blue, and // alpha. const NSInteger bitmapBytesPerRow = imageWidth * FSPImageSimilaritySSIMChannelsCount; const CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); NSCAssert(colorSpace != NULL, @"Unable to get color space"); // Allocate memory for image data. This is the destination in memory // where any drawing to the bitmap context will be rendered. void *bitmapData = malloc(bitmapBytesPerRow * imageHeight); if (bitmapData == NULL) { CGColorSpaceRelease(colorSpace); NSCAssert(false, @"Unable to alloc bitmap data"); } // Create the bitmap context. We want pre-multiplied ARGB, 8-bits // per component. Regardless of what the source image format is // (CMYK, Grayscale, and so on) it will be converted over to the format // specified here by CGBitmapContextCreate. CGContextRef context = CGBitmapContextCreate(bitmapData, imageWidth, imageHeight, 8, // bits per component bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedFirst); CGColorSpaceRelease(colorSpace); if (context == NULL) { free(bitmapData); NSCAssert(false, @"Unable to create context"); } // Draw the image to the bitmap context. Once we draw, the memory // allocated for the context for rendering will then contain the // raw image data in the specified color space. CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image); // Now we can get a pointer to the image data associated with the bitmap // context. uint8_t const *const bytes = (uint8_t const *const)CGBitmapContextGetData(context); NSCAssert(bytes != NULL, @"Unable to get bitmap bytes"); FSPImageData data; data.bytes = bytes; data.context = context; return data; } static void FSPCleanupImageData(const FSPImageData *imageData) { if (imageData != NULL) { CGContextRelease(imageData->context); free((void *)imageData->bytes); } } #pragma mark - SSIM CGFloat FSPComputeSSIMFactorBetween(UIImage *left, UIImage *right) { NSCParameterAssert(left != nil); NSCParameterAssert(right != nil); NSCParameterAssert(CGSizeEqualToSize(left.size, right.size)); NSCAssert1(left.size.width >= FSPImageSimilaritySSIMWindowSize, @"Image width must be >= than %d", FSPImageSimilaritySSIMWindowSize); NSCAssert1(left.size.height >= FSPImageSimilaritySSIMWindowSize, @"Image height must be >= than %d", FSPImageSimilaritySSIMWindowSize); FSPImageData leftImageData = FSPMakeImageDataFromImage(left.CGImage); FSPImageData rightImageData = FSPMakeImageDataFromImage(right.CGImage); int leftChannels[FSPImageSimilaritySSIMWindowArea]; int otherChannels[FSPImageSimilaritySSIMWindowArea]; CGFloat ssimSum = 0; NSUInteger ssimCount = 0; for (NSUInteger x = 0; x < left.size.width - FSPImageSimilaritySSIMWindowSize; x += FSPImageSimilaritySSIMWindowStep) { for (NSUInteger y = 0; y < left.size.height - FSPImageSimilaritySSIMWindowSize; y += FSPImageSimilaritySSIMWindowStep) { for (NSUInteger channel = 0; channel < FSPImageSimilaritySSIMChannelsCount; ++channel) { for (NSUInteger yy = 0; yy < FSPImageSimilaritySSIMWindowSize; ++yy) { for (NSUInteger xx = 0; xx < FSPImageSimilaritySSIMWindowSize; ++xx) { const NSUInteger byteIndex = FSPImageSimilaritySSIMChannelsCount * ((y + yy) * left.size.width + (x + xx)); const NSUInteger i = yy * FSPImageSimilaritySSIMWindowSize + xx; leftChannels[i] = leftImageData.bytes[byteIndex + channel]; otherChannels[i] = rightImageData.bytes[byteIndex + channel]; } } ssimSum += FSPComputeSSIMForChannels(leftChannels, otherChannels); ++ssimCount; } } } FSPCleanupImageData(&leftImageData); FSPCleanupImageData(&rightImageData); return ssimSum / (CGFloat)ssimCount; }