Gimp-Forum.net
Alternative to Drawable.get_tile() for Gimp 3? - Printable Version

+- Gimp-Forum.net (https://www.gimp-forum.net)
+-- Forum: GIMP (https://www.gimp-forum.net/Forum-GIMP)
+--- Forum: Extending the GIMP (https://www.gimp-forum.net/Forum-Extending-the-GIMP)
+---- Forum: Scripting questions (https://www.gimp-forum.net/Forum-Scripting-questions)
+---- Thread: Alternative to Drawable.get_tile() for Gimp 3? (/Thread-Alternative-to-Drawable-get-tile-for-Gimp-3)



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.