Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

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