It is currently Tue Aug 11, 2026 7:14 am


All times are UTC - 5 hours [ DST ]



Post new topic Reply to topic  [ 42 posts ]  Go to page Previous  1, 2, 3  Next
Author Message
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Thu Jul 11, 2024 9:00 am  (#21) 
Offline
GimpChat Member

Joined: Jun 26, 2024
Posts: 8
Thanks for the new plugin, it indeed works, but I'm having trouble with it. It runs very slow on full scanned pages, it takes several minutes to process one (I have AMD Threadripper CPU). And when the "Selection to path" bar finally fills, the plugin window hangs ("not responding" appears in the title). GIMP then constantly consumes full 8 CPUs, it happens with 2.10.34 and 2.10.38. When I force close the plugin window, an error message appears, I'm attaching a screenshot. However, it's possible to save the resulting image when I close it. I'm attaching one of the scanned pages, so you could debug it better.

BTW, ImageJ takes about 2 seconds to process one page with that script I posted earlier. After some trial-and-error, I set the unwanted spot size to "size=0-7".


Attachments:
Full page sample.png
Full page sample.png [ 242.99 KiB | Viewed 4353 times ]
Plugin error after closing.png
Plugin error after closing.png [ 7.06 KiB | Viewed 4353 times ]
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Thu Jul 11, 2024 11:37 am  (#22) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
please let me test, and see I'll get back to you.

_________________
TinT


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Thu Jul 11, 2024 12:48 pm  (#23) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
Yeah I got memory and it hung on me too.
So the solution I can come up with right now is that.
1. Made the remove dots a subfunction in the plug-in
2. Call the subfunction every 500x500 tile at time and then merge the result with our working copy.

I ran it with threshold 12 and 25 maxpixel setting and it seemed to have removed some of the greater/lesser signs in document. It took like 10 minutes to run on mine on this image (but on mine it took longer than 2 seconds to run on original test image though).

You mean 7 max pixels setting? You can try that setting see if it works better for you. Let me know how it goes.

Here's the test result of my 12 threshold and 25 maxpixel setting:
Image
here's the code of the tile processing version:
#!/usr/bin/env python
# Author: (Tin Tran)
# Created On: 2024-07-10
# Remove only black spots in scanned documents?
# Possible Solution: use logics from http://gimpchat.com/viewtopic.php?f=9&t=14045&p=256687&hilit=DivideTransBG.scm#p256687
# which can divide up transparent background to get alpha shapes. Except this problem is just white
# so we'll let user select background color, threshold, and replace all shapes that have less than minimum allowed pixels
#
# License: Whatever GIMP's License is.
# Revisions:
# Rel 0.10: Initial Version
# Rel 0.11: Process 500x500 tile at a time to avoid memory problems.
from gimpfu import *
import random
import math
def remove_dots_sub(image,layeri,bgcolor,threshold,maxpixels):
    #interpolation none scale image so that dots are preserved when we
    #selection to path it
    pdb.gimp_image_scale_full(image,image.width*4,image.height*4,0)
    pdb.gimp_by_color_select(layeri,bgcolor,threshold,CHANNEL_OP_REPLACE,TRUE,FALSE,0,FALSE)
    pdb.plug_in_sel2path(image,layeri)
    active_vectors = pdb.gimp_image_get_active_vectors(image)
    num_strokes,stroke_ids = pdb.gimp_vectors_get_strokes(active_vectors)
    for i in range(0,len(stroke_ids)):
        _type,num_points,controlpoints,closed = pdb.gimp_vectors_stroke_get_points(active_vectors,stroke_ids[i])
        new_vectors = pdb.gimp_vectors_new(image,"single vector")
        pdb.gimp_image_insert_vectors(image,new_vectors,None,0)
        pdb.gimp_vectors_stroke_new_from_points(new_vectors,_type,num_points,controlpoints,closed)
        pdb.gimp_image_select_item(image,CHANNEL_OP_REPLACE,new_vectors)
        mean,std_dev,median,pixels,count_,percentile = pdb.gimp_histogram(layeri,HISTOGRAM_VALUE,0,255)
        #if pixel count is less than or equal to our maximum fill it with color
        if count_ <= maxpixels*16:
            pdb.gimp_context_set_foreground(bgcolor)
            pdb.gimp_edit_fill(layeri,FILL_FOREGROUND)
        #remove when done with this one
        pdb.gimp_image_remove_vectors(image,new_vectors)
    pdb.gimp_selection_none(image)
    pdb.gimp_image_scale_full(image,image.width/4,image.height/4,0)
import math
def remove_dots(image,layer,bgcolor,threshold,maxpixels):
    #When page is too large, we run into hanging problem so let's do it in chunks
    w=image.width; h=image.height;
    tile_dimension = 500; #set this 500x500 so we can process a tile at a time and merge them later
    for y_tile in range(0,int(math.ceil(float(h)/tile_dimension))):
        #show some progress so we know it's not hanging
        pdb.gimp_progress_set_text("Processing row" + str(y_tile+1))
        pdb.gimp_progress_update(float(y_tile/math.ceil(float(h)/tile_dimension)))
        for x_tile in range(0,int(math.ceil(float(w)/tile_dimension))):
            tile_image = pdb.gimp_image_new(tile_dimension,tile_dimension,RGB)
            tile_display = pdb.gimp_display_new(tile_image)
            tile_layer = pdb.gimp_layer_new(tile_image,tile_dimension,tile_dimension,RGBA_IMAGE,'tile',100,LAYER_MODE_NORMAL)
            pdb.gimp_image_insert_layer(tile_image,tile_layer,None,0)
            #get either minimum of (tile dimension or whatever is left to fill image boundary)
            sel_width = min(tile_dimension,w-(x_tile*tile_dimension))
            sel_height = min(tile_dimension,h-(y_tile*tile_dimension))
            pdb.gimp_image_select_rectangle(image,CHANNEL_OP_REPLACE,x_tile*tile_dimension,y_tile*tile_dimension,sel_width,sel_height)
            pdb.gimp_edit_copy(layer)
            floating_sel = pdb.gimp_edit_paste(tile_layer,TRUE)
            pdb.gimp_floating_sel_anchor(floating_sel)

            #call the subfunction to deal with removing dots here
            remove_dots_sub(tile_image,tile_layer,bgcolor,threshold,maxpixels)

            #here we should have that tile without dots, put it on top, position it and merge down
            withoutdots_layer = pdb.gimp_layer_new_from_drawable(tile_layer,image)
            pdb.gimp_image_insert_layer(image,withoutdots_layer,None,0)
            pdb.gimp_layer_set_offsets(withoutdots_layer,x_tile*tile_dimension,y_tile*tile_dimension)
            layer = pdb.gimp_image_merge_down(image,withoutdots_layer,CLIP_TO_BOTTOM_LAYER)

            #we're done with tile so delete it
            pdb.gimp_display_delete(tile_display)

register(
    "python_fu_remove_dots",
    "remove dots",
    "remove dots by filling in with bg color",
    "author name",
    "copyright name",
    "2024.07.10",
    "Remove Dots...",
    "RGB*",      # Alternately use RGB, RGB*, GRAY*, INDEXED etc.
    [
    #INPUT BEGINS
    (PF_IMAGE, "image", "Image", None),
    (PF_DRAWABLE,"layer", "Drawable", None),
    (PF_COLOR, "bgcolor", "Background Color:", (255, 255, 255) ), # extra param is RGB triple
    (PF_SLIDER, "threshold", "Threshold:", 8, (0, 100, 1)),
    (PF_SLIDER, "maxpixels", "Maximum No Of Pixels To Fill With BGColor:", 25, (0, 500, 1)),
    #INPUT ENDS
    ],
    [],
    remove_dots,
    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 (found online years back that I didn't want to constantly look up)
# Since GIMP is free anyways, I thought it would be handy here for me to CUT and PASTE, CHANGE, USE.
#           (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.



Attachments:
File comment: tile processing version
remove-black-dots.zip [2.99 KiB]
Downloaded 121 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Thu Jul 11, 2024 4:53 pm  (#24) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
Caleb13, the provider of itches heheh just kidding.

_________________
TinT


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Thu Jul 11, 2024 11:41 pm  (#25) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
This might be faster as it looks at stroke length instead of pixelcount.
Below ran with 8 threshold and 10 maxpixels
Image

#!/usr/bin/env python
# Author: (Tin Tran)
# Created On: 2024-07-10
# Remove only black spots in scanned documents?
# Possible Solution: use logics from http://gimpchat.com/viewtopic.php?f=9&t=14045&p=256687&hilit=DivideTransBG.scm#p256687
# which can divide up transparent background to get alpha shapes. Except this problem is just white
# so we'll let user select background color, threshold, and replace all shapes that have less than minimum allowed pixels
#
# License: Whatever GIMP's License is.
# Revisions:
# Rel 0.10: Initial Version
# Rel 0.11: Process 500x500 tile at a time to avoid memory problems.
# Rel 0.12: Process looking at length of stroke instead of pixel count.
from gimpfu import *
import random
import math
def remove_dots_sub(image,layeri,bgcolor,threshold,maxpixels):
    #interpolation none scale image so that dots are preserved when we
    #selection to path it
    pdb.gimp_image_scale_full(image,image.width*4,image.height*4,0)
    pdb.gimp_by_color_select(layeri,bgcolor,threshold,CHANNEL_OP_REPLACE,TRUE,FALSE,0,FALSE)
    pdb.plug_in_sel2path(image,layeri)
    active_vectors = pdb.gimp_image_get_active_vectors(image)
    num_strokes,stroke_ids = pdb.gimp_vectors_get_strokes(active_vectors)
    pdb.gimp_context_set_foreground(bgcolor)
    pdb.gimp_context_set_brush_size(5)
    pdb.gimp_context_set_brush("2. Hardness 100")
    pdb.gimp_selection_none(image)
    for i in range(0,len(stroke_ids)):
        _type,num_points,controlpoints,closed = pdb.gimp_vectors_stroke_get_points(active_vectors,stroke_ids[i])
        #BELOW IS SOMETHING I AM TRY NEW BASED ON path length
        length = pdb.gimp_vectors_stroke_get_length(active_vectors,stroke_ids[i],0.1)
        if length < (maxpixels*16)**0.5*4.0:
            for p in range(0,int(math.floor(length))):
                x,y,slope,valid = pdb.gimp_vectors_stroke_get_point_at_dist(active_vectors,stroke_ids[i],p,0.1)
                pdb.gimp_paintbrush_default(layeri,2,[x,y])
        #BELOW IS WHAT IT USED TO DO USING PIXEL COUNT
        #new_vectors = pdb.gimp_vectors_new(image,"single vector")
        #pdb.gimp_image_insert_vectors(image,new_vectors,None,0)
        #pdb.gimp_vectors_stroke_new_from_points(new_vectors,_type,num_points,controlpoints,closed)
        #pdb.gimp_image_select_item(image,CHANNEL_OP_REPLACE,new_vectors)
        #mean,std_dev,median,pixels,count_,percentile = pdb.gimp_histogram(layeri,HISTOGRAM_VALUE,0,255)
        #if pixel count is less than or equal to our maximum fill it with color
        #if count_ <= maxpixels*16:
            #pdb.gimp_context_set_foreground(bgcolor)
            #pdb.gimp_edit_fill(layeri,FILL_FOREGROUND)
        #mean,std_dev,median,pixels,count_,percentile = pdb.gimp_histogram(layeri,HISTOGRAM_VALUE,0,255)
        #if pixel count is less than or equal to our maximum fill it with color
        #if count_ <= maxpixels*16:
            #pdb.gimp_context_set_foreground(bgcolor)
            #pdb.gimp_edit_fill(layeri,FILL_FOREGROUND)
               
        #remove when done with this one
        #pdb.gimp_image_remove_vectors(image,new_vectors)
    pdb.gimp_selection_none(image)
    pdb.gimp_image_scale_full(image,image.width/4,image.height/4,0)
import math
def remove_dots(image,layer,bgcolor,threshold,maxpixels):
    #When page is too large, we run into hanging problem so let's do it in chunks
    w=image.width; h=image.height;
    tile_dimension = 500; #set this 500x500 so we can process a tile at a time and merge them later
    for y_tile in range(0,int(math.ceil(float(h)/tile_dimension))):
        #show some progress so we know it's not hanging
        pdb.gimp_progress_set_text("Processing row" + str(y_tile+1))
        pdb.gimp_progress_update(float(y_tile/math.ceil(float(h)/tile_dimension)))
        for x_tile in range(0,int(math.ceil(float(w)/tile_dimension))):
            tile_image = pdb.gimp_image_new(tile_dimension,tile_dimension,RGB)
            tile_display = pdb.gimp_display_new(tile_image)
            tile_layer = pdb.gimp_layer_new(tile_image,tile_dimension,tile_dimension,RGBA_IMAGE,'tile',100,LAYER_MODE_NORMAL)
            pdb.gimp_image_insert_layer(tile_image,tile_layer,None,0)
            #get either minimum of (tile dimension or whatever is left to fill image boundary)
            sel_width = min(tile_dimension,w-(x_tile*tile_dimension))
            sel_height = min(tile_dimension,h-(y_tile*tile_dimension))
            pdb.gimp_image_select_rectangle(image,CHANNEL_OP_REPLACE,x_tile*tile_dimension,y_tile*tile_dimension,sel_width,sel_height)
            pdb.gimp_edit_copy(layer)
            floating_sel = pdb.gimp_edit_paste(tile_layer,TRUE)
            pdb.gimp_floating_sel_anchor(floating_sel)

            #call the subfunction to deal with removing dots here
            remove_dots_sub(tile_image,tile_layer,bgcolor,threshold,maxpixels)

            #here we should have that tile without dots, put it on top, position it and merge down
            withoutdots_layer = pdb.gimp_layer_new_from_drawable(tile_layer,image)
            pdb.gimp_image_insert_layer(image,withoutdots_layer,None,0)
            pdb.gimp_layer_set_offsets(withoutdots_layer,x_tile*tile_dimension,y_tile*tile_dimension)
            layer = pdb.gimp_image_merge_down(image,withoutdots_layer,CLIP_TO_BOTTOM_LAYER)

            #we're done with tile so delete it
            pdb.gimp_display_delete(tile_display)

register(
    "python_fu_remove_dots",
    "remove dots",
    "remove dots by filling in with bg color",
    "author name",
    "copyright name",
    "2024.07.10",
    "Remove Dots...",
    "RGB*",      # Alternately use RGB, RGB*, GRAY*, INDEXED etc.
    [
    #INPUT BEGINS
    (PF_IMAGE, "image", "Image", None),
    (PF_DRAWABLE,"layer", "Drawable", None),
    (PF_COLOR, "bgcolor", "Background Color:", (255, 255, 255) ), # extra param is RGB triple
    (PF_SLIDER, "threshold", "Threshold:", 8, (0, 100, 1)),
    (PF_SLIDER, "maxpixels", "Maximum No Of Pixels To Fill With BGColor:", 25, (0, 500, 1)),
    #INPUT ENDS
    ],
    [],
    remove_dots,
    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 (found online years back that I didn't want to constantly look up)
# Since GIMP is free anyways, I thought it would be handy here for me to CUT and PASTE, CHANGE, USE.
#           (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.



Attachments:
File comment: Might be faster version that looks at length of stroke and brushes stroke with background color instead of pixel counting
remove-black-dots.zip [3.25 KiB]
Downloaded 125 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Fri Jul 12, 2024 9:20 am  (#26) 
Offline
GimpChat Member

Joined: Jun 26, 2024
Posts: 8
Thanks, it now finishes without errors, but doesn't work right - it increases size of isolated white pixels which are surrounded by black pixels. This happened in your image too, see the table lines in the upper left corner. I'm attaching output images from ImageJ and your script, so you could compare the result. Spot size was set to 7 in both cases.

In any case, your script is much more user-friendly than ImageJ, but still too slow, one page took about 15 minutes on my PC. I wonder how ImageJ Particle Analyzer manages to run so fast, when it's written in Java, of all languages. I still can't post links, it's on Github in this directory:

/imagej/ImageJ/blob/master/ij/plugin/filter/ParticleAnalyzer.java

See lines 17 to 29, there is pseudo-code that explains how their algorithm works. I have no idea if it could be replicated with GIMP, though.


Attachments:
Full page sample python thr 7 max 25.png
Full page sample python thr 7 max 25.png [ 237.62 KiB | Viewed 4295 times ]
Full page sample ImageJ size 0-7.png
Full page sample ImageJ size 0-7.png [ 235.92 KiB | Viewed 4295 times ]
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Fri Jul 12, 2024 12:32 pm  (#27) 
Offline
GimpChat Member
User avatar

Joined: May 24, 2021
Posts: 859
Location: SEA - South East Asia
Now you can post links as you've reached 5 posts :clap :mrgreen:

Caleb13 wrote:
In any case, your script is much more user-friendly than ImageJ, but still too slow, one page took about 15 minutes on my PC. I wonder how ImageJ Particle Analyzer manages to run so fast, when it's written in Java, of all languages. I still can't post links, it's on Github in this directory:

/imagej/ImageJ/blob/master/ij/plugin/filter/ParticleAnalyzer.java

Why don't you use Fiji instead? > https://imagej.net/software/fiji/

Fiji Is Just ImageJ with “batteries-included” distribution of ImageJ and ImageJ2 which includes many useful plugins contributed by the community (Plugins / Process / Particle Analyzer included)
In "Plugins", a scripting interpreter, even javascript if I recall (many years I did not use it), maybe a batch process somewhere..., you might find what you need.
Attachment:
Untitled.jpg
Untitled.jpg [ 165.11 KiB | Viewed 4284 times ]

_________________
Patrice


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Fri Jul 12, 2024 2:00 pm  (#28) 
Offline
GimpChat Member
User avatar

Joined: Dec 26, 2014
Posts: 205
It's obvious now my first script was no more than useless, sorry

My second try, the rendition of your full size sample below was done with the script in this zipped folder

Attachment:
remove-black-bg-pixels.zip [914 Bytes]
Downloaded 128 times

The default value of 3700 was used

It took just over 6 minutes

The dialogue box sometimes shows 'not responding', but the script always finishes if left alone

The highest number you can input is 4080, the lower the number entered the more the text is eroded

At 3700 it is starting to erode the '>' '<' characters

The Script can be found at '<Image>/Filters/Remove single black pixels'

Attachment:
value 3700 6 minutes.png
value 3700 6 minutes.png [ 308.83 KiB | Viewed 4280 times ]


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Fri Jul 12, 2024 10:00 pm  (#29) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
Thought I'd make a new plug-in that fastpixel access and
With help of ChatGPT, it gave me flood-fill function I can work with but was still slow.
Then I thought to only look at blackish pixels and so it reduces the workload calls to floodfill and was
able to get it down to anywhere from 19 seconds to 23 seconds. :D
Image
#!/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.
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):
    global mastervisited,srcWidth,srcHeight,srcRgn,src_pixels,newWidth,newHeight,dstRgn,p_size,dest_pixels,src_pos,newval
    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)):
        group = groups[i]
        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)
    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.
    #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.


Attachments:
File comment: It's the faster 19 second version
fill-bg-less-than.zip [3.46 KiB]
Downloaded 48 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Fri Jul 12, 2024 10:36 pm  (#30) 
Offline
GimpChat Member
User avatar

Joined: Dec 26, 2014
Posts: 205
trandoductin wrote:
Thought I'd make a new plug-in that fastpixel access and
With help of ChatGPT, it gave me flood-fill function I can work with but was still slow.
Then I thought to only look at blackish pixels and so it reduces the workload calls to floodfill and was
able to get it down to anywhere from 19 seconds to 23 seconds. :D
[ Image ]
#!/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.
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):
    global mastervisited,srcWidth,srcHeight,srcRgn,src_pixels,newWidth,newHeight,dstRgn,p_size,dest_pixels,src_pos,newval
    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)):
        group = groups[i]
        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)
    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.
    #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.


That is awesome Tim :bigthup a great result at a mind blowing speed


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Fri Jul 12, 2024 11:23 pm  (#31) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
Thanks Steve, mostly ChatGPT.

_________________
TinT


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Sat Jul 13, 2024 3:02 am  (#32) 
Offline
GimpChat Member

Joined: Jun 26, 2024
Posts: 8
Awesome work, fill-bg-less-than.py took only 18 seconds on my PC! That's fast enough even for processing multiple files. It's more aggresive than ImageJ, it removes parts of ">" signs at MaxPixels=7 setting, for example. I had to lower it to 4 to prevent it, but then it leaves some round dots. I'm attaching comparison image. I think it happens because your script evaulates diagonally-touching pixels differently than ImageJ.

There are other minor issues, too. When it finishes, an error window always appeares, I'm attaching it. And unlike previous versions, it doesn't check if the image has correct mode (RGB), it throws error when I tried to run in on source 1-bit files. Would it be possible to make it work directly on 1-bit images, without converting to RGB first?


Attachments:
Callback error.png
Callback error.png [ 5.09 KiB | Viewed 4231 times ]
fill-bg comparison.png
fill-bg comparison.png [ 13.88 KiB | Viewed 4231 times ]
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Sat Jul 13, 2024 8:22 am  (#33) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
I have added an extra parameter called within radius.
So it doesn't take out the greater/lesser sign so pixel groups has to be within that radius.
and made it so you can use gray scale
#!/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.
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) == FALSE:
        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)
    pdb.gimp_image_convert_grayscale(image)
    #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.


Attachments:
File comment: gray-scale will be converted rgb for processing then back to gray scale when done
fill-bg-less-than.zip [3.67 KiB]
Downloaded 88 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Sat Jul 13, 2024 9:38 am  (#34) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
I think this one is even better since it won't get rid of the lesser/greater sign as much. since those are shaped unlike a round dot.
This will will generally get rid of round shapes (or where the pixels are within a diameter of a generally round dot).
#!/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.1: change to RGB if gray scale then change back to GrayScale. Process only if it's generally a round dot shape.
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),(cx-1, cy+1), (cx+1, cy+1), (cx-1, cy-1), (cx+1, 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):
    global mastervisited,srcWidth,srcHeight,srcRgn,src_pixels,newWidth,newHeight,dstRgn,p_size,dest_pixels,src_pos,newval
    if pdb.gimp_drawable_is_rgb(layer) == FALSE:
        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
        radius = ((len(group)/3.1415)**0.5)*2.5;
        cx = 0; cy = 0;
        for p1 in group:
            cx += p1[0]
            cy += p1[1]
        cx/=len(group)
        cy/=len(group)   
        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)
    pdb.gimp_image_convert_grayscale(image)
    #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.
    #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.


Attachments:
fill-bg-less-than.zip [3.72 KiB]
Downloaded 115 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Sat Jul 13, 2024 10:02 am  (#35) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
This one you can enter like 100 for max pixels and it won't get rid of commas because i made sure that comma has features that would fail because it's not roundish according to this one's calculation it get rids of all roundish dots
#!/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.1: change to RGB if gray scale then change back to GrayScale. Process only if it's generally a round dot shape.
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),(cx-1, cy+1), (cx+1, cy+1), (cx-1, cy-1), (cx+1, 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):
    global mastervisited,srcWidth,srcHeight,srcRgn,src_pixels,newWidth,newHeight,dstRgn,p_size,dest_pixels,src_pos,newval
    if pdb.gimp_drawable_is_rgb(layer) == FALSE:
        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
        radius = ((len(group)/3.1415)**0.5)*2.0*(8.24621125124/7.13660170369*0.95); #not get rid of commas
        cx = 0; cy = 0;
        for p1 in group:
            cx += p1[0]
            cy += p1[1]
        cx/=len(group)
        cy/=len(group)   
        length = 0
        for p1 in group:
            for p2 in group:
                if p1[0]!=p2[0] or p1[1]!=p2[1]: #if different pixels.
                    length = max(((p1[0]-p2[0])**2+(p1[1]-p2[1])**2)**0.5,length)
                    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)
    pdb.gimp_image_convert_grayscale(image)
    #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.
    #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.


Attachments:
File comment: get rid of roundish dots where you can enter even 100 pixels for maxpixels but won't get rid of commas.
fill-bg-less-than.zip [3.85 KiB]
Downloaded 78 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Sun Jul 14, 2024 1:30 pm  (#36) 
Offline
GimpChat Member

Joined: Jun 26, 2024
Posts: 8
I tried all 3 versions, but I liked the one with radius setting (from Sat Jul 13, 2024 9:22 am) the best. Although it deletes small parts of the > signs sometimes, it impacts them even less than ImageJ. Fantastic work! The default setting max=7, radius=3 seems to work best, I'm attaching comparison pic.

The other two preserve the > signs better, but also leave more unwanted dots in the image.

Edit: would it be possible for the script to preserve image mode? So RGB input => RGB output, grayscale input => grayscale output and 1-bit indexed input => 1-bit indexed output?


Attachments:
Bg-fill max7 radius 3 comparison.png
Bg-fill max7 radius 3 comparison.png [ 18.85 KiB | Viewed 4143 times ]
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Sun Jul 14, 2024 4:22 pm  (#37) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
Good to hear,
here's version from post #33 to preserve image modes
#!/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.


Attachments:
File comment: Preserve all image modes.
fill-bg-less-than.zip [3.86 KiB]
Downloaded 105 times

_________________
TinT
Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Mon Jul 15, 2024 4:48 am  (#38) 
Offline
GimpChat Member
User avatar

Joined: Dec 26, 2014
Posts: 205
Caleb13 wrote:
...... the filters fill the insides of number "4", which is highly undesirable. Is there some trick to remove only black spots?

A different approach to the problem of the '4's
Attachment:
find-and-replace-character-4.zip [1.04 KiB]
Downloaded 91 times

The script above tries to find and replace the '4' characters, the full scale sample image below shows one result. All the '4' characters have been replaced but alas with one unwanted error at 2nd column in, 12 up from the bottom

A value needs to be entered, the maximum allowed is 464 but no changes will occur, as the value entered gets lower more of the '4's will be replaced, at 403 the first error occurs and lower values give more errors

In the smaller image below

Left is a crop of the original full scale sample
2nd left, input value was 403
3rd left, input value was 397
4th left input value was 396
On the right the unwanted change can be seen when 403 was used

The downside to this script is that it took 95 minutes to run, If I can work out how Tims getting the super fast speeds that he is I will try and work it into the script, or hopefully Tim might beat me to it
Attachment:
character 4.png
character 4.png [ 17.58 KiB | Viewed 4081 times ]

If the script is run on the small png above you can see the result, it takes about 30 seconds
Attachment:
Full page sample character 4 - 85 minutes input 396.png
Full page sample character 4 - 85 minutes input 396.png [ 320.33 KiB | Viewed 4081 times ]


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Mon Jul 15, 2024 5:43 am  (#39) 
Offline
Script Coder
User avatar

Joined: May 07, 2014
Posts: 4527
Location: Canada
Steve, I have functions in my code that calls setpixel and getpixel, and some globals that it sets up in order to use those functions and then code at the end to push the updated data to image. Hopefully you can work that out.

_________________
TinT


Top
 Post subject: Re: Remove only black spots in scanned documents?
PostPosted: Mon Jul 15, 2024 10:24 am  (#40) 
Offline
GimpChat Member

Joined: Jun 26, 2024
Posts: 8
@trandoductin: thanks again, I edited OP to include link to this last version.

@Steve: an interesting approach, but please keep in mind that my aim was to remove the unwanted black dots around the text.

It's inevitable all these "simple" algorithms will always either remove too much or too little. Probably the only 100% accurate method would be to use true AI (or AGI), i.e. a tool that would understand what the text actually means. Or what it's supposed to mean, in situations when the print quality is so bad that characters' fine details are missing. That happens frequently with the > signs, asterisks and degrees signs in "my" documents...


Top
Post new topic Reply to topic  [ 42 posts ]  Go to page Previous  1, 2, 3  Next

All times are UTC - 5 hours [ DST ]



* Login  



Powered by phpBB3 © phpBB Group