feat(services): add portrait JPEG normalizer for face enrollment
Adds image normalization (480×640 portrait, JPEG ≤200 KB) required by the ISAPI face upload path. Commit this first so the project builds against the already-committed HikvisionIsapiClient.main
parent
f047129c14
commit
e7714452da
|
|
@ -0,0 +1,109 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue