Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,9 @@
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.201" PrivateAssets="All" />
<PackageVersion Include="FFMpegCore.Extensions.SkiaSharp" Version="5.0.3" />
<PackageVersion Include="FFMpegCore" Version="5.4.0" />
<PackageVersion Include="CommunityToolkit.HighPerformance" Version="8.4.2" />
<PackageVersion Include="Microsoft.Extensions.ObjectPool" Version="10.0.5" />
<PackageVersion Include="SkiaSharp" Version="3.119.2" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.2" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
</Project>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Our guiding priciples are:

### Shoutouts

The interpretation of images relies on the fantastic work by the at [SkiaSharp](https://github.com/mono/SkiaSharp), and the video processing relies on the great contributions provided by [FFMpeg.Core](https://github.com/rosenbjerg/FFMpegCore).
The interpretation of images relies on the fantastic work by [ImageSharp](https://github.com/SixLabors/ImageSharp), and the video processing relies on the great contributions provided by [FFMpeg.Core](https://github.com/rosenbjerg/FFMpegCore).

### Contributors

Expand Down
7 changes: 4 additions & 3 deletions src/PdqHash/FFMegExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@
using System.Runtime.CompilerServices;
using FFMpegCore;
using FFMpegCore.Enums;
using SkiaSharp;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace PdqHash;

public static class FFMegImageExt
{
extension(Uri input)
{
public async IAsyncEnumerable<(SKBitmap Screenshot, int Frame, TimeSpan? TotalDuration)> GetSnapshotsFromUri(Size? size = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
public async IAsyncEnumerable<(Image<Rgba32> Screenshot, int Frame, TimeSpan? TotalDuration)> GetSnapshotsFromUri(System.Drawing.Size? size = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var source = FFProbe.Analyse(input);
var baseDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Expand All @@ -30,7 +31,7 @@ await Task.Run(() =>
{
var index = int.Parse(Path.GetFileNameWithoutExtension(file)[5..], CultureInfo.InvariantCulture);

var bitmap = SKBitmap.Decode(file);
var bitmap = SixLabors.ImageSharp.Image.Load<Rgba32>(file);
yield return (bitmap, index, source.PrimaryVideoStream?.Duration);
bitmap.Dispose();
}
Expand Down
3 changes: 1 addition & 2 deletions src/PdqHash/PdqHash.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,12 @@
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="FFMpegCore" />
<PackageReference Include="FFMpegCore.Extensions.SkiaSharp" />
<PackageReference Include="CommunityToolkit.HighPerformance" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
<PackageReference Include="MinVer">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="SkiaSharp" />
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
</Project>
88 changes: 41 additions & 47 deletions src/PdqHash/PdqHasher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.Diagnostics;
using CommunityToolkit.HighPerformance;
using CommunityToolkit.HighPerformance.Buffers;
using SkiaSharp;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;

namespace PdqHash.Hashing;

Expand Down Expand Up @@ -75,56 +77,54 @@ private static void ComputeDCTMatrix(Memory<float> memory)
input = bufferedStream;
}

using var codec = SKCodec.Create(input, out var result);

if (codec == null)
Image<Rgba32> original;
try
{
throw new ArgumentException($"Failed to parse codec from SKImage stream. Reason: {result}");
original = Image.Load<Rgba32>(input);
}

using var original = SKBitmap.Decode(codec);

if (original == null)
catch (Exception ex) when (ex is UnknownImageFormatException or InvalidImageContentException or NotSupportedException)
{
// https://github.com/mono/SkiaSharp/issues/2429
throw new ArgumentException($"Failed to parse input stream as a valid SKImage stream. Ensure the stream is able to read minimum of {SKCodec.MinBufferedBytesNeeded} to parse Codec info.");
throw new ArgumentException($"Failed to parse input stream as a valid image. {ex.Message}", ex);
}

var width = Math.Min(original.Width, 1024);
var height = Math.Min(original.Height, 1024);

using var resized = original.Resize(new SKImageInfo(width, height)
using (original)
{
ColorSpace = SKColorSpace.CreateSrgb(),
}, SKSamplingOptions.Default);
var width = Math.Min(original.Width, 1024);
var height = Math.Min(original.Height, 1024);

var readSeconds = stopwatch.Elapsed.TotalSeconds;
var numCols = resized.Width;
var numRows = resized.Height;
if (width != original.Width || height != original.Height)
{
original.Mutate(x => x.Resize(width, height));
}

using var buffer1 = SpanOwner<float>.Allocate(resized.Height * resized.Width);
using var buffer2 = SpanOwner<float>.Allocate(resized.Height * resized.Width);
var readSeconds = stopwatch.Elapsed.TotalSeconds;
var numCols = original.Width;
var numRows = original.Height;

using var buffer64x64Owner = SpanOwner<float>.Allocate(64 * 64);
using var buffer16x16Owner = SpanOwner<float>.Allocate(16 * 16);
using var buffer1 = SpanOwner<float>.Allocate(numRows * numCols);
using var buffer2 = SpanOwner<float>.Allocate(numRows * numCols);

var buffer64x64 = buffer64x64Owner.Span.AsSpan2D(64, 64);
var buffer16x16 = buffer64x64Owner.Span.AsSpan2D(16, 16);
using var buffer64x64Owner = SpanOwner<float>.Allocate(64 * 64);
using var buffer16x16Owner = SpanOwner<float>.Allocate(16 * 16);

PdqStatistics.ReadDuration.Record(stopwatch.ElapsedMilliseconds);
stopwatch.Restart();
var buffer64x64 = buffer64x64Owner.Span.AsSpan2D(64, 64);
var buffer16x16 = buffer64x64Owner.Span.AsSpan2D(16, 16);

var rv = FromImage(resized, buffer1.Span, buffer2.Span, buffer64x64, buffer16x16);
PdqStatistics.ReadDuration.Record(stopwatch.ElapsedMilliseconds);
stopwatch.Restart();

PdqStatistics.HashDuration.Record(stopwatch.ElapsedMilliseconds);
PdqStatistics.HashesGenerated.Add(1);
var rv = FromImage(original, buffer1.Span, buffer2.Span, buffer64x64, buffer16x16);

return new HashResult(rv.Hash, rv.Quality,
new HashingStatistics(readSeconds, stopwatch.Elapsed.TotalSeconds, numRows * numCols, source));
PdqStatistics.HashDuration.Record(stopwatch.ElapsedMilliseconds);
PdqStatistics.HashesGenerated.Add(1);

return new HashResult(rv.Hash, rv.Quality,
new HashingStatistics(readSeconds, stopwatch.Elapsed.TotalSeconds, numRows * numCols, source));
}
}


public HashResult FromBitmap(SKBitmap resized, string source)
public HashResult FromImage(Image<Rgba32> resized, string source)
{
var stopwatch = Stopwatch.StartNew();

Expand Down Expand Up @@ -168,7 +168,7 @@ protected virtual void Dispose(bool disposing)
}
}

private HashAndQuality FromImage(SKBitmap img, Span<float> buffer1, Span<float> buffer2, Span2D<float> buffer64x64, Span2D<float> buffer16x16)
private HashAndQuality FromImage(Image<Rgba32> img, Span<float> buffer1, Span<float> buffer2, Span2D<float> buffer64x64, Span2D<float> buffer16x16)
{
var numCols = img.Width;
var numRows = img.Height;
Expand Down Expand Up @@ -421,29 +421,23 @@ private static int ComputeJaroszFilterWindowSize(int dimensionSize)
return (dimensionSize + PDQ_JAROSZ_WINDOW_SIZE_DIVISOR - 1) / PDQ_JAROSZ_WINDOW_SIZE_DIVISOR;
}

private static void FillFloatLumaFromBufferImage(SKBitmap img, Span<float> luma)
private static void FillFloatLumaFromBufferImage(Image<Rgba32> img, Span<float> luma)
{
var numCols = img.Width;
var numRows = img.Height;

if (img.ColorSpace != null && img.ColorSpace.IsSrgb is false)
{
throw new InvalidOperationException("Failed to transform image to sRGB color space");
}

for (var row = 0; row < numRows; row++)
{
for (var col = 0; col < numCols; col++)
{
var pixel = img.GetPixel(col, row);
var pixel = img[col, row];

luma[row * numCols + col] = (
LUMA_FROM_R_COEFF * pixel.Red +
LUMA_FROM_G_COEFF * pixel.Green +
LUMA_FROM_B_COEFF * pixel.Blue
LUMA_FROM_R_COEFF * pixel.R +
LUMA_FROM_G_COEFF * pixel.G +
LUMA_FROM_B_COEFF * pixel.B
);
}
}

}
}
1 change: 0 additions & 1 deletion test/PdqHash.Tests/PdqHash.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
<PackageReference Include="coverlet.collector" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Snapshooter.Xunit" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio" />
</ItemGroup>
Expand Down