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