Compare Image Data for Similarities - macOS Swift Console

This tutorial shows how to use various image processing commands to compare two images and determine how similar they are in a Swift macOS Console application using the LEADTOOLS SDK.

Overview  
Summary This tutorial covers how to use various image processing commands to compare images in a Swift macOS Console application.
Completion Time 30 minutes
Project Download tutorial project (7 KB)
Platform Swift macOS Console Application
IDE Xcode
Runtime License Download LEADTOOLS
Try it in another language

Required Knowledge

Get familiar with the basic steps of creating a project by reviewing the Add References and Set a License tutorial, before working on the Compare Image Data for Similarities - macOS Swift Console tutorial.

Create the Project and Add LEADTOOLS References

Start with a copy of the project created in the Add References and Set a License tutorial. If you do not have that project, follow the steps in that tutorial to create it.

The references needed depend upon the purpose of the project. This tutorial requires the following Framework references located at <INSTALL_DIR>\LEADTOOLS23\Bin\Xcode\Frameworks\macOS:

For a complete list of which Framework files are required for your application, refer to Files to be Included in your Application.

Edit the Leadtools-Bridging-Header.h file to add the following imports:

#import <Leadtools.Codecs/Leadtools.Codecs.h> 
#import <Leadtools.ImageProcessing.Color/Leadtools.ImageProcessing.Color.h> 
#import <Leadtools.ImageProcessing.Core/Leadtools.ImageProcessing.Core.h> 
#import <Leadtools.ImageProcessing.Effects/Leadtools.ImageProcessing.Effects.h> 

Set the License File

The License unlocks the features needed for the project. It must be set before any toolkit function is called. For details, including tutorials for different platforms, refer to Setting a Runtime License.

There are two types of runtime licenses:

Note: Adding LEADTOOLS references and setting a license are covered in more detail in the Add References and Set a License tutorial.

Add the Image Comparison Code

With the project created, the references added, and the license set, coding can begin.

This tutorial will compare two images by calculating the proportion of black pixels after using the CombineCommand with the OperationXor flag, the average intensity of all pixels using the StatisticsInformationCommand, the average hue and saturation values using the ColorSeparateCommand, and the average of the difference of magnitudes of each color frequency with the FastFourierTransformCommand.

Using the Project Navigator, open main.swift.

Inside the file, just below SetLicense(), add two new string variables and set them equal to the two separate image file paths, as shown below. For the purposes of this tutorial you can download the test images here. For example, compare 1stRed.png with 2ndRed.png.

let image1Filename: String = "/path/to/image" 
let image2Filename: String = "/path/to/image" 
 
guard let image1: LTRasterImage = LoadImage(file: image1Filename) else { fatalError("Image 1 failed to load.") } 
guard let image2: LTRasterImage = LoadImage(file: image2Filename) else { fatalError("Image 2 failed to load.") } 
 
print("Image 1: \(image1Filename)") 
print("Image 2: \(image2Filename)") 
 
CompareXOR(image1: image1, image2: image2) 
CompareIntensities(image1: image1, image2: image2) 
CompareHSV(image1: image1, image2: image2) 
CompareFourier(image1: image1, image2: image2) 

Add a new function named LoadImage(file: String). This function is called twice inside the guard let statements, as shown above. Add the code below to the LoadImage method to load the given files as RasterImage objects.

func LoadImage(file: String) -> LTRasterImage? { 
    let codecs: LTRasterCodecs = LTRasterCodecs() 
    do { 
        return try codecs.load(file: file) 
    } catch { 
        print(error.localizedDescription) 
    } 
    return nil 
} 

Add four new functions named as below:

Call all four of these functions, as shown above.

Add the below code for the CompareXOR method. In this method we will use the LTCombineCommand. This places one image onto another at a certain location and within a certain rectangle. The way the pixels of the underlying image are affected by the overlaying image depends on the flags we use. In this case we will use a LeadRect that is the same size as the bottom image to use the whole image. We will use the OperationXor flag which will perform a bitwise XOR operation onto each set of top and bottom pixels from each image.

This should create black pixels where the two pixels in the same location in either images are the same, and white everywhere else. To calculate how much is the same between the two images, we will create a region out of only the black pixels and compare it to the sum of all pixels.

func CompareXOR(image1: LTRasterImage, image2: LTRasterImage) { 
    do { 
        let image2Copy = try image2.clone() 
        // Combine the images with XOR 
        let combineCommand: LTCombineCommand = LTCombineCommand() 
        combineCommand.destinationRectangle = LeadRectMake(0, 0, image1.width, image1.height) 
        combineCommand.flags = LTCombineCommandFlags.operationXor 
        combineCommand.sourceImage = image1 
        combineCommand.sourcePoint = LeadPointMake(0, 0) 
        try combineCommand.run(image2Copy) 
 
        // Calculate the percentage of black pixels (where XOR was identical) 
        try image2Copy.addColorToRegion(color: LTRasterColor.black, combineMode: LTRasterRegionCombineMode.set) 
        let ratio: Double = Double(Int(image2Copy.regionArea) / (image2Copy.width * image2Copy.height)) 
        print("Individual pixels: \(String(format: "%.2f",100.0 * ratio))% match") 
    } catch { 
        print(error.localizedDescription) 
    } 
} 

Add the below code for the CompareIntensities method to create an instance of the LTStatisticsInformationCommand, which allows you to only include pixels of a certain range or color in the statistics population. We will use all pixels, from values 0-255 from the master channel. We will gather the images' mean values and compare the two, rescaling it to 0-1.00 instead of 0-255. This will give us the difference in average intensity or brightness.

We can also use this command to derive intensity of the difference of these images by passing it through an XOR CombineCommand like before. This will be calculated from the ratio of identical pixel values to differing pixel values.

func CompareIntensities(image1: LTRasterImage, image2: LTRasterImage) { 
    do { 
        let image1Gray = try image1.clone() 
        let image2Gray = try image2.clone() 
 
        // Convert both images to grayscale 
        let grayscale: LTGrayscaleCommand = LTGrayscaleCommand() 
        grayscale.bitsPerPixel = 8 
        try grayscale.run(image1Gray) 
        try grayscale.run(image2Gray) 
 
        // Compare the average overall intensities 
        let statistics: LTStatisticsInformationCommand = LTStatisticsInformationCommand() 
        statistics.channel = LTRasterColorChannel.master 
        statistics.end = 255 
        statistics.start = 0 
        try statistics.run(image1Gray) 
        let leftIntensity: Double = statistics.mean 
        try statistics.run(image2Gray) 
        let rightIntensity: Double = statistics.mean 
        var similarity: Double = 1.0 - (abs(leftIntensity - rightIntensity) / 255.0) 
        print("Overall image intensities: \(String(format: "%.2f",100.0 * similarity))% match") 
 
        // Check the intensity of the difference 
        let combineCommand: LTCombineCommand = LTCombineCommand() 
        combineCommand.destinationRectangle = LeadRectMake(0, 0, image1Gray.width, image1Gray.height) 
        combineCommand.flags = .operationXor 
        combineCommand.sourceImage = image1Gray 
        combineCommand.sourcePoint = LeadPointMake(0, 0) 
        try combineCommand.run(image2Gray) 
        try statistics.run(image2Gray) 
        similarity = 1.0 - (statistics.mean / 255.0) 
        print("Image difference intensity: \(String(format: "%.2f", 100.0 * similarity))% match") 
    } catch { 
        print(error.localizedDescription) 
    } 
} 

Add the below code for the CompareHSV method to separate the images into Hue, Saturation, and Value channels instead of Red, Green, and Blue. This creates a DestinationImage object with three pages, one for each channel. We can then use the LTStatisticsInformationCommand again to find the average Hue and Saturation for both images by running it on each page of the separated image. The average Value is essentially the image's brightness which is not as useful. We can then compare the difference in average Hue and Saturation, again scaling it to 0-100 from 0-255.

func CompareHSV(image1: LTRasterImage, image2: LTRasterImage) { 
    let colorSeparate: LTColorSeparateCommand = LTColorSeparateCommand() 
    colorSeparate.type = LTColorSeparateCommandType.hsv 
 
    let statistics: LTStatisticsInformationCommand = LTStatisticsInformationCommand() 
    statistics.channel = LTRasterColorChannel.master 
    statistics.end = 255 
    statistics.start = 0 
 
    // Separate the first image, and calculate the average values 
    do { 
        try colorSeparate.run(image1) 
    } catch { 
        print(error.localizedDescription) 
    } 
    var image1Hue: Double = 0 
    var image1Saturation: Double = 0 
    let image1Separated: LTRasterImage = colorSeparate.destinationImage! 
    image1Separated.page = 1 
    do { 
        try statistics.run(image1Separated) 
        image1Hue = statistics.mean 
    } catch { 
        print(error.localizedDescription) 
    } 
    image1Separated.page = 2 
    do { 
        try statistics.run(image1Separated) 
        image1Saturation = statistics.mean 
    } catch { 
        print(error.localizedDescription) 
    } 
 
    // Separate the second image, and calculate the average values 
    do { 
        try colorSeparate.run(image2) 
    } catch { 
        print(error.localizedDescription) 
    } 
    var image2Hue: Double = 0 
    var image2Saturation: Double = 0 
    let image2Separated: LTRasterImage = colorSeparate.destinationImage! 
    image2Separated.page = 1 
    do { 
        try statistics.run(image2Separated) 
        image2Hue = statistics.mean 
    } catch { 
        print(error.localizedDescription) 
    } 
    image2Separated.page = 2 
    do { 
        try statistics.run(image2Separated) 
        image2Saturation = statistics.mean 
    } catch { 
        print(error.localizedDescription) 
    } 
 
    // Calculate the hue difference 
    // Note: 0 is next to 255, so need to compensate for this 
    var hueDiff: Double = abs(image1Hue - image2Hue) 
    if hueDiff > 127 { 
        hueDiff = abs(hueDiff - 255) 
    } 
 
    // Log the hue similarity 
    var similarity: Double = 1.0 - (hueDiff / 255.0) 
    print("Average hue values: \(String(format: "%.2f",100.0 * similarity))% match") 
 
    // Log the saturation similarity 
    similarity = 1.0 - (abs(image1Saturation - image2Saturation) / 255.0) 
    print("Average saturation values: \(String(format: "%.2f",100.0 * similarity))% match") 
} 

Lastly, add the below code for the CompareHSV method to compare the frequency distribution of the two images using the LTFastFourierTransformCommand. The Fast Fourier Transform, or FFT, works by converting the image from a spatial domain to a frequency domain which is useful for things like filtering out noise. We will use this to compare the magnitudes of each frequency. Once we run the command, we can then use the info object's Data property to get the LTComplex numbers associated with each frequency, calculate the magnitude for each frequency, and calculate the difference between the two averages.

func CompareFourier(image1: LTRasterImage, image2: LTRasterImage) { 
    do { 
        let image1Copy: LTRasterImage = try image1.clone() 
        let image2Copy: LTRasterImage = try image2.clone() 
 
        // Ensure the images are the same size 
        if image1Copy.width != image2Copy.width || image1Copy.height != image2Copy.height { 
            // Could use the minimum size, but we'll just set it to a fixed 256 by 256 
            let size: LTSizeCommand = LTSizeCommand() 
            size.flags = LTRasterSizeFlags.bicubic 
            size.height = 256 
            size.width = 256 
            try size.run(image1Copy) 
            try size.run(image2Copy) 
        } 
 
        // Setup the Fast Fourier Transform (FFT) data 
        // Note: We'll want the padding flag so this can handle sizes which aren't a power of two 
        let fftFlags = LTFastFourierTransformCommandFlags.fastFourierTransform.rawValue | LTFastFourierTransformCommandFlags.gray.rawValue | LTFastFourierTransformCommandFlags.padOptimally.rawValue 
        let image1Info: LTFourierTransformInformation = try LTFourierTransformInformation(image: image1Copy, flags: LTFastFourierTransformCommandFlags(rawValue: fftFlags)) 
        let image2Info: LTFourierTransformInformation = try LTFourierTransformInformation(image: image2Copy, flags: LTFastFourierTransformCommandFlags(rawValue: fftFlags)) 
 
        // Apply  FFT to both images 
        let fft: LTFastFourierTransformCommand = LTFastFourierTransformCommand.init(information: image1Info, flags: LTFastFourierTransformCommandFlags(rawValue: fftFlags)) 
        try fft.run(image1Copy) 
        fft.fourierTransformInformation = image2Info 
        try fft.run(image2Copy) 
 
        // Calculate the similarity of frequency based on the magnitudes of each component 
        var magnitudeTotal: Double = 0 
        var magnitudeTotalDiff: Double = 0 
        let length: Int = Int(image1Info.dataSize) 
 
        let image1Data: [LTComplex] = arrayForPointer(image1Info.data!, count: length) 
        let image2Data: [LTComplex] = arrayForPointer(image2Info.data!, count: Int(image2Info.dataSize)) 
        for i in 0..<length { 
            // Calculate the magnitude for eachg entry 
            let image1Entry: LTComplex = image1Data[i] 
            let image1Magnitude: Double = sqrt(image1Entry.r * image1Entry.r + image1Entry.i * image1Entry.i) 
            let image2Entry = image2Data[i] 
            let image2Magnitude: Double = sqrt(image2Entry.r * image2Entry.r + image2Entry.i * image2Entry.i) 
 
            // Add to the totals 
            magnitudeTotal += image1Magnitude + image2Magnitude 
            magnitudeTotalDiff += abs(image1Magnitude - image2Magnitude) 
        } 
 
        // Ensure we don't divide by zero 
        if magnitudeTotal == 0 { 
            magnitudeTotal = 1 
        } 
 
        // Log the result 
        let similarity = (magnitudeTotal - magnitudeTotalDiff) / magnitudeTotal 
        print("Frequency comparison: \(String(format: "%.2f", 100.0 * similarity))% match") 
    } catch { 
        print(error.localizedDescription) 
    } 
} 
 
func arrayForPointer<T>(_ pointer: UnsafePointer<T>, count: Int) -> [T] { 
    let buffer = UnsafeBufferPointer(start: pointer, count: count) 
    return Array(buffer) 
} 

Run the Project

Clean the project to clear any errors by selecting Product -> Clean Build Folder or Shift + Command + K.

Run the project by selecting Product -> Run or Command + R.

If the steps were followed correctly, the application runs and performs multiple calculations on the two images outputting the similarity statistics to the console.

Screenshot showing the output statistics.

Wrap-Up

This tutorial showed how to use various Image Processing commands including the LTCombineCommand, LTGrayscaleCommand, LTStatisticsInformationCommand, LTColorSeparateCommand, LTSizeCommand, and LTFastFourierTransformCommand commands to obtain similarity statistics of two images.

See Also

Help Version 23.0.2024.5.22
Products | Support | Contact Us | Intellectual Property Notices
© 1991-2024 LEAD Technologies, Inc. All Rights Reserved.

Products | Support | Contact Us | Intellectual Property Notices
© 1991-2023 LEAD Technologies, Inc. All Rights Reserved.