def tile_fill(im, tile_positions, from_color, to_color):
    """
    Fills image with to_color.

    This fills the image. It begins checking from the tile_positions. If the
    position is from_color it is changed to to_color. If tile_positions is
    empty then nothing will be done. It returns the edges that were changed so
    you can fill neighboring images.

    Parameters
    ----------
    im : PIL.Image.Image
    tile_positions : set[tuple[int, int]]
    from_color : tuple[int, int, int, optional[int]]
    to_color : tuple[int, int, int, optional[int]]

    Returns
    -------
    edges : list[set[int]]
        Sets of edge positions that were modified. The list contains edges
        starting with the left at position 0 and continues clockwise top = 1,
        right = 2, and bottom = 3).
    """
    edges = [set(), set(), set(), set()]
    pixels = im.load()
    w, h = im.size

    visited = set()
    frontier = tile_positions
    while frontier:
        px, py = frontier.pop()
        visited.add((px, py))
        c = pixels[px, py]
        if c != from_color:
            continue
        pixels[px, py] = to_color
        # check if it is an edge
        if px == 0:
            edges[0].add(py)
        elif py == 0:
            edges[1].add(px)
        elif px == w - 1:
            edges[2].add(py)
        elif py == h - 1:
            edges[3].add(px)
        neighbors = [(x, y) for x in range(px - 1, px + 2)
                     for y in range(py - 1, py + 2)
                     if 0 <= x < w
                     and 0 <= y < h
                     and not (x == px and y == py)]
        for neighbor in neighbors:
            if neighbor not in visited:
                frontier.add(neighbor)
    return edges