# AI2D Application Development Guide

## Overview

`AI2D` is a hardware-accelerated image preprocessing module used on the board before inference. It reduces CPU overhead and improves end-to-end runtime performance. `AI2D` provides **five hardware preprocessing capabilities**:

- `Crop`
- `Shift`
- `Resize`
- `Pad`
- `Affine`

In visual tasks such as object detection, `Resize` and `Pad` are often combined to implement a standard **letterbox** preprocessing flow.

`AI2D` is mainly used during the **deployment stage on the board** to speed up preprocessing and improve inference throughput. For the C++ runtime interface details, refer to:

[AI2D Runtime API](../../api_reference/nncase/ai2d_runtime.md)

The sample source code is located at:

```bash
src/rtsmart/examples/ai/usage_ai2d
```

Build it in that directory:

```bash
./build_app.sh
```

The generated executables are placed in `k230_bin`. Copy the build outputs to the board before running the samples.

When using `AI2D`, note the following:

> **Note**
>
> 1. `Affine` and `Resize` are mutually exclusive and cannot be enabled at the same time.
> 1. `Shift` only supports `Raw16` input format.
> 1. `Pad` fill values are configured per channel, so the number of fill values must match the image channel count.
> 1. Even if you use only one AI2D function, the parameter structures for the other functions still need to be present; just set the corresponding enable flag to `false`.
> 1. When multiple functions are configured together, the execution order is fixed:
>
>    ```text
>    Crop -> Shift -> Resize / Affine -> Pad
>    ```
>
>    Make sure the configuration of each stage stays consistent in terms of shape and data format.

## Preprocessing Methods

### Resize Method

`Resize` is one of the most commonly used image preprocessing operations. It changes the image size before inference and is widely used in detection and classification tasks.

Source location:

```bash
src/rtsmart/examples/ai/usage_ai2d/test_resize
```

Typical flow:

1. Read input data.
1. Initialize the AI2D input tensor.
1. Initialize the AI2D output tensor according to the target shape.
1. Configure `ai2d_resize_param_t`.
1. Construct `ai2d_builder` and call `build_schedule()`.
1. Run the configured preprocessing path through `invoke()`.
1. Read the output tensor data.

The sample resizes an input image to `640 x 320`.

Sample code:

```c++
int main(int argc, char *argv[])
{
    std::cout << "case " << argv[0] << " build " << __DATE__ << " " << __TIME__ << std::endl;
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << "<image> <debug_mode>" << std::endl;
        return -1;
    }

    int debug_mode = atoi(argv[2]);

    // Read image and convert to CHW + RGB format
    cv::Mat ori_img = cv::imread(argv[1]);
    int ori_w = ori_img.cols;
    int ori_h = ori_img.rows;
    std::vector<uint8_t> chw_vec;
    std::vector<cv::Mat> bgrChannels(3);
    cv::split(ori_img, bgrChannels);
    for (auto i = 2; i > -1; i--)
    {
        std::vector<uint8_t> data = std::vector<uint8_t>(bgrChannels[i].reshape(1, 1));
        chw_vec.insert(chw_vec.end(), data.begin(), data.end());
    }

    // Create AI2D input tensor, copy CHW_RGB data into it, and flush to DDR
    dims_t ai2d_in_shape{1, 3, ori_h, ori_w};
    runtime_tensor ai2d_in_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_in_shape, hrt::pool_shared).expect("cannot create input tensor");
    auto input_buf = ai2d_in_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    memcpy(reinterpret_cast<char *>(input_buf.data()), chw_vec.data(), chw_vec.size());
    hrt::sync(ai2d_in_tensor, sync_op_t::sync_write_back, true).expect("write back input failed");

    // Resize target dimensions
    int out_w = 640;
    int out_h = 320;
    int size = out_w * out_h;

    // Create AI2D output tensor
    dims_t ai2d_out_shape{1, 3, out_h, out_w};
    runtime_tensor ai2d_out_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_out_shape, hrt::pool_shared).expect("cannot create input tensor");

    // Configure AI2D parameters. AI2D supports 5 preprocessing methods: crop/shift/pad/resize/affine.
    // Here resize is enabled.
    ai2d_datatype_t ai2d_dtype{ai2d_format::NCHW_FMT, ai2d_format::NCHW_FMT, ai2d_in_tensor.datatype(), ai2d_out_tensor.datatype()};
    ai2d_crop_param_t crop_param{false, 0, 0, 0, 0};
    ai2d_shift_param_t shift_param{false, 0};
    ai2d_pad_param_t pad_param{false, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, ai2d_pad_mode::constant, {0, 0, 0}};
    ai2d_resize_param_t resize_param{true, ai2d_interp_method::tf_bilinear, ai2d_interp_mode::half_pixel};
    ai2d_affine_param_t affine_param{false, ai2d_interp_method::cv2_bilinear, 0, 0, 127, 1, {0.5, 0.1, 0.0, 0.1, 0.5, 0.0}};

    // Build AI2D scheduler
    ai2d_builder builder(ai2d_in_shape, ai2d_out_shape, ai2d_dtype, crop_param, shift_param, pad_param, resize_param, affine_param);
    builder.build_schedule();
    // Run AI2D: preprocess from ai2d_in_tensor to ai2d_out_tensor
    builder.invoke(ai2d_in_tensor, ai2d_out_tensor).expect("error occurred in ai2d running");

    // Retrieve result and save as image
    auto output_buf = ai2d_out_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    cv::Mat image_r = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data());
    cv::Mat image_g = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data() + size);
    cv::Mat image_b = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data() + 2 * size);

    std::vector<cv::Mat> color_vec(3);
    color_vec.clear();
    color_vec.push_back(image_b);
    color_vec.push_back(image_g);
    color_vec.push_back(image_r);
    cv::Mat color_img;
    cv::merge(color_vec, color_img);
    cv::imwrite("test_resize.jpg", color_img);

    return 0;
}
```

Run it on the board:

```bash
./test_resize.elf test.jpg 2
```

Reference result:

![resize_res](https://www.kendryte.com/api/post/attachment?id=507)

### Crop Method

`Crop` extracts a region of interest (ROI) from the original image according to the specified coordinates and size.

Source location:

```bash
src/rtsmart/examples/ai/usage_ai2d/test_crop
```

Typical flow:

1. Read input data.
1. Initialize the AI2D input tensor.
1. Initialize the AI2D output tensor according to the cropped shape.
1. Configure `ai2d_crop_param_t`.
1. Construct `ai2d_builder` and call `build_schedule()`.
1. Run the preprocessing path through `invoke()`.
1. Read the output tensor data.

The reference sample crops a `400 x 400` region starting from `[10, 10]`.

Sample code:

```c++
int main(int argc, char *argv[])
{
    std::cout << "case " << argv[0] << " build " << __DATE__ << " " << __TIME__ << std::endl;
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << "<image> <debug_mode>" << std::endl;
        return -1;
    }

    int debug_mode = atoi(argv[2]);

    // Read image and convert to CHW + RGB format
    cv::Mat ori_img = cv::imread(argv[1]);
    int ori_w = ori_img.cols;
    int ori_h = ori_img.rows;
    std::vector<uint8_t> chw_vec;
    std::vector<cv::Mat> bgrChannels(3);
    cv::split(ori_img, bgrChannels);
    for (auto i = 2; i > -1; i--)
    {
        std::vector<uint8_t> data = std::vector<uint8_t>(bgrChannels[i].reshape(1, 1));
        chw_vec.insert(chw_vec.end(), data.begin(), data.end());
    }

    // Create AI2D input tensor, copy CHW_RGB data into it, and flush to DDR
    dims_t ai2d_in_shape{1, 3, ori_h, ori_w};
    runtime_tensor ai2d_in_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_in_shape, hrt::pool_shared).expect("cannot create input tensor");
    auto input_buf = ai2d_in_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    memcpy(reinterpret_cast<char *>(input_buf.data()), chw_vec.data(), chw_vec.size());
    hrt::sync(ai2d_in_tensor, sync_op_t::sync_write_back, true).expect("write back input failed");

    // Crop parameters
    int crop_x = 10;
    int crop_y = 10;
    int crop_w = 400;
    int crop_h = 400;

    // Create AI2D output tensor
    dims_t ai2d_out_shape{1, 3, crop_h, crop_w};
    runtime_tensor ai2d_out_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_out_shape, hrt::pool_shared).expect("cannot create input tensor");

    // Configure AI2D parameters. Here crop is enabled.
    ai2d_datatype_t ai2d_dtype{ai2d_format::NCHW_FMT, ai2d_format::NCHW_FMT, ai2d_in_tensor.datatype(), ai2d_out_tensor.datatype()};
    ai2d_crop_param_t crop_param{true, crop_x, crop_y, crop_w, crop_h};
    ai2d_shift_param_t shift_param{false, 0};
    ai2d_pad_param_t pad_param{false, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, ai2d_pad_mode::constant, {114, 114, 114}};
    ai2d_resize_param_t resize_param{false, ai2d_interp_method::tf_bilinear, ai2d_interp_mode::half_pixel};
    ai2d_affine_param_t affine_param{false, ai2d_interp_method::cv2_bilinear, 0, 0, 127, 1, {0.5, 0.1, 0.0, 0.1, 0.5, 0.0}};

    // Build AI2D scheduler
    ai2d_builder builder(ai2d_in_shape, ai2d_out_shape, ai2d_dtype, crop_param, shift_param, pad_param, resize_param, affine_param);
    builder.build_schedule();
    // Run AI2D: preprocess from ai2d_in_tensor to ai2d_out_tensor
    builder.invoke(ai2d_in_tensor, ai2d_out_tensor).expect("error occurred in ai2d running");

    // Retrieve result and save as image
    auto output_buf = ai2d_out_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    cv::Mat image_r = cv::Mat(crop_h, crop_w, CV_8UC1, output_buf.data());
    cv::Mat image_g = cv::Mat(crop_h, crop_w, CV_8UC1, output_buf.data() + crop_h * crop_w);
    cv::Mat image_b = cv::Mat(crop_h, crop_w, CV_8UC1, output_buf.data() + 2 * crop_h * crop_w);

    std::vector<cv::Mat> color_vec(3);
    color_vec.clear();
    color_vec.push_back(image_b);
    color_vec.push_back(image_g);
    color_vec.push_back(image_r);
    cv::Mat color_img;
    cv::merge(color_vec, color_img);
    cv::imwrite("test_crop.jpg", color_img);

    return 0;
}
```

Run it on the board:

```bash
./test_crop.elf test.jpg 2
```

Reference result:

![crop_res](https://www.kendryte.com/api/post/attachment?id=505)

### Pad Method

`Pad` is an image-border fill operation used during preprocessing. It changes the final image size by adding pixels to the top, bottom, left, and right edges. The fill values can be customized.

Source location:

```bash
src/rtsmart/examples/ai/usage_ai2d/test_pad
```

Typical flow:

1. Read input data.
1. Initialize the AI2D input tensor.
1. Initialize the AI2D output tensor according to the padded shape.
1. Configure `ai2d_pad_param_t`.
1. Construct `ai2d_builder` and call `build_schedule()`.
1. Run the preprocessing path through `invoke()`.
1. Read the output tensor data.

The sample pads the image by:

- top: `100`
- bottom: `100`
- left: `200`
- right: `200`

and uses `pad_val = {114, 114, 114}`.

Sample code:

```c++
int main(int argc, char *argv[])
{
    std::cout << "case " << argv[0] << " build " << __DATE__ << " " << __TIME__ << std::endl;
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << "<image> <debug_mode>" << std::endl;
        return -1;
    }

    int debug_mode = atoi(argv[2]);

    // Read image and convert to CHW + RGB format
    cv::Mat ori_img = cv::imread(argv[1]);
    int ori_w = ori_img.cols;
    int ori_h = ori_img.rows;
    std::vector<uint8_t> chw_vec;
    std::vector<cv::Mat> bgrChannels(3);
    cv::split(ori_img, bgrChannels);
    for (auto i = 2; i > -1; i--)
    {
        std::vector<uint8_t> data = std::vector<uint8_t>(bgrChannels[i].reshape(1, 1));
        chw_vec.insert(chw_vec.end(), data.begin(), data.end());
    }

    // Create AI2D input tensor, copy CHW_RGB data into it, and flush to DDR
    dims_t ai2d_in_shape{1, 3, ori_h, ori_w};
    runtime_tensor ai2d_in_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_in_shape, hrt::pool_shared).expect("cannot create input tensor");
    auto input_buf = ai2d_in_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    memcpy(reinterpret_cast<char *>(input_buf.data()), chw_vec.data(), chw_vec.size());
    hrt::sync(ai2d_in_tensor, sync_op_t::sync_write_back, true).expect("write back input failed");

    // Padding parameters
    int pad_top    = 100;
    int pad_bottom = 100;
    int pad_left   = 200;
    int pad_right  = 200;
    std::vector<int> pad_val = {114, 114, 114};
    int out_w = ori_w + pad_left + pad_right;
    int out_h = ori_h + pad_top + pad_bottom;
    int size  = out_w * out_h;

    // Create AI2D output tensor
    dims_t ai2d_out_shape{1, 3, out_h, out_w};
    runtime_tensor ai2d_out_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_out_shape, hrt::pool_shared).expect("cannot create input tensor");

    // Configure AI2D parameters. Here pad is enabled.
    ai2d_datatype_t ai2d_dtype{ai2d_format::NCHW_FMT, ai2d_format::NCHW_FMT, ai2d_in_tensor.datatype(), ai2d_out_tensor.datatype()};
    ai2d_crop_param_t crop_param{false, 0, 0, 0, 0};
    ai2d_shift_param_t shift_param{false, 0};
    ai2d_pad_param_t pad_param{true, {{0, 0}, {0, 0}, {pad_top, pad_bottom}, {pad_left, pad_right}}, ai2d_pad_mode::constant, pad_val};
    ai2d_resize_param_t resize_param{false, ai2d_interp_method::tf_bilinear, ai2d_interp_mode::half_pixel};
    ai2d_affine_param_t affine_param{false, ai2d_interp_method::cv2_bilinear, 0, 0, 127, 1, {0.5, 0.1, 0.0, 0.1, 0.5, 0.0}};

    // Build AI2D scheduler
    ai2d_builder builder(ai2d_in_shape, ai2d_out_shape, ai2d_dtype, crop_param, shift_param, pad_param, resize_param, affine_param);
    builder.build_schedule();
    // Run AI2D: preprocess from ai2d_in_tensor to ai2d_out_tensor
    builder.invoke(ai2d_in_tensor, ai2d_out_tensor).expect("error occurred in ai2d running");

    // Retrieve result and save as image
    auto output_buf = ai2d_out_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    cv::Mat image_r = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data());
    cv::Mat image_g = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data() + size);
    cv::Mat image_b = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data() + 2 * size);

    std::vector<cv::Mat> color_vec(3);
    color_vec.clear();
    color_vec.push_back(image_b);
    color_vec.push_back(image_g);
    color_vec.push_back(image_r);
    cv::Mat color_img;
    cv::merge(color_vec, color_img);
    cv::imwrite("test_pad.jpg", color_img);

    return 0;
}
```

Run it on the board:

```bash
./test_pad.elf test.jpg 2
```

Reference result:

![pad_res](https://www.kendryte.com/api/post/attachment?id=506)

### Affine Method

`Affine` is a geometric transform used during image preprocessing. It supports operations such as rotation, translation, and scaling, while preserving straight lines and parallelism.

Source location:

```bash
src/rtsmart/examples/ai/usage_ai2d/test_affine
```

Typical flow:

1. Read input data.
1. Initialize the AI2D input tensor.
1. Initialize the AI2D output tensor according to the transformed shape.
1. Configure `ai2d_affine_param_t`.
1. Construct `ai2d_builder` and call `build_schedule()`.
1. Run the preprocessing path through `invoke()`.
1. Read the output tensor data.

The reference sample uses an affine matrix that:

- scales the image by `0.5`
- translates it by `200` pixels in both `x` and `y`

Sample code:

```c++
int main(int argc, char *argv[])
{
    std::cout << "case " << argv[0] << " build " << __DATE__ << " " << __TIME__ << std::endl;
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << "<image> <debug_mode>" << std::endl;
        return -1;
    }

    int debug_mode = atoi(argv[2]);

    // Read image and convert to CHW + RGB format
    cv::Mat ori_img = cv::imread(argv[1]);
    int ori_w = ori_img.cols;
    int ori_h = ori_img.rows;
    std::vector<uint8_t> chw_vec;
    std::vector<cv::Mat> bgrChannels(3);
    cv::split(ori_img, bgrChannels);
    for (auto i = 2; i > -1; i--)
    {
        std::vector<uint8_t> data = std::vector<uint8_t>(bgrChannels[i].reshape(1, 1));
        chw_vec.insert(chw_vec.end(), data.begin(), data.end());
    }

    // Create AI2D input tensor, copy CHW_RGB data into it, and flush to DDR
    dims_t ai2d_in_shape{1, 3, ori_h, ori_w};
    runtime_tensor ai2d_in_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_in_shape, hrt::pool_shared).expect("cannot create input tensor");
    auto input_buf = ai2d_in_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    memcpy(reinterpret_cast<char *>(input_buf.data()), chw_vec.data(), chw_vec.size());
    hrt::sync(ai2d_in_tensor, sync_op_t::sync_write_back, true).expect("write back input failed");

    // Affine transform matrix: scale 0.5x, translate 200px in X and Y
    std::vector<float> affine_matrix = {0.5, 0.0, 200.0,
                                        0.0, 0.5, 200.0};
    int out_w = (int)(0.5 * ori_w);
    int out_h = (int)(0.5 * ori_h);
    int size  = out_w * out_h;

    // Create AI2D output tensor
    dims_t ai2d_out_shape{1, 3, out_h, out_w};
    runtime_tensor ai2d_out_tensor = host_runtime_tensor::create(typecode_t::dt_uint8, ai2d_out_shape, hrt::pool_shared).expect("cannot create input tensor");

    // Configure AI2D parameters. Here affine is enabled.
    ai2d_datatype_t ai2d_dtype{ai2d_format::NCHW_FMT, ai2d_format::NCHW_FMT, ai2d_in_tensor.datatype(), ai2d_out_tensor.datatype()};
    ai2d_crop_param_t crop_param{false, 0, 0, 0, 0};
    ai2d_shift_param_t shift_param{false, 0};
    ai2d_pad_param_t pad_param{false, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, ai2d_pad_mode::constant, {0, 0, 0}};
    ai2d_resize_param_t resize_param{false, ai2d_interp_method::tf_bilinear, ai2d_interp_mode::half_pixel};
    ai2d_affine_param_t affine_param{true, ai2d_interp_method::cv2_bilinear, 0, 0, 127, 1, affine_matrix};

    // Build AI2D scheduler
    ai2d_builder builder(ai2d_in_shape, ai2d_out_shape, ai2d_dtype, crop_param, shift_param, pad_param, resize_param, affine_param);
    builder.build_schedule();
    // Run AI2D: preprocess from ai2d_in_tensor to ai2d_out_tensor
    builder.invoke(ai2d_in_tensor, ai2d_out_tensor).expect("error occurred in ai2d running");

    // Retrieve result and save as image
    auto output_buf = ai2d_out_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    cv::Mat image_r = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data());
    cv::Mat image_g = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data() + size);
    cv::Mat image_b = cv::Mat(out_h, out_w, CV_8UC1, output_buf.data() + 2 * size);

    std::vector<cv::Mat> color_vec(3);
    color_vec.clear();
    color_vec.push_back(image_b);
    color_vec.push_back(image_g);
    color_vec.push_back(image_r);
    cv::Mat color_img;
    cv::merge(color_vec, color_img);
    cv::imwrite("test_affine.jpg", color_img);

    return 0;
}
```

Run it on the board:

```bash
./test_affine.elf test.jpg 2
```

Reference result:

![affine_res](https://www.kendryte.com/api/post/attachment?id=504)

### Shift Method

`Shift` performs right-shift preprocessing on the input data. Each one-bit right shift divides the original value by `2`. The input format must be `RAW16`.

Source location:

```bash
src/rtsmart/examples/ai/usage_ai2d/test_shift
```

Typical flow:

1. Create or read the input data.
1. Initialize the AI2D input tensor.
1. Initialize the AI2D output tensor.
1. Configure `ai2d_shift_param_t`.
1. Construct `ai2d_builder` and call `build_schedule()`.
1. Run the preprocessing path through `invoke()`.
1. Read the output tensor data.

In the reference sample, a `RAW16` image filled with `240` is created, then shifted right by one bit so all values become `120`.

Sample code:

```c++
int main(int argc, char *argv[])
{
    std::cout << "case " << argv[0] << " build " << __DATE__ << " " << __TIME__ << std::endl;
    if (argc < 2)
    {
        std::cerr << "Usage: " << argv[0] << "<debug_mode:0,1,2>" << std::endl;
        return -1;
    }

    int debug_mode = atoi(argv[1]);

    // Create a 16-bit raw image initialized to 240
    cv::Mat ori_img(320, 320, CV_16UC3, cv::Scalar(240, 240, 240));
    cv::imwrite("ori_img.jpg", ori_img);

    // HWC, BGR
    int ori_w = ori_img.cols;
    int ori_h = ori_img.rows;

    // Create AI2D input tensor
    dims_t ai2d_in_shape{1, ori_h, ori_w, 3};
    runtime_tensor ai2d_in_tensor = host_runtime_tensor::create(typecode_t::dt_uint16, ai2d_in_shape, hrt::pool_shared).expect("cannot create input tensor");
    auto input_buf = ai2d_in_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    memcpy(reinterpret_cast<uint16_t *>(input_buf.data()), ori_img.data, ori_h * ori_w * 3 * sizeof(uint16_t));
    hrt::sync(ai2d_in_tensor, sync_op_t::sync_write_back, true).expect("write back input failed");

    int out_w = ori_w;
    int out_h = ori_h;

    // Create AI2D output tensor
    dims_t ai2d_out_shape{1, out_h, out_w, 3};
    runtime_tensor ai2d_out_tensor = host_runtime_tensor::create(typecode_t::dt_uint16, ai2d_out_shape, hrt::pool_shared).expect("cannot create input tensor");

    // Configure AI2D parameters. Here shift is enabled: right-shift by 1 bit halves all values.
    ai2d_datatype_t ai2d_dtype{ai2d_format::RAW16, ai2d_format::RAW16, ai2d_in_tensor.datatype(), ai2d_out_tensor.datatype()};
    ai2d_crop_param_t crop_param{false, 0, 0, 0, 0};
    ai2d_shift_param_t shift_param{true, 1};
    ai2d_pad_param_t pad_param{false, {{0, 0}, {0, 0}, {0, 0}, {0, 0}}, ai2d_pad_mode::constant, {0, 0, 0}};
    ai2d_resize_param_t resize_param{false, ai2d_interp_method::tf_bilinear, ai2d_interp_mode::half_pixel};
    ai2d_affine_param_t affine_param{false, ai2d_interp_method::cv2_bilinear, 0, 0, 127, 1, {0.5, 0.1, 0.0, 0.1, 0.5, 0.0}};

    // Build AI2D scheduler
    ai2d_builder builder(ai2d_in_shape, ai2d_out_shape, ai2d_dtype, crop_param, shift_param, pad_param, resize_param, affine_param);
    builder.build_schedule();
    // Run AI2D: preprocess from ai2d_in_tensor to ai2d_out_tensor
    builder.invoke(ai2d_in_tensor, ai2d_out_tensor).expect("error occurred in ai2d running");

    // Retrieve result and save as image
    auto output_buf = ai2d_out_tensor.impl()->to_host().unwrap()->buffer().as_host().unwrap().map(map_access_::map_write).unwrap().buffer();
    cv::Mat image_r = cv::Mat(out_h, out_w, CV_16UC3, output_buf.data());
    cv::imwrite("test_shift.jpg", image_r);

    return 0;
}
```

Run it on the board:

```bash
./test_shift.elf 2
```

Reference result:

![shift_res](https://www.kendryte.com/api/post/attachment?id=508)

## Common AI2D Builder Pattern

All AI2D samples use the same core pattern:

1. Read or prepare the input data.
1. Convert image data to the required layout, typically `CHW + RGB`.
1. Create the AI2D input tensor and write the source data to it.
1. Create the AI2D output tensor based on the target shape.
1. Configure:
   - `ai2d_datatype_t`
   - `ai2d_crop_param_t`
   - `ai2d_shift_param_t`
   - `ai2d_pad_param_t`
   - `ai2d_resize_param_t`
   - `ai2d_affine_param_t`
1. Construct `ai2d_builder`.
1. Call `build_schedule()`.
1. Call `invoke()`.
1. Read back the output tensor and inspect or save the result.

This same builder-based pattern is reused in most K230 deployment projects that use AI2D before KPU inference.
