Skip to content

Commit

Permalink
Improve tests using structural similarity index (SSI) to calculate si…
Browse files Browse the repository at this point in the history
…milarity between test image and aolden images
  • Loading branch information
djowel committed Nov 25, 2023
1 parent f83ea6d commit 44dcea2
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 27 deletions.
2 changes: 2 additions & 0 deletions lib/impl/macos/quartz2d/font.mm
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#include <sstream>
#include <cmath>
#include "osx_utils.hpp"
#include <infra/assert.hpp>
#include <artist/resources.hpp>

namespace cycfi::artist
{
Expand Down
49 changes: 40 additions & 9 deletions lib/impl/macos/quartz2d/image.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,46 @@
{
auto path = [NSString stringWithUTF8String : std::string{path_}.c_str() ];
auto image = (__bridge NSImage*) _impl;
auto ref = [image CGImageForProposedRect : nullptr
context : nullptr
hints : nullptr];
auto* rep = [[NSBitmapImageRep alloc] initWithCGImage : ref];
[rep setSize:[image size]];

auto* data = [rep representationUsingType : NSBitmapImageFileTypePNG
properties : @{}];
[data writeToFile : path atomically : YES];

// Get the size of the original image
NSSize imageSize = [image size];

// Create an NSBitmapImageRep with the same dimensions as the original image
NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc]
initWithBitmapDataPlanes : NULL
pixelsWide : imageSize.width
pixelsHigh : imageSize.height
bitsPerSample : 8
samplesPerPixel : 4 // RGBA format
hasAlpha : YES
isPlanar : NO
colorSpaceName : NSDeviceRGBColorSpace
bytesPerRow : 0
bitsPerPixel : 0
];

// Set the properties for the PNG file
NSDictionary *properties = @{
NSImageCompressionFactor : @1.0, // Compression factor (1.0 means no compression)
NSImageColorSyncProfileData : [NSNull null], // No color profile
NSImageInterlaced : @NO // Non-interlaced
};

// Set the current graphics context to the NSBitmapImageRep
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext : [NSGraphicsContext graphicsContextWithBitmapImageRep : bitmapRep]];

// Draw the original image onto the NSBitmapImageRep
[image drawAtPoint : NSZeroPoint fromRect : NSZeroRect operation : NSCompositingOperationCopy fraction : 1.0];

// Restore the graphics state
[NSGraphicsContext restoreGraphicsState];

// Convert the NSBitmapImageRep to NSData with PNG format
NSData* data = [bitmapRep representationUsingType : NSBitmapImageFileTypePNG properties : properties];

// Write the data to the file
[data writeToFile : path atomically : YES];
}

uint32_t* image::pixels()
Expand Down
22 changes: 18 additions & 4 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@
###############################################################################
project(artist_test)

file(
GLOB artist_fonts ${CMAKE_CURRENT_LIST_DIR}/../resources/fonts/*.*
)

if (APPLE)

add_executable(
artist_test MACOSX_BUNDLE
main.cpp
)
if (ARTIST_QUARTZ_2D)
add_executable(
artist_test MACOSX_BUNDLE
main.cpp
detail/osx_init.mm
${artist_fonts}
)
else()
add_executable(
artist_test MACOSX_BUNDLE
main.cpp
${artist_fonts}
)
endif()

if (ARTIST_QUARTZ_2D)
file(
Expand Down
72 changes: 72 additions & 0 deletions test/detail/osx_init.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*=============================================================================
Copyright (c) 2016-2023 Joel de Guzman
Distributed under the MIT License [ https://opensource.org/licenses/MIT ]
=============================================================================*/
#include <Quartz/Quartz.h>
#include <dlfcn.h>
#include <infra/assert.hpp>
#include <artist/resources.hpp>

///////////////////////////////////////////////////////////////////////////////
namespace
{
namespace fs = cycfi::fs;

CFBundleRef get_bundle_from_executable(const char* filepath)
{
NSString* exec_str = [NSString stringWithCString:filepath encoding : NSUTF8StringEncoding];
NSString* mac_os_str = [exec_str stringByDeletingLastPathComponent];
NSString* contents_str = [mac_os_str stringByDeletingLastPathComponent];
NSString* bundleStr = [contents_str stringByDeletingLastPathComponent];
return CFBundleCreate(0, (CFURLRef)[NSURL fileURLWithPath:bundleStr isDirectory : YES]);
}

CFBundleRef get_current_bundle()
{
Dl_info info;
if (dladdr((const void*)get_current_bundle, &info) && info.dli_fname)
return get_bundle_from_executable(info.dli_fname);
return 0;
}

void activate_font(fs::path font_path)
{
auto furl = [NSURL fileURLWithPath : [NSString stringWithUTF8String : font_path.c_str()]];
CYCFI_ASSERT(furl, "Error: Unexpected missing font.");

CFErrorRef error = nullptr;
CTFontManagerRegisterFontsForURL((__bridge CFURLRef) furl, kCTFontManagerScopeProcess, &error);
}

void get_resource_path(char resource_path[])
{
CFBundleRef main_bundle = get_current_bundle();
CFURLRef resources_url = CFBundleCopyResourcesDirectoryURL(main_bundle);
CFURLGetFileSystemRepresentation(resources_url, TRUE, (UInt8*) resource_path, PATH_MAX);
CFRelease(resources_url);
CFRelease(main_bundle);
}
}

struct resource_setter
{
resource_setter();
};

resource_setter::resource_setter()
{
// Before anything else, set the working directory so we can access
// our resources
char resource_path[PATH_MAX];
get_resource_path(resource_path);
cycfi::artist::add_search_path(resource_path);

// Load the user fonts from the Resource folder. Normally this is automatically
// done on application startup, but for plugins, we need to explicitly load
// the user fonts ourself.
for (fs::directory_iterator it{resource_path}; it != fs::directory_iterator{}; ++it)
if (it->path().extension() == ".ttf")
activate_font(it->path());
}

Binary file modified test/macos_golden/quartz2d/composite_ops.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/macos_golden/quartz2d/drop_shadow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/macos_golden/quartz2d/misc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/macos_golden/quartz2d/paths.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/macos_golden/quartz2d/shapes_and_images.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified test/macos_golden/quartz2d/typography.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 changes: 87 additions & 14 deletions test/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include <infra/catch.hpp>
#include <artist/affine_transform.hpp>
#include "app_paths.hpp"
#include <cmath>
#include <cstdint>

using namespace cycfi::artist;
using namespace font_constants;
Expand All @@ -21,6 +23,15 @@ using cycfi::codepoint;
auto constexpr window_size = point{640.0f, 480.0f};
auto constexpr bkd_color = rgba(54, 52, 55, 255);

#if defined(ARTIST_QUARTZ_2D)
// Initialize the fonts and resources
struct resource_setter
{
resource_setter();
};
static resource_setter init_resources;
#endif

void background(canvas& cnv)
{
cnv.add_rect({{0, 0}, window_size});
Expand Down Expand Up @@ -231,19 +242,82 @@ void test_draw(canvas& cnv)
line_styles(cnv);
}

float diff_pixel(uint32_t a, uint32_t b)
// Function to extract RGBA values with generic component names Note: Using
// generic names (x, y, z, w) to make the code agnostic about the order of
// components within uint32_t values.
void extract_rgba(uint32_t pixel, uint8_t& x, uint8_t& y, uint8_t& z, uint8_t& w)
{
auto a1 = a & 0xff;
auto a2 = (a >> 8) & 0xff;
auto a3 = (a >> 16) & 0xff;
auto a4 = (a >> 24) & 0xff;
x = (pixel >> 24) & 0xFF;
y = (pixel >> 16) & 0xFF;
z = (pixel >> 8) & 0xFF;
w = pixel & 0xFF;
}

// Function to calculate structural similarity index (SSI) between two images
double calculate_ssi(uint32_t const img1[], uint32_t const img2[], int const width, int const height)
{
const double c1 = 0.0001; // Small constant to avoid division by zero
const double c2 = 0.0009; // Small constant to avoid division by zero
double mean_x = 0.0, mean_y = 0.0, sigma_x = 0.0, sigma_y = 0.0, sigma_xy = 0.0;

for (int i = 0; i < width * height; ++i)
{
// Extract individual components using generic names x, y, z, w
uint8_t x1, y1, z1, w1;
uint8_t x2, y2, z2, w2;

extract_rgba(img1[i], x1, y1, z1, w1);
extract_rgba(img2[i], x2, y2, z2, w2);

mean_x += x1 + y1 + z1 + w1;
mean_y += x2 + y2 + z2 + w2;
}

mean_x /= (width * height * 4);
mean_y /= (width * height * 4);

for (int i = 0; i < width * height; ++i)
{
// Extract individual components using generic names x, y, z, w
uint8_t x1, y1, z1, w1;
uint8_t x2, y2, z2, w2;

extract_rgba(img1[i], x1, y1, z1, w1);
extract_rgba(img2[i], x2, y2, z2, w2);

double dev_x = x1 - mean_x;
double dev_y = x2 - mean_y;
sigma_x += dev_x * dev_x;
sigma_y += dev_y * dev_y;
sigma_xy += dev_x * dev_y;

dev_x = y1 - mean_x;
dev_y = y2 - mean_y;
sigma_x += dev_x * dev_x;
sigma_y += dev_y * dev_y;
sigma_xy += dev_x * dev_y;

dev_x = z1 - mean_x;
dev_y = z2 - mean_y;
sigma_x += dev_x * dev_x;
sigma_y += dev_y * dev_y;
sigma_xy += dev_x * dev_y;

dev_x = w1 - mean_x;
dev_y = w2 - mean_y;
sigma_x += dev_x * dev_x;
sigma_y += dev_y * dev_y;
sigma_xy += dev_x * dev_y;
}

sigma_x /= (width * height * 4 - 1);
sigma_y /= (width * height * 4 - 1);
sigma_xy /= (width * height * 4 - 1);

auto b1 = b & 0xff;
auto b2 = (b >> 8) & 0xff;
auto b3 = (b >> 16) & 0xff;
auto b4 = (b >> 24) & 0xff;
const double numerator = (2 * mean_x * mean_y + c1) * (2 * sigma_xy + c2);
const double denominator = (mean_x * mean_x + mean_y * mean_y + c1) * (sigma_x + sigma_y + c2);

return float(a1-b1) + float(a2-b2) + float(a3-b3) + float(a4-b4);
return numerator / denominator;
}

void compare_golden(image const& pm, std::string name)
Expand All @@ -265,10 +339,9 @@ void compare_golden(image const& pm, std::string name)
REQUIRE(a != nullptr);
REQUIRE(b != nullptr);

auto diff = 0;
for (auto i = 0; i != (bm_size.x * bm_size.y); ++i)
diff += diff_pixel(a[i], b[i]);
CHECK(diff == 0);
auto ssi = calculate_ssi(a, b, bm_size.x, bm_size.y);
CHECK(ssi > 0.995);
std::cout << "SSI result for " << name << " : " << ssi << std::endl;
}

void typography(canvas& cnv)
Expand Down

0 comments on commit 44dcea2

Please sign in to comment.