|
|
@ -1,27 +1,13 @@ |
|
|
|
#!/usr/bin/env python |
|
|
|
""" |
|
|
|
Installation: place this in plug-ins and mark executable, such as: |
|
|
|
~/.gimp-2.8/plug-ins/ |
|
|
|
(To find your plug-in paths, open GIMP, click Edit, Preferences, |
|
|
|
Folders, Plug-ins) |
|
|
|
|
|
|
|
Upgrading: You must restart GIMP if you upgrade the plugin. |
|
|
|
|
|
|
|
Development Notes: |
|
|
|
- In GIMP, see Help, Prodedure browser to view GIMP's own documentation. |
|
|
|
Python differences from regular API |
|
|
|
(See https://www.youtube.com/watch?v=YHXX3KuB23Q): |
|
|
|
- Change dashes to underscores |
|
|
|
- Change -1 to None |
|
|
|
- do not pass run mode |
|
|
|
- Making a copy of the pixels would be much faster if repetitive get |
|
|
|
pixel calls are necessary. |
|
|
|
|
|
|
|
For each pixel where the alpha is below the threshold, get a new color |
|
|
|
using the nearest opaque pixel. |
|
|
|
""" |
|
|
|
|
|
|
|
import math |
|
|
|
import sys |
|
|
|
from itertools import chain |
|
|
|
import time |
|
|
|
|
|
|
|
from gimpfu import * # by convention, import * |
|
|
|
|
|
|
@ -35,16 +21,60 @@ def idist(pos1, pos2): |
|
|
|
fpos2 = [float(i) for i in pos2] |
|
|
|
return fdist(fpos1, fpos2) |
|
|
|
|
|
|
|
def square_shape(pos, rad): |
|
|
|
left = pos[0] - rad |
|
|
|
right = pos[0] + rad |
|
|
|
top = pos[1] - rad |
|
|
|
bottom = pos[1] + rad |
|
|
|
x = left |
|
|
|
y = top |
|
|
|
v_count = left - right + 1 |
|
|
|
h_count = bottom - (top + 1) |
|
|
|
ender = v_count * 2 + h_count * 2 |
|
|
|
ss_U = 0 |
|
|
|
ss_D = 1 |
|
|
|
ss_L = 2 |
|
|
|
ss_R = 3 |
|
|
|
d = ss_R |
|
|
|
while True: |
|
|
|
yield (x,y) |
|
|
|
if d == "r": |
|
|
|
x += 1 |
|
|
|
if x > right: |
|
|
|
x = right |
|
|
|
y += 1 |
|
|
|
d = ss_D |
|
|
|
elif d == ss_D: |
|
|
|
y += 1 |
|
|
|
if y > bottom: |
|
|
|
y = bottom |
|
|
|
x -= 1 |
|
|
|
d = ss_L |
|
|
|
elif d == ss_L: |
|
|
|
x -= 1 |
|
|
|
if x < left: |
|
|
|
x = left |
|
|
|
y -= 1 |
|
|
|
d = ss_U |
|
|
|
else: |
|
|
|
y -= 1 |
|
|
|
if y < top: |
|
|
|
y = top |
|
|
|
break |
|
|
|
|
|
|
|
|
|
|
|
def find_opaque_pos(near_pos, threshold=255, max_rad=None, drawable=None, w=None, h=None): |
|
|
|
|
|
|
|
def find_opaque_pos(near_pos, good_minimum=255, max_rad=None, drawable=None, w=None, h=None): |
|
|
|
""" |
|
|
|
Sequential arguments: |
|
|
|
near_pos -- This location, or the closest location to it meeting |
|
|
|
criteria, is the search target. |
|
|
|
Keyword arguments: |
|
|
|
threshold -- (0 to 255) If the pixel's alpha is this or higher, get |
|
|
|
it (the closest in location to near_pos). |
|
|
|
good_minimum -- (0 to 255) If the pixel's alpha is this or higher, |
|
|
|
get it (the closest in location to near_pos). |
|
|
|
""" |
|
|
|
if good_minimum < 0: |
|
|
|
good_minimum = 0 |
|
|
|
epsilon = sys.float_info.epsilon |
|
|
|
rad = 0 |
|
|
|
if drawable is None: |
|
|
@ -63,106 +93,193 @@ def find_opaque_pos(near_pos, threshold=255, max_rad=None, drawable=None, w=None |
|
|
|
for dist in side_distances: |
|
|
|
if dist > max_rad: |
|
|
|
max_rad = dist |
|
|
|
for i in range(0, max_rad + 1): |
|
|
|
left = near_pos[0] - i |
|
|
|
right = near_pos[0] + i |
|
|
|
top = near_pos[1] - i |
|
|
|
bottom = near_pos[1] + i |
|
|
|
print("find_opaque_pos({},...) # max_rad:{}".format(near_pos, max_rad)) |
|
|
|
for rad in range(0, max_rad + 1): |
|
|
|
left = near_pos[0] - rad |
|
|
|
right = near_pos[0] + rad |
|
|
|
top = near_pos[1] - rad |
|
|
|
bottom = near_pos[1] + rad |
|
|
|
# For each side of the square, only use positions within the |
|
|
|
# circle: |
|
|
|
y = top |
|
|
|
count = 0 |
|
|
|
next_count = (right + 1) - left |
|
|
|
for x in chain(range(left, right + 1), range(left, right + 1)): |
|
|
|
# if x == right: |
|
|
|
if count >= next_count: |
|
|
|
y = bottom |
|
|
|
count += 1 |
|
|
|
if x < 0: |
|
|
|
continue |
|
|
|
if x > w: |
|
|
|
if x >= w: |
|
|
|
continue |
|
|
|
for y in (top, bottom): |
|
|
|
# top row of square |
|
|
|
if y < 0: |
|
|
|
continue |
|
|
|
if y > h: |
|
|
|
continue |
|
|
|
pos = (x, y) |
|
|
|
dist = idist(near_pos, pos) |
|
|
|
if dist <= float(i) - epsilon: |
|
|
|
all_channels, pixel = pdb.gimp_drawable_get_pixel( |
|
|
|
drawable, |
|
|
|
pos[0], |
|
|
|
pos[1] |
|
|
|
) |
|
|
|
if pixel[3] >= threshold: |
|
|
|
# top row of square |
|
|
|
if y < 0: |
|
|
|
continue |
|
|
|
if y >= h: |
|
|
|
break |
|
|
|
pos = (x, y) |
|
|
|
dist = idist(near_pos, pos) |
|
|
|
if dist <= float(rad) - epsilon: |
|
|
|
print(" navigating verticals {}".format(pos)) |
|
|
|
all_channels, pixel = pdb.gimp_drawable_get_pixel( |
|
|
|
drawable, |
|
|
|
pos[0], |
|
|
|
pos[1] |
|
|
|
) |
|
|
|
if pixel[3] >= good_minimum: |
|
|
|
return pos |
|
|
|
else: |
|
|
|
print(" navigating verticals {} SKIPPED ({} > {})".format(pos, dist, rad)) |
|
|
|
pass |
|
|
|
# For each side of the square (continued) |
|
|
|
count = 0 |
|
|
|
next_count = bottom - (top + 1) |
|
|
|
for y in chain(range(top+1, bottom), range(top+1, bottom)): |
|
|
|
# if y == bottom - 1: |
|
|
|
if count >= next_count: |
|
|
|
x = right |
|
|
|
count += 1 |
|
|
|
if y < 0: |
|
|
|
continue |
|
|
|
if y > h: |
|
|
|
if y >= h: |
|
|
|
continue |
|
|
|
# purposely exclude corners (done in previous loop) |
|
|
|
for x in (left, right): |
|
|
|
if x < 0: |
|
|
|
continue |
|
|
|
if x > w: |
|
|
|
continue |
|
|
|
# top row of square |
|
|
|
pos = (x, y) |
|
|
|
dist = idist(near_pos, pos) |
|
|
|
if dist <= float(i) - epsilon: |
|
|
|
all_channels, pixel = pdb.gimp_drawable_get_pixel( |
|
|
|
drawable, |
|
|
|
pos[0], |
|
|
|
pos[1] |
|
|
|
) |
|
|
|
if pixel[3] >= threshold: |
|
|
|
return pos |
|
|
|
if x < 0: |
|
|
|
continue |
|
|
|
if x >= w: |
|
|
|
continue |
|
|
|
# top row of square |
|
|
|
pos = (x, y) |
|
|
|
dist = idist(near_pos, pos) |
|
|
|
if dist <= float(rad) - epsilon: |
|
|
|
print(" navigating verticals {} SKIPPED ({} > {})".format(pos, dist, rad)) |
|
|
|
all_channels, pixel = pdb.gimp_drawable_get_pixel( |
|
|
|
drawable, |
|
|
|
pos[0], |
|
|
|
pos[1] |
|
|
|
) |
|
|
|
if pixel[3] >= good_minimum: |
|
|
|
return pos |
|
|
|
else: |
|
|
|
print(" navigating verticals {} SKIPPED".format(pos)) |
|
|
|
pass |
|
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
def extend(threshold=254, make_opaque=False): |
|
|
|
def extend(image=None, drawable=None, minimum=1, maximum=254, make_opaque=False, good_minimum=255): |
|
|
|
""" |
|
|
|
Keyword arguments: |
|
|
|
threshold -- (0 to 255) If the pixel's alpha is this or lower, |
|
|
|
change its color |
|
|
|
make_opaque -- Make the pixel within the threshold opaque. This is |
|
|
|
minimum -- (0 to 255) Only edit pixels with at least this for alpha. |
|
|
|
maximum -- (0 to 254) Only edit pixels with at most this for alpha. |
|
|
|
make_opaque -- Make the pixel within the range opaque. This is |
|
|
|
normally for preparing to convert images to indexed color, such as |
|
|
|
Minetest wield_image. |
|
|
|
""" |
|
|
|
if maximum < 0: |
|
|
|
maximum = 0 |
|
|
|
if minimum < 0: |
|
|
|
minimum = 0 |
|
|
|
if maximum > 254: |
|
|
|
maximum = 254 |
|
|
|
# exists, x1, y1, x2, y2 = \ |
|
|
|
# pdb.gimp_selection_bounds(self.img) |
|
|
|
img = gimp.image_list()[0] |
|
|
|
drawable = pdb.gimp_image_active_drawable(img) |
|
|
|
w = pdb.gimp_image_width(img) |
|
|
|
h = pdb.gimp_image_height(img) |
|
|
|
new_channels = 3 |
|
|
|
# pdb.gimp_selection_bounds(self.image) |
|
|
|
if image is None: |
|
|
|
image = gimp.image_list()[0] |
|
|
|
if drawable is None: |
|
|
|
drawable = pdb.gimp_image_active_drawable(image) |
|
|
|
w = pdb.gimp_image_width(image) |
|
|
|
h = pdb.gimp_image_height(image) |
|
|
|
new_channels = 4 |
|
|
|
# ^ new_channels must match the destination channel count, |
|
|
|
# or an ExecutionError occurs. |
|
|
|
if make_opaque: |
|
|
|
new_channels = 4 |
|
|
|
for x in range(w): |
|
|
|
for y in range(h): |
|
|
|
print("Size: {}".format((w, h))) |
|
|
|
total_f = float(w * h) |
|
|
|
count_f = 0.0 |
|
|
|
# ok = True |
|
|
|
for y in range(h): |
|
|
|
# if not ok: |
|
|
|
# break |
|
|
|
for x in range(w): |
|
|
|
# if count_f is None: |
|
|
|
count_f = float(y) * float(w) + float(x) |
|
|
|
# print("checking {}".format(pdb.gimp_drawable_get_pixel(drawable, x, y))) |
|
|
|
all_channels, pixel = pdb.gimp_drawable_get_pixel(drawable, x, y) |
|
|
|
# if all([p == q for p, q in zip(pixel, color_to_edit)]): |
|
|
|
opaque_pos = find_opaque_pos(x, y, drawable=drawable, w=w, h=h) |
|
|
|
pdb.gimp_drawable_set_pixel(drawable, x, y, new_channels, new_color) |
|
|
|
if (pixel[3] >= minimum) and (pixel[3] <= maximum): |
|
|
|
# if all([p == q for p, q in zip(pixel, color_to_edit)]): |
|
|
|
pos = (x, y) |
|
|
|
opaque_pos = find_opaque_pos((x, y), drawable=drawable, w=w, h=h, good_minimum=good_minimum) |
|
|
|
if (opaque_pos is not None): |
|
|
|
if opaque_pos == pos: |
|
|
|
msg = "Uh oh, got own pos when checking for better color than {}...".format(pixel) |
|
|
|
print(msg) |
|
|
|
# pdb.gimp_message(msg) |
|
|
|
gimp.progress_init(msg) |
|
|
|
gimp.progress_update(0.0) |
|
|
|
time.sleep(10) |
|
|
|
# ok = False |
|
|
|
continue |
|
|
|
all_channels, new_pixel = pdb.gimp_drawable_get_pixel(drawable, opaque_pos[0], opaque_pos[1]) |
|
|
|
if new_pixel != pixel: |
|
|
|
if make_opaque: |
|
|
|
new_pixel = (new_pixel[0], new_pixel[1], new_pixel[2], 255) |
|
|
|
|
|
|
|
# print("Changing pixel at {} from {} to {}".format((x, y), pixel, new_pixel)) |
|
|
|
print("Changing pixel at {} using color from {}".format((x, y), opaque_pos)) |
|
|
|
pdb.gimp_drawable_set_pixel(drawable, x, y, new_channels, new_pixel) |
|
|
|
else: |
|
|
|
# msg = "Uh oh, got own {} color {} at {} when checking for color at better position than {}...".format(pixel, new_pixel, opaque_pos, pos) |
|
|
|
# print(msg) |
|
|
|
# pdb.gimp_message(msg) |
|
|
|
# gimp.progress_init(msg) |
|
|
|
# gimp.progress_update(count_f / total_f) |
|
|
|
# count_f = None |
|
|
|
# time.sleep(10) |
|
|
|
# return |
|
|
|
# continue |
|
|
|
pass |
|
|
|
else: |
|
|
|
msg = "Uh oh, the image has no pixels at or above the minimum good alpha." |
|
|
|
print(msg) |
|
|
|
pdb.gimp_message(msg) |
|
|
|
pdb.gimp_drawable_update(drawable,0,0,drawable.width,drawable.height) |
|
|
|
return |
|
|
|
if count_f is not None: |
|
|
|
# count_f += 1.0 |
|
|
|
gimp.progress_update(count_f / total_f) |
|
|
|
pdb.gimp_drawable_update(drawable,0,0,drawable.width,drawable.height) |
|
|
|
|
|
|
|
|
|
|
|
def remove_layer_halo(): |
|
|
|
extend(threshold=0, make_opaque=True) |
|
|
|
pdb.gimp_displays_flush() # update the image |
|
|
|
def remove_layer_halo(image, drawable, minimum, maximum, good_minimum, make_opaque): |
|
|
|
gimp.progress_init("This may take a while...") |
|
|
|
print("options: {}".format((str(image), str(drawable), str(minimum), str(maximum), str(make_opaque), str(good_minimum)))) |
|
|
|
extend(image=image, drawable=drawable, minimum=minimum, maximum=maximum, make_opaque=make_opaque, good_minimum=good_minimum) |
|
|
|
print("updating image...") |
|
|
|
pdb.gimp_displays_flush() # update the image; doesn't work |
|
|
|
print("done (remove_layer_halo)") |
|
|
|
|
|
|
|
|
|
|
|
# See https://jacksonbates.wordpress.com/2015/09/14/python-fu-6-accepting-user-input/ |
|
|
|
# See https://www.youtube.com/watch?v=uSt80abcmJs |
|
|
|
register( |
|
|
|
"python_fu_remove_halo", |
|
|
|
"Remove Halo", |
|
|
|
"Remove alpha", |
|
|
|
"Jake Gustafson", "Jake Gustafson", "2020", |
|
|
|
"Redo the edge...", |
|
|
|
"Remove Halo", # caption |
|
|
|
"RGBA", # RGB* would mean with or without alpha. |
|
|
|
[ |
|
|
|
(PF_SPINNER, "threshold", "Minimum alpha to fix", 254, (0, 255, 1)), |
|
|
|
(PF_BOOL, "make_opaque", "Make the fixed parts opaque.", True), |
|
|
|
(PF_IMAGE, "image", "Current image", None), |
|
|
|
(PF_DRAWABLE, "drawable", "Input layer", None), |
|
|
|
(PF_SPINNER, "minimum", "Minimum alpha to fix (normally 1)", 1, (0, 255, 1)), |
|
|
|
(PF_SPINNER, "maximum", "Maximum alpha to fix (normally 254 unless less damaged)", 254, (0, 255, 1)), |
|
|
|
(PF_SPINNER, "good_minimum", "Find a nearby color >= this (usually maximum+1).", 255, (0, 255, 1)), |
|
|
|
(PF_TOGGLE, "make_opaque", "Make the fixed parts opaque.", True), |
|
|
|
], |
|
|
|
[], |
|
|
|
remove_layer_halo, menu="<Image>/Layer,Remove Halo" |
|
|
|
[], # results |
|
|
|
remove_layer_halo, |
|
|
|
menu="<Image>/Layer" |
|
|
|
) |
|
|
|
|
|
|
|
main() |
|
|
|