#!/usr/bin/env python
# Created by TT
# Comments directed to http://gimpchat.com or http://gimp-forum.net or http://gimpscripts.com
#
# License: GPLv3
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY# without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# To view a copy of the GNU General Public License
# visit: http://www.gnu.org/licenses/gpl.html
#
# ------------
#| Change Log |
# ------------
# Rel 1: Initial release.
# Rel 1.5: Caleb likes this version the best, and wants to preserve image mode.
import math
import random
import time
from gimpfu import *
from array import array #fast pixel operations need this
from collections import deque
def flood_fill(image, x, y, visited,n):
width, height = image.width,image.height
color_to_find = [0,0,0,255]
queue = deque([(x, y)])
group = []
while queue:
cx, cy = queue.popleft()
if (cx, cy) in visited:
continue
visited.add((cx, cy))
group.append((cx, cy))
# if len(group) > n:
# return group, False
# Check neighboring pixels
for nx, ny in [(cx-1, cy), (cx+1, cy), (cx, cy-1), (cx, cy+1)]:
if 0 <= nx < width and 0 <= ny < height and (nx, ny) not in visited:
curpixel = list(getpixel(nx,ny))
if not(curpixel[0] != color_to_find[0] or curpixel[1] != color_to_find[1] or curpixel[2] != color_to_find[2]):
queue.append((nx, ny))
return group, True
def find_groups_of_n_pixels(image, n):
width, height = image.width,image.height
visited = set()
groups_of_n = []
for x in range(width):
if x%50==0:
pdb.gimp_progress_update(float(x)/width)
for y in range(height):
isblack = list(getpixel(x,y))
if isblack[0]<10: #short cut to check for blackish pixels
if (x, y) not in visited:
group, valid = flood_fill(image, x, y, visited, n)
if len(group) <= n:
groups_of_n.append(group)
# Mark all pixels as visited if group exceeds n
visited.update(group)
return groups_of_n
srcWidth = 0
srcHeight = 0
srcRgn = 0
src_pixels = 0
newWidth = 0
newHeight = 0
dstRgn = 0
p_size = 0
dest_pixels = 0
src_pos = 0
newval = 0
def getpixel(x,y):
global src_pixels,srcWidth,p_size
src_pos = (x + srcWidth * y) * p_size
newval = src_pixels[src_pos: src_pos + p_size]
return newval
def setpixel(x,y,newval):
global dest_pixels,p_size,newWidth,newHeight
#newx = newWidth - x - 1
#newy = newHeight - y - 1
newx = x; newy = y;
#The below 2 lines are like setpixel(x,y,newvalue)
dest_pos = (newx + newWidth * newy) * p_size
dest_pixels[dest_pos : dest_pos + p_size] = newval
def convert_bgcolor(bgcolor, p_size):
if p_size == 3:
# Ensure bgcolor has exactly 3 elements
if len(bgcolor) != 3:
raise ValueError("bgcolor must have exactly 3 elements for p_size 3.")
backgroundpixel = array("B", bgcolor)
elif p_size == 4:
# Ensure bgcolor has at least 3 elements
if len(bgcolor) != 3 and len(bgcolor) != 4:
raise ValueError("bgcolor must have exactly 3 or 4 elements for p_size 4.")
# Convert to list and add 255 as the 4th element if necessary
backgroundpixel = array("B", bgcolor + (255,) if len(bgcolor) == 3 else bgcolor)
else:
raise ValueError("Unsupported p_size. Only 3 and 4 are supported.")
return backgroundpixel
def fillbglessthan(image,layer,bgcolor,maxpixels,radius):
global mastervisited,srcWidth,srcHeight,srcRgn,src_pixels,newWidth,newHeight,dstRgn,p_size,dest_pixels,src_pos,newval
if pdb.gimp_drawable_is_rgb(layer) == TRUE:
image_mode = 1
if pdb.gimp_drawable_is_gray(layer) == TRUE:
image_mode = 2
if pdb.gimp_drawable_is_indexed(layer) == TRUE:
image_mode = 3
numbytes,colormap = pdb.gimp_image_get_colormap(image)
if image_mode == 2 or image_mode == 3:
pdb.gimp_image_convert_rgb(image)
start_time = time.time()
mastervisited = []
for y in range(0,layer.height):
row = [0]*layer.width
mastervisited.append(row);
if pdb.gimp_drawable_has_alpha(layer):
pass
else:
pdb.gimp_layer_add_alpha(layer)
dest = pdb.gimp_layer_new(image,layer.width,layer.height,RGBA_IMAGE,'work',100,LAYER_MODE_NORMAL)
pdb.gimp_image_insert_layer(image,dest,None,0)
#[ ... setting up ... ] # initialize the regions and get their contents into arrays:
srcWidth = layer.width
srcHeight = layer.height
srcRgn = layer.get_pixel_rgn(0, 0, srcWidth, srcHeight,False, False)
src_pixels = array("B", srcRgn[0:srcWidth, 0:srcHeight])
newWidth = dest.width
newHeight = dest.height
dstRgn = dest.get_pixel_rgn(0, 0, newWidth, newHeight,True, True)
p_size = len(srcRgn[0,0])
dest_pixels = array("B", "\x00" * (newWidth * newHeight * p_size))
#[ ... then inside the loop over x and y ... ]
#pdb.gimp_message(p_size)
x = 0; y = 0;
src_pos = (x + srcWidth * y) * p_size
newval = src_pixels[src_pos: src_pos + p_size]
backgroundpixel = convert_bgcolor(bgcolor,p_size)
redpixel = convert_bgcolor((255,0,0),p_size)
#pdb.gimp_message(newval[0])
groups = find_groups_of_n_pixels(image,int(maxpixels))
for i in range(0,len(groups)):
pdb.gimp_progress_update(float(i)/len(groups))
group = groups[i]
#additional check to make sure all pixels are within radius
valid = True
for p1 in group:
for p2 in group:
if p1[0]!=p2[0] or p1[1]!=p2[1]: #if different pixels.
if ((p1[0]-p2[0])**2+(p1[1]-p2[1])**2)**0.5 > radius:
valid=False
break
if valid: #if all pixels are within radius then we do work
for p in group:
setpixel(p[0],p[1],backgroundpixel)
# Copy the whole array back to the pixel region:
#pdb.gimp_message(len(dstRgn[0:newWidth, 0:newHeight]))
#pdb.gimp_message(len(dest_pixels.tostring()))
dstRgn[0:newWidth, 0:newHeight] = dest_pixels.tostring()
#need this to update layer
dest.flush()
dest.merge_shadow(True)
dest.update(0, 0, newWidth,newHeight)
pdb.gimp_image_merge_down(image,dest,CLIP_TO_IMAGE)
if image_mode == 1:
pass
elif image_mode == 2:
pdb.gimp_image_convert_grayscale(image)
elif image_mode == 3:
pdb.gimp_image_convert_indexed(image,CONVERT_DITHER_NONE,CONVERT_PALETTE_GENERATE,numbytes/3,FALSE,TRUE,"Unused Palette Name")
#pdb.gimp_message("Time Taken:" + str(time.time()-start_time))
register(
"python_fu_fillbglessthan",
"Fill black pixels with background color when it's groups <= than max pixels",
"Fill black pixels with background color when it's groups <= than max pixels",
"Tin Tran with ChatGPT",
"Tin Tran with ChatGPT",
"2024.07.12",
"A Fill Bg Less Than...",
"", # Alternately use RGB, RGB*, GRAY*, INDEXED etc.
[
#INPUT BEGINS
(PF_IMAGE, "image", "IMAGE:", None), # should be type gimp.image, but None works
(PF_DRAWABLE, "layer", "Source Layer:", None),
(PF_COLOR, "bgcolor", "Background Color:", (255, 255, 255) ), # extra param is RGB triple
(PF_INT, "maxpixels", "MaxPixels (Groups less than this will be filled):", 7), # PF_INT8, PF_INT16, PF_INT32 similar but no difference in Python.
(PF_INT, "radius", "Within radius:", 3), # PF_INT8, PF_INT16, PF_INT32 similar but no difference in Python.
#INPUT ENDS
],
[],
fillbglessthan,
menu="<Image>/Python-Fu")
main()
# Below is all the example input types for INPUTS for the plug-in which can be cut and pasted into #INPUT BEGINS section and edited to taste
# (PF_INT, "p0", "_INT:", 0), # PF_INT8, PF_INT16, PF_INT32 similar but no difference in Python.
# (PF_FLOAT, "p02", "_FLOAT:", 3.141),
# (PF_STRING, "p03", "_STRING:", "foo"), # alias PF_VALUE
# (PF_TEXT, "p04", "TEXT:", "bar"),
# # PF_VALUE
# # Pick one from set of choices
# (PF_OPTION,"p1", "OPTION:", 0, ["0th","1st","2nd"]), # initially 0th is choice
# (PF_RADIO, "p16", "RADIO:", 0, (("0th", 1),("1st",0))), # note bool indicates initial setting of buttons
# # PF_RADIO is usually called a radio button group.
# # SLIDER, ADJUSTMENT types require the extra parameter of the form (min, max, step).
# (PF_TOGGLE, "p2", "TOGGLE:", 1), # initially True, checked. Alias PF_BOOL
# # PF_TOGGLE is usually called a checkbox.
# (PF_SLIDER, "p3", "SLIDER:", 0, (0, 100, 10)),
# (PF_SPINNER, "p4", "SPINNER:", 21, (1, 1000, 50)), # alias PF_ADJUSTMENT
# # Pickers ie combo boxes ie choosers from lists of existing Gimp objects
# (PF_COLOR, "p14", "_COLOR:", (100, 21, 40) ), # extra param is RGB triple
# # PF_COLOUR is an alias by aussie PyGimp author lol
# (PF_IMAGE, "p15", "IMAGE:", None), # should be type gimp.image, but None works
# (PF_FONT, "p17", "FONT:", 0),
# (PF_FILE, "p18", "FILE:", 0),
# (PF_BRUSH, "p19", "BRUSH:", 0),
# (PF_PATTERN, "p20", "PATTERN:", 0),
# (PF_GRADIENT, "p21", "GRADIENT:", 0),
# (PF_PALETTE, "p22", "PALETTE:", 0),
# (PF_LAYER, "p23", "LAYER:", None),
# (PF_CHANNEL, "p24", "CHANNEL:", None), # ??? Usually empty, I don't know why.
# (PF_DRAWABLE, "p25", "DRAWABLE:", None),
# # Mostly undocumented, but work
# (PF_VECTORS, "p26", "VECTORS:", None),
# (PF_FILENAME, "p27", "FILENAME:", 0),
# (PF_DIRNAME, "p28", "DIRNAME:", 0)
# # PF_REGION might work but probably of little use. See gimpfu.py.