AI Inpainting באפליקציות מובייל: ממסכה לתוצאה
צילמתם תמונה נהדרת באייפון, אבל עובר אורח נכנס לפריים. באפליקציה, אתם רוצים למחוק אותו בכמה הקשות — זו העבודה של AI inpainting. טכנית, הפתרון מורכב משלושה שלבים: יצירת מסכה, שליחת התמונה ל-API של המודל, והחלת התוצאה. כל שלב דורש דיוק, אחרת תקבלו חפצים (artifacts) או טעינה איטית. בואו נפרק איך ליישם זאת בפועל, עם קוד ותצורות.
Inpainting היא טכנולוגיה שמחליפה אזור בתמונה בהתבסס על ההקשר שלה. באפליקציות מובייל, אנו משתמשים ב-APIs ענניים (DALL·E 2, Stable Diffusion) כדי ליצור תוכן. אבל האתגר המרכזי הוא ציור המסכה על המכשיר. איכות המסכה קובעת 80% מהתוצאה. מסכה לא מדויקת גורמת למודל להחליף יותר מדי או להשאיר עקבות של האובייקט. אז המשימה הראשונה היא בניית כלי ציור אינטואיטיבי.
מדוע איכות המסכה קובעת את תוצאת ה-Inpainting?
מסכה היא תמונה בינארית שבה פיקסלים לבנים מציינים את אזור העריכה. המודל משתמש בה כרמז. אם המסכה חורגת מעבר לאובייקט, המודל "ממלא" אלמנטים סמוכים, ויוצר מעברים לא טבעיים. אם המסכה צרה מדי, חלקים מהאובייקט שהוסר נשארים. לכן, ציור מדויק עם זום וביטול (undo) הוא חיוני.
איך ליישם ציור מסכה ב-iOS?
ב-iOS, אנו משתמשים ב-CAShapeLayer וב-UIBezierPath. במגע, אנו מציירים עיגולים לאורך נתיב התנועה, וממזגים אותם לנתיב אחד. המסכה הופכת לתמונה בשחור-לבן: רקע שחור — אזור ללא שינוי, משיכות לבנות — אזור העניין.
class MaskDrawingView: UIView {
private var maskLayer = CAShapeLayer()
private var path = UIBezierPath()
private var brushSize: CGFloat = 30
override func draw(_ rect: CGRect) {
UIColor.black.setFill()
UIRectFill(rect)
UIColor.white.setFill()
path.fill()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let point = touch.location(in: self)
let circle = UIBezierPath(arcCenter: point, radius: brushSize / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
path.append(circle)
setNeedsDisplay()
}
func getMaskImage() -> UIImage {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0)
layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}לחלקות, אנו משתמשים ב-class MaskDrawingView: UIView { private var maskLayer = CAShapeLayer() private var path = UIBezierPath() private var brushSize: CGFloat = 30 override func draw(_ rect: CGRect) { UIColor.black.setFill() UIRectFill(rect) UIColor.white.setFill() path.fill() } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } let point = touch.location(in: self) let circle = UIBezierPath(arcCenter: point, radius: brushSize / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true) path.append(circle) setNeedsDisplay() } func getMaskImage() -> UIImage { UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0) layer.render(in: UIGraphicsGetCurrentContext()!) let image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() return image } } ומבצעים אופטימיזציה של העיבוד באמצעות UIGraphicsImageRenderer. ב-iPad עם Apple Pencil, המסכה מדויקת יותר — אנו יכולים להגדיל את רזולוציית הנתיב.
איך ליישם ציור מסכה ב-Android?
ב-Android, אנו משתמשים ב-displayLayer מותאם אישית עם View ו-Canvas. אנו מציירים עיגולים על bitmap נפרד, שמועבר לאחר מכן כמסכה. חשוב לכוונן את ה-anti-aliasing וגודל המברשת לפי צפיפות המסך.
class MaskDrawingView(context: Context) : View(context) {
private val maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
private val canvas = Canvas(maskBitmap)
private val paint = Paint().apply {
color = Color.WHITE
style = Paint.Style.FILL
strokeWidth = brushSize
isAntiAlias = true
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
canvas.drawCircle(event.x, event.y, brushSize / 2, paint)
invalidate()
}
}
return true
}
}לביצועים, אנו משתמשים ב-Paint ב-Android 8+ ונמנעים מהעתקים מיותרים.
איך לשלב את DALL·E 2 Inpainting?
DALL·E 2 מקבל רק PNG עם ערוץ אלפא, שבו פיקסלים שקופים מציינים את אזור העריכה. אז המסכה מוטמעת בערוץ האלפא של התמונה. גודל הבקשה המקסימלי הוא 4 MB. אנו שולחים multipart/form-data:
func inpaint(image: UIImage, mask: UIImage, prompt: String) async throws -> UIImage {
guard let imageData = image.pngData(), let maskData = mask.pngData() else {
throw InpaintError.invalidImage
}
var request = URLRequest(url: URL(string: "https://api.openai.com/v1/images/edits")!)
request.httpMethod = "POST"
request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
let boundary = UUID().uuidString
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
var body = Data()
// image (обязательно PNG, RGBA, макс 4 МБ)
body.appendMultipart(boundary: boundary, name: "image", filename: "image.png", contentType: "image/png", data: imageData)
// mask (PNG, RGBA, прозрачность = область изменения)
body.appendMultipart(boundary: boundary, name: "mask", filename: "mask.png", contentType: "image/png", data: maskData)
// prompt
body.appendMultipart(boundary: boundary, name: "prompt", data: prompt.data(using: .utf8)!)
// size (должен совпадать с размером входного изображения)
body.appendMultipart(boundary: boundary, name: "size", data: "1024x1024".data(using: .utf8)!)
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(ImageResponse.self, from: data)
// Загружаем результат
let (imageData2, _) = try await URLSession.shared.data(from: URL(string: response.data[0].url)!)
return UIImage(data: imageData2)!
}מגבלת DALL·E 2: מקבל רק PNG עם ערוץ אלפא (RGBA). המסכה מועברת דרך שקיפות: פיקסלים שקופים = אזור עריכה. לא מסכה בשחור-לבן כמו ב-SD, אלא ערוץ אלפא. גודל קובץ מקסימלי הוא 4 MB.
אילו חלופות קיימות? Stable Diffusion Inpainting
Stable Diffusion inpainting דרך Replicate מקבל base64 עבור תמונה ומסכה. הפרמטר class MaskDrawingView(context: Context) : View(context) { private val maskBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) private val canvas = Canvas(maskBitmap) private val paint = Paint().apply { color = Color.WHITE style = Paint.Style.FILL strokeWidth = brushSize isAntiAlias = true } override fun onTouchEvent(event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_MOVE -> { canvas.drawCircle(event.x, event.y, brushSize / 2, paint) invalidate() } } return true } } שולט בעומק ההחלפה: 1.0 — החלפה מלאה, 0.5 — מיזוג רך עם התוכן המקורי. זה נותן גמישות רבה יותר מאשר DALL·E 2.
val body: [String: Any] = [
"version": "...", // SD inpainting model
"input": [
"prompt": prompt,
"image": "data:image/jpeg;base64,${imageBase64}",
"mask": "data:image/png;base64,${maskBase64}",
"num_inference_steps": 25,
"guidance_scale": 7.5,
"strength": 0.99
]
]השוואת מודלי inpainting:
| מודל | פורמט קלט | גודל מקסימלי | הערות |
|---|---|---|---|
| DALL·E 2 | PNG עם ערוץ אלפא | 4 MB | איכות גבוהה, דרישות פורמט מחמירות |
| SD דרך Replicate | JPEG/PNG base64 | 10 MB | גמיש, עוצמה מתכווננת, עלות נמוכה יותר לבקשה |
איך לטפל בתמונות בגדלים שונים?
לפני שליחה ל-API, התמונה משתנה לגודל הנדרש (לדוגמה, 1024×1024 עבור DALL·E 2). אנו משתמשים ב-aspect fill כדי לשמור על פרופורציות ולחתוך עודפים. לאחר היצירה, התוצאה מונחת על גבי התמונה המקורית בקואורדינטות המסכה. לשם כך, אנו שומרים את הממדים המקוריים ואת מיקום התצוגה המקדימה.
func resizeAndCrop(_ image: UIImage, to size: CGSize) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
let aspectFill = max(size.width / image.size.width, size.height / image.size.height)
let newSize = CGSize(width: image.size.width * aspectFill, height: image.size.height * aspectFill)
let origin = CGPoint(x: (size.width - newSize.width) / 2, y: (size.height - newSize.height) / 2)
image.draw(in: CGRect(origin: origin, size: newSize))
let result = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return result
} איך להבטיח UX חלק במהלך הטעינה?
בקשת API אורכת בין 1 ל-5 שניות בהתאם למודל וגודל התמונה. אנו מציגים אנימציית טעינה עם התקדמות (עבור SD, ניתן לעקוב באמצעות polling). ב-iOS, השתמשו ב-Bitmap.Config.HARDWARE; ב-Android, func inpaint(image: UIImage, mask: UIImage, prompt: String) async throws -> UIImage { guard let imageData = image.pngData(), let maskData = mask.pngData() else { throw InpaintError.invalidImage } var request = URLRequest(url: URL(string: "https://api.openai.com/v1/images/edits")!) request.httpMethod = "POST" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") let boundary = UUID().uuidString request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") var body = Data() // image (обязательно PNG, RGBA, макс 4 МБ) body.appendMultipart(boundary: boundary, name: "image", filename: "image.png", contentType: "image/png", data: imageData) // mask (PNG, RGBA, прозрачность = область изменения) body.appendMultipart(boundary: boundary, name: "mask", filename: "mask.png", contentType: "image/png", data: maskData) // prompt body.appendMultipart(boundary: boundary, name: "prompt", data: prompt.data(using: .utf8)!) // size (должен совпадать с размером входного изображения) body.appendMultipart(boundary: boundary, name: "size", data: "1024x1024".data(using: .utf8)!) body.append("--\(boundary)--\r\n".data(using: .utf8)!) request.httpBody = body let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(ImageResponse.self, from: data) // Загружаем результат let (imageData2, _) = try await URLSession.shared.data(from: URL(string: response.data[0].url)!) return UIImage(data: imageData2)! } . לאחר קבלת התוצאה, אנו מחילים מעבר cross-fade חלק.
מה כוללת עבודת האינטגרציה שלנו
- ניתוח הדרישות שלכם ובחירת ה-API האופטימלי (DALL·E 2, Stable Diffusion, או שילוב).
- פיתוח ממשק ציור מסכה עם undo/redo, התאמת גודל מברשת וזום.
- אינטגרציה של ה-API הנבחר עם טיפול בשגיאות, ניסיונות חוזרים (retries) וקאשינג.
- עיבוד תמונה: שינוי גודל, חיתוך והנחת התוצאה.
- תיעוד והדרכה לצוות שלכם.
- 3 חודשים של תמיכה טכנית לאחר השחרור.
צרו קשר להערכה טכנית של הפרויקט שלכם — נעזור לבחור את ה-API המתאים וליישם את האינטגרציה בזמן הקצר ביותר. אנו עובדים עם אחריות לתוצאה ועמידה בלוחות זמנים. קבלו ייעוץ — שלחו לנו תיאור של המשימה שלכם, ונעריך אותה כחלק מבדיקה.
לוח זמנים ליישום
- אינטגרציה בסיסית (API אחד + מסך ציור מסכה) — 5 עד 8 ימים.
- עורך מלא (undo/redo, ריבוי APIs, הנחת תוצאה) — 3 עד 4 שבועות.
לוחות הזמנים מותאמים לאחר ניתוח הפרויקט שלכם.







