AI-инпейнтинг в мобильном приложении: от маски до результата
Вы сделали отличное фото на iPhone, но в кадр попал случайный прохожий. В приложении нужно стереть его за пару касаний — это задача для AI-инпейнтинга. Технически решение состоит из трёх этапов: создать маску, отправить изображение на API модели, применить результат. Каждый этап требует точного подхода, иначе получите артефакты или долгую загрузку. Разберём, как реализовать это на практическом уровне с кодом и конфигами.
Инпейнтинг — это технология замены области изображения на основе контекста. В мобильных приложениях мы используем облачные API (DALL·E 2, Stable Diffusion) для генерации контента. Но ключевая сложность — рисование маски на устройстве. От качества маски зависит 80% результата. Если маска неточная, модель заменит лишнее или оставит следы объекта. Поэтому первой задачей стоит реализация интуитивного инструмента рисования.
Почему качество маски определяет результат inpainting?
Маска — бинарное изображение, где белые пиксели обозначают область редактирования. Модель использует её как подсказку. Если маска выходит за границы объекта, модель «дорисует» соседние элементы, что приведёт к неестественному переходу. Если маска слишком узкая, останутся части удаляемого объекта. Поэтому важно реализовать точное рисование с возможностью масштабирования и отмены действий.
Как реализовать рисование маски на iOS?
Для iOS используем CAShapeLayer и UIBezierPath. При касании рисуем круги по траектории движения, объединяя их в один path. Маска получается как чёрно-белое изображение: чёрный фон — неизменяемая область, белые мазки — область интереса.
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 и оптимизируем рендеринг через displayLayer. На iPad с Apple Pencil маска получается точнее — можно увеличить разрешение path.
Как реализовать рисование маски на Android?
На Android используем кастомный View с Canvas и Paint. Рисуем круги на отдельном битмапе, который затем передаётся как маска. Важно настроить сглаживание и размер кисти под плотность экрана.
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
}
}
Для производительности используем Bitmap.Config.HARDWARE на Android 8+ и избегаем лишних копирований.
Как интегрировать DALL·E 2 Inpainting?
DALL·E 2 принимает только PNG с альфа-каналом, где прозрачные пиксели обозначают область редактирования. То есть маска вкладывается в альфа-канал изображения. Максимальный размер запроса — 4 МБ. Отправляем 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 МБ.
Какие альтернативы существуют? Stable Diffusion Inpainting
SD inpainting через Replicate принимает base64 для image и mask. Параметр strength управляет глубиной замены: 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 МБ | Высокое качество, но жёсткие требования по формату |
| SD через Replicate | JPEG/PNG base64 | 10 МБ | Гибкость, настраиваемый strength, ниже стоимость за запрос |
Как обрабатывать изображения разного размера?
Перед отправкой в 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 используем SwiftUI ProgressView, на Android CircularProgressIndicator. После получения результата — плавный переход через cross-fade.
Что входит в нашу работу по интеграции
- Анализ ваших требований и выбор оптимального API (DALL·E 2, SD или комбинация).
- Разработка UI для рисования маски с поддержкой undo/redo, изменения размера кисти, масштабирования.
- Интеграция выбранного API с обработкой ошибок, ретраями и кэшированием.
- Преобразование изображений: ресайз, кроп, наложение результата.
- Документация и обучение вашей команды.
- Пост-релиз техподдержка 3 месяца.
Свяжитесь с нами для технической оценки вашего проекта — мы поможем выбрать подходящий API и реализуем интеграцию в кратчайшие сроки. Работаем с гарантией результата и соблюдением сроков. Получите консультацию — пришлите описание задачи, и мы оценим её в рамках бесплатного аудита.
Сроки реализации
- Базовая интеграция (один API + экран рисования маски) — от 5 до 8 дней.
- Полноценный редактор (undo/redo, мульти-API, наложение) — от 3 до 4 недель.
Сроки уточняются после анализа вашего проекта.







