# RGB Rotate Warp

This example shows usage of [ImageManip](https://docs.luxonis.com/software/depthai-components/nodes/image_manip.md) to crop a
rotated rectangle area on a frame, or perform various image transforms: rotate, mirror, flip, perspective transform.

## Setup

Please run the [install script](https://github.com/luxonis/depthai-python/blob/main/examples/install_requirements.py) to download
all required dependencies. Please note that this script must be ran from git context, so you have to download the
[depthai-python](https://github.com/luxonis/depthai-python) repository first and then run the script

```bash
git clone https://github.com/luxonis/depthai-python.git
cd depthai-python/examples
python3 install_requirements.py
```

For additional information, please follow the [installation guide](https://docs.luxonis.com/software/depthai/manual-install.md).

## Demo

```bash
Controls:
z -rotated rectangle crop, decrease rate
x -rotated rectangle crop, increase rate
c -warp 4-point transform, cycle through modes
v -resize cropped region, or disable resize
h -print controls (help)
```

## Source code

#### Python

```python
#!/usr/bin/env python3

"""
This example shows usage of ImageManip to crop a rotated rectangle area on a frame,
or perform various image transforms: rotate, mirror, flip, perspective transform.
"""

import depthai as dai
import cv2
import numpy as np

keyRotateDecr = 'z'
keyRotateIncr = 'x'
keyResizeInc = 'v'
keyWarpTestCycle = 'c'

def printControls():
    print("=== Controls:")
    print(keyRotateDecr, "-rotated rectangle crop, decrease rate")
    print(keyRotateIncr, "-rotated rectangle crop, increase rate")
    print(keyWarpTestCycle, "-warp 4-point transform, cycle through modes")
    print(keyResizeInc, "-resize cropped region, or disable resize")
    print("h -print controls (help)")

rotateRateMax = 5.0
rotateRateInc = 0.1

resizeMaxW = 800
resizeMaxH = 600
resizeFactorMax = 5

'''
The crop points are specified in clockwise order,
with first point mapped to output top-left, as:
    P0  ->  P1
     ^       v
    P3  <-  P2
'''
P0 = [0, 0]  # top-left
P1 = [1, 0]  # top-right
P2 = [1, 1]  # bottom-right
P3 = [0, 1]  # bottom-left

warpList = [
    # points order, normalized cordinates, description
    # [[[0, 0], [1, 0], [1, 1], [0, 1]], True, "passthrough"],
    # [[[0, 0], [639, 0], [639, 479], [0, 479]], False, "passthrough (pixels)"],
    [[P0, P1, P2, P3], True, "1. passthrough"],
    [[P3, P0, P1, P2], True, "2. rotate 90"],
    [[P2, P3, P0, P1], True, "3. rotate 180"],
    [[P1, P2, P3, P0], True, "4. rotate 270"],
    [[P1, P0, P3, P2], True, "5. horizontal mirror"],
    [[P3, P2, P1, P0], True, "6. vertical flip"],
    [[[-0.1, -0.1], [1.1, -0.1], [1.1, 1.1], [-0.1, 1.1]], True, "7. add black borders"],
    [[[-0.3, 0], [1, 0], [1.3, 1], [0, 1]], True, "8. parallelogram transform"],
    [[[-0.2, 0], [1.8, 0], [1, 1], [0, 1]], True, "9. trapezoid transform"],
]

# Create pipeline
pipeline = dai.Pipeline()

# Define sources and outputs
camRgb = pipeline.create(dai.node.ColorCamera)
manip = pipeline.create(dai.node.ImageManip)

camOut = pipeline.create(dai.node.XLinkOut)
manipOut = pipeline.create(dai.node.XLinkOut)
manipCfg = pipeline.create(dai.node.XLinkIn)

camOut.setStreamName("preview")
manipOut.setStreamName("manip")
manipCfg.setStreamName("manipCfg")

# Properties
camRgb.setPreviewSize(640, 480)
camRgb.setResolution(dai.ColorCameraProperties.SensorResolution.THE_1080_P)
camRgb.setInterleaved(False)
camRgb.setColorOrder(dai.ColorCameraProperties.ColorOrder.BGR)
manip.setMaxOutputFrameSize(2000 * 1500 * 3)

# Linking
camRgb.preview.link(camOut.input)
camRgb.preview.link(manip.inputImage)
manip.out.link(manipOut.input)
manipCfg.out.link(manip.inputConfig)

# Connect to device and start pipeline
with dai.Device(pipeline) as device:

    # Create input & output queues
    qPreview = device.getOutputQueue(name="preview", maxSize=4)
    qManip = device.getOutputQueue(name="manip", maxSize=4)
    qManipCfg = device.getInputQueue(name="manipCfg")

    key = -1
    angleDeg = 0
    rotateRate = 1.0
    resizeFactor = 0
    resizeX = 0
    resizeY = 0
    testFourPt = False
    warpIdx = -1

    printControls()

    while key != ord('q'):
        if key > 0:
            print("Pressed: ", key)
            if key == ord(keyRotateDecr) or key == ord(keyRotateIncr):
                if key == ord(keyRotateDecr):
                    if rotateRate > -rotateRateMax:
                        rotateRate -= rotateRateInc
                if key == ord(keyRotateIncr):
                    if rotateRate < rotateRateMax:
                        rotateRate += rotateRateInc
                testFourPt = False
                print("Crop rotated rectangle, rate per frame: {:.1f} degrees".format(rotateRate))
            elif key == ord(keyResizeInc):
                resizeFactor += 1
                if resizeFactor > resizeFactorMax:
                    resizeFactor = 0
                    print("Crop region not resized")
                else:
                    resizeX = resizeMaxW // resizeFactor
                    resizeY = resizeMaxH // resizeFactor
                    print("Crop region resized to: ", resizeX, 'x', resizeY)
            elif key == ord(keyWarpTestCycle):
                # Disable resizing initially
                resizeFactor = 0
                warpIdx = (warpIdx + 1) % len(warpList)
                testFourPt = True
                testDescription = warpList[warpIdx][2]
                print("Warp 4-point transform: ", testDescription)
            elif key == ord('h'):
                printControls()

        # Send an updated config with continuous rotate, or after a key press
        if key >= 0 or (not testFourPt and abs(rotateRate) > 0.0001):
            cfg = dai.ImageManipConfig()
            if testFourPt:
                test = warpList[warpIdx]
                points, normalized = test[0], test[1]
                point2fList = []
                for p in points:
                    pt = dai.Point2f()
                    pt.x, pt.y = p[0], p[1]
                    point2fList.append(pt)
                cfg.setWarpTransformFourPoints(point2fList, normalized)
            else:
                angleDeg += rotateRate
                rotatedRect = ((320, 240), (400, 400), angleDeg)
                rr = dai.RotatedRect()
                rr.center.x, rr.center.y = rotatedRect[0]
                rr.size.width, rr.size.height = rotatedRect[1]
                rr.angle = rotatedRect[2]
                cfg.setCropRotatedRect(rr, False)
            if resizeFactor > 0:
                cfg.setResize(resizeX, resizeY)
            # cfg.setWarpBorderFillColor(255, 0, 0)
            # cfg.setWarpBorderReplicatePixels()
            qManipCfg.send(cfg)

        for q in [qPreview, qManip]:
            pkt = q.get()
            name = q.getName()
            shape = (3, pkt.getHeight(), pkt.getWidth())
            frame = pkt.getCvFrame()
            if name == "preview" and not testFourPt:
                # Draw RotatedRect cropped area on input frame
                points = np.int0(cv2.boxPoints(rotatedRect))
                cv2.drawContours(frame, [points], 0, (255, 0, 0), 1)
                # Mark top-left corner
                cv2.circle(frame, tuple(points[1]), 10, (255, 0, 0), 2)
            cv2.imshow(name, frame)
        key = cv2.waitKey(1)
```

#### C++

```cpp
#include <iostream>

#include "depthai/depthai.hpp"
#include "utility.hpp"

static constexpr auto keyRotateDecr = 'z';
static constexpr auto keyRotateIncr = 'x';
static constexpr auto keyResizeInc = 'v';
static constexpr auto keyWarpTestCycle = 'c';

void printControls() {
    printf("\n=== Controls:\n");
    printf(" %c -rotated rectangle crop, decrease rate\n", keyRotateDecr);
    printf(" %c -rotated rectangle crop, increase rate\n", keyRotateIncr);
    printf(" %c -warp 4-point transform, cycle through modes\n", keyWarpTestCycle);
    printf(" %c -resize cropped region, or disable resize\n", keyResizeInc);
    printf(" h -print controls (help)\n");
}

static constexpr auto ROTATE_RATE_MAX = 5.0f;
static constexpr auto ROTATE_RATE_INC = 0.1f;

static constexpr auto RESIZE_MAX_W = 800;
static constexpr auto RESIZE_MAX_H = 600;
static constexpr auto RESIZE_FACTOR_MAX = 5;

/* The crop points are specified in clockwise order,
 * with first point mapped to output top-left, as:
 *   P0  ->  P1
 *    ^       v
 *   P3  <-  P2
 */
static const dai::Point2f P0 = {0, 0};  // top-left
static const dai::Point2f P1 = {1, 0};  // top-right
static const dai::Point2f P2 = {1, 1};  // bottom-right
static const dai::Point2f P3 = {0, 1};  // bottom-left
struct warpFourPointTest {
    std::vector<dai::Point2f> points;
    bool normalizedCoords;
    const char* description;
};

std::vector<warpFourPointTest> warpList = {
    //{{{  0,  0},{  1,  0},{  1,  1},{  0,  1}}, true, "passthrough"},
    //{{{  0,  0},{639,  0},{639,479},{  0,479}}, false,"passthrough (pixels)"},
    {{P0, P1, P2, P3}, true, "1. passthrough"},
    {{P3, P0, P1, P2}, true, "2. rotate 90"},
    {{P2, P3, P0, P1}, true, "3. rotate 180"},
    {{P1, P2, P3, P0}, true, "4. rotate 270"},
    {{P1, P0, P3, P2}, true, "5. horizontal mirror"},
    {{P3, P2, P1, P0}, true, "6. vertical flip"},
    {{{-0.1f, -0.1f}, {1.1f, -0.1f}, {1.1f, 1.1f}, {-0.1f, 1.1f}}, true, "7. add black borders"},
    {{{-0.3f, 0}, {1, 0}, {1.3f, 1}, {0, 1}}, true, "8. parallelogram transform"},
    {{{-0.2f, 0}, {1.8f, 0}, {1, 1}, {0, 1}}, true, "9. trapezoid transform"},
};

int main() {
    // Create pipeline
    dai::Pipeline pipeline;

    // Define sources and outputs
    auto camRgb = pipeline.create<dai::node::ColorCamera>();
    auto manip = pipeline.create<dai::node::ImageManip>();

    auto camOut = pipeline.create<dai::node::XLinkOut>();
    auto manipOut = pipeline.create<dai::node::XLinkOut>();
    auto manipCfg = pipeline.create<dai::node::XLinkIn>();

    camOut->setStreamName("preview");
    manipOut->setStreamName("manip");
    manipCfg->setStreamName("manipCfg");

    // Properties
    camRgb->setPreviewSize(640, 480);
    camRgb->setResolution(dai::ColorCameraProperties::SensorResolution::THE_1080_P);
    camRgb->setInterleaved(false);
    camRgb->setColorOrder(dai::ColorCameraProperties::ColorOrder::BGR);
    manip->setMaxOutputFrameSize(2000 * 1500 * 3);

    // Linking
    camRgb->preview.link(camOut->input);
    camRgb->preview.link(manip->inputImage);
    manip->out.link(manipOut->input);
    manipCfg->out.link(manip->inputConfig);

    // Connect to device and start pipeline
    dai::Device device(pipeline);

    // Create input & output queues
    auto qPreview = device.getOutputQueue("preview", 8, false);
    auto qManip = device.getOutputQueue("manip", 8, false);
    auto qManipCfg = device.getInputQueue("manipCfg");

    std::vector<decltype(qPreview)> frameQueues{qPreview, qManip};

    // keep processing data
    int key = -1;
    float angleDeg = 0;
    float rotateRate = 1.0;
    int resizeFactor = 0;
    int resizeX = 0;
    int resizeY = 0;
    bool testFourPt = false;
    int warpIdx = -1;

    printControls();

    while(key != 'q') {
        if(key >= 0) {
            printf("Pressed: %c | ", key);
            if(key == keyRotateDecr || key == keyRotateIncr) {
                if(key == keyRotateDecr) {
                    if(rotateRate > -ROTATE_RATE_MAX) rotateRate -= ROTATE_RATE_INC;
                } else if(key == keyRotateIncr) {
                    if(rotateRate < ROTATE_RATE_MAX) rotateRate += ROTATE_RATE_INC;
                }
                testFourPt = false;
                printf("Crop rotated rectangle, rate: %g degrees", rotateRate);
            } else if(key == keyResizeInc) {
                resizeFactor++;
                if(resizeFactor > RESIZE_FACTOR_MAX) {
                    resizeFactor = 0;
                    printf("Crop region not resized");
                } else {
                    resizeX = RESIZE_MAX_W / resizeFactor;
                    resizeY = RESIZE_MAX_H / resizeFactor;
                    printf("Crop region resized to: %d x %d", resizeX, resizeY);
                }
            } else if(key == keyWarpTestCycle) {
                resizeFactor = 0;  // Disable resizing initially
                warpIdx = (warpIdx + 1) % warpList.size();
                printf("Warp 4-point transform: %s", warpList[warpIdx].description);
                testFourPt = true;
            } else if(key == 'h') {
                printControls();
            }
            printf("\n");
        }

        // Send an updated config with continuous rotate, or after a key press
        if(key >= 0 || (!testFourPt && std::abs(rotateRate) > 0.0001)) {
            dai::ImageManipConfig cfg;
            if(testFourPt) {
                cfg.setWarpTransformFourPoints(warpList[warpIdx].points, warpList[warpIdx].normalizedCoords);
            } else {
                angleDeg += rotateRate;
                dai::RotatedRect rr = {{320, 240},  // center
                                       {640, 480},  //{400, 400}, // size
                                       angleDeg};
                cfg.setCropRotatedRect(rr, false);
            }
            if(resizeFactor > 0) {
                cfg.setResize(resizeX, resizeY);
            }
            // cfg.setWarpBorderFillColor(255, 0, 0);
            // cfg.setWarpBorderReplicatePixels();
            qManipCfg->send(cfg);
        }

        for(const auto& q : frameQueues) {
            auto img = q->get<dai::ImgFrame>();
            auto mat = toMat(img->getData(), img->getWidth(), img->getHeight(), 3, 1);
            cv::imshow(q->getName(), mat);
        }
        key = cv::waitKey(1);
    }
    return 0;
}
```

## Pipeline

### examples/rgb_rotate_warp.pipeline.json

```json
{
  "pipeline": {
    "connections": [
      {
        "node1Id": 0,
        "node1Output": "preview",
        "node1OutputGroup": "",
        "node2Id": 2,
        "node2Input": "in",
        "node2InputGroup": ""
      },
      {
        "node1Id": 0,
        "node1Output": "preview",
        "node1OutputGroup": "",
        "node2Id": 1,
        "node2Input": "inputImage",
        "node2InputGroup": ""
      },
      {
        "node1Id": 4,
        "node1Output": "out",
        "node1OutputGroup": "",
        "node2Id": 1,
        "node2Input": "inputConfig",
        "node2InputGroup": ""
      },
      {
        "node1Id": 1,
        "node1Output": "out",
        "node1OutputGroup": "",
        "node2Id": 3,
        "node2Input": "in",
        "node2InputGroup": ""
      }
    ],
    "globalProperties": {
      "calibData": null,
      "cameraTuningBlobSize": null,
      "cameraTuningBlobUri": "",
      "leonCssFrequencyHz": 700000000.0,
      "leonMssFrequencyHz": 700000000.0,
      "pipelineName": null,
      "pipelineVersion": null,
      "sippBufferSize": 18432,
      "sippDmaBufferSize": 16384,
      "xlinkChunkSize": -1
    },
    "nodes": [
      [
        0,
        {
          "id": 0,
          "ioInfo": [
            [
              [
                "",
                "inputConfig"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 1,
                "name": "inputConfig",
                "queueSize": 8,
                "type": 3,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "raw"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 6,
                "name": "raw",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "still"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 7,
                "name": "still",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "inputControl"
              ],
              {
                "blocking": true,
                "group": "",
                "id": 2,
                "name": "inputControl",
                "queueSize": 8,
                "type": 3,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "video"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 3,
                "name": "video",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "isp"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 4,
                "name": "isp",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "preview"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 5,
                "name": "preview",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "frameEvent"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 8,
                "name": "frameEvent",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ]
          ],
          "name": "ColorCamera",
          "properties": {
            "boardSocket": -1,
            "cameraName": "",
            "colorOrder": 0,
            "fp16": false,
            "fps": 30.0,
            "imageOrientation": -1,
            "initialControl": {
              "aeLockMode": false,
              "aeMaxExposureTimeUs": 0,
              "aeRegion": {
                "height": 0,
                "priority": 0,
                "width": 0,
                "x": 0,
                "y": 0
              },
              "afRegion": {
                "height": 0,
                "priority": 0,
                "width": 0,
                "x": 0,
                "y": 0
              },
              "antiBandingMode": 0,
              "autoFocusMode": 3,
              "awbLockMode": false,
              "awbMode": 0,
              "brightness": 0,
              "captureIntent": 0,
              "chromaDenoise": 0,
              "cmdMask": 0,
              "contrast": 0,
              "controlMode": 0,
              "effectMode": 0,
              "expCompensation": 0,
              "expManual": {
                "exposureTimeUs": 0,
                "frameDurationUs": 0,
                "sensitivityIso": 0
              },
              "frameSyncMode": 0,
              "lensPosAutoInfinity": 0,
              "lensPosAutoMacro": 0,
              "lensPosition": 0,
              "lensPositionRaw": 0.0,
              "lowPowerNumFramesBurst": 0,
              "lowPowerNumFramesDiscard": 0,
              "lumaDenoise": 0,
              "saturation": 0,
              "sceneMode": 0,
              "sharpness": 0,
              "strobeConfig": {
                "activeLevel": 0,
                "enable": 0,
                "gpioNumber": 0
              },
              "strobeTimings": {
                "durationUs": 0,
                "exposureBeginOffsetUs": 0,
                "exposureEndOffsetUs": 0
              },
              "wbColorTemp": 0
            },
            "interleaved": false,
            "isp3aFps": 0,
            "ispScale": {
              "horizDenominator": 0,
              "horizNumerator": 0,
              "vertDenominator": 0,
              "vertNumerator": 0
            },
            "numFramesPoolIsp": 3,
            "numFramesPoolPreview": 4,
            "numFramesPoolRaw": 3,
            "numFramesPoolStill": 4,
            "numFramesPoolVideo": 4,
            "previewHeight": 480,
            "previewKeepAspectRatio": true,
            "previewWidth": 640,
            "rawPacked": null,
            "resolution": 0,
            "sensorCropX": -1.0,
            "sensorCropY": -1.0,
            "stillHeight": -1,
            "stillWidth": -1,
            "videoHeight": -1,
            "videoWidth": -1
          }
        }
      ],
      [
        1,
        {
          "id": 1,
          "ioInfo": [
            [
              [
                "",
                "inputConfig"
              ],
              {
                "blocking": true,
                "group": "",
                "id": 9,
                "name": "inputConfig",
                "queueSize": 8,
                "type": 3,
                "waitForMessage": false
              }
            ],
            [
              [
                "",
                "inputImage"
              ],
              {
                "blocking": true,
                "group": "",
                "id": 10,
                "name": "inputImage",
                "queueSize": 8,
                "type": 3,
                "waitForMessage": true
              }
            ],
            [
              [
                "",
                "out"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 11,
                "name": "out",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ]
          ],
          "name": "ImageManip",
          "properties": {
            "initialConfig": {
              "cropConfig": {
                "cropRatio": 1.0,
                "cropRect": {
                  "xmax": 0.0,
                  "xmin": 0.0,
                  "ymax": 0.0,
                  "ymin": 0.0
                },
                "cropRotatedRect": {
                  "angle": 0.0,
                  "center": {
                    "x": 0.0,
                    "y": 0.0
                  },
                  "size": {
                    "height": 0.0,
                    "width": 0.0
                  }
                },
                "enableCenterCropRectangle": false,
                "enableRotatedRect": false,
                "normalizedCoords": true,
                "widthHeightAspectRatio": 1.0
              },
              "enableCrop": false,
              "enableFormat": false,
              "enableResize": false,
              "formatConfig": {
                "colormap": 0,
                "colormapMax": 255,
                "colormapMin": 0,
                "flipHorizontal": false,
                "flipVertical": false,
                "type": 32
              },
              "interpolation": -1,
              "resizeConfig": {
                "bgBlue": 0,
                "bgGreen": 0,
                "bgRed": 0,
                "enableRotation": false,
                "enableWarp4pt": false,
                "enableWarpMatrix": false,
                "height": 0,
                "keepAspectRatio": true,
                "lockAspectRatioFill": false,
                "normalizedCoords": true,
                "rotationAngleDeg": 0.0,
                "warpBorderReplicate": false,
                "warpFourPoints": [],
                "warpMatrix3x3": [],
                "width": 0
              },
              "reusePreviousImage": false,
              "skipCurrentImage": false
            },
            "meshHeight": 0,
            "meshUri": "",
            "meshWidth": 0,
            "numFramesPool": 4,
            "outputFrameSize": 9000000
          }
        }
      ],
      [
        2,
        {
          "id": 2,
          "ioInfo": [
            [
              [
                "",
                "in"
              ],
              {
                "blocking": true,
                "group": "",
                "id": 12,
                "name": "in",
                "queueSize": 8,
                "type": 3,
                "waitForMessage": true
              }
            ]
          ],
          "name": "XLinkOut",
          "properties": {
            "maxFpsLimit": -1.0,
            "metadataOnly": false,
            "streamName": "preview"
          }
        }
      ],
      [
        3,
        {
          "id": 3,
          "ioInfo": [
            [
              [
                "",
                "in"
              ],
              {
                "blocking": true,
                "group": "",
                "id": 13,
                "name": "in",
                "queueSize": 8,
                "type": 3,
                "waitForMessage": true
              }
            ]
          ],
          "name": "XLinkOut",
          "properties": {
            "maxFpsLimit": -1.0,
            "metadataOnly": false,
            "streamName": "manip"
          }
        }
      ],
      [
        4,
        {
          "id": 4,
          "ioInfo": [
            [
              [
                "",
                "out"
              ],
              {
                "blocking": false,
                "group": "",
                "id": 14,
                "name": "out",
                "queueSize": 8,
                "type": 0,
                "waitForMessage": false
              }
            ]
          ],
          "name": "XLinkIn",
          "properties": {
            "maxDataSize": 5242880,
            "numFrames": 8,
            "streamName": "manipCfg"
          }
        }
      ]
    ]
  }
}
```

### Need assistance?

Head over to [Discussion Forum](https://discuss.luxonis.com/) for technical support or any other questions you might have.
