Alternative to Drawable.get_tile() for Gimp 3? - joeyeroq - 03-25-2025
I'm trying to port a Gimp 2 script to Gimp 3 and got most of it figured out by trying stuff out in the python console.
Code:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import time
from gimpfu import *
def sprites2layers(image, layer, sample, minimal_dimensions):
'''
"Separates objects on one layer to their own layer. Handy for Spritesheets"
Parameters:
image -- The current image.
layer -- The layer of the image that is selected.
sample -- sample every nth pixel of layer tile
minimal_dimensions -- minimal dimension of object to be placed in "objects" layer group
'''
# Start timer
time_start = time.time()
# Check image type (RGB/Greyscale or Indexed) and if layer has alpha, if not stop this script with a warning
no_alpha_warning='This layer has no alpha, please add an alpha channel for this script to work'
if image.base_type == 0:
image_mode = 'RGB'
if layer.bpp != 4:
gimp.message(no_alpha_warning)
return
if image.base_type == 1:
image_mode = 'Greyscale'
if layer.bpp != 2:
gimp.message(no_alpha_warning)
return
if image.base_type == 2:
image_mode = 'Indexed'
if layer.bpp != 2:
gimp.message(no_alpha_warning)
return
tile_width = gimp.tile_width()
tile_height = gimp.tile_height()
layer_copy = layer.copy()
image.add_layer(layer_copy)
# Context setters for pdb.gimp_image_select_contiguous_color()
pdb.gimp_context_set_antialias(0)
pdb.gimp_context_set_feather(0)
pdb.gimp_context_set_sample_merged(0)
pdb.gimp_context_set_sample_criterion(0)
pdb.gimp_context_set_sample_threshold(1)
pdb.gimp_context_set_sample_transparent(0)
# Counters for objects in layer groups
counter_good = 1
counter_bad = 1
# Tile rows and columns of layer_copy
tile_rows = int( (layer_copy.height + tile_height - 1) / tile_height)
tile_cols = int( (layer_copy.width + tile_width - 1) / tile_width)
# create an 'off-screen' copy of layer_copy (not added to the canvas)
layer_offscreen = layer_copy.copy()
# Loop over tiles of layer_offscreen
for tile_row in range(tile_rows):
for tile_col in range(tile_cols):
Tile = layer_offscreen.get_tile(False, tile_row, tile_col)
# Loop over pixels of tiles
for tile_y in range(0, Tile.eheight, sample):
for tile_x in range(0, Tile.ewidth, sample):
pixel = Tile[tile_x, tile_y]
# Split components of imag_mode to get the alpha
# RGBA image
if image_mode == 'RGB':
R,G,B,A = Tile[tile_x, tile_y]
# Greyscale or Indexed image
if image_mode == 'Greyscale' or image_mode == 'Indexed':
I,A = Tile[tile_x, tile_y]
# If pixel is not completely transparent select it and neighbouring non-transparent pixels
if ord( A ) > 0:
layer_pixel_pos_x = tile_col * tile_height + tile_x
layer_pixel_pos_y = tile_row * tile_width + tile_y
pdb.gimp_image_select_contiguous_color(image, 2, layer_copy, layer_pixel_pos_x, layer_pixel_pos_y)
# Create a layer for an object and assign it to 'small objects' layer group or
# 'objects layer group' based on criteria "min_dimensions"
object_layer = layer_copy.copy()
x1, y1, x2, y2 = layer_copy.mask_bounds
# 'small objects' layer group
if x2 - x1 < minimal_dimensions and y2 - y1 < minimal_dimensions:
if counter_bad == 1 and (image_mode == 'RGB' or image_mode == 'Greyscale'):
layer_group_bad = pdb.gimp_layer_group_new(image)
layer_group_bad.name = 'small objects'
image.add_layer(layer_group_bad)
object_layer.name = "trash {:04d} ({:d}, {:d}, {:d}, {:d})".format(counter_bad, x1, y1, x2 - x1, y2 - y1)
counter_bad += 1
if image_mode == 'RGB' or image_mode == 'Greyscale':
image.active_layer = layer_group_bad
# Add object to layer_group_bad at the last position
pdb.gimp_image_insert_layer(image, object_layer, layer_group_bad, len(layer_group_bad.layers))
# 'objects' layer group
else:
if counter_good == 1 and (image_mode == 'RGB' or image_mode == 'Greyscale'):
layer_group_good = pdb.gimp_layer_group_new(image)
layer_group_good.name = 'objects'
image.add_layer(layer_group_good)
object_layer.name = "object {:04d} ({:d}, {:d}, {:d}, {:d})".format(counter_good, x1, y1, x2 - x1, y2 - y1)
counter_good += 1
if image_mode == 'RGB' or image_mode == 'Greyscale':
image.active_layer = layer_group_good
# Add object to layer_group_good at the last position
pdb.gimp_image_insert_layer(image, object_layer, layer_group_good, len(layer_group_good.layers))
# Add object to active layer which is one of the two layer groups
#image.add_layer(object_layer)
# "Auto crop" object layer
object_layer.resize(x2 - x1, y2 - y1, -x1, -y1)
# Remove/erase selection from layer_copy
image.active_layer = layer_copy
pdb.gimp_edit_clear(layer_copy)
# Update tile, by updating layer_offscreen, by copying layer_copy
layer_offscreen = layer_copy.copy()
Tile = layer_offscreen.get_tile(False, tile_row, tile_col)
# Remove layer_copy and layer_offscreen from canvas and memory
image.remove_layer(layer_copy)
del(layer_offscreen)
# Reset context settings to their default values.
pdb.gimp_context_set_defaults()
# End timer and display number of objects, small objects and timer seconds up to milliseconds in error console
time_end = time.time()
gimp.message('INFO:\n-{:d} objects found\n'.format(counter_good - 1) +
'-{:d} small objects found\n'.format(counter_bad - 1) +
'-time taken: {:.3f} seconds'.format(time_end - time_start))
### Parameters
imageParm = (PF_IMAGE , "image" , "Input image" , None)
layerParm = (PF_DRAWABLE, "layer" , "Input layer" , None)
sampleParm = (PF_INT , "sample" , "Sample every nth pixel", 1 )
mindimParm = (PF_INT , "minimal_dimensions", "Minimal dimension" , 10 )
### Registrations
name = "mich-sprites-2-layers"
blurb = "Objects to layers"
help = "Separates objects on one layer to their own layer. Handy for Spritesheets"
author = "Mich"
copyright = "Mich"
date = "2018-01-28"
menu_item = "sprites2layers..."
imagetypes = "*"
params = [imageParm, layerParm, sampleParm, mindimParm]
results = []
function = sprites2layers
menupath = "<Image>/Layer"
### Registrations
register(
name ,
blurb ,
help ,
author ,
copyright ,
date ,
menu_item ,
imagetypes,
params ,
results ,
function ,
menupath
)
main()
I've hit a snag with lines 65 and 129 with "Tile = layer_offscreen.get_tile(False, tile_row, tile_col)".
in Gimp 3 "drawable.get_tile()" doesn't exist, what is the alternative to this?
RE: Alternative to Drawable.get_tile() for Gimp 3? - Ofnuts - 03-25-2025
Looking at the APIs, you would use Gimp.Drawable::get_shadow_buffer() that returns a Gegl.Buffer. Gegl.Buffer is a subclass of Gegl.TileHandler and so has a Gegl.TileHandler::get_tile() method, among others; but this method isn't avaiable in the language bindings. The closest you have is Gegl.Buffer::get() that return a rectangle of pixels.
However, if I guess correctly what your code does, it is simpler to just duplicate the source layer and then crop it around the tile with a layer_copy.resize(). See my ofn3-layer-tiles script.
RE: Alternative to Drawable.get_tile() for Gimp 3? - joeyeroq - 03-25-2025
(Yesterday, 12:20 PM)Ofnuts Wrote: However, if I guess correctly what your code does, it is simpler to just duplicate the source layer and then crop it around the tile with a layer_copy.resize(). See my ofn3-layer-tiles script.
That's exactly what i'm doing at lines 88, 89
Code:
object_layer = layer_copy.copy()
x1, y1, x2, y2 = layer_copy.mask_bounds
and line 121
Code:
object_layer.resize(x2 - x1, y2 - y1, -x1, -y1)
https://developer.gimp.org/api/2.0/libgimp/libgimp-gimptile.html#GimpTile
My problem is not being able to use GimpTiles to quickly loop over pixels, my guestimation is that if i loop over pixels via drawable.get_pixel() directly in stead of looping over gimptiles and than looping over the pixels of the gimptiles the code would run 10x slower. I made this code a while ago circa 2018 and what i remember back then was that if I didnt use GimpTiles, if i can recall 25 or 30 seconds for a heavy sprite sheet versus 2 to 4 seconds when using GimpTiles.
RE: Alternative to Drawable.get_tile() for Gimp 3? - joeyeroq - 03-25-2025
sprites2layers gimp 3
Code:
def sprites2layers(image, layer, sample, minimal_dimensions):
'''
"Separates objects on one layer to their own layer. Handy for Spritesheets"
Parameters:
image -- The current image.
layer -- The layer of the image that is selected.
sample -- sample every nth pixel of layer tile.
minimal_dimensions -- minimal dimension of object to be placed in "objects" layer group.
'''
# Start timer.
time_start = time.time()
# Check image type (RGB/Greyscale or Indexed) and if layer has alpha, if not stop this script with a warning.
no_alpha_warning='Warning\nThis layer has no alpha, please add an alpha channel for this script to work.'
image_base_type = image.get_base_type()
match image_base_type:
case 0:
image_mode = 'RGB'
if layer.get_bpp() != 4:
Gimp.message(no_alpha_warning)
return
case 1:
image_mode = 'Greyscale'
if layer.get_bpp() != 2:
Gimp.message(no_alpha_warning)
return
case 2:
image_mode = 'Indexed'
if layer.get_bpp() != 2:
Gimp.message(no_alpha_warning)
return
layer_copy = layer.copy()
image.insert_layer(layer_copy, layer.get_parent(), 0)
# Context setters for pdb.gimp_image_select_contiguous_color().
Gimp.context_set_antialias(0)
Gimp.context_set_feather(0)
Gimp.context_set_sample_merged(0)
Gimp.context_set_sample_criterion(0)
Gimp.context_set_sample_threshold(1)
Gimp.context_set_sample_transparent(0)
# Counters for objects in layer groups.
counter_good = 1
counter_bad = 1
# Create an 'off-screen' copy of layer_copy (not added to the canvas).
layer_offscreen = layer_copy.copy()
# Loop over pixels.
for pixel_y in range(0, layer_copy.get_height(), sample):
for pixel_x in range(0, layer_copy.get_width(), sample):
pixel = layer_copy.get_pixel(pixel_x, pixel_y).get_rgba()
# Split components of imag_mode to get the alpha
# RGBA image
if image_mode == 'RGB':
R,G,B,A = pixel
# Greyscale or Indexed image
if image_mode == 'Greyscale' or image_mode == 'Indexed':
I,A = pixel
# If pixel is not completely transparent select it and neighbouring non-transparent pixels.
if A > 0:
image.select_contiguous_color(2, layer_copy, pixel_x, pixel_y)
# Create a layer for an object and assign it to 'small objects' layer group or
# 'objects layer group' based on criteria "min_dimensions"
object_layer = layer_copy.copy()
_, x1, y1, x2, y2 = layer_copy.mask_bounds()
# 'small objects' layer group
if x2 - x1 < minimal_dimensions and y2 - y1 < minimal_dimensions:
if counter_bad == 1 and (image_mode == 'RGB' or image_mode == 'Greyscale'):
layer_group_bad = Gimp.GroupLayer.new(image, 'small objects')
image.insert_layer(layer_group_bad, layer.get_parent(), 0)
object_layer.set_name("trash {:04d} ({:d}, {:d}, {:d}, {:d})".format(counter_bad, x1, y1, x2 - x1, y2 - y1))
counter_bad += 1
#if image_mode == 'RGB' or image_mode == 'Greyscale':
# image.active_layer = layer_group_bad
# Add object to layer_group_bad at the last position.
#pdb.gimp_image_insert_layer(image, object_layer, layer_group_bad, len(layer_group_bad.layers))
image.insert_layer(object_layer, layer_group_bad, len(layer_group_bad.get_children()))
# 'objects' layer group.
else:
if counter_good == 1 and (image_mode == 'RGB' or image_mode == 'Greyscale'):
layer_group_good = Gimp.GroupLayer.new(image, 'objects')
image.insert_layer(layer_group_good, layer.get_parent(), 0)
object_layer.set_name("object {:04d} ({:d}, {:d}, {:d}, {:d})".format(counter_good, x1, y1, x2 - x1, y2 - y1))
counter_good += 1
#if image_mode == 'RGB' or image_mode == 'Greyscale':
# image.active_layer = layer_group_good
# Add object to layer_group_good at the last position.
#pdb.gimp_image_insert_layer(image, object_layer, layer_group_good, len(layer_group_good.layers))
image.insert_layer(object_layer, layer_group_good, len(layer_group_good.get_children()))
# "Auto crop" object layer.
object_layer.resize(x2 - x1, y2 - y1, -x1, -y1)
# Remove/erase selection from layer_copy
#image.active_layer = layer_copy
layer_copy.edit_clear()
# Update tile, by updating layer_offscreen, by copying layer_copy
layer_offscreen = layer_copy.copy()
#Tile = layer_offscreen.get_tile(False, tile_row, tile_col)
# Remove layer_copy and layer_offscreen from canvas and memory.
image.remove_layer(layer_copy)
del(layer_copy)
del(layer_offscreen)
# Reset context settings to their default values.
Gimp.context_set_defaults()
# End timer and display number of objects, small objects and timer seconds up to milliseconds in error console
time_end = time.time()
Gimp.message('INFO:\n-{:d} objects found\n'.format(counter_good - 1) +
'-{:d} small objects found\n'.format(counter_bad - 1) +
'-time taken: {:.3f} seconds'.format(time_end - time_start))
Also in GIMP 3 python console:
Code:
import time
image = Gimp.get_images()[0]
layer = image.get_layers()[0]
sprites2layers(image, layer, 1, 10)
I did a quick test port to Gimp 3 but had to take GimpTile code out and just used a Python loop over pixels. The other difference is that I used Python console and not as plugin, but I don't think that would significantly change speed.
results:
GIMP 2.10, 2.710 seconds.
GIMP 3.0.0-1, 109.061 seconds!
I thought it was 10x slower but its much more than that! Any solution to the GimpTile problem?
[attachment=13250]
[attachment=13251]
RE: Alternative to Drawable.get_tile() for Gimp 3? - CmykStudent - 03-26-2025
joeyeroq: Hi! It looks like you're getting individual GeglColor pixels - this will be very slow. You should look into the GeglBuffer API, which lets you retrieve all pixels at once as an array, make your edits, then put that array back into the GeglBuffer.
RE: Alternative to Drawable.get_tile() for Gimp 3? - Ofnuts - 03-26-2025
(Yesterday, 11:00 PM)joeyeroq Wrote: sprites2layers gimp 3
Code:
def sprites2layers(image, layer, sample, minimal_dimensions):
'''
"Separates objects on one layer to their own layer. Handy for Spritesheets"
Parameters:
image -- The current image.
layer -- The layer of the image that is selected.
sample -- sample every nth pixel of layer tile.
minimal_dimensions -- minimal dimension of object to be placed in "objects" layer group.
'''
# Start timer.
time_start = time.time()
# Check image type (RGB/Greyscale or Indexed) and if layer has alpha, if not stop this script with a warning.
no_alpha_warning='Warning\nThis layer has no alpha, please add an alpha channel for this script to work.'
image_base_type = image.get_base_type()
match image_base_type:
case 0:
image_mode = 'RGB'
if layer.get_bpp() != 4:
Gimp.message(no_alpha_warning)
return
case 1:
image_mode = 'Greyscale'
if layer.get_bpp() != 2:
Gimp.message(no_alpha_warning)
return
case 2:
image_mode = 'Indexed'
if layer.get_bpp() != 2:
Gimp.message(no_alpha_warning)
return
layer_copy = layer.copy()
image.insert_layer(layer_copy, layer.get_parent(), 0)
# Context setters for pdb.gimp_image_select_contiguous_color().
Gimp.context_set_antialias(0)
Gimp.context_set_feather(0)
Gimp.context_set_sample_merged(0)
Gimp.context_set_sample_criterion(0)
Gimp.context_set_sample_threshold(1)
Gimp.context_set_sample_transparent(0)
# Counters for objects in layer groups.
counter_good = 1
counter_bad = 1
# Create an 'off-screen' copy of layer_copy (not added to the canvas).
layer_offscreen = layer_copy.copy()
# Loop over pixels.
for pixel_y in range(0, layer_copy.get_height(), sample):
for pixel_x in range(0, layer_copy.get_width(), sample):
pixel = layer_copy.get_pixel(pixel_x, pixel_y).get_rgba()
# Split components of imag_mode to get the alpha
# RGBA image
if image_mode == 'RGB':
R,G,B,A = pixel
# Greyscale or Indexed image
if image_mode == 'Greyscale' or image_mode == 'Indexed':
I,A = pixel
# If pixel is not completely transparent select it and neighbouring non-transparent pixels.
if A > 0:
image.select_contiguous_color(2, layer_copy, pixel_x, pixel_y)
# Create a layer for an object and assign it to 'small objects' layer group or
# 'objects layer group' based on criteria "min_dimensions"
object_layer = layer_copy.copy()
_, x1, y1, x2, y2 = layer_copy.mask_bounds()
# 'small objects' layer group
if x2 - x1 < minimal_dimensions and y2 - y1 < minimal_dimensions:
if counter_bad == 1 and (image_mode == 'RGB' or image_mode == 'Greyscale'):
layer_group_bad = Gimp.GroupLayer.new(image, 'small objects')
image.insert_layer(layer_group_bad, layer.get_parent(), 0)
object_layer.set_name("trash {:04d} ({:d}, {:d}, {:d}, {:d})".format(counter_bad, x1, y1, x2 - x1, y2 - y1))
counter_bad += 1
#if image_mode == 'RGB' or image_mode == 'Greyscale':
# image.active_layer = layer_group_bad
# Add object to layer_group_bad at the last position.
#pdb.gimp_image_insert_layer(image, object_layer, layer_group_bad, len(layer_group_bad.layers))
image.insert_layer(object_layer, layer_group_bad, len(layer_group_bad.get_children()))
# 'objects' layer group.
else:
if counter_good == 1 and (image_mode == 'RGB' or image_mode == 'Greyscale'):
layer_group_good = Gimp.GroupLayer.new(image, 'objects')
image.insert_layer(layer_group_good, layer.get_parent(), 0)
object_layer.set_name("object {:04d} ({:d}, {:d}, {:d}, {:d})".format(counter_good, x1, y1, x2 - x1, y2 - y1))
counter_good += 1
#if image_mode == 'RGB' or image_mode == 'Greyscale':
# image.active_layer = layer_group_good
# Add object to layer_group_good at the last position.
#pdb.gimp_image_insert_layer(image, object_layer, layer_group_good, len(layer_group_good.layers))
image.insert_layer(object_layer, layer_group_good, len(layer_group_good.get_children()))
# "Auto crop" object layer.
object_layer.resize(x2 - x1, y2 - y1, -x1, -y1)
# Remove/erase selection from layer_copy
#image.active_layer = layer_copy
layer_copy.edit_clear()
# Update tile, by updating layer_offscreen, by copying layer_copy
layer_offscreen = layer_copy.copy()
#Tile = layer_offscreen.get_tile(False, tile_row, tile_col)
# Remove layer_copy and layer_offscreen from canvas and memory.
image.remove_layer(layer_copy)
del(layer_copy)
del(layer_offscreen)
# Reset context settings to their default values.
Gimp.context_set_defaults()
# End timer and display number of objects, small objects and timer seconds up to milliseconds in error console
time_end = time.time()
Gimp.message('INFO:\n-{:d} objects found\n'.format(counter_good - 1) +
'-{:d} small objects found\n'.format(counter_bad - 1) +
'-time taken: {:.3f} seconds'.format(time_end - time_start))
Also in GIMP 3 python console:
Code:
import time
image = Gimp.get_images()[0]
layer = image.get_layers()[0]
sprites2layers(image, layer, 1, 10)
I did a quick test port to Gimp 3 but had to take GimpTile code out and just used a Python loop over pixels. The other difference is that I used Python console and not as plugin, but I don't think that would significantly change speed.
results:
GIMP 2.10, 2.710 seconds.
GIMP 3.0.0-1, 109.061 seconds!
I thought it was 10x slower but its much more than that! Any solution to the GimpTile problem?
Ah, so this looks more like my ofn-extract-objects script. It uses a radically different approach:
- alpha to selection
- Select > To path. At that point each stroke of the path is the outline of a sprite(*). In other words I outsource the alpha discrimination to native Gimp code.
- From each stroke I can get a a bounding box and then crop around the sprite.
Also, even in Gimp2, I wouldn't have used tiles but "pixel regions".
(*) technically, if the sprite is hollow, I can get strokes within strokes, but there are ways to determine if a stroke is nested within another.
RE: Alternative to Drawable.get_tile() for Gimp 3? - joeyeroq - 03-26-2025
(9 hours ago)CmykStudent Wrote: joeyeroq: Hi! It looks like you're getting individual GeglColor pixels - this will be very slow. You should look into the GeglBuffer API, which lets you retrieve all pixels at once as an array, make your edits, then put that array back into the GeglBuffer.
How do you use the buffer?
For example how do you print out the first 4 pixels with the buffer?
With a slow loop over pixels in Python Console:
Code:
image = Gimp.get_images()[0]
layer = image.get_layers()[0]
for pixel_x in range(0, layer.get_width()):
if pixel_x == 4:
break
pixel = layer.get_pixel(pixel_x, 0).get_rgba()
print(pixel)
gives result
(red=1.0, green=1.0, blue=1.0, alpha=1.0)
(red=1.0, green=1.0, blue=1.0, alpha=1.0)
(red=1.0, green=1.0, blue=1.0, alpha=1.0)
(red=1.0, green=1.0, blue=1.0, alpha=1.0)
Now with buffer:
Code:
image = Gimp.get_images()[0]
layer = image.get_layers()[0]
buffer = layer.get_buffer()
What do I do from here? probably buffer.get() but what parameters do i put in it?
(2 hours ago)Ofnuts Wrote: Ofnuts
- Also, even in Gimp2, I wouldn't have used tiles but "pixel regions".
What are "pixel regions"?
Please give small snippet to put into Python console.
|