כתבה
איך גרמתי לטסטי צילומי מסך להפסיק לצעוק זאב זאב
ב-Digital Turbine בניתי ב-Java, בעזרת כלי AI לכתיבת קוד, כלי רגרסיה ויזואלית שמשווה Perceptual Hashes במקום פיקסלים. הוא הפחית את הנפילות הלא יציבות בטסטים של צילומי המסך. ככה זה עובד, ולמה הפתרון המתבקש נכשל קודם.
העבירו את העכבר כדי להריץ שוב. הבדלים קטנים עוברים, שינוי אמיתי מסומן.
למה השוואת פיקסלים נכשלה
הגרסה הראשונה עשתה מה שרוב טסטי צילומי המסך עושים: צילום בסיס, צילום חדש, וכישלון אם פיקסל אחד שונה. במוצר מובייל אמיתי זה הטלת מטבע. גופנים עוברים Anti-aliasing קצת אחרת בין הרצות, מכשירים ודרייברים גרפיים מעגלים צבעים אחרת, וקרוסלה או סמן מהבהב אף פעם לא נמצאים באותו פריים פעמיים.
כל אחד מאלה הוציא בילד אדום כשבפועל שום דבר לא היה שבור. אחרי מספיק כאלה, אנשים מפסיקים לקרוא את הכישלונות, ואז גם הרגרסיות האמיתיות עוברות. טסט שנכשל בגלל רעש גרוע מאין טסט בכלל.
להשוות איך המסך נראה, לא את הפיקסלים שלו
Perceptual Hash הוא טביעת אצבע קצרה של איך תמונה נראית לאדם. מקטינים את צילום המסך עד שהפרטים הקטנים נעלמים, מורידים את הצבע ומשאירים רק את המבנה הכללי: איפה בהיר ואיפה כהה. המבנה הזה הופך למספר של 64 ביט. שני צילומי מסך שנראים אותו דבר לעין אנושית מייצרים את אותו מספר, או כמעט אותו מספר, גם כשאלפי פיקסלים בודדים שונים.
Hamming Distance כפסק דין
כדי להשוות שתי טביעות אצבע סופרים כמה ביטים שונים, וזה ה-Hamming Distance. רעש של Anti-aliasing הופך ביט או שניים. כפתור חסר, לייאאוט שבור או מסך שגוי הופכים הרבה. לכן הטסט עובר כשהמרחק מתחת לסף ונכשל כשהוא מעליו, והסף הזה הוא הכפתור שמפריד בין רעש לשינוי אמיתי.
// Simplified sketch of the idea, not the production code.
long hash(BufferedImage img) {
// 1. Shrink and drop colour: tiny details and anti-aliasing disappear.
double[][] px = grayscale(resize(img, 32, 32));
// 2. Keep only the low frequencies: the overall shape of the screen.
double[][] freq = dct(px); // top-left 8x8 block
double median = median(topLeft(freq, 8));
// 3. One bit per frequency: above or below the median.
long bits = 0;
for (int i = 0; i < 64; i++) {
bits = (bits << 1) | (topLeft(freq, 8)[i / 8][i % 8] > median ? 1 : 0);
}
return bits;
}
int distance(long a, long b) {
return Long.bitCount(a ^ b); // Hamming distance: bits that differ
}
boolean looksSame(BufferedImage baseline, BufferedImage actual, int threshold) {
return distance(hash(baseline), hash(actual)) <= threshold;
}כיול הסף
נמוך מדי והרעש חוזר, גבוה מדי ורגרסיות אמיתיות עוברות. את הסף קובעים לפי נתונים: מריצים את החבילה הרבה פעמים מול בילד שלא השתנה כדי לראות כמה הרעש זז, ואז בודקים ששינויים אמיתיים ידועים נוחתים הרבה מעל זה. כשמסך ממשיך להיכשל מסיבות טובות, זה באג במוצר שצריך לפתוח, לא סף שצריך להעלות.
מה השתנה
הנפילות הלא יציבות בטסטים של צילומי המסך ירדו. וחשוב מזה, טסט ויזואלי אדום התחיל שוב להיות משמעותי, אז אנשים קוראים את הכישלונות, ורגרסיות שכן קורות נתפסות לפני הריליס ולא אחריו.