-
Notifications
You must be signed in to change notification settings - Fork 608
Perceptual image precision + 90% speed improvement #628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
…arisons The perceptual precision number is between 0 & 1 that gets translated to a CIE94 tolerance https://en.wikipedia.org/wiki/Color_difference
… iOS 14, tvOS 14, and macOS 11
…and perceptual precision
…cessorKernel implementation
I've just taken this for a spin in our project - so far I've converted a few of our snapshots over to this strategy and images taken in an iOS 15/iPhone 12 simulator that would fail with inperceptible differences on an iOS 16/iPhone 14 simulator are now passing. Unfortunately due to some other packages having a dependency on this library I was not able to switch over to this fork to test this out, I had to collate the changes into a separate file in my project and rename the strategies. I've created a gist containing the file I'm using - I've not imported everything over from this PR, so far just the https://gist.github.com/lukeredpath/9abc51d9eee349c2f209cc0431c8eb6f |
This PR is pure gold, thank you. I tested it on my project and all the headache and compromises we're having here with the differences between apple silicon and intel generated snapshots are gone using this new Looking forward to have this merged on the main branch by the reviewers |
FWIW, I've settled on perceptual precision of 0.98 and precision of 0.995 - the latter seems to account for very minor layout shifts of a few pixels between iOS 15 and iOS 16 without triggering any significant false positives (there's always the possibility that some will slip through with < 1 precision but I can live with that). Performance seems OK in most cases too. |
Is there any value in having |
In my test target I've added a global default by defining a /// The default `perceptualPrecision` to use if a specific value is not provided.
private let defaultPerceptualPrecision: Float = {
#if arch(x86_64)
// When executing on Intel (CI machines) lower the `defaultPerceptualPrecision` to 98% which avoids failing tests
// due to imperceivable differences in anti-aliasing, shadows, and blurs between Intel and Apple Silicon Macs.
return 0.98
#else
// The snapshots were generated on Apple Silicon Macs, so they match 100%.
return 1.0
#endif
}()
// Local extensions that override the default `perceptualPrecision` value with the `defaultPerceptualPrecision` global defined above.
extension Snapshotting where Value == UIView, Format == UIImage {
/// A snapshot strategy for comparing views based on perceptual pixel equality.
static let image = image(perceptualPrecision: defaultPerceptualPrecision)
/// A snapshot strategy for comparing views based on perceptual pixel equality.
///
/// - Parameters:
/// - drawHierarchyInKeyWindow: Utilize the simulator's key window in order to render `UIAppearance` and `UIVisualEffect`s. This option requires a host application for your tests and will _not_ work for framework test targets.
/// - perceptualPrecision: The percentage a pixel must match the source pixel to be considered a match. [98-99% mimics the precision of the human eye.](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e)
/// - size: A view size override.
/// - traits: A trait collection override.
static func image(
drawHierarchyInKeyWindow: Bool = false,
perceptualPrecision: Float = defaultPerceptualPrecision,
size: CGSize? = nil,
traits: UITraitCollection = .init()
) -> Self {
image(
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
precision: 1,
perceptualPrecision: perceptualPrecision,
size: size,
traits: traits
)
}
}
extension Snapshotting where Value == UIViewController, Format == UIImage {
/// A snapshot strategy for comparing view controllers based on perceptual pixel equality.
static let image = image(perceptualPrecision: defaultPerceptualPrecision)
/// A snapshot strategy for comparing view controller views based on perceptual pixel equality.
///
/// - Parameters:
/// - config: A set of device configuration settings.
/// - perceptualPrecision: The percentage a pixel must match the source pixel to be considered a match. [98-99% mimics the precision of the human eye.](http://zschuessler.github.io/DeltaE/learn/#toc-defining-delta-e)
/// - size: A view size override.
/// - traits: A trait collection override.
static func image(
on config: ViewImageConfig,
perceptualPrecision: Float = defaultPerceptualPrecision,
size: CGSize? = nil,
traits: UITraitCollection = .init()
) -> Self {
image(
on: config,
precision: 1,
perceptualPrecision: perceptualPrecision,
size: size,
traits: traits
)
}
} This lets me leave the The |
I can confirm that this change also fixed our issue of tests not passing on M1 when using Intel generated snapshots, or vice versa. 🎉 |
@Kaspik yes if you’ve got perceptual precision at 1 it won’t behave any differently. Try 0.98. |
@Kaspik, yes a
The example images you attached have a 0.3 Delta E value. So a |
@ejensen Thanks Eric, that makes sense. How did you get the image difference value? Asking because I have another examples where even setting I can go with 0.98, but that sounds like a bottom limit recommended by you. |
@Kaspik I'm using a new branch that reports the actual precision of the images when the tests fail. It reports that the precision of the second set of images you attached is 98.59% I will PR that branch if/when this PR is merged. You could attempt to use 0.985 precision for all your test cases. Or can use the branch to find a precision for each of the snapshot cases. 98% is a good limit since it is generally difficult for an untrained eye tell the difference and display color accuracy is often inadequate to even represent the differences within that range. |
Ohh nice, thanks! I'll try that branch on top of this one, great job! |
I have tested this out and it works great for us as well 👍 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ejensen Sorry for the delay! This looks like a great update to us. We're going to merge things and fast-track a release soon. There's a lot upstream, including some bug fixes that will require folks to rerecord some snapshots. This breakage has been holding up release, but we don't think snapshot backwards compatibility is feasible at this time, and it shouldn't hold up all the good things that have been contributed.
Do you think you can get a PR up for the two improvements you commented about?
- Adding a global configuration option for projects that expect to run tests on both Intel/M1
- Adding the extra messaging about the actual precision difference on failure
@simondelphia When comparing the PNG images included in the zip they match with I'm working on a Swift Playground that assists in debugging image differences by outputting all the intermediate values and providing suggestions for precision parameter values. It might help identify the causes of different machine rendering and suggest values that accommodate them. |
@ejensen: According to #628 (comment), doesn't using a perceptual precision of 1 on Apple Silicon Macs cause the library to use the old snapshotting algorithm, and thus wouldn't this defeat the purpose of using Also I will give the |
I tried
On CI with the same calls I got the same failures with the same messages. On CI the snapshots on the original login UIView used 0.98 and 0.99 for precision and perceptual, respectively:
|
I managed to produce a diff using a tool online that gave me something I could actually see as a difference in png files from the same snapshots. Seems to be something about the edge around the capsule button, and I suppose while the actual difference is imperceptible, there are a substantial number of pixels involved. Notably the two failing snapshots are also only dark mode tests and the corresponding light mode ones worked fine. |
On the team I'm using that strategy, everyone locally has the same M1 machine so the snapshots exactly match. We generate new snapshots locally, so everyone's local machine produces snapshots that are byte identical. There's no need for any
This points to the CI machine producing a snapshot of the original login UIView that differs from the image that is saved. This difference might be a color space difference. This PR #665 normalizes the image color spaces before comparison, which might be the solution to resolve your CI image differences. |
We've started to use this new precision parameter recently to mitigate M1 vs Intel issue and have exactly the same setup - 1.0 precision for local runs on M1 and 0.999 for CI runs (we are using Bitrise). For whatever reason though while locally tests fail as expected (both with 1.0 and 0.999 precision, calculated precision is actually negative) the same tests pass on CI with such precision. At the same time when precision is 1.0 on CI they as expected fail. We have parallelised tests enabled. |
The issue sounds similar to the one addressed in #666 where some virtualized macOS environments silently fail due to the lack of Metal support. |
Thanks, indeed CI is running on VM, I'm trying the package from your fork with those changes but it seems like it results in CI timing out although the tests seem to run fast enough =/ |
@IlyaPuchkaTW An alternative approach, is to use an open source tool like Vizzy or Screenshotbot, so that your screenshots are always recorded in CI (hopefully all CI machines have an identical environment). https://github.com/workday/vizzy https://github.com/screenshotbot/screenshotbot-oss (I built Screenshotbot, so I'm a bit biased toward that. I know of teams using it with swift-snapshot-testing) |
…eenshots and verify them on M1. Seems to work up to a perceptable difference of 98% pointfreeco/swift-snapshot-testing#628 (comment)
* Add an optional perceptualPrecision parameter for image snapshot comparisons The perceptual precision number is between 0 & 1 that gets translated to a CIE94 tolerance https://en.wikipedia.org/wiki/Color_difference * Use CIColorThreshold and CIAreaAverage for a 70% faster image diff on iOS 14, tvOS 14, and macOS 11 * Add a unit test demonstrating the difference between pixel precision and perceptual precision * Update the reference image for the image precision test * Backport the threshold filter to macOS 10.13 by creating a CIImageProcessorKernel implementation * Update Sources/SnapshotTesting/Snapshotting/UIImage.swift * Update NSImage.swift Co-authored-by: Stephen Celis <[email protected]> Co-authored-by: Stephen Celis <[email protected]>
* Add an optional perceptualPrecision parameter for image snapshot comparisons The perceptual precision number is between 0 & 1 that gets translated to a CIE94 tolerance https://en.wikipedia.org/wiki/Color_difference * Use CIColorThreshold and CIAreaAverage for a 70% faster image diff on iOS 14, tvOS 14, and macOS 11 * Add a unit test demonstrating the difference between pixel precision and perceptual precision * Update the reference image for the image precision test * Backport the threshold filter to macOS 10.13 by creating a CIImageProcessorKernel implementation * Update Sources/SnapshotTesting/Snapshotting/UIImage.swift * Update NSImage.swift Co-authored-by: Stephen Celis <[email protected]> Co-authored-by: Stephen Celis <[email protected]>
Problem
The existing image matching precision strategy is not good at differentiating between a significant difference in a relatively small portion of a snapshot, and imperceivable differences in a large portion of the snapshot. For example, these snapshots below show that a 99.5% precision value fails an imperceivable background color change while allowing noticeable changes (text and color) to pass:
Solution
This PR adds a new optional
perceptualPrecision
parameter to image snapshotting which determines how perceptually similar a pixel must be to consider it matching. This parameter complements the existing precision parameter that determines the percentage of pixels that must be considered matching in order to consider the whole image matching.This approach is similar to #571 and #580 but uses perceptual distance rather than Euclidean distance of sRGB values. This is significant because the sRGB color space is not perceptually uniform. Pairs of colors with the same Euclidean distance can have large perceptual differences. The left and right colors of each row have the same Euclidean distance:
The
perceptualPrecision
parameter is the inverse of a Delta E value. The following table can be used to determine theperceptualPrecision
that suites your needs:This perceptual color difference calculation is performed by the CILabDeltaE CoreImage Filter which is available on macOS 10.13+, iOS 11+, and tvOS 11+. The use of CoreImage to accelerate the image diffing results in a +90% over the existing byte-by-byte comparison.
Additionally, when macOS 11.0+, iOS 14+, tvOS 14+ is available, theCILabDeltaE
filter is joined with theCIColorThreshold
andCIAreaAverage
filters to accelerate the whole image comparison, resulting in a 97% speed improvement.The
CIColorThreshold
filter has been backported to 10.13+, iOS 11+, and tvOS 11+ usingMPSImageThresholdBinary
so the 97% speed improvement is available in all OS versions.Benchmarks
Addressed Issues
perceptualPrecsion
of >=98% will prevent imperceivable differences from failing assertions while still catching noticeable differencesRelated PRs
UIImage
diffing strategy #580