Expanding the region of interest of an image
The secret to performance is to choose the appropriate data structures which lay out the memory as raw arrays of bytes (or uint8_t values). This is done by creating cython typed memoryviews from numpy arrays or images loaded through PIL. This applies to the border list as well; that is, I create an array of (x, y) coordinates rather than a list of python tuples (which is slow, not contiguous in memory, and requires conversions to access the data from python to C or vice versa. Below is the preliminary code that I have so far, hope this helps.

from libc.stdint cimport *
from libc.string cimport *
cimport libc.math as c_math
import numpy as np
from PIL import Image

cpdef uint8_t[:, :, :] load_image(str image_path):
    cdef uint8_t[:, :, :] image_data
    image = Image.open(image_path).convert("RGBA")
    image_np = np.array(image)
    image_np.setflags(write=1)
    image_data = image_np
    return image_data

cpdef save_image(uint8_t[:, :, :] image_data, str image_path):
    image = Image.fromarray(np.array(image_data)).convert("RGBA")
    image.save(image_path)

cpdef border_enhance(uint8_t[:, :, :] image, uint8_t[:, :, :] mask):
    cdef:
        uint8_t[:, :, :] out
        uint8_t[:] color
        uint8_t[:, :] roi
        uint32_t[:, :] border
        size_t i, j, k
        size_t w = image.shape[0]
        size_t h = image.shape[1]
        size_t num_border
        size_t num_not_roi = 0
        float avg[4]
        size_t avg_count
        size_t x, y
        size_t sx, sy
        int a, b

    roi = np.zeros((w, h), dtype=np.uint8)
    border = np.zeros((w * h, 2), dtype=np.uint32)#assumes no product overflow
    out = image[:, :, :]

    #define ROI from mask; only white pixels are part of the mask?
    for i in range(w):
        for j in range(h):
            color = mask[i, j]
            if color[0] == 255 and color[1] == 255 and color[2] == 255 and color[3] == 255:
                roi[i, j] = True
            else:
                roi[i, j] = False
                num_not_roi += 1

    while num_not_roi:

        #Create border
        k = 0
        for i in range(w):
            for j in range(h):
                if not roi[i, j]:
                    #assumes edges wrap over
                    if roi[i-1, j] or roi[i+1, j] or roi[i, j-1] or roi[i, j+1]:
                        border[k, 0] = i
                        border[k, 1] = j
                        k += 1

        for i in range(k):
            x = border[i, 0]
            y = border[i, 1]
            avg_count = 0
            avg = [0, 0, 0, 0]
            for a in range(-1, 2):
                for b in range(-1, 2):
                    sx = x+a
                    sy = y+b
                    if roi[sx, sy]:
                        #print(np.array(image[sx, sy]))
                        avg[0] += image[sx, sy, 0]
                        avg[1] += image[sx, sy, 1]
                        avg[2] += image[sx, sy, 2]
                        avg[3] += image[sx, sy, 3]
                        avg_count += 1
            for a in range(4):
                avg[a] /= avg_count
                out[x, y, a] = &lt;uint8_t&gt;c_math.round(avg[a])

        for i in range(k):
            x = border[i, 0]
            y = border[i, 1]
            roi[x, y] = True
            num_not_roi -= 1
            #dprint(num_not_roi)

    save_image(out, "./images/out.png")

cdef:
    uint8_t[:, :, :] image
    uint8_t[:, :, :] mask
    size_t count

image = load_image("./images/image.png")
mask = load_image("./images/mask.png")
border_enhance(image, mask)


That being said, the results do not seem particularly correct. The intermediate border step generates the correct pixel results, but the final result is wrong. Here is my output for the out.png after the call to the border_enhance function: