שימו לב: על מנת להריץ את התאים ב-Live Code, יש לייבא תחילה את ספרית numpy ע”י הרצת השורת הבאה:

import numpy as np
import imageio
import matplotlib.pyplot as plt

עיבוד תמונה#

בחלק זה נכיר יכולות נוספות של ספריית numpy - מיסוך (masking) פעולות לאורך ציר של מטריצה. נדגים פעולות אלו על-ידי שימושן בעיבוד תמונות. נראה כיצד מותחים תמונה, מכהים אותה, ומוסיפים לה רעש.

סינון מערך באמצעות פעולת מיסוך (Masking)#

Mask (מסכה) היא מערך בוליאני שמשמש כדי לבחור אילו איברים ממערך אחר (בעל אותם ממדים) ייכללו בפעולה מסוימת, ואילו יישארו מוסתרים או יתעלמו מהם.

אפשר לחשוב על מערך זה כמו מסננת או שכבה אטומה עם חורים : החורים חושפים חלק מהמידע, בעוד שאר האזורים מכוסים.

נניח שיש לנו וקטור s = np.arange(5) המייצג סדרה בת חמישה איברים. ניצור מסכה באותו אורך, m = np.arange(5) % 2 == 0. זהו מערך בוליאני באורך 5. המקומות בהם הערך הוא True מציינים אילו איברים יש לכלול בחישוב, ו־False מציינים על אילו איברים לדלג.

כדי להפעיל את המסכה, נשתמש בתחביר s[m]. אפשר להשתמש בגישה הזו לא רק כדי לשלוף איברים מסוימים, אלא גם כדי לבצע עליהם פעולות — לדוגמא, להגדיל רק את האיברים הזוגיים או לאפס איברים שאינם עומדים בתנאי מסוים.

למטה נבחן כמה דוגמאות נוספות.

נתחיל מהגדרת מערך מסדרה כלשהי:

a = np.arange(15)
print(a)
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14]

כעת נבנה לו Mask בעל אותם מימדים:

mask = (a%3 == 0)
print(mask)
[ True False False  True False False  True False False  True False False
  True False False]

באמצעות הMask, ניתן להחזיר את האיברים שבמיקומם יש True בMask:

print(a[mask])
[ 0  3  6  9 12]

בנוסף, ניתן לבצע פעולות רק על איברים שעברו את הסינון.
בדוגמא להלן נהפוך רק את האיברים שמתחלקים ב-3 לשליליים:

a[mask] *= -1
print(a)
[  0   1   2  -3   4   5  -6   7   8  -9  10  11 -12  13  14]

בעיבוד תמונה יש לMasking שימושים רבים מכיוון שניתן באמצעותו לעשות פעולות על איזורים מסוימים בתמונה.

בדוגמא להלן, הפונקציה segment_image מקבלת תמונה im_mat בגווני אפור וערך סף מסוים th, ויוצרת תמונה שחור לבן ע”י הפעלת סף (th) על ערכי הבהירות הפיקסלים.

def segment_image(im_mat, th):
    new_mat = np.zeros(im_mat.shape)
    new_mat[im_mat >= th] = 255
    return new_mat

ננתח יחד את הפעולות המתבצעות בכל שלב בפונקציה:

  1. בשורה הראשונה הפונקציה מאתחלת מטריצה חדשה של אפסים (כלומר, מטריצה שונה) בעלת אותו גודל כמו המטריצה המקורית

  2. בשורה השניה מתבצע Masking על גבי המטריצה השחורה לפי המיקומים בהם הבהירות בתמונת הקלט הייתה גבוהה מth. פעולת Masking זו משתמשת להשמת הערך 255 במילים פשוטות, בשורה השניה אנו שמים במטריצה השחורה 255 בכל מקום בו הערך במטריצה המקורית גבוה מth

  3. לבסוף, אנו מחזירים את מטריצה החדשה שיצרנו

im_dog = imageio.v3.imread('files/dog.png')
new_image= segment_image(im_dog, 125)
print(new_image)
[[  0.   0.   0. ...   0.   0.   0.]
 [  0.   0.   0. ...   0.   0.   0.]
 [  0.   0.   0. ...   0.   0.   0.]
 ...
 [255. 255. 255. ...   0.   0.   0.]
 [255. 255. 255. ...   0.   0.   0.]
 [255. 255. 255. ...   0.   0.   0.]]
plt.figure(figsize=(6, 4))
plt.subplot(1, 2, 1)
plt.imshow(new_image, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(im_dog, cmap=plt.cm.gray)
plt.show()
_images/c4ea080e21d91e878ed89dee9cc9c1855a2df8059c87ccb71c012a3377a61ca8.png

ביצוע פעולות על מימד/ציר (axis) אחד של המטריצה#

ביחידות הקודמות ראינו את מתודת sum, הסוכמת את איברי המטריצה. לדוגמא:

a = np.array([[4, 2 , 5], [1, 3, 1]])
print(a)
[[4 2 5]
 [1 3 1]]
print(a.sum())
16

כעת, נניח שנרצה לבצע פעולה זו על מימד אחד של המטריצה.
לדוגמא, במקום לסכום את כלל איברי המטריצה, נרצה לסכום את השורות או את העמודות בלבד.

על מנת לעשות זאת, נוכל להשתמש בפרמטר axis במתודה sum, המייצג את הציר עליו יש המטריצה. עבור מטריצה דו מימדית קיימים 2 ערכים אפשריים: 0 לשורות ו-1 לעמודות

print("axis=0: ", a.sum(axis=0))
print("axis=1: ", a.sum(axis=1))
axis=0:  [5 5 6]
axis=1:  [11  5]

שימו לב

במקרה שלנו המשמעות של סכימת השורות/עמודות יכולה מעט להיות לא איטואיטיבית. מה שקורה בפועל הוא שהפעולה מתבצעת בין השורות כאשר axis=0, ובין העמודות כאשר axis=1.

כך שאם לדוגמא, axis=0, נקבל וקטור באורך של שורה בודדת במטריצה המקורית (שזה בעצם מספר העמודות שהיו בה).
במילים אחרות, ניתן לחשוב על axis=0 כפעולה הנעשית על האיברים הנמצאים באותו מיקום בין שורות שונות. באופן דומה ניתן לחשוב על פעולות בין האיברים שנמצאים באותו מיקום בעמודות שונות כאשר axis=1.

נראה כעת דוגמא של שימוש בaxis. הפעם בפונקציית הספרייה np.sort.
בקטע קוד זה נמיין עם axis=0. כלומר מיון יתבצע בין שורות שונות, ונקבל מיון של עמודות המטריצה.

a = np.array([[4, 2 , 5], [1, 3, 1]])
print("The original array: \n", a)
b = np.sort(a, axis=0) # Return new sorted array. equivalent to the "sorted" built-in function
print("New sorted array with axis=0: \n", b)
The original array: 
 [[4 2 5]
 [1 3 1]]
New sorted array with axis=0: 
 [[1 2 1]
 [4 3 5]]

דרך נוספת למיין מערכים היא באמצעות המתודה sort של מערכי numpy (שימו לב שזוהי מתודה שונה מsort של המחלקה list).
בקריאות הללו המיון יתבצע in-place.

a.sort(axis=1) # In-place sorting of the array. equivalent to the "sort" method in lists
print("In-place sorting of the array with axis=1: \n",a)
In-place sorting of the array with axis=1: 
 [[2 4 5]
 [1 1 3]]

כעת נדגים כיצד לכווץ תמונה לאורך ציר מסוים.

לפניכם הפונקציה squeeze_image, המקבלת כקלט את התמונה im, ומספר שלם factor המייצג את פקטור הכיווץ.
הפונקציה תחזיר תמונה חדשה שבה כל עמודה היא הממוצע של factor עמודות מהתמונה המקורית.

לדוגמא, עבור תמונה בגודל (10,12) ופקטור 4, תוחזר תמונה בגודל (10,3):

  • העמודה הראשונה בתמונת הפלט תהיה ממוצע העמודות הראשונה עד הרביעית במטריצת הקלט

  • העמודה השניה בתמונת הפלט תהיה ממוצע העמודות החמישית עד השמינית במטריצת הקלט

  • העמודה הראשונה בתמונת הפלט תהיה ממוצע העמודות ההתשיעית עד השתיים-עשרה במטריצת הקלט

ודאו כי אתם מבינים כל שורה בפתרון. נסו להכניס שינויים את הקוד על מנת לוודא כי הבנתם אותו כשורה.

def squeeze_image(im,factor):
    new_n = im.shape[0]
    new_m = im.shape[1] // factor
    new_mat = np.zeros((new_n,new_m))
    for j in range(new_mat.shape[1]):
        curr_range = range(j*factor,min((j+1)*factor,im.shape[1]))
        new_mat[:,j] = im[:,curr_range].mean(axis=1)
    return new_mat
im_dog = imageio.v3.imread('files/dog.png')
new_image = squeeze_image(im_dog,4)
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.imshow(im_dog, cmap=plt.cm.gray)

plt.subplot(1, 2, 2)
plt.imshow(new_image, cmap=plt.cm.gray)
plt.show()
_images/a5ac51c1ef6fe46667d8c0547ba3519e69caa9053614dbae27d3b84ec896086b.png

הכהיית תמונה#

כעת נראה כיצד להפוך תמונה קיימת לכהה יותר.
בחנו תחילה את מימוש הפונקציה darken_image, המקבלת תמונה ומחזירה אותה כהה יותר בכמות יחידות הבהירות שהועברו ב-dark_strength.

def darken_image(im,dark_strength=150):
    dark_im = im.copy()
    dark_im = im-dark_strength
    return dark_im
im_dog = imageio.v3.imread('files/dog.png')
new_image = darken_image(im_dog,100)
plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(im_dog, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(new_image, cmap=plt.cm.gray)
plt.show()
_images/89dd0cabe82c0a1dc7b43cd83105527edff14976282ad13b49e1be4d5e2fbbb1.png

בחנו את הדוגמאות הבאות וודאו כי הבנתם מדוע מקבלים את הערכים המודפסים:

print(np.uint8(246) + np.uint8(20))
10
/tmp/ipykernel_2880/356383855.py:1: RuntimeWarning: overflow encountered in scalar add
  print(np.uint8(246) + np.uint8(20))
print(np.uint8(10) - np.uint8(20))
246
/tmp/ipykernel_2880/976938613.py:1: RuntimeWarning: overflow encountered in scalar subtract
  print(np.uint8(10) - np.uint8(20))

כדי לפתור את הבעיה, נמיר את הטיפוסים לטיפוס int רגיל לפני ביצוע הפעולה באמצעות np.int_:

a = np.uint8(10)
b = np.uint8(20)
c = np.int_(a) - np.int_(b)
print(c, type(c))
-10 <class 'numpy.int64'>

כעת נוכל להגדיר שכל ערך מתחת ל0 יקבל 0 וכל ערך שמעל 255 יוחלף ב255. על מנת לעשות זאת במטריצה נשתמש בפונקציה (mat, a)np.maximum, המחזירה מטריצה חדשה בה עבור כל איבר במטריצה mat נלקח הערך המקסימלי בין האיבר לa

להלן מימוש darken_image לאחר התיקון:

def darken_image(im,dark_strength):
    dark_im = im.copy()
    dark_im = np.maximum(np.int_(im)-dark_strength,0)
    return np.uint8(dark_im) # Do not forget to convert back to uint8 to save memory!
im_dog = imageio.v3.imread('files/dog.png')
new_image = darken_image(im_dog,150)
plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.imshow(im_dog, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(new_image, cmap=plt.cm.gray)
plt.show()
_images/a49678245993e108c0c3ebc331ea36b0b80888e95c1e53f46acda67c2a10e576.png

תרגול#

ממשו את הפונקציה lighten_image אשר מבהירה את התמונה באופן דומה. ודאו שאינכם מבצעים numerical overflow.

def lighten_image(im,dark_strength=150):
    pass
im_dog = imageio.v3.imread('files/dog.png')
new_image = lighten_image(im_dog,150)

plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.imshow(im_dog, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(new_image, cmap=plt.cm.gray)
plt.show()

רעש בתמונה (Image Noising)#

מה זה רעש בתמונה?
רעש בתמונה הוא מצב בו חלק מהפיקסלים סוטים מערכם האמיתי. אפשר להסתכל על רעש גם כתוספת של ערכים אקראיים לפיקסלים.
התוצאה נראית כמו “גרגירים” או נקודות בהירות או כהות שמפוזרות על פני התמונה.

מתי זה קורה?

  • באופן טבעי: רעש יכול להופיע בצילום אמיתי בגלל חיישן מצלמה לא יציב, תאורה חלשה, או הפרעות אלקטרוניות.

  • באופן מלאכותי: מוסיפים רעש במכוון, למשל כחלק מניסויים בעיבוד תמונה. למשל, כדי לבדוק על כמה האלגוריתם יודע לזהות אובייקטים בתמונות רועשות.

כעת נראה איך אפשר להוסיף רעש אקראי לתמונה בעזרת numpy.

נתבונן בפונקציה bright_noise_im, אשר מוסיפה רעש בהירות (brightness noise) לתמונה.
הפונקציה פועלת על־ידי הוספת ערך אקראי לכל פיקסל — מספר שלם בין 0 ל־50 - כך שכל פיקסל בתמונה נעשה בהיר יותר במידה שונה מעט.

התוצאה היא תמונה שנראית “מגורענת” או “רועשת” מבחינת הבהירות שלה.

def bright_noise_im(im,noise_strength=100):
    noise_im = im.copy()
    noise = np.random.randint(0, noise_strength + 1, size=im.shape, dtype=np.uint8)
    noisy_image = im + noise # np.clip(im + noise, 0, 255)
    return noisy_image

כדי לראות את פעולת הפונקציה, ניצור תמונה אפורה (כולה מכילה את הערך 125). נראה כיצד פונקציית הרעש שלנו משפיעה על התמונה הזו:

im = np.ones((100,100), dtype=np.uint8)*125 # plt.imread('files/dog.png')
new_image = bright_noise_im(im,50)
plt.figure(figsize=(6, 3))
plt.subplot(1, 2, 1)
plt.imshow(im, vmin=0, vmax=255, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(new_image,vmin=0, vmax=255, cmap=plt.cm.gray)
plt.show()
_images/14d09ae06e399addddfa31a4a9e9819e86cac9a36f83ff02ab2c19aab8d1b109.png

לכאורה, הרעש יכול רק להבהיר כל פיקסל בתמונה, זאת מכיוון שהוא מוסיף ערך אי-שלילי לכל פיקסל.

כעת ננסה להוסיף את הרעש לתמונה שלנו:

im_dog = imageio.v3.imread('files/dog.png')
# noisy_image_no_overflow = noise_im_no_overflow(im_dog,50)
bright_noisy_image = bright_noise_im(im_dog,100)
plt.figure(figsize=(8, 6))
plt.subplot(1, 2, 1)
plt.imshow(im_dog, vmin=0, vmax=255, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(bright_noisy_image,vmin=0, vmax=255, cmap=plt.cm.gray)
<matplotlib.image.AxesImage at 0x7f9c8d95b250>
_images/621aa388a7c9512857ea062213a5aee96b3c6c320652870017ebe98cc35612a4.png

מה קרה לנו כאן? נראה שחלק מהפיקסלים שהיו במקור בהירים הפכו למאוד כהים. כמו בדוגמא של הכהיית התמונה, גם פה התרחשה גלישה מספרית (Numerical overflow).

על מנת לתקן זאת, נוכל שוב להגביל את טווח המספרים כך שיהיה בין 0 ל-255.
הפעם נעשה זאת באמצעות המתודה np.clip, התוחמת את ערכים מערך נתון בין 2 ערכים (קראו עליה ברחבה פה):

def bright_noise_im_no_overflow(im,noise_strength=100):
    noise_im = im.copy()
    noise = np.random.randint(0, noise_strength + 1, size=im.shape, dtype=np.uint8)
    noisy_image = np.clip(np.int_(im) + np.int_(noise), 0, 255) # This is the modified line, where we clip the values to range between 0 to 255
    return noisy_image

ואם היינו רוצים רעש שיכול גם להכהות וגם להבהיר פיקסלים?

מכיוון שאנו משתמשים בclip, אנו למעשה תוחמים את ערכים גם מלעלה וגם ולמטה.
לכן, כל שעלינו לעשות זה רק לשנות את טווח הרעש:

def noise_im(im,noise_strength=100):
    noise_im = im.copy()
    noise = np.random.randint(0, noise_strength + 1, size=im.shape, dtype=np.uint8) 
    noisy_image = np.clip(np.int_(im)+noise-noise_strength//2, 0, 255)
    return noisy_image

במימוש החדש, הזזנו את טווח המספרים מהם הוגרל הרעש בכך שחיסרנו noise_strength//2.
בעבר טווח ערכי השינוי היה מ0 עד noise_strength+1, וכעת הטווח השתנה ל-noise_strength//2 עד noise_strength//2.

כך שלדוגמא, עבור הקלט noise_strength=100 הרעש שיתווסף לתמונה יהיה בין -50 ל50

im_dog = imageio.v3.imread('files/dog.png')
noisy_im = noise_im(im_dog,100)
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.imshow(im_dog, vmin=0, vmax=255, cmap=plt.cm.gray)
plt.subplot(1, 2, 2)
plt.imshow(noisy_im,vmin=0, vmax=255, cmap=plt.cm.gray)
plt.show()
_images/1baafe804e5a83743db19ca91dc427cdab09267ea2d8a36c2712e3879bc4709f.png