Есть ли библиотека для деформации/преобразования изображения для python с контролируемыми точками?

Вы берете изображения и отмечаете определенные точки (например, отмечаете область вокруг глаз, носа, рта и т. д. людей), а затем преобразуете их в точки, отмеченные в другом изображении. Что-то типа:

transform(original_image, marked_points_in_the_original, marked_points_in_the_reference)

Кажется, я не могу найти алгоритм, описывающий его, и не могу найти какие-либо библиотеки с ним. Я тоже готов сделать это сам, если смогу найти хороший/легкий для восприятия материал. Я знаю, что это возможно, поскольку я видел некоторые неполные (на самом деле не объясняю, как это сделать) .pdf файлы в Google с ним.

Вот пример отмеченных точек и преобразования, раз вы просили разъяснений. Хотя это не использует 2 человека, как я сказал ранее.


Редактировать: мне удалось заставить работать метод im.transform, но аргумент представляет собой список ((box_x, box_y, box_width, box_height), (x0, y0, x1, y1, x2, y2, x3, y3)), где первая точка - СЗ, вторая ЮЗ, третья СВ и четвертая ЮВ. (0, 0) — это самая левая верхняя часть экрана, насколько я мог судить. Если я все сделал правильно, то этот метод не делает то, что мне нужно.


person smln    schedule 21.02.2011    source источник
comment
преобразовывать - расплывчатое слово. Можете ли вы объяснить, что вы имеете в виду? Вы хотите скопировать части одного изображения и вставить их прямо в другое? Что за регион?   -  person Devin Jeanpierre    schedule 21.02.2011


Ответы (4)


Пример кода, предоставленный Blender, у меня не работает. Кроме того, документация PIL для im.transform неоднозначна. Поэтому я копаюсь в исходном коде PIL и, наконец, выясняю, как использовать интерфейс. Вот мое полное использование:

import numpy as np
from PIL import Image

def quad_as_rect(quad):
    if quad[0] != quad[2]: return False
    if quad[1] != quad[7]: return False
    if quad[4] != quad[6]: return False
    if quad[3] != quad[5]: return False
    return True

def quad_to_rect(quad):
    assert(len(quad) == 8)
    assert(quad_as_rect(quad))
    return (quad[0], quad[1], quad[4], quad[3])

def rect_to_quad(rect):
    assert(len(rect) == 4)
    return (rect[0], rect[1], rect[0], rect[3], rect[2], rect[3], rect[2], rect[1])

def shape_to_rect(shape):
    assert(len(shape) == 2)
    return (0, 0, shape[0], shape[1])

def griddify(rect, w_div, h_div):
    w = rect[2] - rect[0]
    h = rect[3] - rect[1]
    x_step = w / float(w_div)
    y_step = h / float(h_div)
    y = rect[1]
    grid_vertex_matrix = []
    for _ in range(h_div + 1):
        grid_vertex_matrix.append([])
        x = rect[0]
        for _ in range(w_div + 1):
            grid_vertex_matrix[-1].append([int(x), int(y)])
            x += x_step
        y += y_step
    grid = np.array(grid_vertex_matrix)
    return grid

def distort_grid(org_grid, max_shift):
    new_grid = np.copy(org_grid)
    x_min = np.min(new_grid[:, :, 0])
    y_min = np.min(new_grid[:, :, 1])
    x_max = np.max(new_grid[:, :, 0])
    y_max = np.max(new_grid[:, :, 1])
    new_grid += np.random.randint(- max_shift, max_shift + 1, new_grid.shape)
    new_grid[:, :, 0] = np.maximum(x_min, new_grid[:, :, 0])
    new_grid[:, :, 1] = np.maximum(y_min, new_grid[:, :, 1])
    new_grid[:, :, 0] = np.minimum(x_max, new_grid[:, :, 0])
    new_grid[:, :, 1] = np.minimum(y_max, new_grid[:, :, 1])
    return new_grid

def grid_to_mesh(src_grid, dst_grid):
    assert(src_grid.shape == dst_grid.shape)
    mesh = []
    for i in range(src_grid.shape[0] - 1):
        for j in range(src_grid.shape[1] - 1):
            src_quad = [src_grid[i    , j    , 0], src_grid[i    , j    , 1],
                        src_grid[i + 1, j    , 0], src_grid[i + 1, j    , 1],
                        src_grid[i + 1, j + 1, 0], src_grid[i + 1, j + 1, 1],
                        src_grid[i    , j + 1, 0], src_grid[i    , j + 1, 1]]
            dst_quad = [dst_grid[i    , j    , 0], dst_grid[i    , j    , 1],
                        dst_grid[i + 1, j    , 0], dst_grid[i + 1, j    , 1],
                        dst_grid[i + 1, j + 1, 0], dst_grid[i + 1, j + 1, 1],
                        dst_grid[i    , j + 1, 0], dst_grid[i    , j + 1, 1]]
            dst_rect = quad_to_rect(dst_quad)
            mesh.append([dst_rect, src_quad])
    return mesh

im = Image.open('./old_driver/data/train/c0/img_292.jpg')
dst_grid = griddify(shape_to_rect(im.size), 4, 4)
src_grid = distort_grid(dst_grid, 50)
mesh = grid_to_mesh(src_grid, dst_grid)
im = im.transform(im.size, Image.MESH, mesh)
im.show()

До: введите здесь описание изображения После: введите здесь описание изображения

Я предлагаю выполнить приведенный выше код в iPython, а затем распечатать mesh, чтобы понять, какой ввод необходим для im.transform. Для меня выход:

In [1]: mesh
Out[1]:
[[(0, 0, 160, 120), [0, 29, 29, 102, 186, 120, 146, 0]],
 [(160, 0, 320, 120), [146, 0, 186, 120, 327, 127, 298, 48]],
 [(320, 0, 480, 120), [298, 48, 327, 127, 463, 77, 492, 26]],
 [(480, 0, 640, 120), [492, 26, 463, 77, 640, 80, 605, 0]],
 [(0, 120, 160, 240), [29, 102, 9, 241, 162, 245, 186, 120]],
 [(160, 120, 320, 240), [186, 120, 162, 245, 339, 214, 327, 127]],
 [(320, 120, 480, 240), [327, 127, 339, 214, 513, 284, 463, 77]],
 [(480, 120, 640, 240), [463, 77, 513, 284, 607, 194, 640, 80]],
 [(0, 240, 160, 360), [9, 241, 27, 364, 202, 365, 162, 245]],
 [(160, 240, 320, 360), [162, 245, 202, 365, 363, 315, 339, 214]],
 [(320, 240, 480, 360), [339, 214, 363, 315, 453, 373, 513, 284]],
 [(480, 240, 640, 360), [513, 284, 453, 373, 640, 319, 607, 194]],
 [(0, 360, 160, 480), [27, 364, 33, 478, 133, 480, 202, 365]],
 [(160, 360, 320, 480), [202, 365, 133, 480, 275, 480, 363, 315]],
 [(320, 360, 480, 480), [363, 315, 275, 480, 434, 469, 453, 373]],
 [(480, 360, 640, 480), [453, 373, 434, 469, 640, 462, 640, 319]]]
person Warbean    schedule 20.05.2016
comment
как вы указываете, какие части изображения должны быть деформированы? - person azal; 16.10.2018

Аналогичным образом вы можете использовать Python API ImageMagick для выполнения Искажения Шепарда.

уши коалыкоала дергает за уши

person George Profenza    schedule 21.02.2011
comment
Это похоже на то, что я пытаюсь сделать. Я попробую и отчитаюсь, если мне удастся заставить его работать. - person smln; 22.02.2011
comment
ImageMagick работает, но результаты, которые я получил, оказались не такими хорошими, как я надеялся. - person smln; 22.02.2011
comment
это очень хорошо работает для создания анимированных GIF-файлов. я применил постепенное искажение Шепарда с изменением альфы для двух изображений, и результат выглядит следующим образом: i.imgur.com/ 1Lh4i.gif спасибо за подсказку! - person Murat Ayfer; 12.12.2011
comment
@GeorgeProfenza, на самом деле, это конечный продукт :) muratayfer.com/morphin благодаря этой ветке. - person Murat Ayfer; 02.01.2012

Да, есть. Это немного низкоуровнево, но PIL (библиотека изображений Python) выполняет свою функцию. такая трансформация. У меня это никогда не работало (поскольку моя проблема была немного проще), но вы можете поиграть с этим.

Вот хороший ресурс для преобразований PIL (вы бы хотели взглянуть на MESH): http://effbot.org/tag/PIL.Image.Image.transform.


Из документации:

Аналогичен QUAD, но данные представляют собой список целевых прямоугольников и соответствующих исходных четырехугольников.

im.transform(size, MESH, data)

Данные представляют собой кортеж прямоугольников:

data = [((a, b, c, d), (e, f, g, h)), 
        ((i, j, k, l), (m, n, o, p))]

Он преобразует первый прямоугольник во второй.

person Blender    schedule 21.02.2011
comment
Я читал эту документацию раньше, но я не понимаю, что происходит в этой функции. Не могли бы вы объяснить его параметры? Если я правильно понимаю, все, что мне нужно сделать, это создать карту с исходными координатами -> новыми координатами и передать ее как MESH, и это сработает? - person smln; 22.02.2011
comment
Более менее. mesh должен состоять из пар прямоугольников, но вы можете сделать вырожденные 1x1 прямоугольники. Чем больше размер, тем быстрее он идет. - person Blender; 22.02.2011

У меня есть решение с использованием OpenCV путем триангуляции точек преобразования:
Преобразование

Это не выглядит идеальным, но чем больше точек на исходном/целевом изображении, тем лучше результаты.

Код

Вот код, который я использовал для преобразования, внизу вы можете увидеть, как вызвать вашу функцию transform.

#!/bin/env python3

import cv2
import numpy as np

def get_triangulation_indices(points):
    """Get indices triples for every triangle
    """
    # Bounding rectangle
    bounding_rect = (*points.min(axis=0), *points.max(axis=0))

    # Triangulate all points
    subdiv = cv2.Subdiv2D(bounding_rect)
    subdiv.insert(list(points))

    # Iterate over all triangles
    for x1, y1, x2, y2, x3, y3 in subdiv.getTriangleList():
        # Get index of all points
        yield [(points==point).all(axis=1).nonzero()[0][0] for point in [(x1,y1), (x2,y2), (x3,y3)]]

def crop_to_triangle(img, triangle):
    """Crop image to triangle
    """
    # Get bounding rectangle
    bounding_rect = cv2.boundingRect(triangle)

    # Crop image to bounding box
    img_cropped = img[bounding_rect[1]:bounding_rect[1] + bounding_rect[3],
                      bounding_rect[0]:bounding_rect[0] + bounding_rect[2]]
    # Move triangle to coordinates in cropped image
    triangle_cropped = [(point[0]-bounding_rect[0], point[1]-bounding_rect[1]) for point in triangle]
    return triangle_cropped, img_cropped

def transform(src_img, src_points, dst_img, dst_points): 
    """Transforms source image to target image, overwriting the target image.
    """
    for indices in get_triangulation_indices(src_points):
        # Get triangles from indices
        src_triangle = src_points[indices]
        dst_triangle = dst_points[indices]

        # Crop to triangle, to make calculations more efficient
        src_triangle_cropped, src_img_cropped = crop_to_triangle(src_img, src_triangle)
        dst_triangle_cropped, dst_img_cropped = crop_to_triangle(dst_img, dst_triangle)

        # Calculate transfrom to warp from old image to new
        transform = cv2.getAffineTransform(np.float32(src_triangle_cropped), np.float32(dst_triangle_cropped))

        # Warp image
        dst_img_warped = cv2.warpAffine(src_img_cropped, transform, (dst_img_cropped.shape[1], dst_img_cropped.shape[0]), None, flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_REFLECT_101 )

        # Create mask for the triangle we want to transform
        mask = np.zeros(dst_img_cropped.shape, dtype = np.uint8)
        cv2.fillConvexPoly(mask, np.int32(dst_triangle_cropped), (1.0, 1.0, 1.0), 16, 0);

        # Delete all existing pixels at given mask
        dst_img_cropped*=1-mask
        # Add new pixels to masked area
        dst_img_cropped+=dst_img_warped*mask

if __name__ == "__main__":
    # Inputs
    src_img = cv2.imread("woman.jpg")
    dst_img = cv2.imread("cheetah.jpg")
    src_points = np.array([(40, 27), (38, 65), (47, 115), (66, 147), (107, 166), (147, 150), (172, 118), (177, 75), (173, 26), (63, 19), (89, 30), (128, 34), (152, 27), (75, 46), (142, 46), (109, 48), (95, 96), (107, 91), (120, 97), (84, 123), (106, 117), (132, 121), (97, 137), (107, 139), (120, 135)])
    dst_points = np.array([(2, 16), (0, 60), (2, 143), (47, 181), (121, 178), (208, 181), (244, 133), (241, 87), (241, 18), (41, 15), (73, 20), (174, 16), (218, 16), (56, 23), (191, 23), (120, 48), (94, 128), (120, 122), (150, 124), (83, 174), (122, 164), (159, 173), (110, 174), (121, 174), (137, 175)])

    # Apply transformation
    transform(src_img, src_points, dst_img, dst_points)

    # Show result
    cv2.imshow("Transformed", dst_img)

    cv2.waitKey(0)
    cv2.destroyAllWindows()

src_points и dst_points в main-Function были жестко закодированы и соответствуют ориентирам, отмеченным зеленым на изображениях выше. Код был частично вдохновлен эту онлайн-статью, но код был немного подчищен. После ответа на этот вопрос я также создал свой собственный репозиторий FaceChanger на github с интерактивным приложением на Python, используя ту же функциональность, что описана в этом ответе.

Требования

  • Numpy: pip3 install numpy
  • OpenCV: pip3 install opencv-python

Как это работает

Триангуляция

Сначала нам нужно триангулировать изображение, которое преобразует точки с двух верхних изображений в треугольники внизу. Нам нужны треугольники вместо точек, потому что это позволяет нам преобразовывать отдельные треугольники по отдельности, что облегчит нашу жизнь в будущем. Триангуляция выполняется с использованием триангуляции Делоне с OpenCV. Точки первого и второго изображения не обязательно должны давать одну и ту же триангуляцию, поэтому функция get_triangulation_indices возвращает индексы всех углов для каждого треугольника. Используя эти индексы, мы можем сопоставить каждый исходный треугольник с одним целевым треугольником.

Треугольники искривления

Треугольники деформируются с использованием warpAffine-метода OpenCV. Проблема с этим методом заключается в том, что он искажает все изображение, а не только один треугольник, поэтому нам нужно проделать дополнительную работу, чтобы деформировать только треугольники.

Вырезать треугольник

Сначала мы вырезаем только ту часть исходного и целевого изображения, которая содержит исходный или целевой треугольник. Теоретически в этом нет необходимости, но так намного быстрее, потому что тогда нам не нужно каждый раз выполнять деформацию всего изображения. Это делается с помощью crop_to_triangle-метода.

Преобразовать изображение

Затем мы видим, как мы должны исказить изображение, чтобы перейти от исходного треугольника к целевому треугольнику с помощью cv2.getAffineTransform. Это даст нам матрицу преобразования, которую мы можем использовать с cv2.warpAffine, чтобы деформировать наше изображение до пропорций назначения.

Маска в треугольник

Теперь у нас есть проблема, что преобразование деформации изменило не только наши треугольники, но и весь наш src_img_cropped. Итак, теперь нам нужно только вставить пиксели, принадлежащие нашему треугольнику, к целевому изображению. Мы можем использовать cv2.fillConvexPoly для создания маски нашего целевого треугольника и использовать ее для удаления всех пикселей из целевого изображения, которые находятся внутри треугольника, который мы хотим вставить, чтобы добавить искривленный треугольник в это место, которое мы только что очистили. Это делается с помощью манипуляций с массивами Numpy.

Вывод

Это довольно простой метод для достижения поставленной задачи. Однако иногда это приводит к неестественно выглядящим прямым краям и, следовательно, может не подходить для всех целей. Однако качество результата повышается, если вы добавляете больше точек к исходному и целевому изображениям. Вам также необходимо добавить углы исходного и целевого изображений к вашим точкам, если вы хотите, чтобы все изображение было скопировано, иначе целевое изображение будет просто перезаписано частями исходного изображения, что я вижу как особенность. Это также можно комбинировать с распознаванием лиц для создания эффекта смены лица, для чего я лично использую dlib, что дает отличные результаты.

person pascscha    schedule 26.12.2020
comment
Это должно войти в историю как один из величайших ответов StackOverflow! Сейчас я работаю над очень важным личным проектом и искал что-то подобное уже века. Если бы я хотел узнать больше об этой методологии, есть ли у вас какие-либо рекомендации по курсам/ресурсам в Интернете, где я мог бы найти дополнительную информацию? Я знаком с гомографией и другими глобальными операциями деформации изображения, но я новичок в локальном преобразовании. - person kairocks2002; 30.05.2021