Отслеживание горизонта, изображенного на фотографии — одна из тех задач, которые можно решить с помощью анализа изображений в фреймворке Vision.
Этот запрос особенно полезен, когда нужно «выпрямить» фотографию, чтобы улучшить ее качество и реалистичность, особенно при съемке пейзажей или архитектурных сцен, где ровный горизонт имеет решающее значение для визуальной привлекательности.
Чтобы начать выполнение задачи, импортируйте фреймворк Vision.
import Vision
Затем создайте функцию, которая принимает в качестве параметра UIImage
и возвращает HorizonObservation
:
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, чтобы сделать его горизонтальным. Фотография будет снабжена красным прямоугольником на фоне, подчеркивающим вращение, и зеленой линией спереди, подчеркивающей идеальный уровень горизонта.
Сначала создадим само представление.
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
, который будет рисовать линии.
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
) — перемещение вправо — Это гарантирует, что линия будет центрирована в поле зрения и правильно повернута в соответствии с обнаруженным углом горизонта. - Перемещается к начальной точке и добавляет линию к конечной точке, чтобы окончательно нарисовать линию.
Давайте интегрируем линии в представление.
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 на фотографии;
- Вторая, зеленая, будет служить ориентиром для выделения разницы между обнаруженным и идеальным уровнем горизонта.
Теперь, когда мы видим идеальный уровень горизонта и обнаруженный, мы можем попробовать использовать значение угла, чтобы повернуть фотографию и линию горизонта, чтобы они совпали с ориентиром.
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 } } } ... }
Чтобы повернуть как обнаруженную линию горизонта, так и изображение к идеальному уровню горизонта, нам нужно инвертировать значение наблюдаемого угла и применить его в эффекте вращения.
Теперь мы можем использовать это значение для поворота изображения.
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
.
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
, представляющей обнаруженный горизонт.
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))) } ... }