نحوه ترکیب تصاویر در Rust با استفاده از Pixel Math
برای هر کسی که به دنبال یادگیری در مورد پردازش تصویر به عنوان یک طاقچه برنامه نویسی است، ترکیب تصاویر مکان بسیار خوبی برای شروع است. این یکی از ساده ترین و در عین حال مفیدترین تکنیک ها در مورد پردازش تصویر است.
برای کمک به شهود خود، بهتر است یک تصویر را به صورت نمودار ریاضی مقادیر پیکسل که در امتداد مختصات x و y ترسیم شده است تصور کنید. پیکسل بالای سمت راست در یک تصویر مبدا شما است که با مقدار x 0 و مقدار ay 0 مطابقت دارد.
وقتی این را تصور کردید، هر پیکسل در یک تصویر را می توان با استفاده از مختصات آن در این نمودار xy خواند یا تغییر داد. به عنوان مثال، برای یک تصویر مربعی با اندازه 5 پیکسل در 5 پیکسل، مختصات پیکسل مرکزی 2، 2 است. ممکن است انتظار داشته باشید که 3، 3 باشد، اما مختصات تصویر در این زمینه شبیه به شاخص های آرایه عمل می کند و از 0 شروع می شود. برای هر دو محور
پردازش تصویر به این روش به شما کمک می کند تا هر پیکسل را به صورت جداگانه تحلیل کنید و این روند را بسیار ساده تر می کند.
پیش نیازها
تمرکز این مقاله برای شما این است که درک کنید و یاد بگیرید که چگونه تصاویر را با استفاده از زبان برنامه نویسی Rust ترکیب کنید، بدون اینکه وارد جزئیات زبان یا نحو آن شوید. پس نوشتن برنامه های Rust راحت لازم است.
اگر با Rust آشنایی ندارید، به شدت شما را به یادگیری اصول اولیه تشویق می کنم. در اینجا یک دوره تعاملی Rust آمده است که می تواند شما را شروع کند.
فهرست مطالب
مقدمه
ترکیب تصویر به تکنیک ادغام پیکسل ها از چندین تصویر برای ایجاد یک تصویر خروجی واحد که از تمام ورودی های آن مشتق شده است، اشاره دارد. بسته به اینکه کدام عملیات ترکیبی استفاده می شود، خروجی تصویر می تواند با توجه به ورودی های یکسان، بسیار متفاوت باشد.
این تکنیک اساس بسیاری از ابزارهای پیچیده پردازش تصویر است که ممکن است قبلاً با برخی از آنها آشنا باشید. مواردی مانند حذف افراد متحرک از تصاویر در صورت داشتن چندین تصویر، ادغام تصاویر آسمان شب برای ایجاد مسیرهای ستاره ای، و ادغام چندین تصویر با نویز سنگین برای ایجاد یک تصویر کاهش نویز، همگی نمونه هایی از این تکنیک هستند.
برای دستیابی به ترکیب تصاویر در این آموزش، از "ریاضی پیکسل" استفاده خواهیم کرد، که اگرچه یک اصطلاح واقعاً استاندارد نیست، اما به تکنیک انجام عملیات ریاضی روی یک پیکسل یا مجموعه ای از پیکسل ها برای تولید یک پیکسل خروجی اشاره دارد.
به عنوان مثال، برای ترکیب دو تصویر با استفاده از حالت ترکیبی "متوسط"، عملیات میانگین ریاضی را روی تمام پیکسل های ورودی در یک مکان مشخص انجام می دهید تا خروجی را در همان مکان تولید کنید.
ریاضیات پیکسل به عملیات نقطه ای محدود نمی شود، که اساساً عملیاتی است که در طول پردازش تصویر انجام می شود و یک پیکسل خروجی معین را بر اساس پیکسل ورودی از تصاویر منفرد یا چندتایی از یک مکان در سیستم مختصات xy ایجاد می کند.
در تجربه من تا کنون، کل زمینه پردازش تصویر 99٪ ریاضی و 1٪ جادوی سیاه است. عملیات ریاضی روی پیکسل ها و پیکسل های اطراف آن، اساس تکنیک های دستکاری تصویر مانند فشرده سازی، تغییر اندازه، تاری و وضوح، کاهش نویز و غیره است.
ترکیب تصویر چگونه کار می کند
اجرای این تکنیک از نظر فنی ساده است. بیایید یک ترکیب متوسط ساده را مثال بزنیم. در اینجا نحوه کار آن آمده است:
داده های پیکسلی هر دو تصویر را در حافظه، معمولاً در یک آرایه برای هر تصویر بخوانید.
آرایه معمولا 2 بعدی است. هر ورودی در آرایه آرایه دیگری برای تصاویر رنگی است، آرایه ثانویه مقادیر 3 پیکسلی مربوط به کانال های رنگی قرمز، سبز و آبی را نگه می دارد.
برای هر مکان پیکسل:
متوجه خواهید شد که ریاضیات پیکسلی بر اساس هر کانال انجام می شود. این همیشه برای حالتهای ترکیبی که در این آموزش پوشش میدهیم صادق است، اما بسیاری از تکنیکها شامل اعمال ترکیبها بین خود کانالها و چندین بار در یک تصویر است.
راه اندازی پروژه
بیایید با راهاندازی پروژهای شروع کنیم که مبنای خوبی برای کار به ما میدهد.
cargo new --bin image-blender cd image-blender
همچنین برای کمک به انجام این عملیات به یک وابستگی واحد نیاز دارید:
cargo add image
image
یک کتابخانه Rust است که از آن برای کار با تصاویر همه فرمتها و کدگذاریهای استاندارد استفاده میکنیم. همچنین به ما کمک می کند تا بین فرمت های مختلف تبدیل کنیم و دسترسی آسان به داده های پیکسل را به عنوان بافر فراهم می کند.
برای اطلاعات بیشتر در مورد جعبه image
، می توانید به اسناد رسمی مراجعه کنید.
برای دنبال کردن، می توانید از هر دو تصویر استفاده کنید، تنها شرط لازم این است که اندازه و فرمت یکسانی داشته باشند. همچنین می توانید تصاویر استفاده شده در این آموزش را به همراه کد کامل در مخزن GitHub اینجا بیابید.
نحوه خواندن مقادیر پیکسل
اولین قدم بارگذاری تصاویر و خواندن مقادیر پیکسل آنها در یک ساختار داده است که عملیات ما را تسهیل می کند. برای این آموزش، ما از آرایههای Vec
استفاده میکنیم ( Vec<[u8; 3]>
). هر ورودی در Vec
بیرونی نشان دهنده یک پیکسل است و مقادیر کانالی هر پیکسل در [u8; 3]
آرایه
بیایید با ایجاد یک فایل جدید برای نگهداری این کد به نام io.rs شروع کنیم.
// src/io.rs use image::GenericImageView; pub struct SourceData { pub width: usize , pub height: usize , pub image1: Vec <[ u8 ; 3 ]>, pub image2: Vec <[ u8 ; 3 ]>, } pub fn read_pixel_data (image1_path: String , image2_path: String ) -> SourceData { // Open the images let image1 = image::open(image1_path).unwrap(); let image2 = image::open(image2_path).unwrap(); // Compute image dimensions let (width, height) = image1.dimensions(); let (width, height) = (width as usize , height as usize ); // Create arrays to hold input pixel data let mut image1_data: Vec <[ u8 ; 3 ]> = vec! [[ 0 , 0 , 0 ]; width * height]; let mut image2_data: Vec <[ u8 ; 3 ]> = vec! [[ 0 , 0 , 0 ]; width * height]; // Iterate over all pixels in the input image, along with their positions in x & y // coordinates. for (x, y, pixel) in image1.to_rgb8().enumerate_pixels() { // Compute the raw values for each channel in the RGB pixel. let [r, g, b] = pixel. 0 ; // Compute linear index based on 2D index. This is basically computing index in // 1D array based on the row and column index of the pixel in the 2D image. let index = (y * (width as u32 ) + x) as usize ; // Save the channel-wise values in the correct index in data arrays. image1_data[index] = [r, g, b]; } // Iterate over all pixels in the input image, along with their positions in x & y // coordinates. for (x, y, pixel) in image2.to_rgb8().enumerate_pixels() { // Compute the raw values for each channel in the RGB pixel. let [r, g, b] = pixel. 0 ; // Compute linear index based on 2D index. This is basically computing index in // 1D array based on the row and column index of the pixel in the 2D image. let index = (y * (width as u32 ) + x) as usize ; // Save the channel-wise values in the correct index in data arrays. image2_data[index] = [r, g, b]; } SourceData { width, height, image1: image1_data, image2: image2_data, } }
نحوه ترکیب توابع
مرحله بعدی پیاده سازی توابع ترکیبی است که توابع خالصی هستند که دو مقدار پیکسل را به عنوان ورودی می گیرند و مقدار خروجی را برمی گردانند. این از طریق ویژگی BlendOperation
تعریف شده در زیر پیاده سازی می شود. بیایید یک فایل جدید برای میزبانی همه عملیات به نام Operations.rs ایجاد کنیم.
// src/operations.rs pub trait BlendOperation { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ]; }
در مرحله بعد، ما باید این ویژگی را برای همه روشهای ترکیبی که میخواهیم پشتیبانی کنیم، پیادهسازی کنیم.
برای نمایش نتیجه هر یک از حالت های ترکیبی، دو تصویر ورودی زیر با هم ترکیب می شوند
ترکیب متوسط
یک ترکیب متوسط شامل میانگین گیری از طریق کانال مقادیر پیکسل ورودی برای بدست آوردن پیکسل خروجی است.
// src/operations.rs pub struct AverageBlend ; impl BlendOperation for AverageBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ pixel1[ 0 ] / 2 + pixel2[ 0 ] / 2 , pixel1[ 1 ] / 2 + pixel2[ 1 ] / 2 , pixel1[ 2 ] / 2 + pixel2[ 2 ] / 2 , ] } }
Blend را ضرب کنید
ترکیب ضربی شامل ضرب کانالی مقادیر پیکسل ورودی پس از نرمال شدن [¹] برای بدست آوردن پیکسل خروجی است. سپس پیکسل خروجی با ضرب در 255 به محدوده اصلی باز می گردد.
// src/operations.rs pub struct MultiplyBlend ; impl BlendOperation for MultiplyBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ ((pixel1[ 0 ] as f32 / 255 . * pixel2[ 0 ] as f32 / 255 .) * 255 .) as u8 , ((pixel1[ 1 ] as f32 / 255 . * pixel2[ 1 ] as f32 / 255 .) * 255 .) as u8 , ((pixel1[ 2 ] as f32 / 255 . * pixel2[ 2 ] as f32 / 255 .) * 255 .) as u8 , ] } }
مخلوط را روشن کنید
Lighten blend شامل مقایسه کانالی مقادیر پیکسل ورودی، انتخاب پیکسل با مقدار (شدت) بالاتر به عنوان پیکسل خروجی است.
// src/operations.rs pub struct LightenBlend ; impl BlendOperation for LightenBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ pixel1[ 0 ].max(pixel2[ 0 ]), pixel1[ 1 ].max(pixel2[ 1 ]), pixel1[ 2 ].max(pixel2[ 2 ]), ] } }
ترکیب تیره
مخلوط تیره عمل مخالف مخلوط روشن است. این شامل مقایسه کانالی مقادیر پیکسل ورودی، انتخاب پیکسل با کمترین مقدار (شدت) به عنوان پیکسل خروجی است.
// src/operations.rs pub struct DarkenBlend ; impl BlendOperation for DarkenBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ pixel1[ 0 ].min(pixel2[ 0 ]), pixel1[ 1 ].min(pixel2[ 1 ]), pixel1[ 2 ].min(pixel2[ 2 ]), ] } }
ترکیب صفحه نمایش
ترکیب صفحه به ضرب معکوس دو تصویر و سپس معکوس کردن نتیجه اشاره دارد. در پیادهسازی ما، ابتدا پیکسلها باید نرمال شوند [¹] . سپس مقادیر نرمال شده [¹] با کم کردن آنها از 1 معکوس می شوند، سپس ضرب می شوند و دوباره معکوس می شوند.
در نهایت، خروجی در 255 ضرب می شود تا مقدار پیکسل خروجی از حالت عادی خارج شود.
// src/operations.rs pub struct ScreenBlend ; impl BlendOperation for ScreenBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ (( 1 . - (( 1 . - (pixel1[ 0 ] as f32 / 255 .)) * ( 1 . - (pixel2[ 0 ] as f32 / 255 .)))) * u8 ::MAX as f32 ) as u8 , (( 1 . - (( 1 . - (pixel1[ 1 ] as f32 / 255 .)) * ( 1 . - (pixel2[ 1 ] as f32 / 255 .)))) * u8 ::MAX as f32 ) as u8 , (( 1 . - (( 1 . - (pixel1[ 2 ] as f32 / 255 .)) * ( 1 . - (pixel2[ 2 ] as f32 / 255 .)))) * u8 ::MAX as f32 ) as u8 , ] } }
ترکیب گفت نی
ترکیب گفت ن شامل اضافه کردن مقادیر ورودی و سپس بستن نتیجه به حداکثر محدوده عمق رنگ مورد نظر ما است. در این مورد، 0-255 خواهد بود زیرا ما عمق رنگ 8 بیتی را هدف قرار می دهیم.
همچنین باید مقادیر را به u16 تبدیل کنیم تا از از دست رفتن ارزش به دلیل سرریز جلوگیری کنیم. همچنین میتوانیم از مقادیر نرمال شده [¹] برای رسیدن به همان نتیجه استفاده کنیم.
// src/operations.rs pub struct AdditionBlend ; impl BlendOperation for AdditionBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ (pixel1[ 0 ] as u16 + pixel2[ 0 ] as u16 ).clamp( 0 , u8 ::MAX as u16 ) as u8 , (pixel1[ 1 ] as u16 + pixel2[ 1 ] as u16 ).clamp( 0 , u8 ::MAX as u16 ) as u8 , (pixel1[ 2 ] as u16 + pixel2[ 2 ] as u16 ).clamp( 0 , u8 ::MAX as u16 ) as u8 , ] } }
ترکیب تفریق
ترکیب گفت ن شامل کم کردن مقادیر ورودی و سپس بستن نتیجه به حداکثر محدوده عمق رنگ مورد نظر ما است. در این مورد، 0-255 خواهد بود زیرا ما عمق رنگ 8 بیتی را هدف قرار می دهیم.
ما همچنین مقادیر را به i16 تبدیل می کنیم تا از کاهش ارزش به دلیل سرریز و عدم علامت جلوگیری کنیم. همچنین میتوانیم از مقادیر نرمال شده [¹] برای رسیدن به همان نتیجه استفاده کنیم.
// src/operations.rs pub struct SubtractionBlend ; impl BlendOperation for SubtractionBlend { fn perform_operation (& self , pixel1: [ u8 ; 3 ], pixel2: [ u8 ; 3 ]) -> [ u8 ; 3 ] { [ (pixel1[ 0 ] as i16 - pixel2[ 0 ] as i16 ).clamp( 0 , u8 ::MAX as i16 ) as u8 , (pixel1[ 1 ] as i16 - pixel2[ 1 ] as i16 ).clamp( 0 , u8 ::MAX as i16 ) as u8 , (pixel1[ 2 ] as i16 - pixel2[ 2 ] as i16 ).clamp( 0 , u8 ::MAX as i16 ) as u8 , ] } }
نحوه اعمال توابع ترکیبی در تصاویر
مرحله آخر این است که در واقع از عملیات ترکیبی که قبلا ایجاد کرده بودیم استفاده کنید و آنها را روی جفت تصاویر اعمال کنید.
برای دستیابی به این هدف، به تابعی نیاز داریم که بتواند نوع SourceData
را که قبلاً تعریف کرده بودیم، به همراه یک عملیات ترکیبی به عنوان آرگومان دریافت کند و بافر خروجی نهایی را به ما بدهد. بیایید با ایجاد یک فایل جدید برای آن به نام blend.rs شروع کنیم.
// src/blend.rs use image::{ImageBuffer, Rgb}; use crate::{operations::BlendOperation, SourceData}; impl SourceData { pub fn blend_images (& self , operation: impl BlendOperation) -> ImageBuffer<Rgb< u8 >, Vec < u8 >> { let SourceData { width, height, image1, image2, } = self ; // Create a new buffer that has the same size as input images, which will serve as our output data let mut buffer = ImageBuffer::new(*width as u32 , *height as u32 ); // Iterate over all pixels in the output buffer, along with their coordinates for (x, y, output_pixel) in buffer.enumerate_pixels_mut() { // Compute linear index form x & y coordinates. In other words, you have the // row and column indexes here, and you want to compute the array index based // on these two positions. let index = (y * *width as u32 + x) as usize ; // Store pixel values in the given position into variables let pixel1 = image1[index]; let pixel2 = image2[index]; // Compute the blended pixel and convert it into the `Rgb` type, which is then // assigned to the output pixel in the buffer. *output_pixel = Rgb::from(operation.perform_operation(pixel1, pixel2)); } buffer } }
قرار دادن آن همه با هم
اکنون زمان آن رسیده است که از همه چیزهای جدیدی که تا کنون آموخته اید استفاده کنید و آنها را در فایل main.rs کنار هم قرار دهید.
// src/main.rs mod blend; mod io; mod operations; use io::*; use operations::{ AdditionBlend, AverageBlend, DarkenBlend, LightenBlend, MultiplyBlend, ScreenBlend, SubtractionBlend, }; fn main () { let source_data = read_pixel_data( "image1.jpg" .to_string(), "image2.jpg" .to_string()); let output_buffer = source_data.blend_images(AdditionBlend); output_buffer.save( "addition.jpg" ).unwrap(); let output_buffer = source_data.blend_images(AverageBlend); output_buffer.save( "average.jpg" ).unwrap(); let output_buffer = source_data.blend_images(DarkenBlend); output_buffer.save( "darken.jpg" ).unwrap(); let output_buffer = source_data.blend_images(LightenBlend); output_buffer.save( "lighten.jpg" ).unwrap(); let output_buffer = source_data.blend_images(MultiplyBlend); output_buffer.save( "multiply.jpg" ).unwrap(); let output_buffer = source_data.blend_images(ScreenBlend); output_buffer.save( "screen.jpg" ).unwrap(); let output_buffer = source_data.blend_images(SubtractionBlend); output_buffer.save( "subtraction.jpg" ).unwrap(); }
اکنون می توانید برنامه را با استفاده از دستور زیر اجرا کنید و باید تمام تصاویر تولید شده و در پوشه پروژه ذخیره شوند:
cargo run --release
همانطور که قبلاً حدس زده اید، این پیاده سازی فقط برای تصاویر RGB 8 بیتی کار می کند. با این حال، این کد را می توان به راحتی برای پشتیبانی از فرمت های رنگی دیگر مانند Luma 8 بیتی (تک رنگ)، 16 بیتی RGB (تصاویر بسیاری از دوربین های RAW) و غیره گسترش داد.
من به شدت شما را تشویق می کنم که آن را امتحان کنید. همچنین می توانید برای کمک در مورد هر چیزی در این آموزش یا گسترش کد در این آموزش با من تماس بگیرید. خوشحال می شوم به تمام سوالات شما پاسخ دهم. ایمیل بهترین راه برای دسترسی به من است، می توانید به من ایمیل بزنید anshul@anshulsanghi.tech .
واژه نامه
نرمال سازی به فرآیند تغییر مقیاس مقادیر پیکسل اشاره دارد به طوری که مقادیر در فرمت ممیز شناور و در محدوده 0-1 قرار دارند. به عنوان مثال، برای یک تصویر 8 بیتی، رنگ سیاه با 0 (0 در مقدار غیر عادی) و رنگ سفید با 1 (255 در مقدار غیر عادی) نشان داده می شود. مقادیر اعشاری میانی بین 0 و 1 نشان دهنده شدت های مختلف پیکسل بین سیاه و سفید است. عادی سازی به دلایل مختلفی انجام می شود مانند:
جلوگیری از سرریز در حین محاسبات.
تغییر مقیاس تصاویر به همان محدوده بدون توجه به عمق رنگ فردی آنها.
گسترش دامنه دینامیکی احتمالی تصویر.
از کار من لذت می برید؟
برای حمایت از کارم یک قهوه برای من بخرید!
تا دفعه بعد، کد نویسی مبارک و آرزوی آسمان صاف برای شما!
ارسال نظر