Affine Transformations
Image Rotation
אז בחלק הזה נלמד איך לעשות רוטציות לתמונה. ראשית אציג את הדרך הנאיבית בה היווצרו חורים בתמונה (בשל הזזת הפיקסלים) ולאחר מכן נפתור את הבעייה באמצעות שימוש באינטרפולציה שעליה פירטתי בהרחבה בפוסט הבא interpolation.בחלק הבונוס: נלמד איך לבצע את הרוטציה ע״י ממרכז התמונה על ידי שינוי ראשית הצירים שבדיפולט נמצאת בפינה השמאלית העליונה של התמונה.

תאוריה
כשמבצעים רוטציה לתמונה מבצעים את הרוטציה בזווית α שבדרך מתקבלת כפרמטר מהמשתמש.לדגומה בתמונה הבאה, אנחנו מבצעים רוטציה בזווית α מהפיקסל (u,v) אל הפיקסל x,v).

כעת עלינו להכיר כמה כלים מתמטיים שישמשו אותנו עבור ביצוע הרוטציה.
מטריצת הרוטציה/סיבוב (ככל הנראה נתקלתם באזכור שלה באחד מקורסי הלינארית):

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

תמונה נוספת להמחשה:

לכאורה במבט ראשוני הכלי נראה מושלם לביצוע הרוטציה שאנחנו צריכים עבור התמונה.
-פשוט נעבור פיקסל-פיקסל ונבצע את הרוטציה ע״י הפעלה של המטריצה על וקטור הפיקסל, או לחילופין באופן שקול נחשב לכל פיקסל את מיקומי ה ('x,'y) שלו ע״י המשוואה המתאימה לחישוב 'x והמשוואה המתאימה לחישוב 'y.
הערה: בקוד הבא ביצענו רוטציה אך ורק לתמונות עם צבע. ובהמשך נדאג גם לבצע את הרוטציה לתמונות Grayscale
בואו ננסה לבצע זאת:
ראשית נייבא את הספריות הדרושות לנו:
#include <iostream>
#include <cmath>
#include "opencv2/opencv.hpp"
לאחר מכן נכתוב את התוכנית המרכזית להצגת התמונות: (אם משהו לא מובן בחלק זה, נא לקרוא את המדריך הבסיסי לבלוג)
int main() {
cv::Mat img = cv::imread("../lion.jpeg");
cv::Mat rotatedImage(img.rows,img.cols,CV_8UC3);
// Rotating
NaiveRotation(img,rotatedImage,30);
// End of Rotating
// Show the images
cv::imshow("window1",img);
cv::imshow("window2",rotatedImage);
cv::waitKey(0);
// End of Show the images
return 0;
}
וכעת נתבונן בפונקציית הרוטציה הנאיבית שלנו:
void NaiveRotation(cv::Mat src, cv::Mat dst, int angle) {
double rotatedX;
double rotatedY;
double toRadian = 3.141592653589 / 180;
// ---------------------------- RGB HANDLER ----------------------------
for (int x = 0; x < src.cols; x++) {
for (int y = 0; y < src.rows; y++) {
rotatedX = round(x * cos(angle * toRadian) - y * sin(angle * toRadian));
rotatedY = round(x * sin(angle * toRadian) + y * cos(angle * toRadian));
cv::Point2i dstPixel((int) rotatedX, (int) rotatedY);
// Checking if the Interpolation calculations crossed the boundaries
if (dstPixel.x < 0 || dstPixel.x > src.cols - 1 || dstPixel.y < 0 || dstPixel.y > src.rows - 1)
dst.at<cv::Vec3b>(cv::Point(x, y)) = 0;
else { // In case everything is good
dst.at<cv::Vec3b>(dstPixel) = src.at<cv::Vec3b>(cv::Point(x, y));
}
}
}
}
וזו התוצאה שנקבל:

כמו שניתן לראות, אמנם הצלחנו לבצע את הרוטציה אבל אך לא הצלחנו לשמר את איכות התמונה ולמעשה נוצרו חורים בין הפיקסלים. בעיה זו נקראת Aliasing.
הסבר לקוד:
rotatedX = round(x * cos(angle * toRadian) - y * sin(angle * toRadian));
rotatedY = round(x * sin(angle * toRadian) + y * cos(angle * toRadian));
cv::Point2i dstPixel((int) rotatedX, (int) rotatedY);
הערות:
- .כמו בחישוב במחשבון, נצטרך להעביר את הזווית שהמשתמש הכניס לרדיאנים מכיוון שפונקציות ה- cos וה- sin שלנו ב C++ תומכות רק בחישובים עם רדיאנים.
- מכיוון שפיקסלים מיוצגים במחשב ע״ מספרים טבעיים והחישוב שלנו עלול לתת ערכים חיוביים שאינם שלמים, נצטרך לעגל לערך הקרוב ביותר כדי לבחור את הפיקסל המתאים. (מתקשר לבעיית ה aliasing.
if (dstPixel.x < 0 || dstPixel.x > src.cols - 1 || dstPixel.y < 0 || dstPixel.y > src.rows - 1)
dst.at<cv::Vec3b>(cv::Point(x, y)) = 0;
else { // In case everything is good
dst.at<cv::Vec3b>(dstPixel) = src.at<cv::Vec3b>(cv::Point(x, y));
לדוגמה:
כעת נפתור את הבעיה באמצעות אינטרפולציה
כדי לבצע אינטרפולציה, נצטרך את הטרנספורמציה ההופכית, כלומר את המטריצה ההופכית למטריצת הסיבוב שלנו.
אם ניזכר בתכונות מטריצת הסיבוב שציינו בתחילת הבלוג, נשים לב שהמטריצה היא מטריצה אורתוגונלית ולכן המטריצה המשוחלפת שלה שווה למטריצה ההופכית כלומר:

#include <iostream>
#include <cmath>
#include "opencv2/opencv.hpp"
נשתמש ב - enums כדי להקל על בחירת איכות התמונה למשתמש
using namespace std;
enum interpolation_type{
INTERPOLATION_CUBIC,
INTERPOLATION_LINEAR,
INTERPOLATION_NEAREST_NEIGHBOR
};
כעת נשתמש בפונקציות האינטרפולציה שלנו (ראו את המדריך לגביו)
נבצע את הרוטציה שלנו ע״י הטרנספורמציה ההופכית למטריצת הרוטציה:
void RotationFunction(const cv::Mat& src, cv::Mat& dst, int angle, interpolation_type inter_type) {
// The pixels in the new image we want to find right origin pixel for his value.
if (src.channels() != dst.channels())
throw std::invalid_argument(
"Source Image and Destination Image have different channels. (Probably one is greyscale and the other BGR/RGB)");
double rotatedX;
double rotatedY;
double toRadian = 3.141592653589 / 180;
for (int x = 0; x < dst.cols; x++) // We can use the width instead of the cols.
{
for (int y = 0; y < dst.rows; y++) // We cna use the height instead of the rows.
{
rotatedX = x * cos(angle * toRadian) + y * sin(angle * toRadian);
rotatedY = x * (-sin(angle * toRadian)) + y * cos(angle * toRadian);
cv::Point2d srcPoint(rotatedX, rotatedY);
cv::Point2i dstPixel(x, y);
Interpolation_Calculator(src, dst, srcPoint, dstPixel, inter_type);
}
}
}
לבסוף נכתוב את הקוד הדרוש לקריאה והצגת התמונות.
int main() {
cv::Mat img = cv::imread("../lion.jpeg");
//if(img.channels() < 3)
// img_chan = GRAYSCALE;
cv::Mat rotatedImage(img.rows,img.cols,CV_8UC3);
RotationFunction(img,rotatedImage,30,INTERPOLATION_LINEAR);
// Show the images
cv::imshow("Original Image",img);
cv::imshow("Rotated Image",rotatedImage);
cv::waitKey(0);
}
התוצאה שקיבלנו:
אינטרפולציה לשכן הכי קרוב:

אינטרפולציה לינארית:

אינטרפולציה הקיוביק:
חלק בונוס: רוטצית התמונה ממרכז התמונה
כפי שראיתם בקוד הקודם ביצענו רוטציה מראשית הצירים המקורית של התמונה (פינה שמאלית עליונה), אך בפועל וביישומים אמיתיים, היינו רוצים דווקא לבצע את הרוטציה כאשר ראשית הצירים במרכז התמונה.האמת שהמשימה מאוד פשוטה ואינטואיטיבית.
כל מה שנדרש מאיתנו לבצע, זה קודם להוסיף לכל פיקסל (x,y) בתמונת היעד שעבורו אנחנו הולכים למצוא את הדגימה, חצי מהרוחב וחצי מהאורך בהתאם לצירים כלומר (x + 0.5img.width,y + 0.5img.height). לאחר מכן נבצע את הטרנספורמציה ההפוכה עבור הפיקסל החדש שלנו, ולבסוף ברגע שנדגום את הצבע הדרוש, ניתן את ערכו לפיקסל המקורי (x,y).
כן, זה עד כדי כך פשוט.
קוד להמחשה: