|
@@ -0,0 +1,480 @@
|
|
|
|
+using System;
|
|
|
|
+using System.Collections.Generic;
|
|
|
|
+using System.Linq;
|
|
|
|
+using System.Text;
|
|
|
|
+using System.Threading.Tasks;
|
|
|
|
+using System.IO;
|
|
|
|
+using System.Net;
|
|
|
|
+using System.Threading;
|
|
|
|
+using Emgu.CV;
|
|
|
|
+using Emgu.CV.CvEnum;
|
|
|
|
+using Emgu.CV.Structure;
|
|
|
|
+using System.Drawing.Imaging;
|
|
|
|
+using System.Drawing;
|
|
|
|
+using HttpMultipartParser;
|
|
|
|
+using System.Net.NetworkInformation;
|
|
|
|
+using System.Net.Sockets;
|
|
|
|
+
|
|
|
|
+namespace Bird_tool
|
|
|
|
+{
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ class ImageSharpnessServer
|
|
|
|
+ {
|
|
|
|
+ public delegate void LogShow(string message);
|
|
|
|
+ public delegate void LogHandler(string message, LogLevel level);
|
|
|
|
+
|
|
|
|
+ public event LogHandler OnLog;
|
|
|
|
+ public event LogShow OnLogShow;
|
|
|
|
+ private readonly HttpListener _listener;
|
|
|
|
+ private readonly int _port;
|
|
|
|
+ private readonly string _ip;
|
|
|
|
+ private bool _isRunning;
|
|
|
|
+
|
|
|
|
+ public ImageSharpnessServer(string ip, int port = 8080)
|
|
|
|
+ {
|
|
|
|
+ _ip = ip;
|
|
|
|
+ _port = port;
|
|
|
|
+
|
|
|
|
+ _listener = new HttpListener();
|
|
|
|
+ string urlPrefix = $"http://*:{_port}/";
|
|
|
|
+ OnLog?.Invoke($"Server started on {urlPrefix}", LogLevel.info);
|
|
|
|
+ _listener.Prefixes.Add(urlPrefix);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public bool Start()
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ _isRunning = true;
|
|
|
|
+ _listener.Start();
|
|
|
|
+ OnLog?.Invoke($"Server started on port {_port}", LogLevel.info);
|
|
|
|
+ ThreadPool.QueueUserWorkItem(Listen);
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ catch (HttpListenerException ex) when (ex.ErrorCode == 5) // 拒绝访问
|
|
|
|
+ {
|
|
|
|
+ if (_ip == "+" || _ip == "*")
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke("需要注册URL保留,正在尝试...", LogLevel.info);
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ RegisterUrlReservation();
|
|
|
|
+
|
|
|
|
+ // 重试启动
|
|
|
|
+ _listener.Start();
|
|
|
|
+ OnLog?.Invoke($"服务器在端口 {_port} 启动成功", LogLevel.info);
|
|
|
|
+ ThreadPool.QueueUserWorkItem(Listen);
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+ catch (Exception innerEx)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke($"无法启动服务器: {innerEx.Message}", LogLevel.info);
|
|
|
|
+ OnLogShow?.Invoke("无法启动服务器, 请尝试使用管理员模式启动");
|
|
|
|
+ return false;
|
|
|
|
+ throw;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ OnLogShow?.Invoke("无法注册服务, 修改配置文件后再次启动");
|
|
|
|
+ return false;
|
|
|
|
+ throw;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void Stop()
|
|
|
|
+ {
|
|
|
|
+ _isRunning = false;
|
|
|
|
+ _listener.Stop();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void Listen(object state)
|
|
|
|
+ {
|
|
|
|
+ while (_isRunning)
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ var context = _listener.GetContext();
|
|
|
|
+ ThreadPool.QueueUserWorkItem(ProcessRequest, context);
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ if (_isRunning) OnLog?.Invoke($"监听端口失败 Error: {ex.Message}", LogLevel.info);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void RegisterUrlReservation()
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ string command = $"http add urlacl url=http://+:{_port}/ user=Everyone";
|
|
|
|
+
|
|
|
|
+ var process = new System.Diagnostics.Process
|
|
|
|
+ {
|
|
|
|
+ StartInfo = new System.Diagnostics.ProcessStartInfo
|
|
|
|
+ {
|
|
|
|
+ FileName = "netsh",
|
|
|
|
+ Arguments = command,
|
|
|
|
+ Verb = "runas", // 以管理员身份运行
|
|
|
|
+ UseShellExecute = true,
|
|
|
|
+ WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ process.Start();
|
|
|
|
+ process.WaitForExit(5000); // 最多等待5秒
|
|
|
|
+
|
|
|
|
+ if (process.ExitCode == 0)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke("URL保留注册成功", LogLevel.info);
|
|
|
|
+ }
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ throw new Exception($"netsh命令失败,退出代码: {process.ExitCode}");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ throw new Exception($"注册URL保留失败: {ex.Message}");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 获取本机所有IPv4地址
|
|
|
|
+ public List<string> GetLocalIPv4Addresses()
|
|
|
|
+ {
|
|
|
|
+ var ipAddresses = new List<string>();
|
|
|
|
+
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ // 获取所有网络接口
|
|
|
|
+ NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
|
|
|
|
+
|
|
|
|
+ foreach (NetworkInterface iface in interfaces)
|
|
|
|
+ {
|
|
|
|
+ // 跳过非活动接口和回环接口
|
|
|
|
+ if (iface.OperationalStatus != OperationalStatus.Up ||
|
|
|
|
+ iface.NetworkInterfaceType == NetworkInterfaceType.Loopback)
|
|
|
|
+ continue;
|
|
|
|
+
|
|
|
|
+ // 获取IP属性
|
|
|
|
+ IPInterfaceProperties properties = iface.GetIPProperties();
|
|
|
|
+
|
|
|
|
+ // 获取IPv4地址
|
|
|
|
+ foreach (UnicastIPAddressInformation ip in properties.UnicastAddresses)
|
|
|
|
+ {
|
|
|
|
+ if (ip.Address.AddressFamily == AddressFamily.InterNetwork)
|
|
|
|
+ {
|
|
|
|
+ ipAddresses.Add(ip.Address.ToString());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke($"获取IP地址失败: {ex.Message}", LogLevel.info);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return ipAddresses;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void ProcessRequest(object state)
|
|
|
|
+ {
|
|
|
|
+ var context = (HttpListenerContext)state;
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ switch (context.Request.HttpMethod)
|
|
|
|
+ {
|
|
|
|
+ case "POST":
|
|
|
|
+ HandleImageUpload(context);
|
|
|
|
+ break;
|
|
|
|
+ case "GET":
|
|
|
|
+ HandleGetRequest(context);
|
|
|
|
+ break;
|
|
|
|
+ default:
|
|
|
|
+ SendResponse(context, 405, "Method Not Allowed");
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke($"Processing error: {ex}", LogLevel.info);
|
|
|
|
+ SendResponse(context, 500, $"Internal Server Error: {ex.Message}");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void HandleImageUpload(HttpListenerContext context)
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ // 获取内容类型
|
|
|
|
+ string contentType = context.Request.ContentType;
|
|
|
|
+ if (string.IsNullOrEmpty(contentType) || !contentType.Contains("multipart/form-data"))
|
|
|
|
+ {
|
|
|
|
+ SendResponse(context, 400, "Content-Type must be multipart/form-data");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 从Content-Type中提取boundary
|
|
|
|
+ string boundary = GetBoundaryFromContentType(contentType);
|
|
|
|
+ if (string.IsNullOrEmpty(boundary))
|
|
|
|
+ {
|
|
|
|
+ SendResponse(context, 400, "Invalid boundary in Content-Type");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 读取整个请求体
|
|
|
|
+ byte[] requestBody;
|
|
|
|
+ using (var ms = new MemoryStream())
|
|
|
|
+ {
|
|
|
|
+ context.Request.InputStream.CopyTo(ms);
|
|
|
|
+ requestBody = ms.ToArray();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 解析multipart数据
|
|
|
|
+ byte[] imageData = ExtractImageFromMultipart(requestBody, boundary);
|
|
|
|
+ if (imageData == null)
|
|
|
|
+ {
|
|
|
|
+ SendResponse(context, 400, "Failed to extract image from multipart data");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 验证图片数据
|
|
|
|
+ if (imageData.Length == 0)
|
|
|
|
+ {
|
|
|
|
+ SendResponse(context, 400, "Empty image data");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ Directory.CreateDirectory("upload");
|
|
|
|
+ // 保存原始数据用于调试
|
|
|
|
+ File.WriteAllBytes($"upload/upload_{DateTime.Now:yyyyMMddHHmmss}.jpg", imageData);
|
|
|
|
+
|
|
|
|
+ // 计算清晰度
|
|
|
|
+ var (score, isValid) = CalculateSharpness(imageData);
|
|
|
|
+ if (!isValid)
|
|
|
|
+ {
|
|
|
|
+ SendResponse(context, 400, "Unsupported image format or corrupted data");
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ OnLog?.Invoke($"img score:{score}", LogLevel.info);
|
|
|
|
+ // 返回结果
|
|
|
|
+ string response = $"{{\"sharpness_score\": {score}}}";
|
|
|
|
+ SendResponse(context, 200, response, "application/json");
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke($"Upload processing error: {ex}", LogLevel.error);
|
|
|
|
+ SendResponse(context, 500, $"Internal server error: {ex.Message}");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ private string GetBoundaryFromContentType(string contentType)
|
|
|
|
+ {
|
|
|
|
+ // 查找boundary参数
|
|
|
|
+ int boundaryIndex = contentType.IndexOf("boundary=", StringComparison.OrdinalIgnoreCase);
|
|
|
|
+ if (boundaryIndex == -1) return null;
|
|
|
|
+
|
|
|
|
+ // 提取boundary值
|
|
|
|
+ string boundaryValue = contentType.Substring(boundaryIndex + "boundary=".Length).Trim();
|
|
|
|
+
|
|
|
|
+ // 处理带引号的boundary
|
|
|
|
+ if (boundaryValue.StartsWith("\"") && boundaryValue.EndsWith("\""))
|
|
|
|
+ {
|
|
|
|
+ boundaryValue = boundaryValue.Substring(1, boundaryValue.Length - 2);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return boundaryValue;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private byte[] ExtractImageFromMultipart(byte[] requestBody, string boundary)
|
|
|
|
+ {
|
|
|
|
+ // 将boundary转换为字节数组
|
|
|
|
+ byte[] boundaryBytes = Encoding.ASCII.GetBytes($"--{boundary}");
|
|
|
|
+
|
|
|
|
+ // 查找第一个boundary的位置
|
|
|
|
+ int startIndex = FindSequence(requestBody, boundaryBytes, 0);
|
|
|
|
+ if (startIndex == -1) return null;
|
|
|
|
+ startIndex += boundaryBytes.Length;
|
|
|
|
+
|
|
|
|
+ // 查找第二个boundary(文件部分结束的位置)
|
|
|
|
+ int endIndex = FindSequence(requestBody, boundaryBytes, startIndex);
|
|
|
|
+ if (endIndex == -1) return null;
|
|
|
|
+
|
|
|
|
+ // 查找头部结束位置(两个CRLF)
|
|
|
|
+ byte[] headerEndPattern = new byte[] { 0x0D, 0x0A, 0x0D, 0x0A }; // \r\n\r\n
|
|
|
|
+ int headerEndIndex = FindSequence(requestBody, headerEndPattern, startIndex);
|
|
|
|
+ if (headerEndIndex == -1) return null;
|
|
|
|
+
|
|
|
|
+ headerEndIndex += headerEndPattern.Length;
|
|
|
|
+
|
|
|
|
+ // 提取图片数据(从headerEndIndex到endIndex,减去末尾的CRLF)
|
|
|
|
+ int dataLength = endIndex - headerEndIndex - 2; // 减去末尾的CRLF
|
|
|
|
+ if (dataLength <= 0) return null;
|
|
|
|
+
|
|
|
|
+ byte[] imageData = new byte[dataLength];
|
|
|
|
+ Buffer.BlockCopy(requestBody, headerEndIndex, imageData, 0, dataLength);
|
|
|
|
+
|
|
|
|
+ return imageData;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private int FindSequence(byte[] source, byte[] pattern, int startIndex)
|
|
|
|
+ {
|
|
|
|
+ for (int i = startIndex; i <= source.Length - pattern.Length; i++)
|
|
|
|
+ {
|
|
|
|
+ bool found = true;
|
|
|
|
+ for (int j = 0; j < pattern.Length; j++)
|
|
|
|
+ {
|
|
|
|
+ if (source[i + j] != pattern[j])
|
|
|
|
+ {
|
|
|
|
+ found = false;
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ if (found)
|
|
|
|
+ return i;
|
|
|
|
+ }
|
|
|
|
+ return -1;
|
|
|
|
+ }
|
|
|
|
+ private (double score, bool isValid) CalculateSharpness(byte[] imageData)
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ // 使用 System.Drawing 二次验证
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ using (var ms = new MemoryStream(imageData))
|
|
|
|
+ using (var img = Image.FromStream(ms))
|
|
|
|
+ {
|
|
|
|
+ // 图片验证通过
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke($"System.Drawing validation failed: {ex}", LogLevel.info);
|
|
|
|
+ return (-1, false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // OpenCV 处理
|
|
|
|
+ using (var img = new Mat())
|
|
|
|
+ {
|
|
|
|
+ CvInvoke.Imdecode(imageData, ImreadModes.Grayscale, img);
|
|
|
|
+
|
|
|
|
+ if (img.IsEmpty)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke("OpenCV failed to decode image", LogLevel.info);
|
|
|
|
+ return (-1, false);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 使用拉普拉斯算子计算清晰度
|
|
|
|
+ using (var laplacian = new Mat())
|
|
|
|
+ {
|
|
|
|
+ CvInvoke.Laplacian(img, laplacian, DepthType.Cv64F);
|
|
|
|
+
|
|
|
|
+ MCvScalar mean = new MCvScalar();
|
|
|
|
+ MCvScalar stdDev = new MCvScalar();
|
|
|
|
+ CvInvoke.MeanStdDev(laplacian, ref mean, ref stdDev);
|
|
|
|
+
|
|
|
|
+ return (stdDev.V0 * stdDev.V0, true);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch (Exception ex)
|
|
|
|
+ {
|
|
|
|
+ OnLog?.Invoke($"CV processing error: {ex}", LogLevel.error);
|
|
|
|
+ return (-1, false);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private bool IsValidImageData(byte[] data)
|
|
|
|
+ {
|
|
|
|
+ if (data.Length < 4) return false;
|
|
|
|
+
|
|
|
|
+ // JPEG
|
|
|
|
+ if (data[0] == 0xFF && data[1] == 0xD8 && data[2] == 0xFF)
|
|
|
|
+ return true;
|
|
|
|
+
|
|
|
|
+ // PNG
|
|
|
|
+ if (data[0] == 0x89 && data[1] == 0x50 && data[2] == 0x4E && data[3] == 0x47)
|
|
|
|
+ return true;
|
|
|
|
+
|
|
|
|
+ // BMP
|
|
|
|
+ if (data[0] == 0x42 && data[1] == 0x4D)
|
|
|
|
+ return true;
|
|
|
|
+
|
|
|
|
+ // WebP
|
|
|
|
+ if (data.Length > 12 &&
|
|
|
|
+ data[0] == 0x52 && data[1] == 0x49 && data[2] == 0x46 && data[3] == 0x46 &&
|
|
|
|
+ data[8] == 0x57 && data[9] == 0x45 && data[10] == 0x42 && data[11] == 0x50)
|
|
|
|
+ return true;
|
|
|
|
+
|
|
|
|
+ // TIFF (little endian)
|
|
|
|
+ if (data[0] == 0x49 && data[1] == 0x49 && data[2] == 0x2A && data[3] == 0x00)
|
|
|
|
+ return true;
|
|
|
|
+
|
|
|
|
+ // TIFF (big endian)
|
|
|
|
+ if (data[0] == 0x4D && data[1] == 0x4D && data[2] == 0x00 && data[3] == 0x2A)
|
|
|
|
+ return true;
|
|
|
|
+
|
|
|
|
+ return false;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private byte[] ConvertToJpeg(byte[] imageData)
|
|
|
|
+ {
|
|
|
|
+ try
|
|
|
|
+ {
|
|
|
|
+ using (var msIn = new MemoryStream(imageData))
|
|
|
|
+ using (var image = Image.FromStream(msIn))
|
|
|
|
+ using (var msOut = new MemoryStream())
|
|
|
|
+ {
|
|
|
|
+ // 转换为 JPEG 格式,质量为 95%
|
|
|
|
+ var encoderParams = new EncoderParameters(1);
|
|
|
|
+ encoderParams.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, 95L);
|
|
|
|
+
|
|
|
|
+ var jpegEncoder = ImageCodecInfo.GetImageEncoders()
|
|
|
|
+ .FirstOrDefault(e => e.FormatID == ImageFormat.Jpeg.Guid);
|
|
|
|
+
|
|
|
|
+ image.Save(msOut, jpegEncoder, encoderParams);
|
|
|
|
+ return msOut.ToArray();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch
|
|
|
|
+ {
|
|
|
|
+ // 转换失败返回原始数据
|
|
|
|
+ return imageData;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ private void HandleGetRequest(HttpListenerContext context)
|
|
|
|
+ {
|
|
|
|
+ string html = @"<!DOCTYPE html>
|
|
|
|
+<html>
|
|
|
|
+<head><title>Image Sharpness Test</title></head>
|
|
|
|
+<body>
|
|
|
|
+ <h1>Upload Image for Sharpness Evaluation</h1>
|
|
|
|
+ <form action='/' method='post' enctype='multipart/form-data'>
|
|
|
|
+ <input type='file' name='image' accept='image/*' required>
|
|
|
|
+ <button type='submit'>Evaluate</button>
|
|
|
|
+ </form>
|
|
|
|
+</body>
|
|
|
|
+</html>";
|
|
|
|
+
|
|
|
|
+ SendResponse(context, 200, html, "text/html");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ private void SendResponse(HttpListenerContext context, int statusCode, string message, string contentType = "text/plain")
|
|
|
|
+ {
|
|
|
|
+ byte[] buffer = Encoding.UTF8.GetBytes(message);
|
|
|
|
+
|
|
|
|
+ context.Response.StatusCode = statusCode;
|
|
|
|
+ context.Response.ContentType = contentType;
|
|
|
|
+ context.Response.ContentLength64 = buffer.Length;
|
|
|
|
+
|
|
|
|
+ context.Response.OutputStream.Write(buffer, 0, buffer.Length);
|
|
|
|
+ context.Response.Close();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|