110 lines
3.7 KiB
C#
110 lines
3.7 KiB
C#
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Drawing.Imaging;
|
|
using System.IO;
|
|
using System.Runtime.Versioning;
|
|
|
|
namespace HikvisionAttendanceManager.App.Services;
|
|
|
|
[SupportedOSPlatform("windows")]
|
|
internal static class FaceImageNormalizer
|
|
{
|
|
private const int TargetWidth = 480;
|
|
private const int TargetHeight = 640;
|
|
private const int MaxBytes = 200 * 1024;
|
|
private const int MaxQuality = 92;
|
|
private const int MinQuality = 55;
|
|
|
|
public static bool TryNormalizeForEnrollment(byte[] input, out byte[] jpegBytes, out string note)
|
|
{
|
|
jpegBytes = [];
|
|
note = "";
|
|
if (input.Length < 4 || input[0] != 0xFF || input[1] != 0xD8 || input[2] != 0xFF)
|
|
{
|
|
note = "Input is not a JPEG.";
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var sourceStream = new MemoryStream(input);
|
|
using var source = Image.FromStream(sourceStream);
|
|
using var portrait = CropPortrait(source, TargetWidth, TargetHeight);
|
|
jpegBytes = CompressToTargetSize(portrait);
|
|
note = $"normalized={TargetWidth}x{TargetHeight} bytes={jpegBytes.Length}";
|
|
return jpegBytes.Length > 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
note = ex.Message;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static Bitmap CropPortrait(Image source, int targetWidth, int targetHeight)
|
|
{
|
|
var targetAspect = (double)targetWidth / targetHeight;
|
|
var sourceAspect = (double)source.Width / source.Height;
|
|
|
|
int cropWidth;
|
|
int cropHeight;
|
|
if (sourceAspect > targetAspect)
|
|
{
|
|
cropHeight = source.Height;
|
|
cropWidth = (int)Math.Round(cropHeight * targetAspect);
|
|
}
|
|
else
|
|
{
|
|
cropWidth = source.Width;
|
|
cropHeight = (int)Math.Round(cropWidth / targetAspect);
|
|
}
|
|
|
|
var cropX = Math.Max(0, (source.Width - cropWidth) / 2);
|
|
var cropY = Math.Max(0, (source.Height - cropHeight) / 2);
|
|
cropWidth = Math.Min(cropWidth, source.Width - cropX);
|
|
cropHeight = Math.Min(cropHeight, source.Height - cropY);
|
|
|
|
var bitmap = new Bitmap(targetWidth, targetHeight);
|
|
using var graphics = Graphics.FromImage(bitmap);
|
|
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
graphics.DrawImage(source,
|
|
new Rectangle(0, 0, targetWidth, targetHeight),
|
|
new Rectangle(cropX, cropY, cropWidth, cropHeight),
|
|
GraphicsUnit.Pixel);
|
|
return bitmap;
|
|
}
|
|
|
|
private static byte[] CompressToTargetSize(Bitmap bitmap)
|
|
{
|
|
byte[]? best = null;
|
|
for (var quality = MaxQuality; quality >= MinQuality; quality -= 3)
|
|
{
|
|
var candidate = SaveJpeg(bitmap, quality);
|
|
best = candidate;
|
|
if (candidate.Length <= MaxBytes)
|
|
return candidate;
|
|
}
|
|
|
|
return best ?? [];
|
|
}
|
|
|
|
private static byte[] SaveJpeg(Image image, int quality)
|
|
{
|
|
using var stream = new MemoryStream();
|
|
using var encoderParameters = new EncoderParameters(1);
|
|
encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality);
|
|
var codec = ImageCodecInfo.GetImageEncoders().FirstOrDefault(info => info.FormatID == ImageFormat.Jpeg.Guid);
|
|
if (codec is null)
|
|
{
|
|
image.Save(stream, ImageFormat.Jpeg);
|
|
return stream.ToArray();
|
|
}
|
|
|
|
image.Save(stream, codec, encoderParameters);
|
|
return stream.ToArray();
|
|
}
|
|
}
|