// ============================================================================ // Copyright © 2026 Hexagon Technology Center GmbH. All Rights Reserved. // 文件名: FilmEffectProcessor.cs // 描述: 电子胶片效果算子,模拟传统X射线胶片的显示效果 // 功能: // - 窗宽窗位(Window/Level)调整 // - 胶片反转(正片/负片) // - 多种胶片特性曲线(线性、S曲线、对数、指数) // - 边缘增强(模拟胶片锐化效果) // - 使用查找表(LUT)加速处理 // 算法: 窗宽窗位映射 + 特性曲线变换 // 作者: 李伟 wei.lw.li@hexagon.com // ============================================================================ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using XP.ImageProcessing.Core; using Serilog; namespace XP.ImageProcessing.Processors; /// /// 电子胶片效果算子(支持 8 位和 16 位灰度图像) /// 16 位模式下窗宽窗位参数范围扩展到 0-65535,LUT 升级为 ushort[65536] /// public class FilmEffectProcessor : ImageProcessorBase where TDepth : struct, IComparable { private static readonly ILogger _logger = Log.ForContext>(); private byte[] _lut8 = new byte[256]; private ushort[] _lut16 = new ushort[65536]; public FilmEffectProcessor() { Name = LocalizationHelper.GetString("FilmEffectProcessor_Name"); Description = LocalizationHelper.GetString("FilmEffectProcessor_Description"); } protected override void InitializeParameters() { // 窗宽窗位默认值适配:8 位用 0-255,16 位用 0-65535 // 运行时通过 MaxPixelValue 动态决定,参数范围设为最大(65535) Parameters.Add("WindowCenter", new ProcessorParameter( "WindowCenter", LocalizationHelper.GetString("FilmEffectProcessor_WindowCenter"), typeof(int), 32768, 0, 65535, LocalizationHelper.GetString("FilmEffectProcessor_WindowCenter_Desc"))); Parameters.Add("WindowWidth", new ProcessorParameter( "WindowWidth", LocalizationHelper.GetString("FilmEffectProcessor_WindowWidth"), typeof(int), 65535, 1, 65535, LocalizationHelper.GetString("FilmEffectProcessor_WindowWidth_Desc"))); Parameters.Add("Invert", new ProcessorParameter( "Invert", LocalizationHelper.GetString("FilmEffectProcessor_Invert"), typeof(bool), false, null, null, LocalizationHelper.GetString("FilmEffectProcessor_Invert_Desc"))); Parameters.Add("Curve", new ProcessorParameter( "Curve", LocalizationHelper.GetString("FilmEffectProcessor_Curve"), typeof(string), "Linear", null, null, LocalizationHelper.GetString("FilmEffectProcessor_Curve_Desc"), new string[] { "Linear", "Sigmoid", "Logarithmic", "Exponential" })); Parameters.Add("CurveStrength", new ProcessorParameter( "CurveStrength", LocalizationHelper.GetString("FilmEffectProcessor_CurveStrength"), typeof(double), 1.0, 0.1, 5.0, LocalizationHelper.GetString("FilmEffectProcessor_CurveStrength_Desc"))); Parameters.Add("EdgeEnhance", new ProcessorParameter( "EdgeEnhance", LocalizationHelper.GetString("FilmEffectProcessor_EdgeEnhance"), typeof(double), 0.0, 0.0, 3.0, LocalizationHelper.GetString("FilmEffectProcessor_EdgeEnhance_Desc"))); _logger.Debug("InitializeParameters"); } public override Image Process(Image inputImage) { int windowCenter = GetParameter("WindowCenter"); int windowWidth = GetParameter("WindowWidth"); bool invert = GetParameter("Invert"); string curve = GetParameter("Curve"); double curveStrength = GetParameter("CurveStrength"); double edgeEnhance = GetParameter("EdgeEnhance"); bool is16 = typeof(TDepth) == typeof(ushort); if (is16) { BuildLUT16(windowCenter, windowWidth, invert, curve, curveStrength); var img16 = new Image(inputImage.Width, inputImage.Height); var ushortData = inputImage.Data as ushort[,,]; int h = inputImage.Height, w = inputImage.Width; Parallel.For(0, h, y => { for (int x = 0; x < w; x++) img16.Data[y, x, 0] = _lut16[ushortData![y, x, 0]]; }); if (edgeEnhance > 0.01) { using var blurred = new Image(w, h); CvInvoke.GaussianBlur(inputImage, blurred, new System.Drawing.Size(3, 3), 0); Parallel.For(0, h, y => { for (int x = 0; x < w; x++) { int diff = ushortData![y, x, 0] - blurred.Data[y, x, 0]; int enhanced = img16.Data[y, x, 0] + (int)(diff * edgeEnhance); img16.Data[y, x, 0] = (ushort)Math.Clamp(enhanced, 0, 65535); } }); } _logger.Debug("Process(16bit): WC={WC}, WW={WW}", windowCenter, windowWidth); return (img16 as Image)!; } else { BuildLUT8( Math.Clamp(windowCenter, 0, 255), Math.Clamp(windowWidth, 1, 255), invert, curve, curveStrength); var byteInput = inputImage as Image; var resultImg = byteInput!.Clone(); int h = inputImage.Height, w = inputImage.Width; for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) resultImg.Data[y, x, 0] = _lut8[resultImg.Data[y, x, 0]]; if (edgeEnhance > 0.01) { using var blurred = byteInput.SmoothGaussian(3); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { float diff = byteInput.Data[y, x, 0] - blurred.Data[y, x, 0]; int enhanced = resultImg.Data[y, x, 0] + (int)(diff * edgeEnhance); resultImg.Data[y, x, 0] = (byte)Math.Clamp(enhanced, 0, 255); } } _logger.Debug("Process(8bit): WC={WC}, WW={WW}", windowCenter, windowWidth); return (resultImg as Image)!; } } private void BuildLUT8(int wc, int ww, bool invert, string curve, double strength) { double halfW = ww / 2.0; double low = wc - halfW, high = wc + halfW; for (int i = 0; i < 256; i++) { double normalized = ww <= 1 ? (i >= wc ? 1.0 : 0.0) : Math.Clamp((i - low) / (high - low), 0.0, 1.0); double mapped = ApplyCurve(normalized, curve, strength); if (invert) mapped = 1.0 - mapped; _lut8[i] = (byte)Math.Clamp((int)(mapped * 255.0), 0, 255); } } private void BuildLUT16(int wc, int ww, bool invert, string curve, double strength) { double halfW = ww / 2.0; double low = wc - halfW, high = wc + halfW; for (int i = 0; i < 65536; i++) { double normalized = ww <= 1 ? (i >= wc ? 1.0 : 0.0) : Math.Clamp((i - low) / (high - low), 0.0, 1.0); double mapped = ApplyCurve(normalized, curve, strength); if (invert) mapped = 1.0 - mapped; _lut16[i] = (ushort)Math.Clamp((int)(mapped * 65535.0), 0, 65535); } } private static double ApplyCurve(double x, string curve, double strength) => curve switch { "Sigmoid" => ApplySigmoid(x, strength), "Logarithmic" => ApplyLogarithmic(x, strength), "Exponential" => ApplyExponential(x, strength), _ => x }; /// S曲线(Sigmoid):增强中间调对比度 private static double ApplySigmoid(double x, double strength) { double k = strength * 10.0; return 1.0 / (1.0 + Math.Exp(-k * (x - 0.5))); } /// 对数曲线:提亮暗部,压缩亮部 private static double ApplyLogarithmic(double x, double strength) { double c = strength; return Math.Log(1.0 + c * x) / Math.Log(1.0 + c); } /// 指数曲线:压缩暗部,增强亮部 private static double ApplyExponential(double x, double strength) { double c = strength; return (Math.Exp(c * x) - 1.0) / (Math.Exp(c) - 1.0); } }