استفاده از BitmapData در پردازش تصویر
نویسنده: هادی مزارعی
تاریخ: ۱۴۰۴/۰۵/۰۸ ۱۷:۵۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Ces.SignatureExtractor ارتباط دارد ولی مشخصا بدلیل داشتن یک منبع کد، در این پست به آن اشاره شده استExtract در این کتابخانه با کمک ChatGPT بهبود یافته.نکته: خواهشمندم هر مطلب اصلاحی و تکمیلی در خصوص مطلب جاری مد نظر بود حتما ارسال بفرمایید.
System.Drawing.Imaging قرار دارد که با استفاده از متدهای LockBits و UnlockBits استفاده میشود و این کلاس قابلیت ارثبری ندارد (Not inheritable). کلاس BitmapData خصیصههای (Attributes) یک تصویر Bitmap را مشخص میکند. ویژگیهای (Properties) موجود در این کلاس شامل Width, Height, Stride, Scan0 و یک لیست شمارنده با نام PixelFormat میباشد:LockBit تصویر را در حافظه قفل میکند و با توجه به پارامتر PixelFormat، فضایی که هر پیکسل باید در حافظه جهت نگهداری اطلاعات رنگ اشغال کند مشخص میشود. این مقدار برای هر پیکسل میتواند 24 یا 32 یا 64 و... بیت باشد. بنابراین در زمان خوانش خانههای حافظه باید بدانیم که چند بایت متعلق به یک پیکسل است که این موضوع تحت نام Stride یا پهنا/گام پیکسل شناخته میشود.Bitmap است و تصویر را از آدرس ارسال شده ایجاد خواهد کرد.Bitmap است و با فرمت Format32bppArgb تعریف شده است. در این لیست شمارشی میتوانید انواع فرمتها را مشاهده کنید. در ادامه علت انتخاب Format32bppArgb توضیح داده شده.Rectangle) به اندازه تصویر اصلی است که به عنوان پارامتر متد LockBit جهت تعیین محدودهای از تصویر که باید در حافظه قفل شود تعریف شده است (ضروری نیست ولی برای تمیزی کد تعریف شده است).BitmapData است که متد LockBit بر میگرداند. اولین پارامتر، ناحیهای از تصویر اصلی است که باید در حافظه قفل شود. محددهای که قفل میشود بدون تغییر خواهند ماند تا عملیات پایان یابد و در انتها با متد UnlockBits آزاد خواهد شد. پارامتر دوم نحوه قفلگذاری حافظه است که تصویر اصلی در حالت ReadOnly و تصویر نهایی WriteOnly میباشد. پارامتر سوم که PixelFormat است برای تصویر اصلی Format24bppRgb و برای تصویر خروجی Format32bppArgb میباشد.bmpDataOriginal میباشد.Format24bppRgb) که هر 8 بیت برای یک رنگ مورد نیاز است و رنگهای آبی، قرمز و سبز مجموعا 24 بیت نیاز دارند (3 بایت).Alpha در پیکسل را نگهداری کرد باید نوع Format32bppArgb که 32 بیت فضا نیاز دارد را انتخاب کرد که 8 بیت برای نگهداری مقدار Alpha مورد نیاز است که مجموعا 32 بیت اشغال خواهد کرد (4 بایت).Stride برای تصویر اصلی 600 و برای تصویر خروجی 1200 خواهد بود و دلیل آن این است که هر پیکسل با توجه به PixelFormat دارای پهنای متفاوتی است و نگاشت 200 پیکسل در حافظه نیازمند 600 بیت فضا است (200*3) و برای تصویر خروجی نیز 1200 بیت (1200*4).نکته: جهت کار با حافظه بصورت مستقیم باید دستورات بصورتunsafeاجرا شوند. استفاده از*نیز بدین منظور میباشد.
Bitmap نیست که بصورت یک آرایه دو بعدی پیکسلها خوانده میشود.Scan0 (همان Padding یا فاصله) ضربدر مقدار y (خط اسکن) ضربدر پهنای تصویر (Stride = پهنا * گام هر پیکسل). مقداری که در دو متغیر rowOriginal و rowSignature بدست میآید آدرس اولین خانه از حافظه در هر ردیف تصویر اصلی است که در حلقه بعدی تمام خانههای بعد از این نشانگر به عنوان اطلاعات رنگ ردیف جاری در تصویر اصلی خوانده خواهد شد.byte* rowOriginal = ptrOriginal + (y * originalStride); byte* rowSignature = ptrSignature + (y * signatureStride);
y نقطه شروع هر ردیف بدست آمده و در حلقه x باید به ازای پهنای تصویر (برحسب پیکسل) موقعیت هر پیکسل در حافظه را بخوانیم و با توجه به اینکه اطلاعات هر پیکسل در تصویر اصلی دارای سه مقدار 8 بیتی است بنابراین مشخصات RGB بصورت زیر بدست میآید (به ضریب 3 توجه کنید - تصویر اصلی 24 بیتی میباشد):byte b = rowOriginal[x * 3 + 0]; byte g = rowOriginal[x * 3 + 1]; byte r = rowOriginal[x * 3 + 2];
GetBrightness قابل دریافت است ولی محاسبه زیر دقیقتر می باشد.float brightness = (0.299f * r + 0.587f * g + 0.114f * b) / 255f; byte alpha = (byte)(255 * (1.0f - brightness));
نکته: درخصوص محاسبه روشنایی عبارت Standard Luminance Formula را در وب جستجو کنید.
precision به متد ارسال شده، میتوان دادههای رنگ جدید را (original یا custom) در حافظه بنویسیم (به ضریب 4 توجه کنید - تصویر نهایی 32 بیتی میباشد):rowSignature[x * 4 + 0] = finalColor.B; rowSignature[x * 4 + 1] = finalColor.G; rowSignature[x * 4 + 2] = finalColor.R; rowSignature[x * 4 + 3] = alpha; // Alpha
اگر اطلاعات رنگ جاری خارج از دامنه اعداد باشد مقدار 0 با شفافیت 0 را جایگزین میکنیم که عملا بخش خارج از خطوط امضا Transparent خواهد شد.
UnlockBits حافظه قفل شده را آزاد میکنیم (Unlock میشود ولی حافظه آزاد نمیشود مگر آنکه شیء Bitmap را Dispose کنیم که این مورد در انتهای عملیات و بدلیل استفاده از using در تعریف متغیر انجام خواهد شد).Extract می باشد (با استفاده از BitmapData).BitmapData حدود 50 ms. یعنی افزایش ده برابری سرعت پردازش.public Bitmap Extract(string imagePath, bool useOriginalColor = true, Color? customColor = null, byte precision = 200)
{
if (!useOriginalColor && !customColor.HasValue)
throw new ArgumentNullException(nameof(customColor));
using var imgOriginal = new Bitmap(Image.FromFile(imagePath));
var width = imgOriginal.Width;
var height = imgOriginal.Height;
// Final image for drawing extracted signature
var imgSignature = new Bitmap(width, height, PixelFormat.Format32bppArgb);
var rect = new Rectangle(0, 0, width, height);
// Lock both images for direct pixel access
var bmpDataOriginal = imgOriginal.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
var bmpDataSignature = imgSignature.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
int originalStride = bmpDataOriginal.Stride;
int signatureStride = bmpDataSignature.Stride;
unsafe
{
byte* ptrOriginal = (byte*)bmpDataOriginal.Scan0;
byte* ptrSignature = (byte*)bmpDataSignature.Scan0;
for (int y = 0; y < height; y++)
{
byte* rowOriginal = ptrOriginal + (y * originalStride);
byte* rowSignature = ptrSignature + (y * signatureStride);
for (int x = 0; x < width; x++)
{
byte b = rowOriginal[x * 3 + 0];
byte g = rowOriginal[x * 3 + 1];
byte r = rowOriginal[x * 3 + 2];
float brightness = (0.299f * r + 0.587f * g + 0.114f * b) / 255f;
byte alpha = (byte)(255 * (1.0f - brightness));
// چشم پوشی از نقاط سفید و یا نواحی طوسی متمایل به سفید
// البته این مقدار که برنامه باید ازآن به عنوان نقاط اطراف
//خط امضا در نظر بگیرد به توسط پارامتر ورودی تابع تعیین می شود
if (r <= precision && g <= precision && b <= precision)
{
Color finalColor = useOriginalColor ? Color.FromArgb(r, g, b) : customColor.Value;
rowSignature[x * 4 + 0] = finalColor.B;
rowSignature[x * 4 + 1] = finalColor.G;
rowSignature[x * 4 + 2] = finalColor.R;
rowSignature[x * 4 + 3] = alpha; // Alpha
}
else
{
// Transparent
rowSignature[x * 4 + 0] = 0;
rowSignature[x * 4 + 1] = 0;
rowSignature[x * 4 + 2] = 0;
rowSignature[x * 4 + 3] = 0;
}
}
}
}
imgOriginal.UnlockBits(bmpDataOriginal);
imgSignature.UnlockBits(bmpDataSignature);
// برش و ناحیه امضا و حذف ناحیه شفاف خارج از محدوده امضا
var bounds = GetNonTransparentBounds(imgSignature);
if (bounds.Width <= 0 || bounds.Height <= 0)
return new Bitmap(1, 1); // Empty result if nothing found
var result = new Bitmap(bounds.Width, bounds.Height);
using (Graphics g = Graphics.FromImage(result))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.DrawImage(imgSignature, new Rectangle(0, 0, bounds.Width, bounds.Height), bounds, GraphicsUnit.Pixel);
}
return result;
}
private Rectangle GetNonTransparentBounds(Bitmap bitmap)
{
int top = -1, bottom = -1, left = -1, right = -1;
BitmapData data = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly,
PixelFormat.Format32bppArgb);
int stride = data.Stride;
int width = bitmap.Width;
int height = bitmap.Height;
unsafe
{
byte* ptr = (byte*)data.Scan0;
// بدست آوردن مرز بالا و پایین ناحیه امضا
for (int y = 0; y < height; y++)
{
byte* row = ptr + (y * stride);
for (int x = 0; x < width; x++)
{
byte alpha = row[x * 4 + 3];
if (alpha != 0)
{
if (top == -1)
top = y;
bottom = y;
break;
}
}
}
// بدست آوردن مرز چپ و راست ناحیه امضا
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
byte* row = ptr + (y * stride);
byte alpha = row[x * 4 + 3];
if (alpha != 0)
{
if (left == -1)
left = x;
right = x;
break;
}
}
}
}
bitmap.UnlockBits(data);
if (top == -1 || left == -1)
return Rectangle.Empty;
return new Rectangle(left, top, right - left + 1, bottom - top + 1);
}