סיפורי אופטימיזציה: תמונות ממוזערות

פרויקט ארדואינו שעבדתי עליו לאחרונה כלל מסך מגע קטן, וכדי שהממשק יהיה אטרקטיבי החלטתי לשלב בו לחצנים עם עיצוב גרפי. גודל של לחצן נוח לתפעול, עבור המקרה הספציפי הזה, הוא בסביבות 36×36 פיקסלים; המסך עובד בסכמת צביעה שמצריכה 2 בייטים לפיקסל, ובסך הכל צריך כעשרה לחצנים, כלומר כ-26KB. איפה ואיך מאחסנים את המידע הזה באופן שיהיה גם נוח, גם מהיר לגישה וגם חסכוני ככל האפשר?

קוד עם המון נתונים ב-Arduino IDE
קוד עם המון נתונים ב-Arduino IDE

פסילת מועמדים

מודול המסך שבו השתמשתי כלל חריץ מובנה לכרטיס MicroSD, אך זה לא ממש עוזר, בעיקר מכיוון שהתקשורת עם הכרטיס קצת איטית. זה טוב לשמירת תמונות גדולות שמועלות למסך לעתים רחוקות, אך הגרפיקה של הלחצנים צריכה להופיע ולהיעלם שוב ושוב בקצב די מהיר, בלי להפריע לדברים נוספים שקורים במערכת. כמו כן, שימוש בכרטיס מחייב עבודה עם ספריה נוספת שמכבידה על הקוד, ומי יודע אילו עוד צרות היא תחולל. חוץ מזה, זה לא קצת בזבוז להחזיק כרטיס שלם בשביל 26KB?

אפשרות נוספת היא לטעון את התמונות איכשהו לזכרון. לארדואינו עצמו אין מספיק מקום ב-EEPROM (שגודלו 1KB בלבד בדגם Uno בו השתמשתי) או ב-SRAM (בעל 2KB, שמשרתים גם את שאר התוכנה), כך שעבור אופציות אלה נצטרך רכיב זיכרון חיצוני, וזה אומר – שוב – תקשורת איטית יחסית וגם פינים נוספים של הארדואינו העמוס ממילא.

נשארנו, אם כן, עם זיכרון ה-Flash שבו צרוב קוד התוכנה. הקוד עצמו תפס משהו כמו 20KB, כך שנותרו לנו כ-12K פנויים, וידוע שבעזרת פעולות progmem אפשר לאחסן שם גם נתונים. אם כך, השאלה הופכת לממוקדת יותר: איך להפוך 26KB לפחות מ-12KB, ואיך להכניס את המידע הזה לקוד?

שלב ראשון: בחירת כיווץ הנתונים

ישנם, כידוע, אינספור אלגוריתמים לכיווץ תמונה (ובכלל). אחד מהוותיקים והקלאסיים שבהם נקרא RLE- Run Length Encoding. אלגוריתם זה "רץ" על הפיקסלים שבתמונה לפי הסדר ומחפש רצפים של פיקסלים זהים. רצפים כאלה, שבמקור נראים בערך ככה:

פפפפפפפפפפפפפפפפפפ… (50 פעמים)

הוא כותב בקובץ המכווץ בסגנון כזה:

פ50

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

שלב שני: צמצום הפאלטה

ההיגיון מחייב שככל שיש פחות צבעים שונים בתמונה, כך כיווץ ה-RLE יהיה בממוצע יעיל יותר. להקטנה של מבחר הצבעים יש יתרון נוסף: במקום לשמור כל פיקסל בצבע האמתי שלו, שתופס כאמור שני בייטים, אנחנו יכולים לשמור בזיכרון פאלטה (Palette), "לוח צבעים" קבוע עם מספר קטן יחסית של צבעים וקוד מספרי עבור כל אחד מהם. לדוגמה, אם נצליח לצמצם את מספר הצבעים בתמונה מסוימת ל-256, נוכל להקטין את גודל הקובץ פי שניים עוד לפני שהתחלנו לכווץ אותו, בעלות של 512 בייטים עבור הפאלטה; ואם נרד ל-16 צבעים בלבד, הקובץ המקורי יקטן פי ארבעה והפאלטה תעלה לנו רק 32 בייטים.

במצב האופטימלי תהיה לנו פאלטה אחת קטנה שתשרת את כל התמונות גם יחד. כדי לממש את זה, לקחתי את הגרפיקות שעיצבתי עבור כל הלחצנים השונים וסידרתי אותן, בתוכנת ציור מקצועית (Corel Paint Shop Pro X5), לתמונה אחת משולבת. את הגרפיקות יצרתי מראש, כמובן, כך שישתמשו בצבעים אחידים ככל האפשר ויהיו פשוטות יחסית. לאחר מכן, שוב בעזרת תוכנת הציור, הפחתתי את "עומק הצבע" של התמונה המשולבת ל-4 ביט, בשיטה שנתנה את התוצאות הכי נעימות ויזואלית. הנה התוצאה עבור אחד מעשרת הלחצנים:

הדגמה של הפחתת עומק צבע ל-4 ביט (16 צבעים)
הדגמה של הפחתת עומק צבע ל-4 ביט (16 צבעים). התמונות מוגדלות פי 4, המקור מימין.

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

שלב שלישי: RLE הלכה למעשה

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

כעת, בסביבת הפיתוח Delphi XE2, כתבתי בזריזות תוכנה שקוראת את הקבצים הגולמיים ומריצה עליהם כיווץ RLE חלקי – כל שורת פיקסלים בנפרד, כדי להקל על הארדואינו בשלב הפענוח והציור. כמו כן, הרצפים שהתוכנה הזו מפיקה הם באורך מרבי של 16. מכיוון שיש 16 צבעים ו-16 אורכים אפשריים של רצפים, המידע נשמר בצורה מסודרת בבייטים נפרדים: ארבעת הביטים הראשונים בכל בייט מייצגים את קוד הצבע בפאלטה, וארבעת הביטים האחרונים את מספר הפיקסלים בצבע זה. לסיום, התוכנה פולטת את כל הבייטים האלה בפורמט טקסט קריא ונוח להעתקה:

צילום מסך של התוכנה לכיווץ RLE
צילום מסך של התוכנה לכיווץ RLE

הלחצן מימין למעלה, אגב, מציג את פאלטת הצבעים בפורמט 5-6-5 המקובל במסכי LCD-TFT כמו זה שאיתו עבדתי: 5 ביטים לאדום, 6 לירוק ו-5 לכחול, סה"כ 16 (שני בייטים, זוכרים?). אפרופו, חידה למתקדמים: האם אתם יודעים למה דווקא הירוק קיבל יותר ביטים מהאחרים?

שלב רביעי: על המסך

התמונה בפורמט הגולמי תפסה 3,888 בייטים. אחרי הכיווץ, כפי שרשום מעל תיבת הטקסט בתמונה למעלה, היא ירדה ל-485 בייטים בלבד, ולמעשה זה היה הלחצן ה"בזבזני" ביותר – החסכוני ביותר תפס 192 בייטים בלבד. כל העשרה ביחד תפסו 3,093 בייטים. המידע הועתק ידנית מהתוכנה לתוך קוד הארדואינו, ופונקציה ייעודית שכתבתי קראה ממנו את הבייטים, הפרידה לרכיבי צבע ואורך וציירה בהתאם קווים קצרים על התצוגה.

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

להרשמה
הודע לי על
8 Comments
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

תשובה לחידה: החליטו שדווקא הצבע הירוק יקבל את הביט הנוסף בגלל שזה הצבע שהעין האנושית הכי רגישה אליו.

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

יפה מאודדדד

אבל איפה המסך ? התוצאה? ואיזה מסך זה איך משתמשים בו?

כתבה מעולה: מעניינת וכתובה טוב.

מדהים!!!
אני מתפעל מהפוסטים שלך כל פעם מחדש.