add node MatchTemplate (#17)

* add node MatchTemplate
This commit is contained in:
Bug, Ltd.
2025-02-10 13:38:40 +01:00
committed by GitHub
parent 3e497c36ca
commit 275254b836
7 changed files with 178 additions and 7 deletions
+8
View File
@@ -159,6 +159,14 @@ const customLabels = {
scale_factor: 'scale factor',
},
},
MatchTemplate: {
output: {
detected_box: 'detected box',
debug_detection: 'debug - detection',
debug_template_mask: 'debug - template (mask)',
debug_template_image: 'debug - template (image)',
},
},
};
const customLooks = {
XYPlotQueue: {
+2
View File
@@ -3,3 +3,5 @@ json5
python-dotenv
pyyaml
pyaml
opencv-python
webcolors
+1 -1
View File
@@ -18,7 +18,7 @@ def import_all(load_env=True):
if DEBUG:
MODULES.append('debug')
if MAD_SCIENTIST:
pass
MODULES.append('experimental')
for mod in MODULES:
importlib.import_module('...nodes.' + mod, package=__name__)
+147
View File
@@ -0,0 +1,147 @@
import torch
from torchvision.transforms import v2 as transforms
import cv2
from webcolors import hex_to_rgb
from ..collection.register_nodes import register_node
from ..shared.utils import tensor_to_cv, cv_to_tensor
ALL_METHODS = {
# 'ccoeff': cv2.TM_CCOEFF,
'ccoeff_normed': cv2.TM_CCOEFF_NORMED,
# 'ccorr': cv2.TM_CCORR,
'ccorr_normed': cv2.TM_CCORR_NORMED,
# 'sqdiff': cv2.TM_SQDIFF,
'sqdiff_normed': cv2.TM_SQDIFF_NORMED,
}
@register_node('Match Template (OpenCV)', 'experimental')
class MatchTemplate:
"""Search for a template inside an image, using OpenCV Template matching method.
- method: detection method, either 'ccoeff_normed', 'ccorr_normed' or 'sqdiff_normed'.
- Recommended: 'ccoeff_normed'
"""
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
'required': {
'image': ('IMAGE',),
'template': ('IMAGE',),
'method': (
list(ALL_METHODS.keys()),
{
'default': 'ccoeff_normed',
'tooltip': 'detection algorithm, default is ccoeff_normed',
},
),
'threshold': (
'FLOAT',
{'min': 0, 'max': 1, 'default': 0.5, 'step': 0.01},
),
'template_fill_hex': (
'STRING',
{
'default': '#999',
'tooltip': 'background color to apply to template',
},
),
}
}
FUNCTION = 'run'
RETURN_TYPES = ('MASK', 'FLOAT', 'STRING', 'IMAGE', 'MASK', 'IMAGE')
RETURN_NAMES = (
'mask',
'confidence',
'detected_box',
'debug_detection',
'debug_template_mask',
'debug_template_image',
)
OUTPUT_TOOLTIPS = (
'mask',
'confidence level',
'detected box (x, y, w, h)',
'detection image',
'detection - template (mask)',
'detection - template (image)',
)
DESCRIPTION = (
'Search for a template inside an image, using OpenCV Template matching method.'
)
def run(
self,
image: torch.Tensor,
template: torch.Tensor,
method,
threshold: float,
template_fill_hex: str,
) -> torch.Tensor:
img_cv = tensor_to_cv(image)
tpl_cv = tensor_to_cv(template)
# tpl_cv = cv2.imread(
# '/mnt/nvm/enc/sd/comfyui/tests/20241029 - test logo/create mask/2 - logo_cleaned - cropped - transparent.png', cv2.IMREAD_UNCHANGED)
meth = ALL_METHODS[method]
# fill the template with color
tpl_cv_mask = tpl_cv[:, :, 3] == 0
tpl_cv = tpl_cv.copy()
(r, g, b) = hex_to_rgb(template_fill_hex)
tpl_cv[tpl_cv_mask] = [r, g, b, 255]
tpl_cv = cv2.cvtColor(tpl_cv, cv2.COLOR_BGRA2BGR)
# detect template
result = cv2.matchTemplate(img_cv, tpl_cv, meth)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
# if the method is TM_SQDIFF or TM_SQDIFF_NORMED, take minimum
if meth in [cv2.TM_SQDIFF, cv2.TM_SQDIFF_NORMED]:
(x, y) = min_loc
else:
(x, y) = max_loc
# if value is less than threshold, exit now
# if (max_val < threshold):
# return (image, max_val, None, None, None, None)
# draw the rectangle
# w, h = tpl_cv.shape[::-1] # or 2?
w = tpl_cv.shape[1]
h = tpl_cv.shape[0]
cv2.rectangle(img_cv, (x, y), (x + w, y + h), (255, 0, 0), (w + h) // 100 + 1)
# create mask from template
template_sum = template.sum(axis=3)
tpl_mask = torch.where(template_sum > 0, 1.0, 0.0)
tpl_mask = 1.0 - tpl_mask
# pad the mask
img_w = img_cv.shape[1]
img_h = img_cv.shape[0]
mask = transforms.functional.pad(
tpl_mask,
padding=(x, y, img_w - x - w, img_h - y - h),
fill=1,
padding_mode='constant',
)
# format box coordinates
box_coords = '{0}, {1}, {2}, {3}'.format(x, y, w, h)
return (
mask,
max_val,
box_coords,
cv_to_tensor(img_cv),
tpl_mask,
cv_to_tensor(tpl_cv),
)
-4
View File
@@ -90,10 +90,6 @@ class XYPlotQueue:
index: int,
dim2: list[str] = [''],
):
print('------------------------------------')
print('index = ', index)
print('------------------------------------')
if index < 0: # value has been reset (completion, interrupt, errors)
index = 0
+11 -2
View File
@@ -16,16 +16,25 @@ class AnyReturnTypes(tuple):
return ANY_TYPE
def tensor_to_pillow(tensor: torch.tensor) -> Image.Image:
def tensor_to_pillow(tensor: torch.Tensor) -> Image.Image:
return Image.fromarray(
np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
)
def pillow_to_tensor(image: Image.Image) -> torch.tensor:
def pillow_to_tensor(image: Image.Image) -> torch.Tensor:
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def tensor_to_cv(tensor: torch.Tensor):
# return np.clip(255 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
return (tensor.numpy().squeeze(0) * 255).astype(np.uint8) # <- works
def cv_to_tensor(image) -> torch.Tensor:
return torch.from_numpy(image.astype(np.float32) / 255.0).unsqueeze(0)
class EmptyNode:
def __init__(self):
pass
+9
View File
@@ -188,6 +188,15 @@ const customLabels = {
scale_factor: 'scale factor',
},
},
// EXPERIMENTAL
MatchTemplate: {
output: {
detected_box: 'detected box',
debug_detection: 'debug - detection',
debug_template_mask: 'debug - template (mask)',
debug_template_image: 'debug - template (image)',
},
},
} as const as Record<string, { [key in Scope]?: Record<string, string> }>
const customLooks = {