SauvolaBinarization.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Drawing;
  5. using System.Drawing.Imaging;
  6. using System.Linq;
  7. using System.Runtime.InteropServices;
  8. using System.Text;
  9. using System.Threading.Tasks;
  10. namespace SauvolaTest
  11. {
  12. public static class SauvolaBinarization
  13. {
  14. /// <summary>
  15. /// Sauvola 二值化算法实现
  16. /// </summary>
  17. public static class SauvolaBinarizer
  18. {
  19. /// <summary>
  20. /// 应用 Sauvola 二值化
  21. /// </summary>
  22. /// <param name="source">8bpp 灰度源图像</param>
  23. /// <param name="windowSize">局部窗口大小 (奇数)</param>
  24. /// <param name="k">修正系数 (0.2-0.5)</param>
  25. /// <param name="r">动态范围 (通常 128)</param>
  26. public static Bitmap Apply(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0)
  27. {
  28. if (source == null) throw new ArgumentNullException(nameof(source));
  29. if (source.PixelFormat != PixelFormat.Format8bppIndexed)
  30. {
  31. throw new ArgumentException("输入图像必须是 8bpp 灰度格式。请先调用 ConvertToGrayscaleFast。", nameof(source));
  32. }
  33. int width = source.Width;
  34. int height = source.Height;
  35. Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
  36. // 复制调色板
  37. result.Palette = source.Palette;
  38. BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed);
  39. BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
  40. try
  41. {
  42. unsafe
  43. {
  44. byte* srcPtr = (byte*)srcData.Scan0;
  45. byte* resPtr = (byte*)resData.Scan0;
  46. int stride = srcData.Stride;
  47. // 1. 构建积分图 (Integral Image) 和 平方积分图
  48. // 使用 long 防止溢出 (最大像素和: 255 * 10^6 约等于 2.5*10^8, int 足够,但平方和需要 long)
  49. // 尺寸宽+1, 高+1 以便处理边界
  50. long[] integral = new long[(width + 1) * (height + 1)];
  51. long[] squaredIntegral = new long[(width + 1) * (height + 1)];
  52. Stopwatch sw = Stopwatch.StartNew();
  53. for (int y = 0; y < height; y++)
  54. {
  55. long rowSum = 0;
  56. long rowSqSum = 0;
  57. for (int x = 0; x < width; x++)
  58. {
  59. byte pixel = srcPtr[y * stride + x];
  60. rowSum += pixel;
  61. rowSqSum += (long)pixel * pixel;
  62. // I(y+1, x+1) = I(y, x+1) + RowSum
  63. int currentIndex = (y + 1) * (width + 1) + (x + 1);
  64. int topIndex = y * (width + 1) + (x + 1);
  65. integral[currentIndex] = integral[topIndex] + rowSum;
  66. squaredIntegral[currentIndex] = squaredIntegral[topIndex] + rowSqSum;
  67. }
  68. }
  69. sw.Stop();
  70. Console.WriteLine("积分图耗时:{0}", sw.ElapsedMilliseconds);
  71. int halfWin = windowSize / 2;
  72. Stopwatch Sauvolastopwatch = new Stopwatch();
  73. // 2. 计算阈值并二值化
  74. for (int y = 0; y < height; y++)
  75. {
  76. for (int x = 0; x < width; x++)
  77. {
  78. // 窗口边界
  79. int x1 = Math.Max(0, x - halfWin);
  80. int y1 = Math.Max(0, y - halfWin);
  81. int x2 = Math.Min(width - 1, x + halfWin);
  82. int y2 = Math.Min(height - 1, y + halfWin);
  83. // 窗口内像素数
  84. int count = (x2 - x1 + 1) * (y2 - y1 + 1);
  85. // 从积分图获取区域和
  86. long sum = GetRegionSum(integral, x1, y1, x2, y2, width);
  87. long sqSum = GetRegionSum(squaredIntegral, x1, y1, x2, y2, width);
  88. // 均值
  89. double mean = (double)sum / count;
  90. // 方差 = E[X^2] - (E[X])^2
  91. double variance = ((double)sqSum / count) - (mean * mean);
  92. if (variance < 0) variance = 0; // 防止浮点误差
  93. double stdDev = Math.Sqrt(variance);
  94. // Sauvola 公式
  95. double threshold = mean * (1 + k * ((stdDev / r) - 1));
  96. // 比较
  97. byte currentPixel = srcPtr[y * stride + x];
  98. resPtr[y * stride + x] = (byte)(currentPixel > threshold ? 255 : 0);
  99. }
  100. }
  101. Sauvolastopwatch.Stop();
  102. Console.WriteLine("二值化耗时:{0}", Sauvolastopwatch.ElapsedMilliseconds);
  103. }
  104. }
  105. finally
  106. {
  107. source.UnlockBits(srcData);
  108. result.UnlockBits(resData);
  109. }
  110. return result;
  111. }
  112. private static long GetRegionSum(long[] integralImg, int x1, int y1, int x2, int y2, int width)
  113. {
  114. // 积分图坐标需要 +1
  115. int w = width + 1;
  116. // A - B - C + D
  117. // A: (y2+1, x2+1)
  118. // B: (y1, x2+1)
  119. // C: (y2+1, x1)
  120. // D: (y1, x1)
  121. long a = integralImg[(y2 + 1) * w + (x2 + 1)];
  122. long b = integralImg[y1 * w + (x2 + 1)];
  123. long c = integralImg[(y2 + 1) * w + x1];
  124. long d = integralImg[y1 * w + x1];
  125. return a - b - c + d;
  126. }
  127. /// <summary>
  128. /// 高效串行版 Sauvola 二值化 (使用 fixed 指针,无 Parallel 限制)
  129. /// </summary>
  130. public static Bitmap ApplyFast(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0)
  131. {
  132. if (source == null) throw new ArgumentNullException(nameof(source));
  133. if (source.PixelFormat != PixelFormat.Format8bppIndexed)
  134. {
  135. throw new ArgumentException("输入图像必须是 8bpp 灰度格式。", nameof(source));
  136. }
  137. int width = source.Width;
  138. int height = source.Height;
  139. Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
  140. result.Palette = source.Palette;
  141. BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed);
  142. BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
  143. try
  144. {
  145. unsafe
  146. {
  147. byte* srcPtr = (byte*)srcData.Scan0;
  148. byte* resPtr = (byte*)resData.Scan0;
  149. int stride = srcData.Stride;
  150. // 预计算常数
  151. double kDivR = k / r;
  152. int halfWin = windowSize / 2;
  153. int wPlus1 = width + 1;
  154. // 1. 构建积分图
  155. int[] integral = new int[(width + 1) * (height + 1)];
  156. long[] squaredIntegral = new long[(width + 1) * (height + 1)];
  157. // 固定数组以使用指针加速构建
  158. fixed (int* pInt = integral)
  159. fixed (long* pSq = squaredIntegral)
  160. {
  161. for (int y = 0; y < height; y++)
  162. {
  163. int rowSum = 0;
  164. long rowSqSum = 0;
  165. // 指针定位到当前行和上一行的起始位置
  166. // 积分图每行有 width + 1 个元素
  167. int* prevRowInt = pInt + y * wPlus1;
  168. int* currRowInt = pInt + (y + 1) * wPlus1;
  169. long* prevRowSq = pSq + y * wPlus1;
  170. long* currRowSq = pSq + (y + 1) * wPlus1;
  171. byte* rowSrc = srcPtr + y * stride;
  172. for (int x = 0; x < width; x++)
  173. {
  174. byte pixel = rowSrc[x];
  175. rowSum += pixel;
  176. rowSqSum += (long)pixel * pixel;
  177. // 指针偏移 x+1
  178. currRowInt[x + 1] = prevRowInt[x + 1] + rowSum;
  179. currRowSq[x + 1] = prevRowSq[x + 1] + rowSqSum;
  180. }
  181. }
  182. }
  183. // 2. 串行二值化 (使用 fixed 指针加速读取)
  184. // 对于大多数中等分辨率图像,串行+指针比 Parallel+数组索引更快或相当
  185. fixed (int* pInt = integral)
  186. fixed (long* pSq = squaredIntegral)
  187. {
  188. for (int y = 0; y < height; y++)
  189. {
  190. byte* rowSrc = srcPtr + y * stride;
  191. byte* rowRes = resPtr + y * stride;
  192. for (int x = 0; x < width; x++)
  193. {
  194. // 窗口边界
  195. int x1 = x - halfWin;
  196. int y1 = y - halfWin;
  197. int x2 = x + halfWin;
  198. int y2 = y + halfWin;
  199. // 边界裁剪
  200. if (x1 < 0) x1 = 0;
  201. if (y1 < 0) y1 = 0;
  202. if (x2 >= width) x2 = width - 1;
  203. if (y2 >= height) y2 = height - 1;
  204. // 计算窗口内像素数
  205. int w = x2 - x1 + 1;
  206. int h = y2 - y1 + 1;
  207. int count = w * h;
  208. // 从积分图获取区域和 (指针访问)
  209. // 索引: (y * wPlus1) + x
  210. int idxA = (y2 + 1) * wPlus1 + (x2 + 1);
  211. int idxB = y1 * wPlus1 + (x2 + 1);
  212. int idxC = (y2 + 1) * wPlus1 + x1;
  213. int idxD = y1 * wPlus1 + x1;
  214. int sum = pInt[idxA] - pInt[idxB] - pInt[idxC] + pInt[idxD];
  215. long sqSum = pSq[idxA] - pSq[idxB] - pSq[idxC] + pSq[idxD];
  216. // 计算均值和方差
  217. double mean = (double)sum / count;
  218. double variance = ((double)sqSum / count) - (mean * mean);
  219. if (variance < 0) variance = 0;
  220. double stdDev = Math.Sqrt(variance);
  221. // Sauvola 阈值公式
  222. double threshold = mean * (1.0 + kDivR * stdDev - k);
  223. // 二值化
  224. rowRes[x] = (byte)(rowSrc[x] > threshold ? 255 : 0);
  225. }
  226. }
  227. }
  228. }
  229. }
  230. finally
  231. {
  232. source.UnlockBits(srcData);
  233. result.UnlockBits(resData);
  234. }
  235. return result;
  236. }
  237. /// <summary>
  238. /// 高效串行版 Sauvola 二值化 (使用 fixed 指针,无 Parallel 限制)
  239. /// </summary>
  240. public static Bitmap ApplyParallelGCHandle(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0)
  241. {
  242. if (source == null) throw new ArgumentNullException(nameof(source));
  243. if (source.PixelFormat != PixelFormat.Format8bppIndexed)
  244. {
  245. throw new ArgumentException("输入图像必须是 8bpp 灰度格式。", nameof(source));
  246. }
  247. int width = source.Width;
  248. int height = source.Height;
  249. Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
  250. result.Palette = source.Palette;
  251. BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed);
  252. BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
  253. try
  254. {
  255. unsafe
  256. {
  257. byte* srcPtr = (byte*)srcData.Scan0;
  258. byte* resPtr = (byte*)resData.Scan0;
  259. int stride = srcData.Stride;
  260. // 预计算常数
  261. double kDivR = k / r;
  262. int halfWin = windowSize / 2;
  263. int wPlus1 = width + 1;
  264. // 1. 构建积分图
  265. int[] integral = new int[(width + 1) * (height + 1)];
  266. long[] squaredIntegral = new long[(width + 1) * (height + 1)];
  267. // 固定数组以使用指针加速构建
  268. fixed (int* pInt = integral)
  269. fixed (long* pSq = squaredIntegral)
  270. {
  271. for (int y = 0; y < height; y++)
  272. {
  273. int rowSum = 0;
  274. long rowSqSum = 0;
  275. // 指针定位到当前行和上一行的起始位置
  276. // 积分图每行有 width + 1 个元素
  277. int* prevRowInt = pInt + y * wPlus1;
  278. int* currRowInt = pInt + (y + 1) * wPlus1;
  279. long* prevRowSq = pSq + y * wPlus1;
  280. long* currRowSq = pSq + (y + 1) * wPlus1;
  281. byte* rowSrc = srcPtr + y * stride;
  282. for (int x = 0; x < width; x++)
  283. {
  284. byte pixel = rowSrc[x];
  285. rowSum += pixel;
  286. rowSqSum += (long)pixel * pixel;
  287. // 指针偏移 x+1
  288. currRowInt[x + 1] = prevRowInt[x + 1] + rowSum;
  289. currRowSq[x + 1] = prevRowSq[x + 1] + rowSqSum;
  290. }
  291. }
  292. }
  293. // 2. 串行二值化 (使用 fixed 指针加速读取)
  294. // 对于大多数中等分辨率图像,串行+指针比 Parallel+数组索引更快或相当
  295. fixed (int* pInt = integral)
  296. fixed (long* pSq = squaredIntegral)
  297. {
  298. for (int y = 0; y < height; y++)
  299. {
  300. byte* rowSrc = srcPtr + y * stride;
  301. byte* rowRes = resPtr + y * stride;
  302. for (int x = 0; x < width; x++)
  303. {
  304. // 窗口边界
  305. int x1 = x - halfWin;
  306. int y1 = y - halfWin;
  307. int x2 = x + halfWin;
  308. int y2 = y + halfWin;
  309. // 边界裁剪
  310. if (x1 < 0) x1 = 0;
  311. if (y1 < 0) y1 = 0;
  312. if (x2 >= width) x2 = width - 1;
  313. if (y2 >= height) y2 = height - 1;
  314. // 计算窗口内像素数
  315. int w = x2 - x1 + 1;
  316. int h = y2 - y1 + 1;
  317. int count = w * h;
  318. // 从积分图获取区域和 (指针访问)
  319. // 索引: (y * wPlus1) + x
  320. int idxA = (y2 + 1) * wPlus1 + (x2 + 1);
  321. int idxB = y1 * wPlus1 + (x2 + 1);
  322. int idxC = (y2 + 1) * wPlus1 + x1;
  323. int idxD = y1 * wPlus1 + x1;
  324. int sum = pInt[idxA] - pInt[idxB] - pInt[idxC] + pInt[idxD];
  325. long sqSum = pSq[idxA] - pSq[idxB] - pSq[idxC] + pSq[idxD];
  326. // 计算均值和方差
  327. double mean = (double)sum / count;
  328. double variance = ((double)sqSum / count) - (mean * mean);
  329. if (variance < 0) variance = 0;
  330. double stdDev = Math.Sqrt(variance);
  331. // Sauvola 阈值公式
  332. double threshold = mean * (1.0 + kDivR * stdDev - k);
  333. // 二值化
  334. rowRes[x] = (byte)(rowSrc[x] > threshold ? 255 : 0);
  335. }
  336. }
  337. }
  338. }
  339. }
  340. finally
  341. {
  342. source.UnlockBits(srcData);
  343. result.UnlockBits(resData);
  344. }
  345. return result;
  346. }
  347. /// <summary>
  348. /// 并行版 Sauvola 二值化 (使用 GCHandle 解决 fixed 指针在 Lambda 中的限制)
  349. /// </summary>
  350. /// <param name="source">8bpp 灰度源图像</param>
  351. /// <param name="windowSize">局部窗口大小 (奇数)</param>
  352. /// <param name="k">修正系数 (0.2-0.5)</param>
  353. /// <param name="r">动态范围 (通常 128)</param>
  354. public static Bitmap ApplyParallel(Bitmap source, int windowSize = 25, double k = 0.2, double r = 128.0)
  355. {
  356. if (source == null) throw new ArgumentNullException(nameof(source));
  357. if (source.PixelFormat != PixelFormat.Format8bppIndexed)
  358. {
  359. throw new ArgumentException("输入图像必须是 8bpp 灰度格式。请先调用 ConvertToGrayscaleFast。", nameof(source));
  360. }
  361. int width = source.Width;
  362. int height = source.Height;
  363. Bitmap result = new Bitmap(width, height, PixelFormat.Format8bppIndexed);
  364. // 复制调色板
  365. result.Palette = source.Palette;
  366. BitmapData srcData = source.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, PixelFormat.Format8bppIndexed);
  367. BitmapData resData = result.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
  368. // 预计算常数
  369. double kDivR = k / r;
  370. int halfWin = windowSize / 2;
  371. int wPlus1 = width + 1; // 积分图宽度
  372. // 分配积分图数组
  373. // 使用 int 存储普通积分图以优化缓存 (假设图像总和不超过 int.MaxValue)
  374. int[] integral = new int[wPlus1 * (height + 1)];
  375. // 平方积分图必须使用 long
  376. long[] squaredIntegral = new long[wPlus1 * (height + 1)];
  377. try
  378. {
  379. unsafe
  380. {
  381. byte* srcPtr = (byte*)srcData.Scan0;
  382. byte* resPtr = (byte*)resData.Scan0;
  383. int stride = srcData.Stride;
  384. // ---------------------------------------------------------
  385. // 1. 构建积分图 (串行)
  386. // ---------------------------------------------------------
  387. // 这里可以使用 fixed,因为不在 Parallel 内部
  388. fixed (int* pInt = integral)
  389. fixed (long* pSq = squaredIntegral)
  390. {
  391. for (int y = 0; y < height; y++)
  392. {
  393. int rowSum = 0;
  394. long rowSqSum = 0;
  395. // 指针定位到当前行和上一行
  396. int* prevRowInt = pInt + y * wPlus1;
  397. int* currRowInt = pInt + (y + 1) * wPlus1;
  398. long* prevRowSq = pSq + y * wPlus1;
  399. long* currRowSq = pSq + (y + 1) * wPlus1;
  400. byte* rowSrc = srcPtr + y * stride;
  401. for (int x = 0; x < width; x++)
  402. {
  403. byte pixel = rowSrc[x];
  404. rowSum += pixel;
  405. rowSqSum += (long)pixel * pixel;
  406. // 积分图递推公式
  407. currRowInt[x + 1] = prevRowInt[x + 1] + rowSum;
  408. currRowSq[x + 1] = prevRowSq[x + 1] + rowSqSum;
  409. }
  410. }
  411. }
  412. // ---------------------------------------------------------
  413. // 2. 并行二值化 (使用 GCHandle)
  414. // ---------------------------------------------------------
  415. // 使用 GCHandle 固定数组内存,防止 GC 移动它们
  416. // 这样获取的指针可以在 Parallel.For 的 Lambda 中安全使用
  417. GCHandle hIntegral = GCHandle.Alloc(integral, GCHandleType.Pinned);
  418. GCHandle hSqIntegral = GCHandle.Alloc(squaredIntegral, GCHandleType.Pinned);
  419. try
  420. {
  421. // 获取固定内存的地址指针
  422. int* pIntFixed = (int*)hIntegral.AddrOfPinnedObject();
  423. long* pSqFixed = (long*)hSqIntegral.AddrOfPinnedObject();
  424. // 并行处理每一行
  425. Parallel.For(0, height, y =>
  426. {
  427. byte* rowSrc = srcPtr + y * stride;
  428. byte* rowRes = resPtr + y * stride;
  429. for (int x = 0; x < width; x++)
  430. {
  431. // 计算窗口边界
  432. int x1 = x - halfWin;
  433. int y1 = y - halfWin;
  434. int x2 = x + halfWin;
  435. int y2 = y + halfWin;
  436. // 边界裁剪
  437. if (x1 < 0) x1 = 0;
  438. if (y1 < 0) y1 = 0;
  439. if (x2 >= width) x2 = width - 1;
  440. if (y2 >= height) y2 = height - 1;
  441. // 窗口像素数量
  442. int w = x2 - x1 + 1;
  443. int h = y2 - y1 + 1;
  444. int count = w * h;
  445. // 计算积分图索引
  446. // 公式: Index = y * wPlus1 + x
  447. int idxA = (y2 + 1) * wPlus1 + (x2 + 1);
  448. int idxB = y1 * wPlus1 + (x2 + 1);
  449. int idxC = (y2 + 1) * wPlus1 + x1;
  450. int idxD = y1 * wPlus1 + x1;
  451. // 获取区域和 (指针访问,无边界检查开销)
  452. int sum = pIntFixed[idxA] - pIntFixed[idxB] - pIntFixed[idxC] + pIntFixed[idxD];
  453. long sqSum = pSqFixed[idxA] - pSqFixed[idxB] - pSqFixed[idxC] + pSqFixed[idxD];
  454. // 计算均值
  455. double mean = (double)sum / count;
  456. // 计算方差: E[X^2] - (E[X])^2
  457. double variance = ((double)sqSum / count) - (mean * mean);
  458. // 防止浮点误差导致负数
  459. if (variance < 0) variance = 0;
  460. // 标准差
  461. double stdDev = Math.Sqrt(variance);
  462. // Sauvola 阈值公式: T = m * (1 + k * (s/r - 1))
  463. // 优化为: T = m * (1 + (k/r)*s - k)
  464. double threshold = mean * (1.0 + kDivR * stdDev - k);
  465. // 二值化判定
  466. rowRes[x] = (byte)(rowSrc[x] > threshold ? 255 : 0);
  467. }
  468. });
  469. }
  470. finally
  471. {
  472. // 必须释放 GCHandle,否则会导致内存泄漏
  473. if (hIntegral.IsAllocated) hIntegral.Free();
  474. if (hSqIntegral.IsAllocated) hSqIntegral.Free();
  475. }
  476. }
  477. }
  478. finally
  479. {
  480. source.UnlockBits(srcData);
  481. result.UnlockBits(resData);
  482. }
  483. return result;
  484. }
  485. // 可选:快速近似平方根 (如果 Math.Sqrt 仍太慢)
  486. private static double ApproxSqrt(double x)
  487. {
  488. if (x <= 0) return 0;
  489. // 牛顿迭代法一次迭代,或者使用位操作黑客技巧
  490. // 这里简单返回 Math.Sqrt,因为在 .NET Core/.NET 5+ 中 Math.Sqrt 已经非常优化
  491. // 如果在 .NET Framework 且极追求性能,可替换为特定算法
  492. return Math.Sqrt(x);
  493. }
  494. }
  495. }
  496. }