Site icon AppTractor

Отслеживание угла горизонта на изображении с помощью фреймворка Vision

Отслеживание горизонта, изображенного на фотографии — одна из тех задач, которые можно решить с помощью анализа изображений в фреймворке 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:) работает следующим образом:

  1. Преобразует переданный UIImage в CIImage.
  2. Запрашивает DetectHorizonRequest.
  3. ХранитHorizonObservation, полученный из обработанного изображения методом perform(on:orientation:).
  4. Возвращает результат.

HorizonObservation возвращает:

Прежде чем перейти к интеграции обнаружения горизонта, имейте в виду, что 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, представляющий собой обнаруженный угол горизонта, на основе которого создается линия, имеющая наклон.

Функция рисует линию в соответствии со следующей логикой:

  1. Получает центр прямоугольника рисования, к которому будет привязана линия.
  2. Вычисляет половину ширины прямоугольника, которая используется для продления линии в обе стороны от центра
  3. Объявляет переменные для горизонтального и вертикального смещения. dx и dy указывают, на какое расстояние нужно отступить от центра представления, чтобы нарисовать конечные точки линии. Подумайте вот о чем: если вы расположили линейку по центру области рисования, то эти смещения определяют, насколько нужно сдвинуть концы влево/вправо (dx) и вверх/вниз (dy) в зависимости от угла наклона горизонта. Они гарантируют, что независимо от наклона линии, она останется в центре, а ее конечные точки будут правильно расположены в соответствии с обнаруженным углом.
  4. Она вычисляет их:
    • если угол не равен нулю, они рассчитываются с помощью косинуса — чтобы определить, насколько влево или вправо нужно двигаться, — и синуса — насколько вверх или вниз нужно двигаться — от определенного угла;
    • Если угол равен нулю, он просто присваивает dx половину длины, а dy — ноль, в результате чего получается идеально горизонтальная линия, что и требуется для построения линии, работающей как направляющая.
  5. Определяет начальную и конечную точки линии горизонта: начальная точка линии устанавливается в (centerX - dx, centerY - dy) — перемещение влево — и конечная точка в (centerX + dx, centerY + dy) — перемещение вправо — Это гарантирует, что линия будет центрирована в поле зрения и правильно повернута в соответствии с обнаруженным углом горизонта.
  6. Перемещается к начальной точке и добавляет линию к конечной точке, чтобы окончательно нарисовать линию.

Давайте интегрируем линии в представление.

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, чтобы поместить их между изображением и кнопкой:

  1. Первая линия, красная, будет представлять обнаруженную линию горизонта сразу после анализа, проведенного Vision на фотографии;
  2. Вторая, зеленая, будет служить ориентиром для выделения разницы между обнаруженным и идеальным уровнем горизонта.

Теперь, когда мы видим идеальный уровень горизонта и обнаруженный, мы можем попробовать использовать значение угла, чтобы повернуть фотографию и линию горизонта, чтобы они совпали с ориентиром.

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)))
        }
    ...
}

Источник

Exit mobile version