Впровадження редагування зображень з штучним інтелектом (Inpainting) в мобільному додатку
Inpainting замінює вибрану область зображення новим змістом, сгенерованим за промптом, зі збереженням контексту решти картинки. Видалити випадкового перехожого з фото, змінити фон за портретом, додати об'єкт у сцену. Технічно завдання розбивається на три частини: створення маски на пристрої, відправка зображення + маски на API, відображення результату.
Рисування маски: найкритичніша частина UX
Маска — це чорно-біле зображення того ж розміру, що оригінал: білі пікселі — область для змін, чорні — зберігти.
На iOS — спеціальний UIView з CALayer для рисування:
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
}
}
На Android — Canvas + Paint у спеціальному View:
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
}
}
Важливо: маска має бути тієї ж роздільної здатності, що оригінал. Якщо користувач рисує на попередньому перегляді 375×375 pt, а оригінал 4032×3024 px — потрібно масштабувати маску перед відправкою.
Інтеграція з API
DALL-E 2 Inpainting
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 МБ. Якщо зображення JPEG — потрібна конвертація у PNG з додаванням альфа-каналу.
Stable Diffusion Inpainting через Replicate
val body: [String: Any] = [
"version": "...", // SD inpainting модель
"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 // 1.0 = повна заміна, 0.5 = м'яке змішування
]
]
SD inpainting через Replicate приймає base64 для image та mask. strength контролює, наскільки модель відходить від оригіналу в області маски: 0.99 — майже повна заміна, 0.5 — змішування з оригіналом.
Трансформація зображення під вимоги API
DALL-E 2 вимагає рівно 1024×1024 (або 256, 512). Якщо користувач вибрав фото 4032×3024 — потрібно змінити розмір:
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
}
Після inpainting — якщо результат потрібно повернути до оригінальних пропорцій, накладаємо результат назад на оригінальне зображення в координатах маски.
Терміни
Екран з рисуванням маски + inpainting через DALL-E 2 — 5–8 днів. Повноцінний редактор з undo/redo маски, масштабуванням кисті, попереднім переглядом наложення результату, SD Inpainting — 3–4 тижні.







