Python: быстрый способ удаления черного пикселя с изображения

У меня есть изображение, содержащее черные пиксели. Это могут быть вертикальные линии, а также простые точки. Я хотел бы заменить эти пиксели средним значением соседних пикселей (слева и справа).

Левый и правый соседи черного пикселя имеют отличное от черного значение.

enter image description here

На данный момент у меня есть это:

import numpy as np
from matplotlib import pyplot as plt
import time



#Creating test img
test_img = np.full((2048, 2048, 3), dtype = np.uint8, fill_value = (255,127,127))

#Draw vertical black line
test_img[500:1000,1500::12] = (0,0,0)
test_img[1000:1500,1000::24] = (0,0,0)
#Draw black point
test_img[250,250] = (0,0,0)
test_img[300,300] = (0,0,0)

#Fill hole functions
def fill_hole(img):

    #Find coords of black pixek
    imggray = img[:,:,0]

    
    coords = np.column_stack(np.where(imggray < 1))
    print(len(coords))

    #Return if no black pixel
    if len(coords) == 0:
        return img

    percent_black = len(coords)/(img.shape[0]*img.shape[1]) * 100
    print(percent_black)
    
    #Make a copy of input image
    out = np.copy(img)

    #Iterate on all black pixels
    for p in coords:

            #Edge management
            if p[0] < 1 or p[0] > img.shape[0] - 1 or p[1] < 1 or p[1] > img.shape[1] - 1:
                continue

            #Get left and right of each pixel
            left = img[p[0], p[1] - 1]
            right = img[p[0], p[1] + 1]

            #Get new pixel value
            r = int((int(left[0])+int(right[0])))/2
            g = int((int(left[1])+int(right[1])))/2
            b = int((int(left[2])+int(right[2])))/2

            out[p[0],p[1]] = [r,g,b] 
    return out

#Function call
start = time.time()
img = fill_hole(test_img)
end = time.time()
print(end - start)

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

Есть ли способ оптимизировать это?

Я не пробовал это на вашем изображении, но подозреваю, что OpenCVinpaint() сделает что-то очень близкое к тому, что вы хотите, очень быстро docs.opencv.org/4.x/d7/d8b/…

Mark Setchell 23.04.2022 10:00

На самом деле это просто горизонтальная свертка с весами [0.5, 0, 0.5], применяемыми через маску черных пикселей.

Mark Setchell 23.04.2022 10:09

@MarkSetchell Кажется, это стоит написать в качестве ответа.

Karl Knechtel 23.04.2022 10:38

@KarlKnechtel В данный момент у меня нет ничего, на чем я мог бы запустить Python, и мне не очень нравится отправлять непроверенный код. Надеюсь на выходных доберусь до машины.

Mark Setchell 23.04.2022 11:04

@MarckSetchell Спасибо за ваши предложения. Я попытаюсь использовать функцию convolve2d

Fedour 23.04.2022 11:09
3 метода стилизации элементов HTML
3 метода стилизации элементов HTML
Когда дело доходит до применения какого-либо стиля к нашему HTML, существует три подхода: встроенный, внутренний и внешний. Предпочтительным обычно...
Формы c голосовым вводом в React с помощью Speechly
Формы c голосовым вводом в React с помощью Speechly
Пытались ли вы когда-нибудь заполнить веб-форму в области электронной коммерции, которая требует много кликов и выбора? Вас попросят заполнить дату,...
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Стилизация и валидация html-формы без использования JavaScript (только HTML/CSS)
Будучи разработчиком веб-приложений, легко впасть в заблуждение, считая, что приложение без JavaScript не имеет права на жизнь. Нам становится удобно...
Flatpickr: простой модуль календаря для вашего приложения на React
Flatpickr: простой модуль календаря для вашего приложения на React
Если вы ищете пакет для быстрой интеграции календаря с выбором даты в ваше приложения, то библиотека Flatpickr отлично справится с этой задачей....
В чем разница между Promise и Observable?
В чем разница между Promise и Observable?
Разберитесь в этом вопросе, и вы значительно повысите уровень своей компетенции.
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Что такое cURL в PHP? Встроенные функции и пример GET запроса
Клиент для URL-адресов, cURL, позволяет взаимодействовать с множеством различных серверов по множеству различных протоколов с синтаксисом URL.
2
5
97
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы всегда применяете одну и ту же операцию к черному пикселю. Так что это очень параллелизуемо. Разделите изображение на меньшие прямоугольники и поместите потоки и/или процессы для работы с каждым маленьким прямоугольником. Вы можете попробовать настроить размер прямоугольника, чтобы получить наилучшую производительность.

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

Спасибо за ответ. Я не упомянул об этом, но я уже делаю. Я делю очень большое изображение на маленькое изображение размером 1024 * 1024 и использую потоки.

Fedour 23.04.2022 09:44

Каков размер исходного изображения?

John Doe 23.04.2022 09:58

Пожалуйста, проверьте мое редактирование. Я предлагаю некоторые улучшения, основанные на структуре ваших черных пикселей.

John Doe 23.04.2022 10:22

В общем, циклы for в массивах numpy обычно вызывают замедление работы, и в большинстве случаев их можно избежать с помощью встроенных функций numpy. В вашем случае рассмотрите возможность использования свертки на изображении, см. в качестве ссылки: Python получает среднее значение соседей в матрице со значением na

Ответ принят как подходящий

Обратите внимание, что я добавил значительно более быструю реализацию с numba в конце ответа.

Я хотел убедиться, что это работает правильно с изображением хитрее, а не с простым персиковым фоном, поэтому я исказил бедный старый Паддингтон размером 2048x2048 так же, как и ваше граффити:

enter image description here

Обратите внимание, что это просто неприятное, неточное представление JPEG, потому что исходное изображение слишком велико для imgur.

Затем я запустил этот код:

#!/usr/bin/env python3

import cv2
import numpy as np

# Load image 2048x2048 RGB
im = cv2.imread('paddington.png')

# Make mask of black pixels, True where black
blackMask = np.all(im==0, axis=-1)
cv2.imwrite('DEBUG-blackMask.png', (blackMask*255).astype(np.uint8))

# Convolve with [0.5, 0, 0.5] to set each pixel to average of its left and right neighbours
kernel = np.array([0.5, 0, 0.5], dtype=float).reshape(1,-1)
print(kernel.shape)
convolved = cv2.filter2D(im, ddepth=-1, kernel=kernel, borderType=cv2.BORDER_REPLICATE)
cv2.imwrite('DEBUG-convolved.png', convolved)

# Choose either convolved or original image at each pixel
res = np.where(blackMask[...,None], convolved, im)
cv2.imwrite('result.png', res)

И результат (еще один неприятный JPEG с измененным размером):

enter image description here

Тайминги здесь и, вероятно, могут быть улучшены в дальнейшем - не знаете, какие тайминги достигли вашего кода или что вам нужно:

In [55]: %timeit blackMask = np.all(im==0, axis=-1)
22.3 ms ± 29.1 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [56]: %timeit convolved = cv2.filter2D(im, ddepth=-1, kernel=kernel, borderType=cv2.BORDER_REPLICATE
    ...: )
2.66 ms ± 3.07 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

In [57]: %timeit res = np.where(blackMask[...,None], convolved, im)
22.7 ms ± 76.2 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Итак, около 46 мс в целом. Обратите внимание, что вы можете закомментировать все строки, которые создают выходные изображения, называемые DEBUG-xxx.png, поскольку они предназначены только для отладки и называются так, поэтому я могу легко очистить их после тестирования.

Я думаю, что это будет очень хорошо работать под numba, но в настоящее время llvmlite не поддерживается на моем M1 Mac, поэтому я не могу попробовать. Здесь чем-то похож на numba.


Оптимизации

Я подумал о некоторых оптимизациях кода выше. Кажется, что два аспекта медленнее, чем могли бы быть: создание маски и вставка свернутых значений в массив. Итак, рассматривая сначала создание маски, я изначально сделал:

blackMask = np.all(im==0, axis=-1)

и это заняло 22 мс. Я пробовал это с numexpr вот так:

import numexpr as ne
R=im[...,0]
G=im[...,1]
B=im[...,2]
blackMask = ne.evaluate('(R==0)&(G==0)&(B==0)')

и это дает тот же результат, но занимает всего 1,88 мс вместо 22 мс, поэтому полезная экономия составляет 20 мс.

Что касается третьей части, вставки свернутых значений в выходной массив, я обнаружил, что с помощью numexpr это тоже можно сделать быстрее.

Итак, вместо:

res = np.where(blackMask[...,None], convolved, im)

Я использовал:

blackMask3 = np.dstack((blackMask, blackMask, blackMask))
res = ne.evaluate("where(blackMask3, convolved, im)")

Это сократило время с 22 мс до 6 мс на моей машине. Таким образом, общее время теперь уменьшено с 46 мс до 10,5 мс (1,88 мс + 2,66 мс + 6 мс).


Я по-прежнему убежден, что это можно сделать с помощью Numba значительно быстрее, поскольку он действительно попадает в зону наибольшего удовольствия Numba с большим изображением и распараллеливаемым кодом. Однако я не смог установить Numba на свой M1 Mac, поэтому я нашел ОЧЕНЬ СЛАБЫЙ Intel Celeron, на который можно было установить Numba, и запустил следующий код.

Недорогая машина Intel Celeron стоимостью 200 фунтов стерлингов (4 ядра, 8 ГБ ОЗУ DDR4, диск eMMC) превзошла M1 Mac стоимостью 5000 фунтов стерлингов (12 ядер, 32 ГБ ОЗУ DDR5, твердотельный накопитель NVMe) в 3 раза, показав чуть более 3 мс:

#!/usr/bin/env python3

import cv2
import numpy as np
import numba as nb

@nb.jit('void(uint8[:,:,::3])', parallel=True)
def removeLines(im):
    # Ensure image is 3-channel
    assert (im.ndim == 3) and (im.shape[2] == 3)

    h, w = im.shape[0], im.shape[1]
    for y in nb.prange(h):
        for x in range(1,w-1):
            # Check if black, ignore if not
            sum = im[y,x,0] + im[y,x,1] + im[y,x,2]
            if sum != 0: continue

            # Pixel is black.
            # Replace with mean of left and right neighbours in all channels
            im[y, x, 0] = im[y, x-1, 0] // 2 + im[y, x+1, 0] // 2
            im[y, x, 1] = im[y, x-1, 1] // 2 + im[y, x+1, 1] // 2
            im[y, x, 2] = im[y, x-1, 2] // 2 + im[y, x+1, 2] // 2
    return

# Load image
im = cv2.imread('paddington.png')
removeLines(im)
cv2.imwrite('result.png', im)

Большое спасибо за этот ответ. Тем временем мне удалось найти решение, выполнив свертку для каждого канала RGB и используя маску. Но это было намного медленнее, чем ваше решение!

Fedour 23.04.2022 22:44

PIL загружает изображение как RGBA с 4 байтами на пиксель. Затем можно .view() массив как np.uint32 для более быстрого сравнения и where. Массив BGR из OpenCV можно рассматривать как 'S3' или 'V3' dtype, но тогда операции с массивом для меня не быстрее.

user7138814 24.04.2022 13:51

Другие вопросы по теме