Skip to content

Extracting Multiple Signals from One Image: Boundaries for OCR, Object Detection, and QR Processing

A dark 3D workbench where a visitor-pass image passes shared qualification, splits into OCR, object detection, and QR stations, and then converges on a policy decision
An image is uploaded once, but a service may need several kinds of information from it. Share input qualification and decoding while keeping each analysis result and failure meaning separate.

A single visitor pass can contain a name and company, a portrait, and an access QR code. The image is one input, but the service needs more than one answer.

This article is not a feature list for OCR, object detection, and QR decoding. It explains where to qualify the input, how to preserve useful results when one path fails, and how to keep detected facts separate from business decisions when three independent analyzers read the same image.

The example uses the bluetape4k-image Spring Boot Image Intelligence API. A visitor pass is the representative scenario, but the goal is not an all-purpose visitor-pass product. The focus is an executable, reusable baseline for qualification, parallel processing, partial failure, and policy separation.

One image rarely contains only one kind of information

Section titled “One image rarely contains only one kind of information”

When an image API starts from a single feature name, its response naturally narrows to that feature. An OCR API returns text, a barcode API returns a code value, and an object-detection API returns labels and confidence scores. Real business inputs, however, rarely arrive already separated by feature.

ScenarioWhat OCR readsWhat object detection findsWhat barcode or QR readsExample policy
Visitor or access passName, company, purposeFace or sensitive regionsAccess identifierAllow, manual review, restrict public preview
Shipping labelRecipient, address, itemHandling marks or document regionsTracking numberMask address, request another photo
Product labelProduct name, ingredients, expiryLogo or product regionProduct codeReview required labels, hold registration

All three scenarios lead to the same question: “How should a service extract different kinds of information from one image?” It is more natural for independent analyzers to read the same qualified image and combine their results later than for one feature to become an internal step of another.

OCR accuracy, detector selection, and QR error correction remain separate concerns. Part 1 begins with the boundaries required to connect all three capabilities to one API rather than with the internals of each algorithm.

Fix the processing boundaries with a visitor-pass scenario

Section titled “Fix the processing boundaries with a visitor-pass scenario”

The representative scenario is visitor-pass intake for an event venue. After a user uploads the pass, the service tries to obtain four things:

  1. Use OCR to determine whether readable text such as the visitor’s name and company exists.
  2. Use object detection to determine whether there is one face and whether the image includes sensitive regions.
  3. Decode QR data and determine whether there is one access identifier beginning with visitor:.
  4. Pass the three results to visitor-pass policy and choose ALLOW, MANUAL_REVIEW, REJECT, or QUARANTINE.

Steps 1 through 3 produce analysis facts from the image. Step 4 is a business decision made by the service. Reading visitor:PASS-001 from a QR code is not enough to allow access. Policy must also consider the face count, OCR result, sensitive regions, and any processing failures.

A diagram that overlays OCR text blocks, a face detection region, and an access QR region in different colors on one visitor pass and explains each result
The analyzers see the same image coordinate system but do not produce the same answer. OCR returns text, object detection returns classified objects, and QR decoding returns encoded values.

The processing paths share input, not answers

Section titled “The processing paths share input, not answers”

An integrated API does not need to force all three results into one common type. What they share is a qualified input image and a request lifetime. Their result fields and the meaning of “nothing found” remain specific to each path.

Processing pathRepresentative result in the current APIMeaning of an empty resultUnavailableFailed
OCRtext, pageCountNo readable textOCR provider not configuredTimeout or provider exception
Object detectionlabel, category, confidence, detectorNo object foundDetector not configuredDetector execution failed
Barcode or QRtext, format, providerNo code foundReader not configuredDecoder execution failed
Policyaction, reasonsNot applicableInsufficient analysis evidenceDefer automatic decision to review

Lower-level bluetape4k-image results may include coordinate regions and richer structures. The current integrated example, however, exposes only the fields shown above in its HTTP response. Instead of copying every library value into the API contract, it selects the data its consumers need.

Separate OCR, object detection, and barcode or QR result contract cards connect independently to a visitor-pass policy card, with no shared inheritance relationship
The three analysis results remain separate contracts. Policy consumes these results; it is not a parent type for them.

Qualify the input first and decode it once

Section titled “Qualify the input first and decode it once”

Before running three analyzers in parallel, the service determines whether the uploaded image is eligible to begin analysis. ImageUploadQualifier checks for an empty file, declared media type, compressed byte size, actual file signature, decodability, dimensions, and total pixel count.

The boundary must also state what this step does not decide. Whether OCR can read the text, whether the image contains a face, and whether it contains a QR code are analysis outcomes, not upload-qualification rules. Moving them into shared qualification would turn legitimate OCR or QR results into input errors.

val qualified = qualifier.qualify(upload)
val results = workflow.analyze(
image = qualified.image, // ImmutableImage decoded once
)

After qualification, the bytes are decoded once into an ImmutableImage. OCR, object detection, and QR processing read the same immutable image object. They do not reread the multipart upload or decode the same image independently.

Operating an OCR service in production explains why compressed bytes and decoded pixel size need separate limits, along with the operational constraints of native OCR engines such as Tesseract. For multipart and in-memory input boundaries from an API-design perspective, see Kotlin API input boundaries.

Run independent work in parallel and retain each result

Section titled “Run independent work in parallel and retain each result”

After qualification, ImageIntelligenceWorkflow uses suspendParallelFlow from bluetape4k-workflow to run three tasks. The following pseudocode reduces the implementation to its essential structure:

suspendParallelFlow("image-intelligence-analysis") {
execute("ocr") {
context["analysis.ocr"] = runOcr(image)
WorkReport.success(context)
}
execute("detection") {
context["analysis.detection"] = runDetection(image)
WorkReport.success(context)
}
execute("barcode") {
context["analysis.barcode"] = runBarcode(image)
WorkReport.success(context)
}
}

Each path has its own timeout and Semaphore. A slow OCR provider should not force the QR provider to use the same concurrency limit. These settings are more than performance decoration: they protect native resources and CPU workloads with different cost profiles.

The meaning of WorkReport.Success deserves special attention. It does not mean “all three analyzers produced values.” It means “the workflow task recorded the promised result contract in WorkContext.” An OCR path can record AnalysisResult.Failed and still return WorkReport.Success because it successfully delivered that failure result.

A vertical flow that qualifies an image upload, decodes it once, runs independent OCR, object detection, and barcode or QR tasks, and passes partial results into visitor-pass policy
The three paths read the same image but keep distinct results and failure meanings. Shared qualification decides only whether analysis may begin; policy makes the business decision after aggregation.

Do not disguise partial failure as normal success

Section titled “Do not disguise partial failure as normal success”

Each processing path distinguishes four outcomes:

  • Completed: The analyzer produced a value.
  • Empty: The provider ran successfully but found nothing.
  • Unavailable: The provider is not configured or cannot currently be used.
  • Failed: The provider ran but could not produce a result because of a timeout or execution error.

If Empty and Failed both become an empty array, policy cannot distinguish “the image has no QR code” from “the service could not inspect the QR code.” Those cases can lead to different approval decisions, so the API preserves both states.

ImageIntelligenceAggregator aggregates the three results as follows:

Aggregate statusExample pathsInterpretation in the current visitor-pass policy
COMPLETEDAll paths are Completed or EmptyALLOW when all required visitor-pass information is valid
PARTIALOCR Failed; object detection and QR CompletedPreserve successful results and return MANUAL_REVIEW
FAILEDNo usable result remainsPreserve failure reasons and return MANUAL_REVIEW

Aggregate status and business decision are not the same value. Even when every path runs normally and the aggregate status is COMPLETED, policy chooses REJECT if the QR value does not begin with visitor:. Conversely, an aggregate status of FAILED does not automatically become HTTP 500 or an arbitrary rejection. The current policy returns MANUAL_REVIEW with specific reasons so that a person can inspect the request.

External request cancellation is another kind of event. GuardedAnalysisRunner converts only its own timeout into a path-specific Failed(timeout) result and rethrows cancellation from the parent coroutine. Wrapping a cancelled request as three failed analyses would mix the work lifecycle with business results.

Separate detected facts from business policy

Section titled “Separate detected facts from business policy”

Finding a face region is an analysis fact. Whether the service blurs the face, restricts access to the original, or routes the pass to manual review is a policy decision.

VisitorPassPolicy combines sensitive regions, QR format and prefix, face count, OCR content, and path failure states. Because policy is not embedded in an analysis provider, a shipping-label or product-label service can keep the same OCR, detection, and QR orchestration while replacing only its decision rules.

The separation also helps with privacy controls. A detector can report the presence and location of a face, while policy and post-processing decide whether to blur it in an API preview or restrict the original behind separate authorization. Replacing an analysis model and changing a privacy policy do not have to become the same deployment unit.

The example’s demo profile uses deterministic fixtures for OCR and object detection. Only the QR path runs a real ZxingBarcodeReader. This combination reproduces orchestration and failure contracts; it does not prove the accuracy or throughput of a production ML detector. A real service still needs a production ImageDetector, quality measurement, model versioning, and drift monitoring.

What this example provides—and what it does not

Section titled “What this example provides—and what it does not”

The example is not a finished product for a particular business. It is an executable baseline composed from four boundaries:

Reusable baselineDecisions left to the service
Upload qualification and one-time decodingMalware scanning, retention and deletion, per-tenant quotas
Independent OCR, detection, and QR executionProduction OCR and ML providers, model quality thresholds
Preservation of Completed, Empty, Unavailable, and FailedRetries, circuit breakers, process isolation
Separation of analysis facts and visitor-pass policyEncryption, masking, access control, audit history

Choosing an image backend is a separate decision. From pure JVM to libvips compares processing cost and operational constraints with measured results. Running three analyzers in parallel does not make decoding and preprocessing costs disappear.

Part 1 established the overall processing boundaries. The remaining articles narrow each boundary to implementation and test behavior:

Part 2 will examine how the input boundary distinguishes a declared MIME type from the actual signature, a small compressed file from an excessive decoded pixel count, and a readable image header from a full decode failure.

When reading the implementation, start with the README and then follow the input, orchestration, provider, and policy boundaries:

Comments

Leave a note or reaction with your GitHub account.