فاز 4 · درس 1

مبانی تصویر در یادگیری ماشین: پیکسل‌ها، کانال‌ها و فضاهای رنگی

نوع
ساختنی
زمان
حدود 45 دقیقه
زبان‌ها
Python
یک حسگر نوری شبکه‌ای و سه صفحه‌ی فیلتر رنگی قرمز، سبز و آبی به صورت لایه‌لایه در امتداد یکدیگر
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. مفاهیم پایه
  4. نگاهی گذرا به کل پایپ‌لاین پیش‌پردازش
  5. پیکسل یک نمونه است، نه یک مربع
  6. چرا سه کانال؟
  7. دو قرارداد برای چینش ابعاد: HWC و CHW
  8. بازه‌های بایتی و نوع داده (dtype)
  9. فضاهای رنگی و چرایی وجود آن‌ها
  10. نسبت تصویر، تغییر اندازه و درون‌یابی
  11. پیاده‌سازی
  12. مرحله‌ی 1: ساخت تنسور تصویر و بررسی ابعاد آن
  13. مرحله‌ی 2: جداسازی کانال‌ها و بازچینی ابعاد
  14. مرحله‌ی 3: تبدیل به تصویر خاکستری و HSV
  15. مرحله‌ی 4: نرمال‌سازی، استانداردسازی و بازگشت به حالت اول
  16. مرحله‌ی 5: پیاده‌سازی تغییر اندازه از صفر
  17. استفاده‌ی عملی
  18. تحویل پروژه
  19. تمرین‌ها
  20. اصطلاحات کلیدی
  21. برای مطالعه‌ی بیشتر

یکی از خسته‌کننده‌ترین خطاهایی که در توسعه‌ی سیستم‌های بینایی ماشین تجربه می‌کنید، باگ‌های بی‌صدایی هستند که هیچ خطایی چاپ نمی‌کنند، اما دقت مدل را ناگهان به قهقرا می‌برند. کافی است تصویری با فرمت BGR را به مدلی بدهید که روی RGB آموزش دیده، یا ترتیب کانال‌ها را از CHW به HWC اشتباه بفرستید؛ شبکه بدون کرش کردن خروجی اشتباه تولید می‌کند و روزها وقت صرف دیباگ کردن ساختار شبکه می‌شود، در حالی که ریشه‌ی مشکل در همان مرحله‌ی ورودی و لود تصویر بوده است.

درک عمیق این که یک تصویر در حافظه چگونه نگهداری می‌شود، مفهوم دقیق پیکسل به عنوان یک نمونه نوری نه یک مربع، و سازوکار فضاهای رنگی مختلف مثل RGB، HSV و مقادیر خاکستری، سنگ بنای تسلط بر یادگیری عمیق در بینایی ماشین است. در این درس یاد می‌گیرید که چطور پایپ‌لاین پیش‌پردازش ورودی مدل‌های آماده‌ی PyTorch را از پایه با NumPy و ابزارهای استاندارد بسازید و تضمین کنید داده‌ها دقیقاً با همان فرمت مورد انتظار وارد شبکه می‌شوند.

تصویر، تنسوری از نمونه‌های نور است. هر مدل بینایی ماشینی که تا به حال استفاده کرده‌اید یا بعداً به کار خواهید برد، کارش را دقیقاً از همین واقعیت شروع می‌کند.

اهداف یادگیری

  • توضیح اینکه چطور یک صحنه‌ی پیوسته به پیکسل‌ها گسسته‌سازی می‌شود و چرا تصمیم‌های مربوط به نمونه‌برداری و کوانتیزاسیون، سقف کارایی تمام مدل‌های بعدی را تعیین می‌کنند
  • خواندن، برش زدن و بررسی تصاویر در قالب آرایه‌های NumPy و جابه‌جایی مسلط بین چینش‌های HWC و CHW
  • تبدیل فضاهای رنگی RGB ،grayscale ،HSV و YCbCr به یکدیگر و توجیه چرایی وجود هر یک
  • اعمال پیش‌پردازش‌های سطح پیکسل (نرمال‌سازی، استانداردسازی، تغییر اندازه و کانال-اول) دقیقاً به همان شکلی که مدل‌های بینایی ماشین ازپیش‌آموزش‌دیده‌ی PyTorch انتظار دارند

صورت مسئله

هر مقاله‌ای که بخوانید، هر وزن ازپیش‌آموزش‌دیده‌ای که دانلود کنید و هر API بینایی ماشینی که فراخوانی کنید، نوع خاصی از کدگذاری را برای ورودی فرض می‌گیرد. اگر تصویری از نوع uint8 را به مدلی بدهید که ورودی float32 می‌خواهد، کد همچنان اجرا می‌شود — و در سکوت کامل، خروجی پرت‌وپلا تولید می‌کند. اگر تصویر BGR را به شبکه‌ای بدهید که روی RGB آموزش دیده است، دقت مدل 10 درصد افت می‌کند. اگر ورودی با چینش کانال-آخر (channels-last) را به مدلی بدهید که کانال-اول (channels-first) می‌خواهد، نخستین لایه‌ی کانولوشن بعد ارتفاع را به چشم یک کانال ویژگی در نظر می‌گیرد. هیچ‌کدام از این‌ها خطایی نمی‌دهند؛ فقط معیارهای ارزیابی‌تان خراب می‌شوند و باید یک هفته تمام وقت بگذارید تا باگی را پیدا کنید که ریشه‌اش فقط در نحوه‌ی بارگذاری فایل بوده است.

کانولوشن اصلاً عملیات پیچیده‌ای نیست، به شرطی که بدانید قرار است روی چه چیزی بلغزد. بخش دشوار ماجرا اینجاست که مفهوم «تصویر» برای دوربین، دیکودر JPEG، کتابخانه‌ی PIL، کتابخانه‌ی OpenCV، کتابخانه‌ی torchvision و کرنل CUDA تفاوت دارد. هر بخش از این پشته ترتیب محورها، بازه‌ی بایتی و قراردادهای کانالیِ خاص خودش را دارد. مهندس بینایی ماشینی که نتواند این تفاوت‌ها را درست مدیریت کند، پایپ‌لاین‌های معیوب روانه‌ی محیط عملیاتی خواهد کرد.

این درس زیربنای کار را محکم می‌کند تا ادامه‌ی این فاز روی پایه‌های درستی بنا شود. در پایان این درس می‌دانید پیکسل چیست، چرا هر پیکسل به‌جای یک عدد با سه عدد توصیف می‌شود، «نرمال‌سازی با آماره‌های ImageNet» در عمل چه کار می‌کند و چطور بین دو یا سه چینشی جابه‌جا شوید که در تمام درس‌های بعدی این فاز فرض گرفته خواهند شد.

مفاهیم پایه

نگاهی گذرا به کل پایپ‌لاین پیش‌پردازش

تمام سیستم‌های بینایی ماشین در محیط عملیاتی، توالی یکسانی از تبدیل‌های برگشت‌پذیر هستند. اگر یک مرحله را اشتباه بروید، مدل ورودی متفاوتی نسبت به آنچه رویش آموزش دیده می‌بیند.

فایل تصویر(JPEG/PNG)رمزگشاییuint8 HWCتبدیلفضای رنگی(RGB/BGR/YCbCr)تغییر اندازهضلع کوتاه‌تربرش از مرکزاندازه مدلتقسیم بر 255float32 [0,1]تفریق میانگینتقسیم بر انحراف معیارترانهادهHWC → CHWدسته‌بندیCHW → NCHWمدل

دو جعبه‌ی قرمز و آبی بخش‌هایی هستند که 80 درصد خطاهای پنهان در آن‌ها رخ می‌دهد: جا انداختن استانداردسازی و چینش اشتباه.

پیکسل یک نمونه است، نه یک مربع

حسگر دوربین، تعداد فوتون‌هایی را می‌شمارد که روی شبکه‌ای از آشکارسازهای کوچک فرود می‌آیند. هر آشکارساز نور را در کسری از ثانیه تجمیع می‌کند و ولتاژی متناسب با تعداد فوتون‌های برخوردی تولید می‌نماید. حسگر سپس این ولتاژ را به یک عدد صحیح گسسته‌سازی می‌کند. به این ترتیب، هر آشکارساز به یک پیکسل تبدیل می‌شود.

Continuous scene                 Sensor grid                     Digital image
(infinite detail)                (H x W detectors)               (H x W integers)

    ~~~~~                        +--+--+--+--+--+                 210 198 180 155 120
   ~   ~   ~                     |  |  |  |  |  |                 205 195 178 152 118
  ~ light ~      ---->           +--+--+--+--+--+     ---->       200 190 175 150 115
   ~~~~~                         |  |  |  |  |  |                 195 185 170 148 112
                                 +--+--+--+--+--+                 188 180 165 145 108

در این مرحله دو تصمیم گرفته می‌شود که سقف کیفیت تمام مراحل بعدی را مشخص می‌کنند:

  • نمونه‌برداری مکانی (Spatial sampling) تعیین می‌کند در هر درجه از صحنه چه تعداد آشکارساز قرار گیرد. اگر تعداد کم باشد، لبه‌ها دندانه‌دار می‌شوند (الیاسینگ (تداخل فرکانسی) یا همان aliasing). اگر زیاد باشد، فضای ذخیره‌سازی و حجم محاسبات سر به فلک می‌کشد.
  • کوانتیزاسیون شدت نور (Intensity quantization) تعیین می‌کند ولتاژ با چه دقتی در دسته‌های عددی قرار گیرد. کدگذاری 8 بیتی 256 سطح شدت ارائه می‌دهد و برای نمایشگرها استاندارد است. دقت‌های 10، 12 و 16 بیتی گرادیان‌های نرم‌تری ایجاد می‌کنند و در تصویربرداری پزشکی، تصاویر HDR و پایپ‌لاین‌های حسگر خام (raw) اهمیت دارند.

پیکسل یک مربع رنگیِ دارای مساحت نیست؛ بلکه یک مقدار اندازه‌گیری‌شده است. وقتی ابعاد تصویری را تغییر می‌دهید یا آن را می‌چرخانید، در واقع دارید همان شبکه‌ی اندازه‌گیری را بازنمونه‌برداری (resample) می‌کنید.

چرا سه کانال؟

یک آشکارساز، فوتون‌ها را در کل طیف مرئی ثبت می‌کند — که حاصل آن همان تصویر خاکستری (grayscale) است. حسگر برای ثبت رنگ، این شبکه را با موزاییکی از فیلترهای قرمز، سبز و آبی می‌پوشاند. پس از مرحله‌ی درون‌یابی موزاییکی (demosaicing)، هر موقعیت مکانی دارای سه عدد صحیح می‌شود: پاسخ آشکارسازهای مجاور با فیلترهای قرمز، سبز و آبی. این سه عدد صحیح، سه‌گانه‌ی RGB یک پیکسل را می‌سازند.

One pixel in memory:

    (R, G, B) = (210, 140, 30)   <- reddish-orange

An H x W RGB image:

    shape (H, W, 3)     stored as   H rows of W pixels of 3 values
                                    each in [0, 255] for uint8

عدد سه هیچ ویژگی جادویی خاصی ندارد. دوربین‌های عمق‌سنج یک کانال Z هم اضافه می‌کنند. ماهواره‌ها باندهای فروسرخ و فرابنفش دارند. اسکن‌های پزشکی اغلب یک کانال دارند (مثل رادیوگرافی و CT) یا چند کاناله هستند (تصویربرداری ابرطیفی). تعداد کانال‌ها آخرین محور تنسور را تشکیل می‌دهد؛ لایه‌های کانولوشن یاد می‌گیرند که در امتداد این کانال‌ها ترکیب انجام دهند.

دو قرارداد برای چینش ابعاد: HWC و CHW

یک تنسور واحد، اما با دو شیوه‌ی مرتب‌سازی. هر کتابخانه‌ای یکی را انتخاب می‌کند.

HWC (height, width, channels)           CHW (channels, height, width)

   W ->                                    H ->
  +-----+-----+-----+                     +-----+-----+
H |R G B|R G B|R G B|                   C |R R R R R R|
| +-----+-----+-----+                   | +-----+-----+
v |R G B|R G B|R G B|                   v |G G G G G G|
  +-----+-----+-----+                     +-----+-----+
                                          |B B B B B B|
                                          +-----+-----+

   PIL, OpenCV, matplotlib,              PyTorch, most deep learning
   almost every image file on disk       frameworks, cuDNN kernels

چینش CHW به این دلیل به وجود آمد که کرنل‌های کانولوشن در امتداد H و W می‌لغزند. با قرار دادن محور کانال در ابتدا، هر کرنل در هر کانال یک صفحه‌ی دوبعدی پیوسته در حافظه (contiguous) می‌بیند؛ این ویژگی کار برداری‌سازی را تمیزتر و بهینه‌تر می‌کند. فرمت‌های ذخیره‌سازی روی دیسک ساختار HWC را حفظ می‌کنند؛ چون این حالت با نحوه‌ی خروج خطوط روبش (scanlines) از حسگر هم‌خوانی دارد.

تبدیل تک‌خطی زیر را هزاران بار تایپ خواهید کرد:

img_chw = img_hwc.transpose(2, 0, 1)      # NumPy
img_chw = img_hwc.permute(2, 0, 1)        # PyTorch tensor

نمایش بصری چیدمان در حافظه:

CHW — ذخیره کانال‌ها به صورت صفحات روی‌هم (PyTorch, cuDNN)HWC — ذخیره درهم‌پیکسل‌ها (PIL, OpenCV, JPEG)transpose(2, 0, 1)transpose(1, 2, 0)سطر 0: R G B | R G B | R G B ...سطر 1: R G B | R G B | R G B ...سطر 2: R G B | R G B | R G B ...صفحه R: کل مقادیر قرمز H x Wصفحه G: کل مقادیر سبز H x Wصفحه B: کل مقادیر آبی H x W

بازه‌های بایتی و نوع داده (dtype)

سه قرارداد رایج داریم:

قرارداد نوع داده (dtype) بازه کجا دیده می‌شود
خام (Raw) uint8 [0, 255] فایل‌های روی دیسک، خروجی PIL و OpenCV
نرمال‌شده (Normalized) float32 [0.0, 1.0] پس از اجرای img.astype('float32') / 255
استانداردشده (Standardized) float32 تقریباً بین [-2, +2] پس از کسر میانگین و تقسیم بر انحراف معیار

شبکه‌های عصبی کانولوشنی با ورودی‌های استانداردشده آموزش دیده‌اند. آماره‌های ImageNet یعنی mean=[0.485, 0.456, 0.406] و std=[0.229, 0.224, 0.225]، میانگین حسابی و انحراف معیار سه کانال روی کل مجموعه‌ی آموزشی ImageNet هستند که با پیکسل‌های نرمال‌شده در بازه‌ی [0, 1] محاسبه شده‌اند. فرستادن داده‌ی خام uint8 به مدلی که منتظر مقادیر اعشاری استانداردشده است، شایع‌ترین خطای پنهان در پروژه‌های کاربردی بینایی ماشین محسوب می‌شود.

فضاهای رنگی و چرایی وجود آن‌ها

RGB فرمت ثبت تصاویر است، اما لزوماً مفیدترین نمایش داده برای یک مدل نیست.

 RGB               HSV                       YCbCr / YUV

 R red             H hue (angle 0-360)       Y luminance (brightness)
 G green           S saturation (0-1)        Cb chroma blue-yellow
 B blue            V value/brightness (0-1)  Cr chroma red-green

 Linear to         Separates color from      Separates brightness from
 sensor output     brightness. Useful for    color. JPEG and most video
                   color thresholding, UI    codecs compress the chroma
                   sliders, simple filters   channels harder because the
                                             human eye is less sensitive
                                             to chroma detail than to Y.

برای بیشتر CNNهای مدرن ورودی RGB می‌فرستید. در این شرایط با سایر فضاها سروکار خواهید داشت:

  • HSV — کدهای کلاسیک بینایی ماشین، قطعه‌بندی بر پایه‌ی رنگ، تنظیم تعادل سفیدی.
  • YCbCr — خواندن ساختار داخلی JPEG، پایپ‌لاین‌های ویدیو، مدل‌های وضوح‌افزایی (super-resolution) که فقط روی مؤلفه‌ی Y کار می‌کنند.
  • Grayscale — در OCR، مدل‌های تحلیل اسناد و هر جایی که رنگ به‌جای سیگنال، یک متغیر مزاحم باشد.

تبدیل RGB به تصویر خاکستری یک جمع وزن‌دار است نه یک میانگین ساده؛ چون چشم انسان به رنگ سبز حساس‌تر از قرمز یا آبی است:

Y=0.299R+0.587G+0.114B(ITU-R BT.601, the classic weights)Y = 0.299 R + 0.587 G + 0.114 B \qquad \text{(ITU-R BT.601, the classic weights)}

نسبت تصویر، تغییر اندازه و درون‌یابی

هر مدلی ورودی با ابعاد ثابتی می‌گیرد (224x224 در اکثر مدل‌های دسته‌بندی ImageNet و 384x384 یا 512x512 در مدل‌های جدید تشخیص اشیا). تصاویر شما به‌ندرت با این ابعاد هم‌اندازه‌اند. سه گزینه‌ی مهم برای تغییر اندازه وجود دارد:

  • تغییر اندازه‌ی ضلع کوچک‌تر و سپس برش از مرکز (Center crop) — دستورالعمل استاندارد ImageNet. نسبت ابعاد حفظ می‌شود، اما نواری از پیکسل‌های لبه دور ریخته خواهد شد.
  • تغییر اندازه همراه با پدینگ (Resize and pad) — نسبت ابعاد و تمام پیکسل‌ها را حفظ می‌کند، ولی نوارهای سیاهی در حاشیه به جا می‌گذارد. گزینه‌ی استاندارد در سیستم‌های تشخیص شیء و OCR.
  • تغییر اندازه مستقیم به ابعاد هدف — تصویر را می‌کشد. ارزان است، ساختار هندسی را تغییر می‌دهد، ولی برای بسیاری از وظایف دسته‌بندی جواب می‌دهد.

روش درون‌یابی مشخص می‌کند در زمان جابه‌جایی شبکه‌ی پیکسل‌های قدیم و جدید، مقادیر پیکسل‌های میانی چگونه محاسبه شوند:

Nearest neighbour     fastest, blocky, only choice for masks/labels
Bilinear              fast, smooth, default for most image resizing
Bicubic               slower, sharper on upscaling
Lanczos               slowest, best quality, used for final display

یک قاعده‌ی تجربی: Bilinear برای آموزش، Bicubic یا Lanczos برای فایل‌هایی که قرار است انسان به آن‌ها نگاه کند، و Nearest برای هر داده‌ای که شامل شناسه‌های عددی صحیح کلاس‌ها باشد.

اندازه خروجی کانولوشنW، K، S، P را بکشید
CONV OUTPUT SIZEThe spatial size out of a convolution follows one formula: floor((W − K + 2P) / S) + 1. Padding P adds border so the kernel can reach the edges and keep size; stride S skips positions and shrinks the output. "Same" padding picks P so the output matches the input; "valid" padding uses P = 0 and shrinks by K − 1.padded input = 34 (grey = padding, box = kernel)output positions = 32
اندازه مکانی خروجی از یک کانولوشن از فرمول روبه‌رو پیروی می‌کند: floor((W − K + 2P) / S) + 1. لایه‌گذاری P لبه‌هایی اضافه می‌کند تا کرنل بتواند به کناره‌ها برسد و اندازه را حفظ کند؛ گام (stride) S از روی موقعیت‌ها رد می‌شود و خروجی را کوچک می‌کند. لایه‌گذاری «same» مقدار P را طوری انتخاب می‌کند که خروجی با ورودی هم‌اندازه شود؛ لایه‌گذاری «valid» از P = 0 استفاده می‌کند و اندازه را به میزان K − 1 کوچک می‌نماید.

پیاده‌سازی

مرحله‌ی 1: ساخت تنسور تصویر و بررسی ابعاد آن

کار را با یک تصویر مصنوعی قطعی شروع می‌کنیم تا این بخش اول بدون نیاز به اینترنت و صرفاً با NumPy اجرا شود. دیکود کردن فایل یک مرز مجزاست: به‌محض اینکه دیکودر JPEG یا PNG بایت‌های RGB را برگرداند، تمامی عملیات‌های تنسوری بعدی یکسان خواهند بود.

import numpy as np

def synthetic_rgb(h=128, w=192, seed=0):
    rng = np.random.default_rng(seed)
    yy, xx = np.meshgrid(np.linspace(0, 1, h), np.linspace(0, 1, w), indexing="ij")
    r = (np.sin(xx * 6) * 0.5 + 0.5) * 255
    g = yy * 255
    b = (1 - yy) * xx * 255
    rgb = np.stack([r, g, b], axis=-1) + rng.normal(0, 6, (h, w, 3))
    return np.clip(rgb, 0, 255).astype(np.uint8)

arr = synthetic_rgb()

print(f"type:   {type(arr).__name__}")
print(f"dtype:  {arr.dtype}")
print(f"shape:  {arr.shape}     # (H, W, C)")
print(f"min:    {arr.min()}")
print(f"max:    {arr.max()}")
print(f"pixel at (0, 0): {arr[0, 0]}")

خروجی مورد انتظار: shape: (H, W, 3)، نوع داده‌ی dtype: uint8، بازه‌ی [0, 255]. این همان نمایش متعارف و دیکودشده است؛ فرقی ندارد که این بایت‌ها از دوربین خوانده شده باشند، از یک فایل تصویر یا همین تولیدکننده‌ی مصنوعی.

مرحله‌ی 2: جداسازی کانال‌ها و بازچینی ابعاد

کانال‌های R ،G و B را جداگانه بیرون می‌کشیم و سپس چینش HWC را برای استفاده در PyTorch به CHW تغییر می‌دهیم.

R = arr[:, :, 0]
G = arr[:, :, 1]
B = arr[:, :, 2]
print(f"R shape: {R.shape}, mean: {R.mean():.1f}")
print(f"G shape: {G.shape}, mean: {G.mean():.1f}")
print(f"B shape: {B.shape}, mean: {B.mean():.1f}")

arr_chw = arr.transpose(2, 0, 1)
print(f"\nHWC shape: {arr.shape}")
print(f"CHW shape: {arr_chw.shape}")

نتیجه سه صفحه‌ی خاکستری است، یکی برای هر کانال. چینش CHW فقط ترتیب محورها را عوض می‌کند؛ تا وقتی ساختار حافظه اجازه دهد، اصلاً نیازی به کپی کردن داده‌ها نیست.

مرحله‌ی 3: تبدیل به تصویر خاکستری و HSV

ابتدا تبدیل به تصویر خاکستری با جمع وزن‌دار و سپس پیاده‌سازی دستی RGB به HSV.

def rgb_to_grayscale(rgb):
    weights = np.array([0.299, 0.587, 0.114], dtype=np.float32)
    return (rgb.astype(np.float32) @ weights).astype(np.uint8)

def rgb_to_hsv(rgb):
    rgb_f = rgb.astype(np.float32) / 255.0
    r, g, b = rgb_f[..., 0], rgb_f[..., 1], rgb_f[..., 2]
    cmax = np.max(rgb_f, axis=-1)
    cmin = np.min(rgb_f, axis=-1)
    delta = cmax - cmin

    h = np.zeros_like(cmax)
    mask = delta > 0
    argmax = np.argmax(rgb_f, axis=-1)
    rmax = mask & (argmax == 0)
    gmax = mask & (argmax == 1)
    bmax = mask & (argmax == 2)
    h[rmax] = ((g[rmax] - b[rmax]) / delta[rmax]) % 6
    h[gmax] = ((b[gmax] - r[gmax]) / delta[gmax]) + 2
    h[bmax] = ((r[bmax] - g[bmax]) / delta[bmax]) + 4
    h = h * 60.0

    s = np.divide(delta, cmax, out=np.zeros_like(delta), where=cmax > 0)
    v = cmax
    return np.stack([h, s, v], axis=-1)

gray = rgb_to_grayscale(arr)
hsv = rgb_to_hsv(arr)
print(f"gray shape: {gray.shape}, range: [{gray.min()}, {gray.max()}]")
print(f"hsv   shape: {hsv.shape}")
print(f"hue range: [{hsv[..., 0].min():.1f}, {hsv[..., 0].max():.1f}] degrees")
print(f"sat range: [{hsv[..., 1].min():.2f}, {hsv[..., 1].max():.2f}]")
print(f"val range: [{hsv[..., 2].min():.2f}, {hsv[..., 2].max():.2f}]")

فام (Hue) بر حسب درجه و اشباع (Saturation) و روشنایی (Value) در بازه‌ی [0, 1] به دست می‌آیند. این خروجی با قرارداد hsv_full در OpenCV هماهنگ است.

مرحله‌ی 4: نرمال‌سازی، استانداردسازی و بازگشت به حالت اول

تبدیل بایت‌های خام به همان تنسوری که یک مدل ImageNet ازپیش‌آموزش‌دیده نیاز دارد، و سپس معکوس کردن آن فرایند.

mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)

def preprocess_imagenet(rgb_uint8):
    x = rgb_uint8.astype(np.float32) / 255.0
    x = (x - mean) / std
    x = x.transpose(2, 0, 1)
    return x

def deprocess_imagenet(chw_float32):
    x = chw_float32.transpose(1, 2, 0)
    x = x * std + mean
    x = np.clip(x * 255.0, 0, 255).astype(np.uint8)
    return x

x = preprocess_imagenet(arr)
print(f"preprocessed shape: {x.shape}     # (C, H, W)")
print(f"preprocessed dtype: {x.dtype}")
print(f"preprocessed mean per channel:  {x.mean(axis=(1, 2)).round(3)}")
print(f"preprocessed std  per channel:  {x.std(axis=(1, 2)).round(3)}")

roundtrip = deprocess_imagenet(x)
max_diff = np.abs(roundtrip.astype(int) - arr.astype(int)).max()
print(f"roundtrip max pixel diff: {max_diff}    # should be 0 or 1")

میانگین هر کانال باید نزدیک به صفر و انحراف معیار آن نزدیک به یک باشد. جفت‌توابع پیش‌پردازش و بازگشت دقیقاً همان کاری را انجام می‌دهند که در پس‌زمینه‌ی هر فراخوانی transforms.Normalize در torchvision رخ می‌دهد.

مرحله‌ی 5: پیاده‌سازی تغییر اندازه از صفر

روش نزدیک‌ترین همسایه (Nearest neighbor) هر مختصات خروجی را به یک پیکسل مبدأ نسبت می‌دهد. درون‌یابی Bilinear چهار پیکسل اطراف را پیدا می‌کند و آن‌ها را بر اساس فاصله با هم ترکیب می‌نماید. هر دو پیاده‌سازی زیر از مختصات تراز با اندپوینت (endpoint-aligned) استفاده می‌کنند تا اولین و آخرین پیکسل مبدأ ثابت بمانند.

def resize_coordinates(source_length, target_length):
    if target_length == 1:
        return np.zeros(1, dtype=np.float32)
    return np.linspace(0, source_length - 1, target_length, dtype=np.float32)

def nearest_resize(image, target_height, target_width):
    y = np.rint(resize_coordinates(image.shape[0], target_height)).astype(int)
    x = np.rint(resize_coordinates(image.shape[1], target_width)).astype(int)
    return image[y[:, None], x[None, :]]

def bilinear_resize(image, target_height, target_width):
    y = resize_coordinates(image.shape[0], target_height)
    x = resize_coordinates(image.shape[1], target_width)
    y0 = np.floor(y).astype(int)
    x0 = np.floor(x).astype(int)
    y1 = np.minimum(y0 + 1, image.shape[0] - 1)
    x1 = np.minimum(x0 + 1, image.shape[1] - 1)
    wy = (y - y0)[:, None, None]
    wx = (x - x0)[None, :, None]

    source = image.astype(np.float32)
    top = source[y0[:, None], x0[None, :]] * (1 - wx)
    top += source[y0[:, None], x1[None, :]] * wx
    bottom = source[y1[:, None], x0[None, :]] * (1 - wx)
    bottom += source[y1[:, None], x1[None, :]] * wx
    result = top * (1 - wy) + bottom * wy
    return np.clip(np.rint(result), 0, 255).astype(image.dtype)

target_height = arr.shape[0] * 3
target_width = arr.shape[1] * 3
nearest = nearest_resize(arr, target_height, target_width)
bilinear = bilinear_resize(arr, target_height, target_width)

def local_roughness(x):
    gy = np.diff(x.astype(float), axis=0)
    gx = np.diff(x.astype(float), axis=1)
    return float(np.abs(gy).mean() + np.abs(gx).mean())

for name, out in [("nearest", nearest), ("bilinear", bilinear)]:
    print(f"{name:>8}  shape={out.shape}  roughness={local_roughness(out):6.2f}")

روش Nearest بیشترین زبری را دارد، چون لبه‌های تیز را حفظ می‌کند. روش Bilinear نرم‌تر است، چون هر پیکسل جدید دو موقعیت را روی هر محور با هم ترکیب می‌کند. کد تکمیلی قابل اجرا همین ایده‌ی تفکیک‌پذیر را با کرنل مکعبی Catmull-Rom به چهار همسایه در هر بعد تعمیم می‌دهد و هر سه نتیجه را بدون نیاز به هیچ کتابخانه‌ی تصویری چاپ می‌کند.

استفاده‌ی عملی

فریم‌ورک PyTorch همین عملیات‌ها را روی تنسورهای دسته‌ای (batched) و با آگاهی از دستگاه پردازشی انجام می‌دهد. کد زیر اندازه‌ی ضلع کوچک‌تر را تغییر می‌دهد، یک برش از مرکز می‌گیرد، هر کانال را استانداردسازی می‌کند و همان تنسور NCHW مد نظر مدل‌های ازپیش‌آموزش‌دیده را می‌سازد.

import torch
import torch.nn.functional as F

image_hwc = torch.from_numpy(synthetic_rgb(256, 320))
batch = image_hwc.permute(2, 0, 1).unsqueeze(0).float() / 255.0

height, width = batch.shape[-2:]
scale = 256 / min(height, width)
resized_height = round(height * scale)
resized_width = round(width * scale)
batch = F.interpolate(
    batch,
    size=(resized_height, resized_width),
    mode="bilinear",
    align_corners=False,
    antialias=True,
)

top = (resized_height - 224) // 2
left = (resized_width - 224) // 2
batch = batch[:, :, top:top + 224, left:left + 224]

mean = torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1)
std = torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1)
batch = (batch - mean) / std

print(f"tensor dtype: {batch.dtype}")
print(f"batched shape: {tuple(batch.shape)}")
print(f"per-channel mean: {batch.mean(dim=(0, 2, 3)).tolist()}")
print(f"per-channel std:  {batch.std(dim=(0, 2, 3)).tolist()}")

چهار مرحله، با همین ترتیب دقیق: تبدیل بایت‌ها به مقادیر اعشاری و جابه‌جایی HWC به NCHW، تغییر اندازه‌ی ضلع کوچک‌تر به 256، گرفتن برش 224x224 از مرکز، و در نهایت کسر میانگین ImageNet و تقسیم بر انحراف معیار آن. جابه‌جا کردن این ترتیب، خروجی نهایی مدل را در سکوت تغییر خواهد داد.

تحویل پروژه

این درس خروجی‌های زیر را به همراه دارد:

  • outputs/prompt-vision-preprocessing-audit.md — پرامپتی که شناسنامه‌ی هر مدل یا دیتاست را به یک چک‌لیست دقیق از شروط تغییرناپذیر پیش‌پردازش تبدیل می‌کند تا تیم به آن‌ها پایبند بماند.
  • outputs/skill-image-tensor-inspector.md — مهارتی که با دریافت هر تنسور یا آرایه‌ی تصویری، نوع داده، چینش، بازه‌ی عددی و وضعیت آن را از نظر خام، نرمال یا استاندارد بودن گزارش می‌کند.

تمرین‌ها

  1. (ساده) یک آرایه‌ی uint8 با ابعاد 2x2 به فرمت RGB با چهار رنگ متمایز بسازید. چینش HWC را به CHW و سپس برعکس تبدیل کنید، ابعاد هر دو را چاپ کنید و نشان دهید داده‌ها در مسیر رفت و برگشت کاملاً حفظ شده‌اند.
  2. (متوسط) تابع standardize(img, mean, std) و معکوس آن را طوری بنویسید که هر دو در کنار هم تست roundtrip_max_diff <= 1 را روی هر تصویر uint8 پاس کنند. این توابع باید با فراخوانی یکسان، هم روی یک تصویر منفرد در HWC و هم روی یک دسته در NCHW کار کنند.
  3. (سخت) یک تنسور 3 کاناله‌ی استانداردشده با ImageNet بردارید و آن را از یک کانولوشن 1x1 عبور دهید که ترکیب وزن‌دار RGB را به یک کانال تصویر خاکستری تبدیل کند. وزن‌ها را روی مقادیر [0.299, 0.587, 0.114] مقداردهی اولیه کرده و قفل (freeze) کنید، و بررسی نمایید که خروجی تا حد خطای محاسبات ممیز شناور با خروجی تابع دستی rgb_to_grayscale شما تطابق دارد. چه تبدیلات فضای رنگی کلاسیک دیگری را می‌توان به شکل کانولوشن 1x1 نوشت؟

اصطلاحات کلیدی

اصطلاح در گفتار عامیانه معنای واقعی آن
پیکسل (Pixel) «یک مربع رنگی» یک نمونه از شدت نور در یک نقطه از شبکه — سه عدد برای تصویر رنگی، یک عدد برای تصویر خاکستری
کانال (Channel) «همان رنگ» یکی از شبکه‌های مکانی موازی که در یک تنسور تصویر روی هم چیده شده‌اند؛ آخرین بعد در HWC، اولین بعد در CHW
HWC / CHW «شکل یا ابعاد» ترتیب محورها در تنسور تصویر؛ دیسک و PIL از HWC استفاده می‌کنند، PyTorch و cuDNN از CHW
نرمال‌سازی (Normalize) «تنظیم مقیاس تصویر» تقسیم بر 255 برای قرار گرفتن مقادیر در بازه‌ی [0, 1] — لازم است اما کافی نیست
استانداردسازی (Standardize) «صفر کردن میانگین» کسر میانگین و تقسیم بر انحراف معیار برای هر کانال، به‌گونه‌ای که توزیع ورودی با توزیع داده‌های آموزش مدل مطابقت داشته باشد
تبدیل به تصویر خاکستری (Grayscale conversion) «میانگین‌گیری از کانال‌ها» یک جمع وزن‌دار با ضرایب 0.299/0.587/0.114 که با ادراک چشم انسان از شدت نور هم‌خوانی دارد
درون‌یابی (Interpolation) «نحوه‌ی انتخاب پیکسل‌ها در تغییر اندازه» قاعده‌ای که مقادیر خروجی را در شرایطی که شبکه‌ی جدید با شبکه‌ی قدیم هم‌تراز نیست مشخص می‌کند — Nearest برای برچسب‌ها، Bilinear برای آموزش و Bicubic برای نمایش
نسبت تصویر (Aspect ratio) «عرض به ارتفاع» نسبتی که «تغییر اندازه و پدینگ» را از «تغییر اندازه و کشیدگی» متمایز می‌کند

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسفایلی روی دیسک به یک آرایه NumPy با ابعاد (224, 224, 3) و نوع داده uint8 دیکود می‌شود. هر عدد نشان‌دهنده چیست؟
  2. پیش از درستصویری را با Pillow بارگذاری کرده و به آرایه‌ای با ابعاد (480, 640, 3) می‌رسید. آن را به‌صورت یک تنسور دسته‌ای (1, 480, 640, 3) به یک لایه Conv2d پای‌تورچ با in_channels=3 می‌فرستید. چه اتفاقی می‌افتد؟
  3. پس از درسچرا مدل‌های از پیش آموزش‌دیده ImageNet انتظار ورودی‌های استانداردسازی‌شده با mean=[0.485, 0.456, 0.406] و std=[0.229, 0.224, 0.225] را دارند؟
  4. پس از درساندازه یک ماسک بخش‌بندی (شناسه‌های کلاس صحیح 0..20) را از 500x500 به 224x224 تغییر می‌دهید. کدام روش درون‌یابی صحیح است؟
  5. پس از درستبدیل RGB به تصویر خاکستری (grayscale) از وزن‌های 0.299 R + 0.587 G + 0.114 B استفاده می‌کند نه 0.333 R + 0.333 G + 0.333 B. چرا؟

سؤالات متداول

چرا مدل‌های PyTorch از چینش CHW به جای HWC استفاده می‌کنند؟

چون کرنل‌های کانولوشن روی محورهای ارتفاع و عرض می‌لغزند. با قرار گرفتن بعد کانال در ابتدا، هر کرنل به یک صفحه‌ی دوبعدی پیوسته در حافظه دسترسی دارد که باعث برداری‌سازی سریع‌تر محاسبات در GPU و پردازنده می‌شود.

تفاوت نرمال‌سازی با استانداردسازی در تصاویر چیست؟

نرمال‌سازی مقادیر بایت‌ها (0 تا 255) را به بازه‌ی [0, 1] می‌برد. استانداردسازی سپس با کسر میانگین و تقسیم بر انحراف معیار هر کانال، توزیع داده‌ها را حول صفر و با واریانس واحد تنظیم می‌کند تا منطبق بر شیوه‌ی آموزش مدل شود.

چرا در تبدیل RGB به تصویر خاکستری نباید میانگین ساده گرفت؟

چشم انسان به نور سبز حساس‌تر از قرمز و آبی است؛ به همین دلیل از جمع وزن‌دار ضرایب (0.299 برای قرمز، 0.587 برای سبز و 0.114 برای آبی) استفاده می‌شود تا روشنایی ادراکی حفظ گردد.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.