1 /*
2  * Copyright 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.camera.integration.diagnose
18 
19 import android.graphics.PointF
20 import android.graphics.RectF
21 import android.util.Log
22 import android.util.Size
23 import com.google.mlkit.vision.barcode.common.Barcode
24 
25 /**
26  * Calibration object that checks camera alignment with list of {@link Barcode}.
27  *
28  * @param previewViewSize - size of the devices' preview view.
29  */
30 class Calibration(private val previewViewSize: Size) {
31 
32     // barcodeCoordinates
33     private lateinit var barCodes: List<Barcode>
34     private var topLeft: PointF? = null
35     private var topRight: PointF? = null
36     private var bottomLeft: PointF? = null
37     private var bottomRight: PointF? = null
38 
39     // gridlines to be drawn
40     var topGrid: Pair<PointF, PointF>? = null
41     var bottomGrid: Pair<PointF, PointF>? = null
42     var leftGrid: Pair<PointF, PointF>? = null
43     var rightGrid: Pair<PointF, PointF>? = null
44 
45     var topY: Float? = null
46     var bottomY: Float? = null
47     var leftX: Float? = null
48     var rightX: Float? = null
49 
50     // threshold box
51     // TODO: switch back to private if I don't need to draw threshold box
52     var thresholdTopLeft: RectF? = null
53     var thresholdTopRight: RectF? = null
54     var thresholdBottomLeft: RectF? = null
55     var thresholdBottomRight: RectF? = null
56 
57     var isAligned: Boolean = false
58 
analyzenull59     fun analyze(barCodes: List<Barcode>) {
60         setBarcodes(barCodes)
61         calculateBarcodeCoordinates()
62         calculateGridLines()
63         isAligned = checkAlignment()
64         Log.d(TAG, "isAligned = $isAligned")
65     }
66 
setBarcodesnull67     private fun setBarcodes(barCodes: List<Barcode>) {
68         this.barCodes = barCodes
69     }
70 
calculateBarcodeCoordinatesnull71     private fun calculateBarcodeCoordinates() {
72         topLeft = findCenterPoint("top-left")
73         topRight = findCenterPoint("top-right")
74         bottomLeft = findCenterPoint("bottom-left")
75         bottomRight = findCenterPoint("bottom-right")
76     }
77 
78     /**
79      * Calculate the target grid lines based on the average distance between x & y coordinates
80      * corresponding {@link Barcode}.
81      */
calculateGridLinesnull82     private fun calculateGridLines() {
83         if (!hasBarcodes()) {
84             Log.d(TAG, " hasBarcodes = ${hasBarcodes()}, has ${barCodes.size} : $barCodes")
85             return
86         }
87         Log.d(TAG, "has all barcodes")
88 
89         // calculate grid
90         topY = (topLeft!!.y + topRight!!.y) / 2
91         topGrid = Pair(PointF(0F, topY!!), PointF(previewViewSize.width.toFloat(), topY!!))
92         bottomY = (bottomLeft!!.y + bottomRight!!.y) / 2
93         bottomGrid = Pair(PointF(0F, bottomY!!), PointF(previewViewSize.width.toFloat(), bottomY!!))
94         leftX = (topLeft!!.x + bottomLeft!!.x) / 2
95         leftGrid = Pair(PointF(leftX!!, 0F), PointF(leftX!!, previewViewSize.height.toFloat()))
96         rightX = (topRight!!.x + bottomRight!!.x) / 2
97         rightGrid = Pair(PointF(rightX!!, 0F), PointF(rightX!!, previewViewSize.height.toFloat()))
98     }
99 
getThresholdBoxnull100     private fun getThresholdBox(x: Float, y: Float): RectF {
101         return RectF(x - THRESHOLD, y - THRESHOLD, x + THRESHOLD, y + THRESHOLD)
102     }
103 
containPointnull104     private fun containPoint(point: PointF?, thresholdBox: RectF?): Boolean {
105         return point?.let { thresholdBox?.contains(it.x, it.y) } == true
106     }
107 
108     /**
109      * @return true if all {@link Barcode} center points are found within the corresponding
110      *   threshold boxes calculated around intersection points of the target grid line.
111      */
checkAlignmentnull112     private fun checkAlignment(): Boolean {
113         // create threshold boxes around the grid's intersection points
114         topY?.let {
115             thresholdTopLeft = leftX?.let { getThresholdBox(it, topY!!) }
116             thresholdTopRight = rightX?.let { getThresholdBox(it, topY!!) }
117         }
118         bottomY?.let {
119             thresholdBottomLeft = leftX?.let { getThresholdBox(it, bottomY!!) }
120             thresholdBottomRight = rightX?.let { getThresholdBox(it, bottomY!!) }
121         }
122         // check if all barcode center points are within threshold
123         return containPoint(topLeft, thresholdTopLeft) &&
124             containPoint(topRight, thresholdTopRight) &&
125             containPoint(bottomLeft, thresholdBottomLeft) &&
126             containPoint(bottomRight, thresholdBottomRight)
127     }
128 
129     // check if all 4 barcodes are detected
hasBarcodesnull130     private fun hasBarcodes(): Boolean {
131         return topLeft != null && topRight != null && bottomLeft != null && bottomRight != null
132     }
133 
findCenterPointnull134     private fun findCenterPoint(position: String): PointF? {
135         for (barcode in barCodes) {
136             val boundingBox = barcode.boundingBox
137             if (barcode.rawValue == position && boundingBox != null) {
138                 return PointF(boundingBox.exactCenterX(), boundingBox.exactCenterY())
139             }
140         }
141         return null
142     }
143 
144     companion object {
145         private const val TAG = "Calibration"
146         private const val THRESHOLD = 4F
147     }
148 }
149