e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
169 lines
6.5 KiB
Swift
169 lines
6.5 KiB
Swift
// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import CoreGraphics
|
|
import Foundation
|
|
|
|
// MARK: - Detection Result
|
|
|
|
/// The result of running text detection on an image.
|
|
///
|
|
/// Contains the detected text boxes and per-stage timing metrics for performance analysis.
|
|
struct DetectionResult {
|
|
/// Detected text region bounding polygons with confidence scores.
|
|
let boxes: [DetectionBox]
|
|
/// Seconds spent in preprocessing (resize, normalize, HWC-to-CHW).
|
|
let preprocessTime: TimeInterval
|
|
/// Seconds spent in ONNX Runtime inference.
|
|
let inferenceTime: TimeInterval
|
|
/// Seconds spent in DB postprocessing (threshold, contours, polygon offset / unclip).
|
|
let postprocessTime: TimeInterval
|
|
/// Total detection time (preprocess + inference + postprocess).
|
|
let totalTime: TimeInterval
|
|
/// Detection model input tensor shape, e.g. [1, 3, H, W].
|
|
let inputTensorShape: [Int]
|
|
}
|
|
|
|
// MARK: - Detection Engine Errors
|
|
|
|
enum DetectionEngineError: LocalizedError {
|
|
case noOutputTensor
|
|
case unexpectedOutputShape([Int])
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .noOutputTensor:
|
|
return "Detection model produced no output tensor"
|
|
case .unexpectedOutputShape(let shape):
|
|
return "Unexpected detection output shape: \(shape), expected [1, 1, H, W]"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - DetectionEngine
|
|
|
|
/// Orchestrates text detection: CGImage → preprocess → ORT inference → postprocess.
|
|
///
|
|
/// This is the integration layer that composes `DetPreprocessor`, `ORTSessionManager`, and
|
|
/// `DBPostProcessor` into a single callable unit.
|
|
///
|
|
/// Usage:
|
|
/// ```swift
|
|
/// let engine = try DetectionEngine(sessionManager: manager)
|
|
/// let result = try await engine.detect(cgImage)
|
|
/// print("Found \(result.boxes.count) text regions in \(result.totalTime)s")
|
|
/// ```
|
|
class DetectionEngine {
|
|
private let sessionManager: ORTSessionManager
|
|
private let preprocessor: DetPreprocessor
|
|
private(set) var modelConfig: InferenceConfig
|
|
|
|
/// Initialize with an existing ORTSessionManager (models must already be loaded).
|
|
///
|
|
/// Loads the detection model config file from the bundle to configure
|
|
/// the preprocessor and postprocessor.
|
|
///
|
|
/// - Parameter sessionManager: A loaded ORTSessionManager with detection model ready.
|
|
/// - Throws: If the model config cannot be loaded or required fields are missing.
|
|
init(sessionManager: ORTSessionManager) throws {
|
|
self.sessionManager = sessionManager
|
|
|
|
// Load detection model config
|
|
let detPaths = try ModelConfig.detection()
|
|
let cfg = try InferenceConfig.load(from: detPaths.configPath)
|
|
self.modelConfig = cfg
|
|
self.preprocessor = try DetPreprocessor(config: cfg)
|
|
}
|
|
|
|
private func makePostProcessor(effective: EffectiveOCRParams) -> DBPostProcessor {
|
|
DBPostProcessor(
|
|
thresh: effective.detThresh,
|
|
boxThresh: effective.detBoxThresh,
|
|
maxCandidates: modelConfig.postProcess.maxCandidates,
|
|
unclipRatio: effective.detUnclipRatio
|
|
)
|
|
}
|
|
|
|
/// Run detection on a CGImage, returning bounding polygons with confidence scores.
|
|
///
|
|
/// Steps: DetResizeForTest → NormalizeImage → ToCHW → ORT inference → DBPostProcess
|
|
///
|
|
/// - Parameters:
|
|
/// - image: The input image to detect text regions in.
|
|
/// - runtimeParams: Detection postprocess / resize overrides (see ``OCRRuntimeParams``).
|
|
/// - Returns: A `DetectionResult` with boxes and per-stage timing metrics.
|
|
func detect(_ image: CGImage, runtimeParams: OCRRuntimeParams = .noOverrides) async throws -> DetectionResult {
|
|
let eff = OCRParameterResolver.effective(modelConfig: modelConfig, runtime: runtimeParams)
|
|
return try await detect(effective: eff) {
|
|
try preprocessor.preprocess(image, detResize: eff.detResize)
|
|
}
|
|
}
|
|
|
|
private func detect(
|
|
effective: EffectiveOCRParams,
|
|
preprocess: () throws -> PreprocessResult
|
|
) async throws -> DetectionResult {
|
|
let postprocessor = makePostProcessor(effective: effective)
|
|
// Step 1: Preprocess
|
|
let preprocessStart = CFAbsoluteTimeGetCurrent()
|
|
let preprocessed = try preprocess()
|
|
let preprocessTime = CFAbsoluteTimeGetCurrent() - preprocessStart
|
|
|
|
// Step 2: Run ORT inference
|
|
let inferenceStart = CFAbsoluteTimeGetCurrent()
|
|
let outputs = try await sessionManager.runDetection(
|
|
inputData: preprocessed.tensorData,
|
|
shape: preprocessed.tensorShape
|
|
)
|
|
let inferenceTime = CFAbsoluteTimeGetCurrent() - inferenceStart
|
|
|
|
// Step 3: Extract the probability map from ORT output
|
|
// The det model outputs a single tensor with shape [1, 1, H, W]
|
|
guard let firstOutput = outputs.values.first else {
|
|
throw DetectionEngineError.noOutputTensor
|
|
}
|
|
let outputData = firstOutput.data
|
|
let outputShape = firstOutput.shape
|
|
|
|
// Output shape should be [1, 1, H, W]
|
|
guard outputShape.count == 4 else {
|
|
throw DetectionEngineError.unexpectedOutputShape(outputShape)
|
|
}
|
|
let tensorHeight = outputShape[2]
|
|
let tensorWidth = outputShape[3]
|
|
|
|
// Step 4: Postprocess
|
|
let postprocessStart = CFAbsoluteTimeGetCurrent()
|
|
let boxes = postprocessor.process(
|
|
outputTensor: outputData,
|
|
tensorHeight: tensorHeight,
|
|
tensorWidth: tensorWidth,
|
|
originalWidth: preprocessed.originalSize.width,
|
|
originalHeight: preprocessed.originalSize.height
|
|
)
|
|
let postprocessTime = CFAbsoluteTimeGetCurrent() - postprocessStart
|
|
|
|
let totalTime = preprocessTime + inferenceTime + postprocessTime
|
|
|
|
return DetectionResult(
|
|
boxes: boxes,
|
|
preprocessTime: preprocessTime,
|
|
inferenceTime: inferenceTime,
|
|
postprocessTime: postprocessTime,
|
|
totalTime: totalTime,
|
|
inputTensorShape: preprocessed.tensorShape
|
|
)
|
|
}
|
|
}
|