نوشتن برنامه چند رشته ای پراکندگی تصویر با Rust 🦀🖼️

پیشنهاد ویژه
اگر دنبال بهترین سایت برای اعزام دانشجو و مهاجرت به ترکیه با مجوز رسمی می گردی بزن رو دکمه پایین
پروژه را اینجا ببینید: https://github.com/NabeelAhmed1721/ordered-dithering/
من در چند ماه گذشته به زبان برنامه نویسی Rust علاقه مند شده ام. با توجه به جاوا اسکریپت، یک زبان با تایپ ضعیف، متوجه شدم که Rust بسیار سختگیرانهتر است و در هنگام ایجاد یک جریان کنترل نیاز به تفکر بیشتری دارد.
فقط برای ثبت، dithering معمولا در CPU انجام نمی شود. این وظیفه ای است که برای یک GPU مناسب است، جایی که می تواند از موازی سازی استفاده کند. من از این پروژه به عنوان راهی برای یادگیری در مورد Rust و Multithreading استفاده کردم. اگر می خواهید راهنمای انجام دیترینگ در یک GPU انجام شود، به این نگاه کنید.
بررسی اجمالی
به زبان ساده، دیترینگ فرآیند افزودن نویز به داده های کوانتیزه شده است. برای درک داده های کوانتیزه شده، به حذف اطلاعات مورد نیاز برای نمایش برخی داده ها فکر کنید، به عنوان مثال، از رنگ کمتر برای بیان یک تصویر استفاده کنید.
توجه به این نکته مهم است که تداخل یک اصطلاح کلی در پردازش اطلاعات است. با استفاده از آن در صدا، رادار، آب و هوا و بسیاری از برنامه های کاربردی دیگر، به تصاویر محدود نمی شود.
چندین تکنیک پراکندگی تصویر وجود دارد، پروژه من از Ordered یا Bayer Dithering استفاده می کند. آیا کاربردی ترین است؟ احتمالا نه. اما اگر از من بپرسید، فکر می کنم از نظر بصری جالب ترین به نظر می رسد.
به هر حال، قبل از اینکه به خود پروژه بپردازیم، در اینجا نتایجی وجود دارد که شما را به ادامه خواندن ترغیب می کند:
دستور Dithering
برای پراکندگی تصویر با استفاده از پراکندگی مرتب، باید هر پیکسل در تصویر منبع را با یک پالت ورودی و یک ماتریس آستانه (که معمولاً به عنوان ماتریس بایر یا فیلتر نامیده میشود) مقایسه کنیم.
برای ثبات، پروژه ما از یک پالت رنگ 8 بیتی استفاده می کند که شامل رنگ های زیر است:
const PALETTE: [Color; 8] = [
Color(0, 0, 0), // black
Color(255, 0, 0), // red
Color(0, 255, 0), // green
Color(0, 0, 255), // blue
Color(255, 255, 0), // yellow
Color(255, 0, 255), // magenta
Color(0, 255, 255), // cyan
Color(255, 255, 255), // white
];
شما می توانید از هر مجموعه رنگی استفاده کنید، با این حال، من متوجه شدم که طیف رنگی 8 بیتی دارای تعادل قابل اعتمادی از رنگ ها است که برای اکثر تصاویر کار می کند.
برای ایجاد یک ماتریس آستانه، میتوانیم از فرمول بازگشتی زیر برای ایجاد یک ماتریس بایر برای
سطح :
کجا برای هر
سطح، ماتریس است
و شامل اعداد از
به
.
اعتبار کامل این معادله و تشکر ویژه از این مقاله فوق العاده سورما است.
با این حال، در عمل، تولید این نوع محاسبات در طول زمان اجرا به سرعت گران می شود، بنابراین ارجاع از یک ماتریس از پیش تولید شده منطقی تر است. از این رو، چرا برنامه من از یک از پیش تولید شده استفاده می کند
ماتریس آستانه که به شکل زیر است:
تفاوت در اندازه های ماتریس نشان دهنده پیچیدگی الگوی پراکندگی در تصویر خروجی است. یک کوچک
ماتریس خروجی با کنتراست قابل توجه و الگوی پراکندگی ناهموار تولید می کند. در حالی که بزرگتر
ماتریس منجر به کنتراست صاف تر با الگوی ریزش دانه می شود. با این حال، بازده های کاهشی با اندازه های ماتریس بزرگتر وجود دارد – من متوجه شدم که یک
ماتریس بهترین عملکرد را برای صاف کردن رنگ ها دارد اما آن را نیز حفظ می کند نگاهی پر از پیکسل.
برای کاهش پیچیدگی، ماتریس را به صورت یک آرایه دو بعدی بیان می کنم و از طریق محاسبات زیر می توانم مختصات xy تصویر را به مقداری تبدیل کنم که بتوانم از آن برای فهرست بندی آرایه استفاده کنم:
// 8x8 Bayer Matrix
const MATRIX_WIDTH: u32 = 8;
const MATRIX: [u16; 64] = [
0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26, 12, 44, 4, 36,
14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22, 3, 35, 11, 43, 1, 33, 9, 41,
51, 19, 59, 27, 49, 17, 57, 25, 15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55,
23, 61, 29, 53, 21,
];
fn get_bayer(x: u32, y: u32) -> u16 {
let pos = x % MATRIX_WIDTH
+ ((y * MATRIX_WIDTH) % (MATRIX_WIDTH * MATRIX_WIDTH));
MATRIX[pos as usize]
}
با این حال، باید مقدار آستانه را از آن ترسیم کنیم
به
زیرا مقادیر رنگ RGB تصویر ورودی بین 0 تا 255 خواهد بود. برای حل این مشکل، از یک فرمول نگاشت محدوده ساده استفاده کردم:
pub fn map_range_value(
value: u32,
range_in: (u32, u32),
range_out: (u32, u32),
) -> u32 {
return (value - range_in.0) * (range_out.1 - range_out.0)
/ (range_in.1 - range_in.0)
+ range_out.0;
}
با ترکیب آنچه می دانیم، می توانیم مقدار آستانه خود را برای یک پیکسل معین در مختصات xy با عبارت زیر محاسبه کنیم:
let bay = utility::map_range_value(
Dither::get_bayer(x, y),
(0, 64),
(0, 255),
);
برای محاسبه مقدار کوانتیزه یک رنگ پیکسل معین، عدد را ضرب می کنیم bay
ارزش با a spread
ارزش. سپس محصول با رنگ ورودی جمع می شود که با a تنظیم می شود gamma
ارزش:
let quantize_value = |c: u8| -> u8 {
f32::min(
255.0 * f32::powf(f32::from(c) / 255.0, self.gamma)
+ self.spread * f32::from(bay),
255.0,
) as u8
};
let query_color =
Color(quantize_value(r), quantize_value(g), quantize_value(b));
در نهایت استفاده می کنیم query_color
برای جستجوی نزدیکترین تطابق در پالت تعریف شده. سپس نزدیکترین تطابق رنگ به عنوان مقدار پیکسل در مختصات xy داده شده در تصویر خروجی تنظیم می شود. تکرار این فرآیند برای هر پیکسل در یک تصویر ورودی منجر به یک تصویر خروجی پراکنده می شود.
چند رشته ای
از آنجایی که فرآیند dithering خود می تواند بر روی هر پیکسل به طور مستقل اجرا شود، به عبارت دیگر، یک پیکسل جداگانه نیازی به دانستن وضعیت پیکسل دیگر ندارد، این یک فرصت ایده آل برای چند رشته ای ایجاد می کند. خوشبختانه، به دلیل بررسی قرض Rust، چند رشته ای ساده و شهودی است. مواجهه با باگ های رایج مانند مسابقه داده ها، قفل ها و نشت حافظه سخت تر است.
بسته به برنامه کاربردی، مدیریت چند رشته ای ممکن است دشوار باشد. به نظر من تجسم جریان کنترل برای جلوگیری از سردرگمی هنگام برنامهنویسی مفید است. انتظار دارم برنامه من چگونه اجرا شود به شرح زیر است:
تقسیم تصویر به تکههای مجزا و مجزا، امکان جمعآوری راحت را فراهم میکند. از آنجایی که به هر رشته یک شناسه اختصاص داده شده است، می توانیم از فرمول های زیر برای محاسبه مکان های شروع و پایان هر چاک استفاده کنیم:
let thread_location_start =
(area / self.thread_count) * thread_id;
let mut thread_location_end =
(area / self.thread_count) * (thread_id + 1) - 1;
// looping through specific chunk
for i in thread_location_start..thread_location_end {
// dithering logic...
}
برای شناسایی و مدیریت موضوعات، ماژول کمکی جداگانه ای به نام ایجاد کردم worker
. در داخل ماژول، یک ساختار فراخوانی شده است Manager
همه رشته ها را ذخیره می کند (به صورت جداگانه نامیده می شود Worker
) در یک vec
و a را اجرا می کند collect
روش زمانی که نخ ها کامل می شوند. به طور کلی، این به من امکان داد تا منطق چند رشته ای را انتزاعی کنم و کد خود را قابل مدیریت تر نگه دارم.
let mut manager = worker::Manager::<DitherJob>::new(THREAD_COUNT);
let worker_count = manager.worker_count;
let dither = Arc::new(Dither::new(
worker_count,
reference_image,
&PALETTE,
GAMMA,
SPREAD,
));
manager.set_workers(&|id| {
let dither = Arc::clone(&dither);
thread::spawn(move || dither.dither_section(id))
});
manager.collect(&mut output);
توجه کنید که چگونه
Dither
پیچیده شده استArc::new
. برای به اشتراک گذاشتن ایمن مالکیت داده ها در سراسر رشته ها، باید از شمارنده مرجع اتمی (Arc) استفاده شود. تعداد مالکان را نگه می دارد و زمانی که هیچ رشته ای از آن استفاده نمی کند، مقدار را کاهش می دهد.
نتیجه
به طور کلی، من از نحوه اجرای برنامه راضی هستم. با اینکه هنوز Rust را یاد میگیرم، این پروژه به من کمک کرد تا در استفاده از زبان اعتماد به نفس بیشتری پیدا کنم و به من امکان داد تا ایدههای جدید در پردازش تصویر را کشف کنم.
امیدوارم از خواندن مقاله من لذت برده باشید، و دوباره می توانید پروژه زیر را بررسی کنید:
https://github.com/NabeelAhmed1721/ordered-dithering/
متشکرم،
نبیل احمد