Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update resynthesizer heal selection plugin to work with recent 2.99 release #136

Open
wants to merge 8 commits into
base: deprecations
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 230 additions & 132 deletions PluginScripts/plugin-heal-selection.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/python3
WarpspeedSCP marked this conversation as resolved.
Show resolved Hide resolved

'''
Gimp plugin "Heal selection"
Expand Down Expand Up @@ -26,154 +26,252 @@
http://www.gnu.org/copyleft/gpl.html

'''
import gi
import sys

from gimpfu import *
gi.require_version('Gimp', '3.0')
gi.require_version('GimpUi', '3.0')

# Python 2 gettext.install("resynthesizer", Gimp.locale_directory(), unicode=True)
gettext.install("resynthesizer", Gimp.locale_directory())
from gi.repository import GLib
from gi.repository import GObject
from gi.repository import Gimp
from gi.repository import GimpUi

PLUGIN_NAME = 'resynthesizer-heal-selection'
def N_(message): return message
def _(message): return GLib.dgettext(None, message)

debug = False

def heal_selection(timg, tdrawable, samplingRadiusParam=50, directionParam=0, orderParam=0):
'''
Create stencil selection in a temp image to pass as source (corpus) to plugin resynthesizer,
which does the substantive work.
'''
if pdb.gimp_selection_is_empty(timg):
pdb.gimp_message(_("You must first select a region to heal."))
return
def dprint(s: str):
if debug: print(s)

class HealSel (Gimp.PlugIn):
## GimpPlugIn virtual methods ##
def do_set_i18n(self, procname):
return True, 'gimp30-python', None

def do_query_procedures(self):
return [PLUGIN_NAME]

def do_create_procedure(self, name):
if name == PLUGIN_NAME:
procedure: Gimp.ImageProcedure = Gimp.ImageProcedure.new(self, name,
Gimp.PDBProcType.PLUGIN,
self.run, None)
procedure.set_image_types("RGB*, GRAY*")
procedure.set_sensitivity_mask(
Gimp.ProcedureSensitivityMask.DRAWABLE)
procedure.set_documentation(_("resynthesizer heal selection"),
_("heal selection with the resynthesizer algorithm"),
name)
procedure.set_menu_label(_("_heal selection"))
procedure.set_attribution("James Henstridge",
"James Henstridge",
"1999,2007")
procedure.add_menu_path("<Image>/Filters/Enhance")

procedure.add_int_argument(
name="samplingRadiusParam",
nick=_("Sampling radius"),
blurb=_("The sampling radius (in pixels)"),
min=1,
max=1000,
value=50,
flags=GObject.ParamFlags.READWRITE
)

direction_choice = Gimp.Choice.new()

direction_choice.add("all_around", 0, _("All around"), "")
direction_choice.add("sides_only", 1, _("Sides only"), "")
direction_choice.add("above_and_below", 2,
_("Above and below only"), "")

procedure.add_choice_argument("directionParam", _("Direct_ion"), _("Where to sample pixels from"),
direction_choice, "all_around", GObject.ParamFlags.READWRITE)
order_choice = Gimp.Choice.new()
order_choice.add("random", 0, _("Random"), "")
order_choice.add("inwards", 1, _("Inwards"), "")
order_choice.add("outwards", 2, _("Outwards"), "")

procedure.add_choice_argument("orderParam", _("Order"), _("The order to fill the selection in"),
order_choice, "random", GObject.ParamFlags.READWRITE)
return procedure
return None

def run(self, procedure: Gimp.Procedure, run_mode: Gimp.RunMode, image: Gimp.Image, layers, config, data):
if Gimp.Selection.is_empty(image):
Gimp.message("You must first select a region to heal.")
return procedure.new_return_values(Gimp.PDBStatusType.CALLING_ERROR, GLib.Error(None, None, "Select something first."))

if run_mode == Gimp.RunMode.INTERACTIVE:
GimpUi.init(PLUGIN_NAME)
dialog = GimpUi.ProcedureDialog(procedure=procedure, config=config)
dialog.fill(None)
if not dialog.run():
dialog.destroy()
return procedure.new_return_values(Gimp.PDBStatusType.CANCEL, GLib.Error())
else:
dialog.destroy()

samplingRadius: int = config.get_property('samplingRadiusParam')
directionParam: str = config.get_property('directionParam')
order: str = config.get_property('orderParam')

image.undo_group_start()

# select the bounds of the bottom-most layer.
target_bounds = layers[0].mask_bounds()

temp: Gimp.Image = image.duplicate()

if not temp:
raise RuntimeError("Failed duplicate image")

pdb.gimp_image_undo_group_start(timg)
if debug:
try:
disp: Gimp.Display = Gimp.Display.new(image=temp)
Gimp.displays_flush()
except RuntimeError: # thrown if non-interactive
pass
from time import sleep
sleep(2)

selected_drawables = temp.get_selected_drawables()
if len(selected_drawables) == 0:
raise RuntimeError("No drawables selected.")

work_drawable: Gimp.Layer = selected_drawables[0]
if not work_drawable:
raise RuntimeError("Failed get active drawable")

targetBounds = tdrawable.mask_bounds
selection: Gimp.Selection = image.get_selection()

# In duplicate image, create the sample (corpus).
# (I tried to use a temporary layer but found it easier to use duplicate image.)
tempImage = pdb.gimp_image_duplicate(timg)
if not tempImage:
raise RuntimeError("Failed duplicate image")
orig_selection: Gimp.Channel = selection.save(temp)
if not selection.grow(temp, samplingRadius):
Gimp.message("Could not grow selection")
return procedure.new_return_values(Gimp.PDBStatusType.EXECUTION_ERROR, GLib.Error(None, None, "couldn't grow the selection somehow."))

# !!! The drawable can be a mask (grayscale channel), don't restrict to layer.
work_drawable = pdb.gimp_image_get_active_drawable(tempImage)
if not work_drawable:
raise RuntimeError("Failed get active drawable")
# !!! Note that if selection is a bordering ring already, growing expanded it inwards.
# Which is what we want, to make a corpus inwards.
grown_selection: Gimp.Channel = selection.save(temp)

# Cut hole where the original selection was, so we don't sample from it.
temp.select_item(Gimp.ChannelOps.SUBTRACT, orig_selection)

# crop the temp image to size of selection to save memory and for directional healing!!
frisketBounds = grown_selection.mask_bounds()
frisketLowerLeftX = frisketBounds.x1
frisketLowerLeftY = frisketBounds.y1
frisketUpperRightX = frisketBounds.x2
frisketUpperRightY = frisketBounds.y2

dprint(f"{frisketBounds=}")

targetLowerLeftX = target_bounds.x1
targetLowerLeftY = target_bounds.y1
targetUpperRightX = target_bounds.x2
targetUpperRightY = target_bounds.y2

'''
grow and punch hole, making a frisket iow stencil iow donut

'''
orgSelection = pdb.gimp_selection_save(tempImage) # save for later use
pdb.gimp_selection_grow(tempImage, samplingRadiusParam)
# ??? returns None , docs say it returns SUCCESS

# !!! Note that if selection is a bordering ring already, growing expanded it inwards.
# Which is what we want, to make a corpus inwards.

grownSelection = pdb.gimp_selection_save(tempImage)

# Cut hole where the original selection was, so we don't sample from it.
# !!! Note that gimp enums/constants are not prefixed with GIMP_
pdb.gimp_image_select_item(tempImage, CHANNEL_OP_SUBTRACT, orgSelection)

'''
Selection (to be the corpus) is donut or frisket around the original target T
xxx
xTx
xxx
'''

# crop the temp image to size of selection to save memory and for directional healing!!
frisketBounds = grownSelection.mask_bounds
frisketLowerLeftX = frisketBounds[0]
frisketLowerLeftY = frisketBounds[1]
frisketUpperRightX = frisketBounds[2]
frisketUpperRightY = frisketBounds[3]
targetLowerLeftX = targetBounds[0]
targetLowerLeftY = targetBounds[1]
targetUpperRightX = targetBounds[2]
targetUpperRightY = targetBounds[3]

frisketWidth = frisketUpperRightX - frisketLowerLeftX
frisketHeight = frisketUpperRightY - frisketLowerLeftY

# User's choice of direction affects the corpus shape, and is also passed to resynthesizer plugin
if directionParam == 0: # all around
dprint(f"{target_bounds=}")

frisketWidth = frisketUpperRightX - frisketLowerLeftX
frisketHeight = frisketUpperRightY - frisketLowerLeftY

dprint(f"{frisketWidth=}, {frisketHeight=}")

newWidth, newHeight, newLLX, newLLY = (0, 0, 0, 0)
direction = 0
# User's choice of direction affects the corpus shape, and is also passed to resynthesizer plugin
if directionParam == 'all_around': # all around
direction = 0
# Crop to the entire frisket
newWidth, newHeight, newLLX, newLLY = ( frisketWidth, frisketHeight,
frisketLowerLeftX, frisketLowerLeftY )
elif directionParam == 1: # sides
newWidth, newHeight, newLLX, newLLY = (
frisketWidth,
frisketHeight,
frisketLowerLeftX,
frisketLowerLeftY
)
elif directionParam == 'sides_only': # sides
direction = 1
# Crop to target height and frisket width: XTX
newWidth, newHeight, newLLX, newLLY = ( frisketWidth, targetUpperRightY-targetLowerLeftY,
frisketLowerLeftX, targetLowerLeftY )
elif directionParam == 2: # above and below
newWidth, newHeight, newLLX, newLLY = (
frisketWidth,
targetUpperRightY-targetLowerLeftY,
frisketLowerLeftX,
targetLowerLeftY
)
elif directionParam == 'above_and_below': # above and below
direction = 2
# X Crop to target width and frisket height
# T
# X
newWidth, newHeight, newLLX, newLLY = ( targetUpperRightX-targetLowerLeftX, frisketHeight,
targetLowerLeftX, frisketLowerLeftY )
# Restrict crop to image size (condition of gimp_image_crop) eg when off edge of image
newWidth = min(pdb.gimp_image_width(tempImage) - newLLX, newWidth)
newHeight = min(pdb.gimp_image_height(tempImage) - newLLY, newHeight)
pdb.gimp_image_crop(tempImage, newWidth, newHeight, newLLX, newLLY)

# Encode two script params into one resynthesizer param.
# use border 1 means fill target in random order
# use border 0 is for texture mapping operations, not used by this script
if not orderParam :
newWidth, newHeight, newLLX, newLLY = (
targetUpperRightX-targetLowerLeftX,
frisketHeight,
targetLowerLeftX,
frisketLowerLeftY
)

dprint(f"{newWidth=} {newHeight=} {newLLX=} {newLLY=}")

# Restrict crop to image size (condition of gimp_image_crop) eg when off edge of image
newWidth = min(temp.get_width() - newLLX, newWidth)
newHeight = min(temp.get_height() - newLLY, newHeight)

dprint(f"resized {newWidth=} {newHeight=}")

temp.crop(newWidth, newHeight, newLLX, newLLY)

# default, just to declare the value.
useBorder = 1

# Encode two script params into one resynthesizer param.
# use border 1 means fill target in random order
# use border 0 is for texture mapping operations, not used by this script
if order == 'random':
useBorder = 1 # User wants NO order, ie random filling
elif orderParam == 1 : # Inward to corpus. 2,3,4
useBorder = directionParam+2 # !!! Offset by 2 to get past the original two boolean values
else:
elif order == 'inwards': # Inward to corpus. 2,3,4
# !!! Offset by 2 to get past the original two boolean values
useBorder = direction + 2
else:
# Outward from image center.
# 5+0=5 outward concentric
# 5+1=6 outward from sides
# 5+2=7 outward above and below
useBorder = directionParam+5

# Note that the old resynthesizer required an inverted selection !!

if debug:
try:
gimp.Display(tempImage)
gimp.displays_flush()
except RuntimeError: # thrown if non-interactive
pass
from time import sleep
sleep(2)

# Not necessary to restore image to initial condition of selection, activity,
# the original image should not have been changed,
# and the resynthesizer should only heal, not change selection.

# Note that the API hasn't changed but use_border param now has more values.
pdb.plug_in_resynthesizer(timg, tdrawable, 0,0, useBorder, work_drawable, -1, -1, 0.0, 0.117, 16, 500)

# Clean up (comment out to debug)
gimp.delete(tempImage)
pdb.gimp_image_undo_group_end(timg)


register(
"python-fu-heal-selection",
N_("Heal the selection from surroundings as if using the heal tool."),
"Requires separate resynthesizer plugin.",
"Lloyd Konneker",
"2009 Lloyd Konneker", # Copyright
"2009",
N_("_Heal selection..."),
"RGB*, GRAY*",
[
(PF_IMAGE, "image", "Input image", None),
(PF_DRAWABLE, "drawable", "Input drawable", None),
(PF_INT, "samplingRadiusParam", _("Context sampling width (pixels):"), 50),
(PF_OPTION,"directionParam", _("Sample from:"),0,[_("All around"),_("Sides"),_("Above and below")]),
(PF_OPTION, "orderParam", _("Filling order:"), 0, [_("Random"),
_("Inwards towards center"), _("Outwards from center") ])
],
[],
heal_selection,
menu="<Image>/Filters/Enhance",
domain=("resynthesizer", Gimp.locale_directory())
)

main()
useBorder = direction + 5

# Note that the old resynthesizer required an inverted selection !!

# Not necessary to restore image to initial condition of selection, activity,
# the original image should not have been changed,
# and the resynthesizer should only heal, not change selection.

# Note that the API hasn't changed but use_border param now has more values.
pdb: Gimp.PDB = Gimp.get_pdb()
pdb_proc: Gimp.Procedure = pdb.lookup_procedure('plug-in-resynthesizer')
pdb_config: Gimp.ProcedureConfig = pdb_proc.create_config()
pdb_config.set_property('run-mode', Gimp.RunMode.NONINTERACTIVE)
pdb_config.set_property('image', image)
# A hacky way to pass in python arrays directly,
# see: https://gitlab.gnome.org/GNOME/gobject-introspection/-/issues/492
pdb_config.set_core_object_array('drawables', layers)
pdb_config.set_property('h-tile', 0)
pdb_config.set_property('v-tile', 0)
pdb_config.set_property('use-border', useBorder)
pdb_config.set_property('corpus-drawable', work_drawable)
pdb_config.set_property('input-map', None)
pdb_config.set_property('output-map', None)
pdb_config.set_property('map-weight', 0.0)
pdb_config.set_property('autism', 0.117)
pdb_config.set_property('neighbours', 16)
pdb_config.set_property('trys', 500)
pdb_proc.run(pdb_config)

# Clean up (comment out to debug)
temp.delete()
image.undo_group_end()
return procedure.new_return_values(Gimp.PDBStatusType.SUCCESS, GLib.Error())

Gimp.main(HealSel.__gtype__, sys.argv)
Loading