WPF 使用GDI+提取圖片主色調(diào)并生成Mica材質(zhì)特效背景
當(dāng)前位置:點晴教程→知識管理交流
→『 技術(shù)文檔交流 』
先看效果,在淺色模式下: 在深色模式下:
測試項目在Github上開源: TwilightLemon/MicaImageTest: WPF 使用GDI+提取圖片主色調(diào)并生成Mica材質(zhì)特效背景? 一、簡要原理和設(shè)計1.1 Mica效果Mica效果是Windows 11的一個新特性,旨在為應(yīng)用程序提供一種更柔和的背景效果。它通過使用桌面壁紙的顏色和紋理來創(chuàng)建一個靜態(tài)的模糊背景效果。一個大致的模擬過程如下:
在倉庫代碼中給出了所有組件的實現(xiàn),如果你想調(diào)整效果,可以修改以下幾個值: 1 public static void ApplyMicaEffect(this Bitmap bitmap,bool isDarkmode) 2 { 3 bitmap.AdjustContrast(isDarkmode?-1:-20);//Light Mode通常需要一個更高的對比度 4 bitmap.AddMask(isDarkmode);//添加遮罩層 5 bitmap.ScaleImage(2);//放大圖像(原始圖像一般為500x500)以提高輸出圖像質(zhì)量 6 var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); 7 bitmap.GaussianBlur(ref rect, 80f, false);//按需要調(diào)整模糊半徑 8 } 1.2 主色調(diào)提取與微調(diào)從原始圖像中提取主色調(diào),主要過程如下:
之后為了適配UI,保證亮度、飽和度適合用于呈現(xiàn)內(nèi)容,還要對顏色進(jìn)行微調(diào):
最后計算焦點顏色(FocusAccentColor)只需要根據(jù)顏色模式調(diào)整亮度即可。 二、使用方法將代碼倉庫中的 首先開啟項目允許使用UnSafe代碼: <PropertyGroup> <!-- 允許使用UnSafe代碼 --> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> </PropertyGroup> 導(dǎo)入本地圖像文件,計算主色調(diào)、焦點色調(diào)并應(yīng)用Mica效果背景: var image=new BitmapImage(new Uri(ImagePath)); SelectedImg = image; var bitmap = image.ToBitmap(); //major color var majorColor = bitmap.GetMajorColor().AdjustColor(IsDarkMode); var focusColor = majorColor.ApplyColorMode(IsDarkMode); App.Current.Resources["AccentColor"] = new SolidColorBrush(majorColor); App.Current.Resources["FocusedAccentColor"] = new SolidColorBrush(focusColor); //background bitmap.ApplyMicaEffect(IsDarkMode); BackgroundImg = bitmap.ToBitmapImage(); 其中, 三、注意事項
最后附上 1 using System.Drawing; 2 using System.Drawing.Drawing2D; 3 using System.Drawing.Imaging; 4 using System.IO; 5 using System.Reflection; 6 using System.Runtime.InteropServices; 7 using System.Windows.Media.Imaging; 8 9 namespace MicaImageTest; 10 11 public static class ImageHelper 12 { 13 #region 處理模糊圖像 14 [DllImport("gdiplus.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] 15 private static extern int GdipBitmapApplyEffect(IntPtr bitmap, IntPtr effect, ref Rectangle rectOfInterest, bool useAuxData, IntPtr auxData, int auxDataSize); 16 /// <summary> 17 /// 獲取對象的私有字段的值 18 /// </summary> 19 /// <typeparam name="TResult">字段的類型</typeparam> 20 /// <param name="obj">要從其中獲取字段值的對象</param> 21 /// <param name="fieldName">字段的名稱.</param> 22 /// <returns>字段的值</returns> 23 /// <exception cref="System.InvalidOperationException">無法找到該字段.</exception> 24 /// 25 internal static TResult GetPrivateField<TResult>(this object obj, string fieldName) 26 { 27 if (obj == null) return default(TResult); 28 Type ltType = obj.GetType(); 29 FieldInfo lfiFieldInfo = ltType.GetField(fieldName, BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic); 30 if (lfiFieldInfo != null) 31 return (TResult)lfiFieldInfo.GetValue(obj); 32 else 33 throw new InvalidOperationException(string.Format("Instance field '{0}' could not be located in object of type '{1}'.", fieldName, obj.GetType().FullName)); 34 } 35 36 [StructLayout(LayoutKind.Sequential)] 37 private struct BlurParameters 38 { 39 internal float Radius; 40 internal bool ExpandEdges; 41 } 42 [DllImport("gdiplus.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] 43 private static extern int GdipCreateEffect(Guid guid, out IntPtr effect); 44 private static Guid BlurEffectGuid = new Guid("{633C80A4-1843-482B-9EF2-BE2834C5FDD4}"); 45 [DllImport("gdiplus.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] 46 private static extern int GdipSetEffectParameters(IntPtr effect, IntPtr parameters, uint size); 47 public static IntPtr NativeHandle(this Bitmap Bmp) 48 { 49 // 通過反射獲取Bitmap的私有字段nativeImage的值,該值為GDI+的內(nèi)部圖像句柄 50 //新版(8.0.1)Drawing的Nuget包中字段由 nativeImage變更為_nativeImage 51 return Bmp.GetPrivateField<IntPtr>("_nativeImage"); 52 } 53 [DllImport("gdiplus.dll", SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] 54 private static extern int GdipDeleteEffect(IntPtr effect); 55 public static void GaussianBlur(this Bitmap Bmp, ref Rectangle Rect, float Radius = 10, bool ExpandEdge = false) 56 { 57 int Result; 58 IntPtr BlurEffect; 59 BlurParameters BlurPara; 60 if ((Radius < 0) || (Radius > 255)) 61 { 62 throw new ArgumentOutOfRangeException("半徑必須在[0,255]范圍內(nèi)"); 63 } 64 BlurPara.Radius = Radius; 65 BlurPara.ExpandEdges = ExpandEdge; 66 Result = GdipCreateEffect(BlurEffectGuid, out BlurEffect); 67 if (Result == 0) 68 { 69 IntPtr Handle = Marshal.AllocHGlobal(Marshal.SizeOf(BlurPara)); 70 Marshal.StructureToPtr(BlurPara, Handle, true); 71 GdipSetEffectParameters(BlurEffect, Handle, (uint)Marshal.SizeOf(BlurPara)); 72 GdipBitmapApplyEffect(Bmp.NativeHandle(), BlurEffect, ref Rect, false, IntPtr.Zero, 0); 73 // 使用GdipBitmapCreateApplyEffect函數(shù)可以不改變原始的圖像,而把模糊的結(jié)果寫入到一個新的圖像中 74 GdipDeleteEffect(BlurEffect); 75 Marshal.FreeHGlobal(Handle); 76 } 77 else 78 { 79 throw new ExternalException("不支持的GDI+版本,必須為GDI+1.1及以上版本,且操作系統(tǒng)要求為Win Vista及之后版本."); 80 } 81 } 82 #endregion 83 84 public static System.Windows.Media.Color GetMajorColor(this Bitmap bitmap) 85 { 86 int skip = Math.Max(1, Math.Min(bitmap.Width, bitmap.Height) / 100); 87 88 Dictionary<int, ColorInfo> colorMap = []; 89 int pixelCount = 0; 90 91 for (int h = 0; h < bitmap.Height; h += skip) 92 { 93 for (int w = 0; w < bitmap.Width; w += skip) 94 { 95 Color pixel = bitmap.GetPixel(w, h); 96 97 // 量化顏色 (減少相似顏色的數(shù)量) 98 int quantizedR = pixel.R / 16 * 16; 99 int quantizedG = pixel.G / 16 * 16; 100 int quantizedB = pixel.B / 16 * 16; 101 102 // 排除極端黑白色 103 int averange = (pixel.R + pixel.G + pixel.B) / 3; 104 if (averange < 24) continue; 105 if (averange > 230) continue; 106 107 int colorKey = (quantizedR << 16) | (quantizedG << 8) | quantizedB; 108 109 if (colorMap.TryGetValue(colorKey, out ColorInfo info)) 110 { 111 info.Count++; 112 info.SumR += pixel.R; 113 info.SumG += pixel.G; 114 info.SumB += pixel.B; 115 } 116 else 117 { 118 colorMap[colorKey] = new ColorInfo 119 { 120 Count = 1, 121 SumR = pixel.R, 122 SumG = pixel.G, 123 SumB = pixel.B 124 }; 125 } 126 pixelCount++; 127 } 128 } 129 130 if (pixelCount == 0 || colorMap.Count == 0) 131 return System.Windows.Media.Colors.Gray; 132 133 var weightedColors = colorMap.Values.Select(info => 134 { 135 float r = info.SumR / (float)info.Count / 255f; 136 float g = info.SumG / (float)info.Count / 255f; 137 float b = info.SumB / (float)info.Count / 255f; 138 139 // 轉(zhuǎn)換為HSL來檢查飽和度和亮度 140 RgbToHsl(r, g, b, out float h, out float s, out float l); 141 142 // 顏色越飽和越有可能是主色調(diào),過亮或過暗的顏色權(quán)重降低 143 float weight = info.Count * s * (1 - Math.Abs(l - 0.6f) * 1.8f); 144 145 return new 146 { 147 R = info.SumR / info.Count, 148 G = info.SumG / info.Count, 149 B = info.SumB / info.Count, 150 Weight = weight 151 }; 152 }) 153 .OrderByDescending(c => c.Weight); 154 155 if (weightedColors.First() is { } dominantColor) 156 { 157 // 取權(quán)重最高的顏色 158 return System.Windows.Media.Color.FromRgb( 159 (byte)dominantColor.R, 160 (byte)dominantColor.G, 161 (byte)dominantColor.B); 162 } 163 164 return System.Windows.Media.Colors.Gray; 165 } 166 167 private class ColorInfo 168 { 169 public int Count { get; set; } 170 public int SumR { get; set; } 171 public int SumG { get; set; } 172 public int SumB { get; set; } 173 } 174 175 public static System.Windows.Media.Color AdjustColor(this System.Windows.Media.Color col, bool isDarkMode) 176 { 177 // 轉(zhuǎn)換為HSL色彩空間,便于調(diào)整亮度和飽和度 178 RgbToHsl(col.R / 255f, col.G / 255f, col.B / 255f, out float h, out float s, out float l); 179 180 bool isNearGrayscale = s < 0.15f; // 判斷是否接近灰度 181 182 // 1. 基于UI模式進(jìn)行初步亮度調(diào)整 183 if (isDarkMode) 184 { 185 // 在暗色模式下,避免顏色過暗,提高整體亮度 186 if (l < 0.5f) 187 l = 0.3f + l * 0.5f; 188 189 if (isNearGrayscale) 190 l = Math.Max(l, 0.4f); // 確保足夠明亮 191 } 192 else 193 { 194 // 在亮色模式下,避免顏色過亮,降低整體亮度 195 if (l > 0.5f) 196 l = 0.3f + l * 0.4f; 197 198 if (isNearGrayscale) 199 l = Math.Min(l, 0.6f); // 確保不過亮 200 } 201 202 // 2. 調(diào)整飽和度 203 if (!isNearGrayscale) 204 { 205 if (s > 0.7f) 206 { 207 // 高飽和度降低,但是暗色模式需要更鮮明的顏色 208 s = isDarkMode ? 0.7f - (s - 0.7f) * 0.2f : 0.65f - (s - 0.7f) * 0.4f; 209 } 210 else if (s > 0.4f) 211 { 212 // 中等飽和度微調(diào) 213 s = isDarkMode ? s * 0.85f : s * 0.75f; 214 } 215 else if (s > 0.1f) // 低飽和度但不是接近灰度 216 { 217 // 低飽和度增強(qiáng),尤其在暗色模式下 218 s = isDarkMode ? Math.Min(0.5f, s * 1.5f) : Math.Min(0.4f, s * 1.3f); 219 } 220 } 221 222 // 3. 特殊色相區(qū)域的處理 223 if (!isNearGrayscale) // 僅處理有明顯色相的顏色 224 { 225 // 紅色區(qū)域 (0-30° 或 330-360°) 226 if ((h <= 0.08f) || (h >= 0.92f)) 227 { 228 if (isDarkMode) 229 { 230 // 暗色模式下紅色需要更高飽和度和亮度 231 s = Math.Min(0.7f, s * 1.1f); 232 l = Math.Min(0.8f, l * 1.15f); 233 } 234 else 235 { 236 // 亮色模式下紅色降低飽和度,避免刺眼 237 s *= 0.8f; 238 l = Math.Max(0.4f, l * 0.9f); 239 } 240 } 241 // 綠色區(qū)域 (90-150°) 242 else if (h >= 0.25f && h <= 0.42f) 243 { 244 if (isDarkMode) 245 { 246 // 暗色模式下綠色提高亮度,降低飽和度,避免熒光感 247 s *= 0.85f; 248 l = Math.Min(0.7f, l * 1.2f); 249 } 250 else 251 { 252 // 亮色模式下綠色降低飽和度更多 253 s *= 0.75f; 254 } 255 } 256 // 藍(lán)色區(qū)域 (210-270°) 257 else if (h >= 0.58f && h <= 0.75f) 258 { 259 if (isDarkMode) 260 { 261 // 暗色模式下藍(lán)色提高亮度和飽和度 262 s = Math.Min(0.85f, s * 1.2f); 263 l = Math.Min(0.7f, l * 1.25f); 264 } 265 else 266 { 267 // 亮色模式下藍(lán)色保持中等飽和度 268 s = Math.Min(0.7f, Math.Max(0.4f, s)); 269 } 270 } 271 // 黃色區(qū)域 (30-90°) 272 else if (h > 0.08f && h < 0.25f) 273 { 274 if (isDarkMode) 275 { 276 // 暗色模式下黃色需要降低飽和度,提高亮度 277 s *= 0.8f; 278 l = Math.Min(0.75f, l * 1.2f); 279 } 280 else 281 { 282 // 亮色模式下黃色大幅降低飽和度 283 s *= 0.7f; 284 l = Math.Max(0.5f, l * 0.9f); 285 } 286 } 287 } 288 289 290 291 // 5. 最終亮度修正 - 確保在各種UI模式下都有足夠的對比度 292 if (isDarkMode && l < 0.3f) l = 0.3f; // 暗色模式下確保最小亮度 293 if (!isDarkMode && l > 0.7f) l = 0.7f; // 亮色模式下確保最大亮度 294 295 // 轉(zhuǎn)換回RGB 296 HslToRgb(h, s, l, out float r, out float g, out float b); 297 298 // 確保RGB值在有效范圍內(nèi) 299 byte R = (byte)Math.Max(0, Math.Min(255, r * 255)); 300 byte G = (byte)Math.Max(0, Math.Min(255, g * 255)); 301 byte B = (byte)Math.Max(0, Math.Min(255, b * 255)); 302 303 return System.Windows.Media.Color.FromRgb(R, G, B); 304 } 305 public static System.Windows.Media.Color ApplyColorMode(this System.Windows.Media.Color color,bool isDarkMode) 306 { 307 RgbToHsl(color.R/255f,color.G/255f, color.B/255f,out float h, out float s, out float l); 308 if (isDarkMode) 309 l = Math.Max(0.05f, l - 0.1f); 310 else 311 l = Math.Min(0.95f, l + 0.1f); 312 313 HslToRgb(h, s, l, out float r, out float g, out float b); 314 return System.Windows.Media.Color.FromRgb((byte)(r * 255), (byte)(g * 255), (byte)(b * 255)); 315 } 316 317 private static void RgbToHsl(float r, float g, float b, out float h, out float s, out float l) 318 { 319 float max = Math.Max(r, Math.Max(g, b)); 320 float min = Math.Min(r, Math.Min(g, b)); 321 322 // 計算亮度 323 l = (max + min) / 2.0f; 324 325 // 默認(rèn)值初始化 326 h = 0; 327 s = 0; 328 329 if (max == min) 330 { 331 // 無色調(diào) (灰色) 332 return; 333 } 334 335 float d = max - min; 336 337 // 計算飽和度 338 s = l > 0.5f ? d / (2.0f - max - min) : d / (max + min); 339 340 // 計算色相 341 if (max == r) 342 { 343 h = (g - b) / d + (g < b ? 6.0f : 0.0f); 344 } 345 else if (max == g) 346 { 347 h = (b - r) / d + 2.0f; 348 } 349 else // max == b 350 { 351 h = (r - g) / d + 4.0f; 352 } 353 354 h /= 6.0f; 355 356 // 確保h在[0,1]范圍內(nèi) 357 h = Math.Max(0, Math.Min(1, h)); 358 } 359 360 private static void HslToRgb(float h, float s, float l, out float r, out float g, out float b) 361 { 362 // 確保h在[0,1]范圍內(nèi) 363 h = ((h % 1.0f) + 1.0f) % 1.0f; 364 365 // 確保s和l在[0,1]范圍內(nèi) 366 s = Math.Max(0, Math.Min(1, s)); 367 l = Math.Max(0, Math.Min(1, l)); 368 369 if (s == 0.0f) 370 { 371 // 灰度顏色 372 r = g = b = l; 373 return; 374 } 375 376 float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s; 377 float p = 2.0f * l - q; 378 379 r = HueToRgb(p, q, h + 1.0f / 3.0f); 380 g = HueToRgb(p, q, h); 381 b = HueToRgb(p, q, h - 1.0f / 3.0f); 382 } 383 384 private static float HueToRgb(float p, float q, float t) 385 { 386 // 確保t在[0,1]范圍內(nèi) 387 t = ((t % 1.0f) + 1.0f) % 1.0f; 388 389 if (t < 1.0f / 6.0f) 390 return p + (q - p) * 6.0f * t; 391 if (t < 0.5f) 392 return q; 393 if (t < 2.0f / 3.0f) 394 return p + (q - p) * (2.0f / 3.0f - t) * 6.0f; 395 return p; 396 } 397 public static BitmapImage ToBitmapImage(this Bitmap Bmp) 398 { 399 BitmapImage BmpImage = new(); 400 using (MemoryStream lmemStream = new()) 401 { 402 Bmp.Save(lmemStream, ImageFormat.Png); 403 BmpImage.BeginInit(); 404 BmpImage.StreamSource = new MemoryStream(lmemStream.ToArray()); 405 BmpImage.EndInit(); 406 } 407 return BmpImage; 408 } 409 410 public static Bitmap ToBitmap(this BitmapImage img){ 411 using MemoryStream outStream = new(); 412 BitmapEncoder enc = new PngBitmapEncoder(); 413 enc.Frames.Add(BitmapFrame.Create(img)); 414 enc.Save(outStream); 415 return new Bitmap(outStream); 416 } 417 418 public static void AddMask(this Bitmap bitmap,bool darkmode) 419 { 420 var color1 = darkmode ? Color.FromArgb(150, 0, 0, 0) : Color.FromArgb(160, 255, 255, 255); 421 var color2 = darkmode ? Color.FromArgb(180, 0, 0, 0) : Color.FromArgb(200, 255, 255, 255); 422 using Graphics g = Graphics.FromImage(bitmap); 423 using LinearGradientBrush brush = new( 424 new Rectangle(0, 0, bitmap.Width, bitmap.Height), 425 color1, 426 color2, 427 LinearGradientMode.Vertical); 428 g.FillRectangle(brush, new Rectangle(0, 0, bitmap.Width, bitmap.Height)); 429 } 430 public static void AdjustContrast(this Bitmap bitmap, float contrast) 431 { 432 contrast = (100.0f + contrast) / 100.0f; 433 contrast *= contrast; 434 435 BitmapData data = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), 436 ImageLockMode.ReadWrite, bitmap.PixelFormat); 437 438 int width = bitmap.Width; 439 int height = bitmap.Height; 440 441 unsafe 442 { 443 for (int y = 0; y < height; y++) 444 { 445 byte* row = (byte*)data.Scan0 + (y * data.Stride); 446 for (int x = 0; x < width; x++) 447 { 448 int idx = x * 3; 449 450 float blue = row[idx] / 255.0f; 451 float green = row[idx + 1] / 255.0f; 452 float red = row[idx + 2] / 255.0f; 453 454 // 轉(zhuǎn)換為HSL 455 RgbToHsl(red, green, blue, out float h, out float s, out float l); 456 457 // 調(diào)整亮度以增加對比度 458 l = (((l - 0.5f) * contrast) + 0.5f); 459 460 // 轉(zhuǎn)換回RGB 461 HslToRgb(h, s, l, out red, out green, out blue); 462 463 row[idx] = (byte)Math.Max(0, Math.Min(255, blue * 255.0f)); 464 row[idx + 1] = (byte)Math.Max(0, Math.Min(255, green * 255.0f)); 465 row[idx + 2] = (byte)Math.Max(0, Math.Min(255, red * 255.0f)); 466 } 467 } 468 } 469 470 bitmap.UnlockBits(data); 471 } 472 473 public static void ScaleImage(this Bitmap bitmap, double scale) 474 { 475 // 計算新的尺寸 476 int newWidth = (int)(bitmap.Width * scale); 477 int newHeight = (int)(bitmap.Height * scale); 478 479 // 創(chuàng)建目標(biāo)位圖 480 Bitmap newBitmap = new Bitmap(newWidth, newHeight, bitmap.PixelFormat); 481 482 // 設(shè)置高質(zhì)量繪圖參數(shù) 483 using (Graphics graphics = Graphics.FromImage(newBitmap)) 484 { 485 graphics.CompositingQuality = CompositingQuality.HighQuality; 486 graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; 487 graphics.SmoothingMode = SmoothingMode.HighQuality; 488 graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; 489 490 // 繪制縮放后的圖像 491 graphics.DrawImage(bitmap, 492 new Rectangle(0, 0, newWidth, newHeight), 493 new Rectangle(0, 0, bitmap.Width, bitmap.Height), 494 GraphicsUnit.Pixel); 495 } 496 bitmap = newBitmap; 497 } 498 499 public static void ApplyMicaEffect(this Bitmap bitmap,bool isDarkmode) 500 { 501 bitmap.AdjustContrast(isDarkmode?-1:-20); 502 bitmap.AddMask(isDarkmode); 503 bitmap.ScaleImage(2); 504 var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height); 505 bitmap.GaussianBlur(ref rect, 80f, false); 506 } 507 }
該文章在 2025/6/5 10:14:17 編輯過 |
關(guān)鍵字查詢
相關(guān)文章
正在查詢... |