Разработка
Отслеживание угла горизонта на изображении с помощью фреймворка Vision
Отслеживание горизонта, изображенного на фотографии — одна из тех задач, которые можно решить с помощью анализа изображений в фреймворке Vision.
Отслеживание горизонта, изображенного на фотографии — одна из тех задач, которые можно решить с помощью анализа изображений в фреймворке Vision.
Этот запрос особенно полезен, когда нужно «выпрямить» фотографию, чтобы улучшить ее качество и реалистичность, особенно при съемке пейзажей или архитектурных сцен, где ровный горизонт имеет решающее значение для визуальной привлекательности.
Чтобы начать выполнение задачи, импортируйте фреймворк Vision.
import Vision
Затем создайте функцию, которая принимает в качестве параметра UIImage
и возвращает HorizonObservation
:
xxxxxxxxxx
func detectHorizon(uiImage: UIImage) async throws -> HorizonObservation? {
// 1. The image to process
guard let ciImage = CIImage(image: uiImage) else { return nil }
do{
// 2. The request
let request = DetectHorizonRequest()
// 3. The result coming from performing the analysis
let result = try await request.perform(on: ciImage, orientation: .up)
// 4. The observation
return result
} catch {
print("Error detecting the horizon line: \(error)")
}
return nil
}
detectHorizon(uiImage:)
работает следующим образом:
- Преобразует переданный
UIImage
вCIImage
. - Запрашивает
DetectHorizonRequest
. - Хранит
HorizonObservation
, полученный из обработанного изображения методомperform(on:orientation:)
. - Возвращает результат.
HorizonObservation
возвращает:
- свойство
angle
— наблюдаемый горизонт в радианах; -
transform
—CGAffineTransform
для применения к обнаруженному горизонту.
Прежде чем перейти к интеграции обнаружения горизонта, имейте в виду, что DetectHorizonRequest
возвращает optional HorizonObservation
. Этот проект был протестирован с 39 различными фотографиями, и обнаружил горизонт только на 47% из них, в основном возвращая nil на фотографиях с углом горизонта, равным 0.
Интеграция в SwiftUI
Мы собираемся реализовать представление, отображающее изображение, которое будет повернуто в зависимости от значения угла горизонта, обнаруженного Vision, чтобы сделать его горизонтальным. Фотография будет снабжена красным прямоугольником на фоне, подчеркивающим вращение, и зеленой линией спереди, подчеркивающей идеальный уровень горизонта.
Сначала создадим само представление.
xxxxxxxxxx
import SwiftUI
import Vision
struct HorizonLineView: View {
@State private var observation: HorizonObservation? = nil
@State private var imageSize: CGSize = .zero
var body: some View {
VStack(alignment: .center){
ZStack{
// The guideline rectangle
Rectangle()
.frame(width: imageSize.width + 10, height: imageSize.height + 10)
.foregroundStyle(.red)
// The picture to detect
Image("picture")
.resizable()
.scaledToFit()
.background(GeometryReader { proxy in
Color.clear.onAppear {
self.imageSize = proxy.size
}
})
.frame(height: 400)
}
Button("Rotate Horizon") {
rotateThePicture()
}.buttonStyle(.bordered)
.foregroundStyle(.green)
}.padding()
}
// Triggers the detection
func rotateThePicture() {
Task{
guard let image = UIImage(named: "picture") else { return }
self.observation = try await self.detectHorizon(uiImage: image)
}
}
func detectHorizon(uiImage: UIImage) async throws -> HorizonObservation? {...}
}
HorizonLineView
— это представление, отображающее красный прямоугольник прямо за изображением, и кнопку, включающую обнаружение горизонта. Оно использует GeometryReader
для получения размера изображения и применения его, увеличенного на 10, к красному прямоугольнику — так, чтобы он был виден за фотографией.
Функция rotateThePicture()
, вызываемая кнопкой, выполняет обнаружение горизонта.
Когда обнаружение будет выполнено, мы хотим, чтобы горизонт был наложен с 2 разными линиями:
- зеленая, работающая как ориентир, чтобы понять, какой уровень должен быть идеальным;
- красная, появляющаяся сразу после обнаружения горизонта и подчеркивающая уровень горизонта на картинке.
Чтобы добиться такого результата, мы создаем пользовательский Shape
, который будет рисовать линии.
xxxxxxxxxx
struct HorizonLineShape: Shape {
// The detected horizon angle in radians
let angle: Double
func path(in rect: CGRect) -> Path {
var path = Path()
// 1. Get the center of the drawing rectangle
let centerX = rect.midX
let centerY = rect.midY
// 2. Set the total length of the horizon line to the width of the rect
let lineLength = rect.width
let halfLength = lineLength / 2
// 3. Declare variables for horizontal and vertical offsets
let dx: CGFloat
let dy: CGFloat
// 4. Calculate the horizontal and vertical offsets
if angle != 0 {
// Compute based on the angle
dx = cos(angle) * halfLength
dy = sin(angle) * halfLength
} else {
// If the angle is zero, the line is horizontal
dx = halfLength
dy = 0
}
// 5. Determine the start and end points of the horizon line
let start = CGPoint(x: centerX - dx, y: centerY - dy)
let end = CGPoint(x: centerX + dx, y: centerY + dy)
// 6. Move to the start point and add a line to the end point
path.move(to: start)
path.addLine(to: end)
return path
}
}
HorizonLineShape
принимает в качестве параметра Double
, представляющий собой обнаруженный угол горизонта, на основе которого создается линия, имеющая наклон.
Функция рисует линию в соответствии со следующей логикой:
- Получает центр прямоугольника рисования, к которому будет привязана линия.
- Вычисляет половину ширины прямоугольника, которая используется для продления линии в обе стороны от центра
- Объявляет переменные для горизонтального и вертикального смещения.
dx
иdy
указывают, на какое расстояние нужно отступить от центра представления, чтобы нарисовать конечные точки линии. Подумайте вот о чем: если вы расположили линейку по центру области рисования, то эти смещения определяют, насколько нужно сдвинуть концы влево/вправо (dx
) и вверх/вниз (dy
) в зависимости от угла наклона горизонта. Они гарантируют, что независимо от наклона линии, она останется в центре, а ее конечные точки будут правильно расположены в соответствии с обнаруженным углом. - Она вычисляет их:
- если угол не равен нулю, они рассчитываются с помощью косинуса — чтобы определить, насколько влево или вправо нужно двигаться, — и синуса — насколько вверх или вниз нужно двигаться — от определенного угла;
- Если угол равен нулю, он просто присваивает
dx
половину длины, аdy
— ноль, в результате чего получается идеально горизонтальная линия, что и требуется для построения линии, работающей как направляющая.
- Определяет начальную и конечную точки линии горизонта: начальная точка линии устанавливается в (
centerX - dx, centerY - dy
) — перемещение влево — и конечная точка в (centerX + dx, centerY + dy
) — перемещение вправо — Это гарантирует, что линия будет центрирована в поле зрения и правильно повернута в соответствии с обнаруженным углом горизонта. - Перемещается к начальной точке и добавляет линию к конечной точке, чтобы окончательно нарисовать линию.
Давайте интегрируем линии в представление.
xxxxxxxxxx
import SwiftUI
import Vision
struct HorizonLineView: View {
...
var body: some View {
...
Image("picture")
...
// The detected horizon line
if let horizon = observation{
HorizonLineShape(angle: horizon.angle.value)
.stroke(Color.red, lineWidth: 3.5)
.animation(.linear(duration: 5), value: horizon)
}
// The horizon guideline
HorizonLineShape(angle: 0.0)
.stroke(.green, lineWidth: 3.5)
}
Button("Rotate Horizon") {
..
}
...
}
}
Создайте 2 экземпляра HorizonLineShape
, чтобы поместить их между изображением и кнопкой:
- Первая линия, красная, будет представлять обнаруженную линию горизонта сразу после анализа, проведенного Vision на фотографии;
- Вторая, зеленая, будет служить ориентиром для выделения разницы между обнаруженным и идеальным уровнем горизонта.
Теперь, когда мы видим идеальный уровень горизонта и обнаруженный, мы можем попробовать использовать значение угла, чтобы повернуть фотографию и линию горизонта, чтобы они совпали с ориентиром.
xxxxxxxxxx
struct HorizonLineView: View {
...
// 1. The State variable storing the inverted angle value
@State var radians: Double = 0.0
var body: some View {
...
}
func rotateThePicture() {
Task{
...
if let obs = observation {
// 2. Assigning the inverted angle value
self.radians = -1 * obs.angle.value
}
}
}
...
}
Чтобы повернуть как обнаруженную линию горизонта, так и изображение к идеальному уровню горизонта, нам нужно инвертировать значение наблюдаемого угла и применить его в эффекте вращения.
Теперь мы можем использовать это значение для поворота изображения.
xxxxxxxxxx
import SwiftUI
import Vision
struct HorizonLineView: View {
...
var body: some View {
...
Image("picture")
...
.rotationEffect(Angle(radians: radians)).animation(.linear(duration: 2).delay(2), value: observation)
...
}
А чтобы применить тот же эффект вращения и к красной линии, только уже после того, как она попала в иерархию просмотра, мы создаем кастомный переход.
CustomTransition
соответствует протоколу Transition
, производя поворот только тогда, когда красная линия полностью видна в представлении, используя свойство isIdentity
.
xxxxxxxxxx
struct CustomTransition: Transition{
// The angle value to rotate
let radians: Double
func body(content: Content, phase: TransitionPhase) -> some View {
// Rotate the content only when the view is already in the hierarchy
content.rotationEffect(Angle(radians: phase.isIdentity ? radians : 0.0))
}
}
Применим его к фигуре HorizonLineShape
, представляющей обнаруженный горизонт.
xxxxxxxxxx
import SwiftUI
import Vision
struct HorizonLineView: View {
...
var body: some View {
...
if let horizon = observation{
HorizonLineShape(angle: horizon.angle.value)
...
.transition(CustomTransition(radians: radians).animation(.linear(duration: 2).delay(2)))
}
...
}
-
Программирование3 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков6 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8