پردازش تصویر در OpenCV
هیستوگرامها - ۱ : پیدا کردن، رسم و تحلیل!
هدف#
یاد بگیرید:
- هیستوگرامها را با استفاده از توابع OpenCV و Numpy پیدا کنید
- هیستوگرامها را با استفاده از توابع OpenCV و Matplotlib رسم کنید
- با این توابع آشنا خواهید شد: cv.calcHist()، np.histogram() و غیره.
تئوری#
هیستوگرام (Histogram) چیست؟ میتوانید هیستوگرام را به عنوان یک نمودار یا گراف در نظر بگیرید که دیدگاه کلی درباره توزیع شدت (Intensity Distribution) یک تصویر به شما میدهد. این یک نمودار است که مقادیر پیکسل (از ۰ تا ۲۵۵، نه همیشه) روی محور X و تعداد متناظر پیکسلها در تصویر روی محور Y قرار دارد.
این روشی دیگر برای درک تصویر است. با نگاه به هیستوگرام یک تصویر، درکی از کنتراست، روشنایی، توزیع شدت و غیره از آن تصویر به دست میآورید. تقریباً تمام ابزارهای پردازش تصویر امروزی ویژگیهایی برای هیستوگرام ارائه میدهند. در زیر تصویری از وبسایت Cambridge in Color آمده است و توصیه میکنم برای جزئیات بیشتر از آن سایت بازدید کنید.

میتوانید تصویر و هیستوگرام آن را ببینید. (به خاطر داشته باشید که این هیستوگرام برای تصویر در مقیاس خاکستری رسم شده، نه تصویر رنگی.) ناحیه سمت چپ هیستوگرام مقدار پیکسلهای تاریکتر در تصویر و ناحیه سمت راست مقدار پیکسلهای روشنتر را نشان میدهد. از هیستوگرام میتوانید ببینید که ناحیه تاریک بیشتر از ناحیه روشن است و مقدار تنهای میانی (مقادیر پیکسل در محدوده میانی، مثلاً حدود ۱۲۷) بسیار کم است.
یافتن هیستوگرام#
اکنون که دیدگاهی درباره هیستوگرام داریم، میتوانیم بررسی کنیم که چگونه آن را پیدا کنیم. هم OpenCV و هم Numpy دارای توابع داخلی برای این کار هستند. قبل از استفاده از این توابع، باید برخی اصطلاحات مرتبط با هیستوگرامها را بدانیم.
BINS: هیستوگرام بالا تعداد پیکسلها برای هر مقدار پیکسل را نشان میدهد، یعنی از ۰ تا ۲۵۵. به این ترتیب به ۲۵۶ مقدار برای نمایش هیستوگرام بالا نیاز دارید. اما اگر لازم نباشد تعداد پیکسلها را برای هر مقدار پیکسل به طور جداگانه پیدا کنید، بلکه تعداد پیکسلها را در یک فاصله از مقادیر پیکسل بخواهید؟ مثلاً اگر بخواهید تعداد پیکسلهای بین ۰ تا ۱۵، سپس ۱۶ تا ۳۱، ...، ۲۴۰ تا ۲۵۵ را پیدا کنید. فقط به ۱۶ مقدار برای نمایش هیستوگرام نیاز خواهید داشت. و این همان چیزی است که در مثال آموزشهای OpenCV درباره هیستوگرامها نشان داده میشود.
پس آنچه انجام میدهید این است که کل هیستوگرام را به ۱۶ قسمت فرعی تقسیم میکنید و مقدار هر قسمت فرعی مجموع تمام تعداد پیکسلهای درون آن است. این هر قسمت فرعی «BIN» نام دارد. در حالت اول، تعداد binها ۲۵۶ بود (یکی برای هر پیکسل) در حالی که در حالت دوم، فقط ۱۶ است. BINS با اصطلاح histSize در مستندات OpenCV نمایش داده میشود.
DIMS: تعداد پارامترهایی است که برای آنها داده جمعآوری میکنیم. در این مورد، دادههایی را فقط درباره یک چیز جمعآوری میکنیم: مقدار شدت (intensity value). پس اینجا برابر ۱ است.
RANGE: محدوده مقادیر شدتی است که میخواهید اندازهگیری کنید. معمولاً [0,256] است، یعنی تمام مقادیر شدت.
۱. محاسبه هیستوگرام در OpenCV#
اکنون از تابع cv.calcHist() برای یافتن هیستوگرام استفاده میکنیم. بیایید با تابع و پارامترهای آن آشنا شویم:
cv.calcHist(images, channels, mask, histSize, ranges[, hist[, accumulate]])
- images: تصویر منبع از نوع uint8 یا float32 است. باید در براکتهای مربع داده شود، یعنی "[img]".
- channels: همچنین در براکتهای مربع داده میشود. اندیس کانالی است که هیستوگرام را برای آن محاسبه میکنیم. برای مثال، اگر ورودی تصویر در مقیاس خاکستری باشد، مقدار آن [0] است. برای تصویر رنگی، میتوانید [0]، [1] یا [2] را برای محاسبه هیستوگرام به ترتیب از کانالهای آبی، سبز یا قرمز ارسال کنید.
- mask: تصویر ماسک. برای یافتن هیستوگرام تصویر کامل، به عنوان "None" داده میشود. اما اگر بخواهید هیستوگرام ناحیه خاصی از تصویر را پیدا کنید، باید یک تصویر ماسک برای آن ناحیه ایجاد کنید و به عنوان ماسک بدهید. (بعداً یک مثال نشان خواهم داد.)
- histSize: این نمایانگر تعداد BIN ما است. باید در براکتهای مربع داده شود. برای مقیاس کامل، [256] را ارسال میکنیم.
- ranges: این RANGE ما است. معمولاً [0,256] است.
پس بیایید با یک تصویر نمونه شروع کنیم. به سادگی یک تصویر را در حالت مقیاس خاکستری بارگذاری کرده و هیستوگرام کامل آن را پیدا کنیم.
img = cv.imread('home.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
hist = cv.calcHist([img],[0],None,[256],[0,256])
hist یک آرایه 256x1 است که هر مقدار متناظر با تعداد پیکسلهای آن تصویر با مقدار پیکسل متناظر آن است.
۲. محاسبه هیستوگرام در Numpy#
Numpy نیز تابعی به نام np.histogram() ارائه میدهد. پس به جای تابع calcHist()، میتوانید خط زیر را امتحان کنید:
hist,bins = np.histogram(img.ravel(),256,[0,256])
hist مانند چیزی است که قبلاً محاسبه کردیم. اما bins دارای ۲۵۷ عنصر خواهد بود، زیرا Numpy بازهها را به صورت 0-0.99، 1-1.99، 2-2.99 و غیره محاسبه میکند. پس محدوده نهایی 255-255.99 خواهد بود. برای نمایش آن، ۲۵۶ را نیز در انتهای bins اضافه میکنند. اما به آن ۲۵۶ نیازی نداریم. تا ۲۵۵ کافی است.
حالا باید هیستوگرامها را رسم کنیم، اما چطور؟
رسم هیستوگرامها#
دو روش برای این کار وجود دارد: 1. روش کوتاه: استفاده از توابع رسم Matplotlib 1. روش طولانی: استفاده از توابع رسم OpenCV
۱. استفاده از Matplotlib#
Matplotlib دارای یک تابع رسم هیستوگرام است: matplotlib.pyplot.hist()
این تابع مستقیماً هیستوگرام را پیدا کرده و رسم میکند. نیازی به استفاده از calcHist() یا np.histogram() برای یافتن هیستوگرام ندارید. کد زیر را ببینید:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
plt.hist(img.ravel(),256,[0,256]); plt.show()
نموداری مانند زیر خواهید دید:

یا میتوانید از نمودار معمولی matplotlib استفاده کنید که برای رسم BGR مناسبتر است. برای این کار، ابتدا باید دادههای هیستوگرام را پیدا کنید. کد زیر را امتحان کنید:
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread('home.jpg')
assert img is not None, "file could not be read, check with os.path.exists()"
color = ('b','g','r')
for i,col in enumerate(color):
histr = cv.calcHist([img],[i],None,[256],[0,256])
plt.plot(histr,color = col)
plt.xlim([0,256])
plt.show()
نتیجه:

میتوانید از نمودار بالا استنتاج کنید که آبی در برخی نواحی مقادیر بالایی در تصویر دارد (این به وضوح به دلیل آسمان است).
۲. استفاده از OpenCV#
در این روش، مقادیر هیستوگرام را همراه با مقادیر bin به صورت مختصات x,y تنظیم میکنید تا بتوانید آن را با استفاده از تابع cv.line() یا cv.polyline() رسم کنید تا تصویری مانند تصویر بالا تولید شود. این کار از قبل در نمونههای رسمی OpenCV-Python موجود است. کد را در samples/python/hist.py بررسی کنید.
اعمال ماسک#
از cv.calcHist() برای یافتن هیستوگرام تصویر کامل استفاده کردیم. اگر بخواهید هیستوگرام برخی نواحی تصویر را پیدا کنید چه؟ فقط یک تصویر ماسک با رنگ سفید روی ناحیهای که میخواهید هیستوگرام آن را پیدا کنید و سیاه در سایر نواحی ایجاد کنید. سپس آن را به عنوان ماسک ارسال کنید.
img = cv.imread('home.jpg', cv.IMREAD_GRAYSCALE)
assert img is not None, "file could not be read, check with os.path.exists()"
# create a mask
mask = np.zeros(img.shape[:2], np.uint8)
mask[100:300, 100:400] = 255
masked_img = cv.bitwise_and(img,img,mask = mask)
# Calculate histogram with mask and without mask
# Check third argument for mask
hist_full = cv.calcHist([img],[0],None,[256],[0,256])
hist_mask = cv.calcHist([img],[0],mask,[256],[0,256])
plt.subplot(221), plt.imshow(img, 'gray')
plt.subplot(222), plt.imshow(mask,'gray')
plt.subplot(223), plt.imshow(masked_img, 'gray')
plt.subplot(224), plt.plot(hist_full), plt.plot(hist_mask)
plt.xlim([0,256])
plt.show()
نتیجه را ببینید. در نمودار هیستوگرام، خط آبی هیستوگرام تصویر کامل و خط سبز هیستوگرام ناحیه ماسکشده را نشان میدهد.
