<p>The <a href="https://arxiv.org/pdf/1704.03155v2.pdf" rel="nofollow noreferrer">paper</a> contains a diagram of the output format. Instead of specifying the box in a usual way, it is specified as a set of distances (up, right, down, and left) from an offset (x, y), in addition to an angle A, the amount box has rotated counterclockwise.
<a href="https://i.stack.imgur.com/syHjM.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/syHjM.png" alt="image from Arxiv paper"></a></p>

<p>Note that the <code>scores</code> and <code>geometry</code> are indexed by <code>y, x</code>, opposite of any logic below <code>offset</code> calculation. Therefore, to get the geometry components of a highest scoring <code>y, x</code>:</p>

<pre><code>high_scores_yx = np.where(scores[0][0] &gt;= np.max(scores[0][0]))
y, x = high_scores_yx[0][0], high_scores_yx[1][0]
h_upper, w_right, h_lower, w_left, A = geometry[0,:,y,x]
</code></pre>

<p>The code uses <code>offset</code> to store the offset of the lower-right corner of the rectangle. Since it's the lower-right, it only needs <code>w_right</code> and <code>h_lower</code>, which in the code, are <code>x1_data</code> and <code>x2_data</code>, respectively.</p>

<p><a href="https://i.stack.imgur.com/HWSMl.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/HWSMl.png" alt="box_diagram"></a></p>

<p>The location of the lower-right corner, with respect to the original offset <code>offsetX, offsetY</code>, depends on the angle of rotation. Below, the dotted lines show the axes orientation. The components to get from the original to the lower-bottom offset are labelled in violet (horizontal) and purple (vertical). Note that the <code>sin(A) * w_right</code> component is <em>subtracted</em> because <code>y</code> gets bigger as you go lower, in this coordinate system.</p>

<p><a href="https://i.stack.imgur.com/JhCqU.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/JhCqU.png" alt="rotation_diagram"></a></p>

<p>So that explains</p>

<pre><code>offset = ([offsetX + cosA * x1_data[x] + sinA * x2_data[x], offsetY - sinA * x1_data[x] + cosA * x2_data[x]])
</code></pre>

<p>Next: <code>p1</code> and <code>p3</code> are the lower-left and upper-right corners of the rectangle, respectively, with rotation taken into account. <code>center</code> is just the average of these two points.</p>

<p>Finally, <code>-1*angle * 180.0 / math.pi</code> converts the original counterclockwise, radians angle into a clockwise-based, degrees angle (so final output angle should be negative for objects rotated counterclockwise). This is for compatibility with the CV2 <code>boxPoints</code> method, used in:</p>

<p><a href="https://github.com/opencv/opencv/blob/7fb70e170154d064ef12d8fec61c0ae70812ce3d/samples/dnn/text_detection.py" rel="nofollow noreferrer">https://github.com/opencv/opencv/blob/7fb70e170154d064ef12d8fec61c0ae70812ce3d/samples/dnn/text_detection.py</a></p>
Decoding geometry output of EAST text detection
<p>I'm trying to use the EAST model in OpenCV to detect text in images. I'm successfuly getting the output after I run an image through a network but I'm having a hard time understanding how the decode function I use works. I know that I get 5 numbers as output from the model and I think it's the distances from a point to the top, bottom, left and right sides of the rectangle, respectively, and the angle of rotation at the end. I'm not sure what the decode function does to get the bounding box for the text region.</p>

<p>I know why the offset is multiplied by 4 (it's shrunk by 4 when run through the model). I know why h and w are what they are. I'm not sure about anything after that.</p>

<p>scores are the confidence scores for each region; 
geometry are the geometry values for each region (the 5 numbers I mentioned)
scoreThresh is just a threshold for the non-maximum suppresion</p>

<pre><code>def decode(scores, geometry, scoreThresh):
    detections = []
    confidences = []

    ############ CHECK DIMENSIONS AND SHAPES OF geometry AND scores ############
    assert len(scores.shape) == 4, "Incorrect dimensions of scores"
    assert len(geometry.shape) == 4, "Incorrect dimensions of geometry"
    assert scores.shape[0] == 1, "Invalid dimensions of scores"
    assert geometry.shape[0] == 1, "Invalid dimensions of geometry"
    assert scores.shape[1] == 1, "Invalid dimensions of scores"
    assert geometry.shape[1] == 5, "Invalid dimensions of geometry"
    assert scores.shape[2] == geometry.shape[2], "Invalid dimensions of scores and geometry"
    assert scores.shape[3] == geometry.shape[3], "Invalid dimensions of scores and geometry"
    height = scores.shape[2]
    width = scores.shape[3]
    for y in range(0, height):

        # Extract data from scores
        scoresData = scores[0][0][y]
        x0_data = geometry[0][0][y]
        x1_data = geometry[0][1][y]
        x2_data = geometry[0][2][y]
        x3_data = geometry[0][3][y]
        anglesData = geometry[0][4][y]
        for x in range(0, width):
            score = scoresData[x]

            # If score is lower than threshold score, move to next x
            if(score &lt; scoreThresh):
                continue

            # Calculate offset
            offsetX = x * 4.0
            offsetY = y * 4.0
            angle = anglesData[x]

            # Calculate cos and sin of angle
            cosA = math.cos(angle)
            sinA = math.sin(angle)
            h = x0_data[x] + x2_data[x]
            w = x1_data[x] + x3_data[x]

            # Calculate offset
            offset = ([offsetX + cosA * x1_data[x] + sinA * x2_data[x], offsetY - sinA * x1_data[x] + cosA * x2_data[x]])

            # Find points for rectangle
            p1 = (-sinA * h + offset[0], -cosA * h + offset[1])
            p3 = (-cosA * w + offset[0],  sinA * w + offset[1])
            center = (0.5*(p1[0]+p3[0]), 0.5*(p1[1]+p3[1]))
            detections.append((center, (w,h), -1*angle * 180.0 / math.pi))
            confidences.append(float(score))

    # Return detections and confidences
    return [detections, confidences]
</code></pre>