#!/usr/bin/env python
from gimpfu import *
import pygtk
pygtk.require('2.0')
import gtk, gobject, time
# create an output function that redirects to gimp's Error Console
def gprint( text ):
pdb.gimp_message(text)
return
# See analyze_select_help.txt for program overview.
class color_analyzer:
def __init__(self, image):
# transmit error messages to gimp console
pdb.gimp_message_set_handler( ERROR_CONSOLE )
gprint('color_analyzer initializing')
self.image = image
# Following Analyze Mode variables are interrelated, change one
# change all accordingly
self.PARTIAL_EXCLUDE = 0
self.PARTIAL_AS_FULL = 1
self.PARTIAL_AS_PARTIAL = 2
self.analyze_opt_vals = [self.PARTIAL_EXCLUDE, self.PARTIAL_AS_FULL,
self.PARTIAL_AS_PARTIAL]
self.analyze_opt_lbls = ['Exclude', 'Full Value', 'Partial Value']
self.reset_vars()
def reset_vars(self):
# These are vars that bld_results_header() or bld_results_msg()
# expect analyze_rgn_tasklet() to have left for them.
self.image_name = self.image.name
self.drawable = pdb.gimp_image_get_active_drawable(self.image)
self.drawable_name = self.drawable.name
self.has_alpha = self.drawable.has_alpha
self.is_rgb = self.drawable.is_rgb
if self.is_rgb:
self.n_chns = 3
else:
self.n_chns = 1
self.num_pixels = 0
self.does_intersect = False
self.sel_bounds = (0,) * 4
self.min_vals = [256] * 3
self.max_vals = [-1] * 3
self.mid_vals = [127.5] * 3
self.delta_vals = [0.0] * 3
self.thrshld_vals = [0.0] * 3
# self.num_alpha_part = 0
# self.num_sel_part = 0
def analyze_rgn_tasklet(self):
# Read pixels in current selection or whole drawable to find
# Max & Min RGB values and also Mid values and thresholds which
# define that color range. Builds histogram of numbers of pixels
# which have each R, G, & B val, also a similar histogram of
# each unique RGB combination. Works on Greyscale too.
# This is time consuming (I blame Python) so we must update a
# progress bar to reassure the user.
# Therefore routine is setup to be called as a GTK Idle Function
# and it periodically yields control so GTK can update the dialog
# window and the user can even quit in the middle.
start_time = time.time()
self.reset_vars()
image = self.image
drawable = self.drawable
has_alpha = self.has_alpha
n_chns = self.n_chns
each_channel = range(n_chns)
NUM_PX_SLICE = 100000 # Num pixels to process before yielding control.
# Get boundry box of the intersection of this drawable and the image's selection area.
# If no selection is currently specified, the intersection is considered to be the
# whole drawable.
self.does_intersect, sel_x1, sel_y1, sel_width, sel_height \
= pdb.gimp_drawable_mask_intersect(drawable)
if not self.does_intersect:
# Mostly stopping processing but need self.sel_bounds
# relative to drawable coords for results msg header.
# (problem is drawable_mask_intersect() sets any
# negative x1, y1 to 0)
non_empty, x1, y1, x2, y2 = pdb.gimp_selection_bounds(image)
offset_x, offset_y = pdb.gimp_drawable_offsets(drawable)
self.sel_bounds = (x1 - offset_x, y1 - offset_y,
x2 - x1, y2 - y1)
else:
self.sel_bounds = (sel_x1, sel_y1, sel_width, sel_height)
sel_x2 = sel_x1 + sel_width - 1
sel_y2 = sel_y1 + sel_height - 1
srcRgn = drawable.get_pixel_rgn(sel_x1, sel_y1,
sel_width, sel_height,
False, False)
src_pixels = bytearray(srcRgn[:,:])
bpp = srcRgn.bpp
# To simplify code, all drawable types are ultimately handled as if
# they have an alpha channel and a selection mask, where either is
# not actually there we supply default values of full opacity (255)
has_sel = not pdb.gimp_selection_is_empty(image)
if has_sel:
offset_x, offset_y = pdb.gimp_drawable_offsets(drawable)
sel_mask = image.selection #returns a channel
selRgn = sel_mask.get_pixel_rgn(sel_x1 + offset_x,
sel_y1 + offset_y,
sel_width, sel_height,
False, False)
sel_pixels = bytearray(selRgn[:,:])
s = 0
sel, alpha = 255, 255 # default: fully selected and opaque
p_lim = sel_width * sel_height * bpp
NOT_PARTIAL_EXCLUDE = self.analyze_partial_mode != self.PARTIAL_EXCLUDE
# Setting up function ptr here saves execing IF stmnt in pixel loop
if self.PARTIAL_AS_FULL == self.analyze_partial_mode:
do_partial_pixel = self.do_pixel_full_val
elif self.PARTIAL_AS_PARTIAL == self.analyze_partial_mode:
do_partial_pixel = self.do_pixel_partial_val
# loop thru each pixel checking if selected and adding to RGB
# & unique color histograms. We use an outer loop to break work
# into slices so we can periodically return control to gtk.main
# so progress bar can update.
slice_size = bpp * NUM_PX_SLICE # must be a multiple of bytes per pixel
slc = 0
# Show user info about drawable we're about to work on
msg = self.bld_results_header() + '\n\nResults Pending...'
self.result_label.set_markup('' + msg + '')
self.progressbar.set_text('Working...')
p = 0
while p < p_lim: # for each Slice of slice_size
slc_lim = min(p + slice_size, p_lim)
self.progressbar.set_fraction(p / float(p_lim - 1))
# while gtk.events_pending(): # Allow progress bar to update.
# gtk.main_iteration()
yield True # Allow gtk.main to get control back
while p < slc_lim: # for each pixel within slice
if has_sel:
sel = sel_pixels[s]
s += 1
if has_alpha:
alpha = src_pixels[p + n_chns]
if sel == 255 == alpha: # pixel fully selected and opaque
self.num_pixels += 1
self.do_pixel_full_val(src_pixels[p:p+n_chns])
elif NOT_PARTIAL_EXCLUDE and sel and alpha:
# pixel at least partially selected and opaque
self.num_pixels += 1
# if alpha != 255:
# self.num_alpha_part += 1
# if sel != 255:
# self.num_sel_part += 1
# analyze each color channel of this RGB or Grey pixel
do_partial_pixel(src_pixels[p:p+n_chns], alpha, sel)
p += bpp
if self.num_pixels > 0: # Maybe no pixels were selected.
# summarize results from histogram and unique colours
for c in each_channel: # for each color channel
self.delta_vals[c] = (self.max_vals[c] - self.min_vals[c]) / 2.0
self.mid_vals[c] = self.min_vals[c] + self.delta_vals[c]
# msg = 'num partial alphas found: {:d}'.format(self.num_alpha_part)
# msg += '\nnum partial sels found: {:d}'.format(self.num_sel_part)
# gprint(msg)
end_time = time.time() - start_time
mins = int(end_time)/60
secs = end_time % 60
self.progressbar.set_fraction(1.0)
self.progressbar.set_text('Analysis Completed in {} min {:.2f} sec'.format(mins, secs))
msg = self.bld_results_msg()
self.result_label.set_markup('' + msg + '')
yield False # Returning False means Idle Time Function will be removed.
def do_pixel_full_val(self, RGBorG, alpha=255, sel=255):
# Due to use of function ptrs, must have default defs for alpha, sel
# though they're not used here.
# RGBorG is a bytearray at this point.
for c, val in enumerate(RGBorG): # for each color channel in pixel
if val < self.min_vals[c]:
self.min_vals[c] = val
if self.max_vals[c] < val:
self.max_vals[c] = val
def do_pixel_partial_val(self, RGBorG, alpha, sel):
# RGBorG is a bytearray at this point.
mult = sel * alpha
for c, val in enumerate(RGBorG): # for each color channel in pixel
val *= mult # "pro-rate" val according to what percentage it is
val /= 65025 # selected and opaque
if val < self.min_vals[c]:
self.min_vals[c] = val
if self.max_vals[c] < val:
self.max_vals[c] = val
def bld_results_header(self):
sel_bounds = self.sel_bounds
if self.is_rgb:
d_type = "RGB"
else:
d_type = "Greyscale"
if self.has_alpha:
alpha = " w/ Alpha"
else:
alpha = ' '
COL1_W, COL2_W = 26, 15
msg = 'Selection bounds box:'.ljust(COL1_W) + \
'Image Name:'.ljust(COL2_W) + self.image_name
msg += '\n' + ' X:{:7,} Y:{:7,}'.format(
sel_bounds[0], sel_bounds[1]).ljust(COL1_W) + \
'Drawable Name:'.ljust(COL2_W) + self.drawable_name
msg += '\n' + ' W:{:7,} H:{:7,}'.format(
sel_bounds[2], sel_bounds[3]).ljust(COL1_W) + \
'Drawable Type:'.ljust(COL2_W) + d_type + alpha
return msg
def bld_results_msg(self):
msg = self.bld_results_header()
if not self.does_intersect:
msg += '\n\nDrawable not within Selection Bounds.'
return msg
if self.num_pixels == 0:
msg += '\n\nNo pixels were selected.'
return msg
each_channel = range(self.n_chns)
if self.is_rgb:
type_label = "RGB Vals:"
# RGBorG_colhead = ('Red', 'Green', 'Blue')
else:
type_label = "Grey Vals:"
# RGBorG_colhead = ('Grey',)
COL1_W = 18
msg += '\n\nNum pixels selected: {:,}'.format(self.num_pixels)
msg += ('\nMin ' + type_label).ljust(COL1_W)
for c in each_channel:
msg += '{:8d}'.format(self.min_vals[c])
msg += ('\nMax ' + type_label).ljust(COL1_W)
for c in each_channel:
msg += '{:8d}'.format(self.max_vals[c])
msg += ('\nMid-pt ' + type_label).ljust(COL1_W + 2)
for c in each_channel:
msg += '{:8.1f}'.format(self.mid_vals[c])
msg += '\nThreshold(s):'.ljust(COL1_W + 2)
for c in each_channel:
msg += '{:8.1f}'.format(self.delta_vals[c])
return msg
def build_dialog(self):
HOMOGENEOUS = True
NOT_HOMOGENEOUS = False
EXPAND = True
NOT_EXPAND = False
FILL = True
NOT_FILL = False
V_SPACE = 3
V_PAD = 3
H_PAD = 3
# Create a Frame to pack other containers and widgets into.
# A frame can only directly contain one widget or other container.
frame = gtk.Frame('RGB or Grey Analysis Results')
vbox = gtk.VBox(NOT_HOMOGENEOUS, 0)
frame.add(vbox)
# Add other child containers and widgets to define
# the 'Analysis' section.
self.result_label = gtk.Label('')
self.result_label.set_selectable(True)
self.result_label.set_text(
'Click [Analyze] to examine whole active layer' +
'\nor current selection')
self.result_label.set_alignment(0, 0) #Left just. & Vert top
self.result_label.set_padding(H_PAD, 0)
vbox.pack_start(self.result_label, NOT_EXPAND, NOT_FILL, V_PAD)
# Build row of 'Analyze' option btns
vbox.pack_start(gtk.HSeparator(), EXPAND, FILL, 0)
hbox = gtk.HBox(NOT_HOMOGENEOUS, 0)
vbox.pack_start(hbox, NOT_EXPAND, NOT_FILL, V_PAD)
lbl = gtk.Label('Partially Selected/\nTransparent Areas:')
hbox.pack_start(lbl, NOT_EXPAND, NOT_FILL, H_PAD)
rbtn, rbtn_actv = None, None
for val, text in zip(self.analyze_opt_vals, self.analyze_opt_lbls):
rbtn = gtk.RadioButton(rbtn, text)
rbtn.connect('toggled', self.a_mode_callback, val)
hbox.pack_start(rbtn, NOT_EXPAND, NOT_FILL, H_PAD)
if not rbtn_actv: # first rbtn we created will be
rbtn_actv = rbtn # the active one.
rbtn.set_active(True) # get toggled msg sent out
rbtn_actv.set_active(True) # so callback() inits vars
# Create [Analyze] button.
bbox = gtk.HButtonBox()
bbox.set_layout(gtk.BUTTONBOX_END)
bbox.set_spacing(0)
hbox.pack_end(bbox, NOT_EXPAND, NOT_FILL, H_PAD)
self.anlys_btn = gtk.Button('Analyze')
self.anlys_btn.connect('clicked', self.anlys_callback, 'Analyze')
bbox.add(self.anlys_btn)
# Add progress bar as bottom row w/in Analysis frame.
self.progressbar = gtk.ProgressBar()
vbox.pack_start(self.progressbar, NOT_EXPAND, NOT_FILL, 0)
return frame
def anlys_callback(self, widget, data):
# Callback for [Analyze] btn: We call our Analyze routine
# as an Idle Time Function so progress bar will update and
# user can quit during processing.
analyze_tasklet = self.analyze_rgn_tasklet()
gobject.idle_add(analyze_tasklet.next)
def a_mode_callback(self, widget, data):
# Callback for Analysis Partial Mode Radio btns:
if widget.get_active():
self.analyze_partial_mode = data
class analyzer_selector(color_analyzer):
def __init__(self, image):
color_analyzer.__init__(self, image)
# Following context variables are interrelated, change one
# change all accordingly
self.my_context = [False] * 4
self.context_opt_lbl = ['Feather Edges', 'Antialiasing',
'Select Transparent', 'Sample Merged']
self.context_funcs = [pdb.gimp_context_set_feather,
pdb.gimp_context_set_antialias,
pdb.gimp_context_set_sample_transparent,
pdb.gimp_context_set_sample_merged]
self.context_feather_idx = 0
self.undo_wasnt_enabled = False
self.undo_enable_ok = False
def build_dialog(self):
# Our parent creates widgets in a Analyze Color Range frame,
# we add to that a dialog window we build.
analysis_frame = color_analyzer.build_dialog(self)
HOMOGENEOUS = True
NOT_HOMOGENEOUS = False
EXPAND = True
NOT_EXPAND = False
FILL = True
NOT_FILL = False
WINDOW_BORDER = 5
V_SPACE = 3
H_PAD = 3
TABLE_X_SPACE = 3
TABLE_Y_SPACE = 3
TBL_BTN_SEC_H_SPACE = 30 # Between right of table and Btn section
row_labels = ('Mid Vals:' ,'Thresholds:')
color_labels = (' Red' ,' Green', ' Blue')
empty_labels= ('',) * 3
spinner_labels = color_labels, empty_labels
self.mid_val_thresh_adj_lst = []
self.range_thresh_adj_lst = []
thresh_adj_lst = [self.mid_val_thresh_adj_lst,
self.range_thresh_adj_lst]
self.diag_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.diag_window.set_title('Analyze & Select by Color Range')
self.diag_window.set_border_width(WINDOW_BORDER)
self.diag_window.connect('destroy', self.delete_event)
# Here we just set a handler for delete_event that immediately
# exits GTK.
# self.diag_window.connect('delete_event', self.delete_event)
uber_box = gtk.VBox(NOT_HOMOGENEOUS, V_SPACE)
self.diag_window.add(uber_box)
uber_box.pack_start(analysis_frame, NOT_EXPAND, NOT_FILL, 0)
# ==== Build the 'Specify Selection' section.
tooltips = gtk.Tooltips()
frame = gtk.Frame('Specify color range to select')
# frame.set_label_align(0.0, 1.0)
uber_box.pack_start(frame, NOT_EXPAND, NOT_FILL, 0)
vbox = gtk.VBox(NOT_HOMOGENEOUS, V_SPACE)
frame.add(vbox)
# Build row of Selection Mode options
hbox = gtk.HBox(NOT_HOMOGENEOUS, 0)
vbox.pack_start(hbox, NOT_EXPAND, NOT_FILL, 0)
lbl = gtk.Label('Selection Mode: ')
hbox.pack_start(lbl, NOT_EXPAND, NOT_FILL, H_PAD)
rbtn_lbl = ['Replace', 'Add', 'Subtract', 'Intersect']
rbtn_data = [CHANNEL_OP_REPLACE, CHANNEL_OP_ADD,
CHANNEL_OP_SUBTRACT, CHANNEL_OP_INTERSECT]
rbtn = None
for c in range(4):
rbtn = gtk.RadioButton(rbtn, rbtn_lbl[c])
rbtn.connect('toggled', self.sel_mode_callback, rbtn_data[c])
hbox.pack_start(rbtn, NOT_EXPAND, NOT_FILL, H_PAD)
if c == 0:
rbtn_actv = rbtn
rbtn.set_active(True) # get toggled msg sent out
rbtn_actv.set_active(True) # so callback() inits vars
# Build row of Selection Context Options
hbox = gtk.HBox(NOT_HOMOGENEOUS, 0)
vbox.pack_start(hbox, NOT_EXPAND, NOT_FILL, 0)
context_opt_lbl = self.context_opt_lbl
for c in range(4):
chk_btn = gtk.CheckButton(context_opt_lbl[c])
chk_btn.connect("toggled", self.sel_ctxt_callback, c)
hbox.pack_start(chk_btn, NOT_EXPAND, NOT_FILL, H_PAD)
# Build 'Feather Radius' threshold widgets
hbox = gtk.HBox(NOT_HOMOGENEOUS, 0)
vbox.pack_start(hbox, NOT_EXPAND, NOT_FILL, 0)
lbl = gtk.Label('Feather Radius')
thresh_adj = gtk.Adjustment(pdb.gimp_context_get_feather_radius()[0],
0, 100, .1, 1)
spinner = gtk.SpinButton(thresh_adj, 1, 1)
spinner.set_wrap(True)
spinner.set_numeric(True)
spinner.set_snap_to_ticks(True)
self.f_radius_thresh_adj = thresh_adj
self.f_radius_spinner = spinner
hbox.pack_start(spinner, NOT_EXPAND, NOT_FILL, H_PAD)
hbox.pack_start(lbl, NOT_EXPAND, NOT_FILL, 0)
self.f_radius_box = hbox
# Build Table for Mid Pt. Color and Threshold widgets
hbox = gtk.HBox(NOT_HOMOGENEOUS, TBL_BTN_SEC_H_SPACE)
vbox.pack_start(hbox, NOT_EXPAND, NOT_FILL, V_SPACE)
table = gtk.Table(len(row_labels),
1 + (len(spinner_labels[0]) * 2),
NOT_HOMOGENEOUS)
table.set_row_spacings(TABLE_Y_SPACE)
table.set_col_spacings(TABLE_X_SPACE)
hbox.pack_start(table, NOT_EXPAND, NOT_FILL, H_PAD)
row = 0
# R, G & B threshold widgets
for r_lbl in row_labels:
col = 0
lbl = gtk.Label(r_lbl)
table.attach(lbl, col, col + 1, row, row + 1,
gtk.FILL, gtk.FILL) # fill so justify method works
lbl.set_alignment(0, .5) #Left just. & Vert center
col += 1
for spin_lbl in spinner_labels[row]:
if '' != spin_lbl:
lbl = gtk.Label()
lbl.set_markup('' + spin_lbl + '')
table.attach(lbl, col, col + 1, row, row + 1, 0, 0)
col += 1
thresh_adj = gtk.Adjustment(0, 0, 255, 0.5)
spinner = gtk.SpinButton(thresh_adj, 0.5, 1)
spinner.set_wrap(True)
spinner.set_numeric(True)
spinner.set_snap_to_ticks(True)
thresh_adj_lst[row] += [thresh_adj]
table.attach(spinner, col, col + 1, row, row + 1, 0, 0)
col += 1
row += 1
# Add [Select] btn to lower right corner of Select Frame
vbox = gtk.VBox(NOT_HOMOGENEOUS, 0)
hbox.pack_end(vbox, NOT_EXPAND, NOT_FILL, H_PAD)
bbox = gtk.HButtonBox()
bbox.set_layout(gtk.BUTTONBOX_END)
bbox.set_spacing(0)
vbox.pack_end(bbox, NOT_EXPAND, NOT_FILL, 0)
btn = gtk.Button('Select')
btn.connect('clicked', self.select_callback)
bbox.add(btn)
# Add [Load Vals] btn above Select] btn
bbox = gtk.HButtonBox()
bbox.set_layout(gtk.BUTTONBOX_END)
bbox.set_spacing(0)
vbox.pack_end(bbox, NOT_EXPAND, NOT_FILL, 0)
btn = gtk.Button('Load Vals')
btn.connect('clicked', self.load_callback)
bbox.add(btn)
tooltips.set_tip(btn, 'Load Mid Vals & Thresholds\nfrom Analysis')
# Build last section, [Close] btn
bbox = gtk.HButtonBox()
bbox.set_layout(gtk.BUTTONBOX_END)
bbox.set_spacing(0)
uber_box.pack_end(bbox, NOT_EXPAND, NOT_FILL, 0)
btn = gtk.Button('Close')
btn.connect('clicked', self.delete_event)
bbox.add(btn)
self.diag_window.show_all()
self.f_radius_box.hide()
return uber_box
def delete_event(self, widget, event=None, data=None):
gtk.main_quit()
return False
def load_callback(self, widget):
# Callback for [Load] btn:
# We copy vals from Analysis into our color selection
# threshold widgets.
if self.is_rgb:
for c in range(3):
self.mid_val_thresh_adj_lst[c].set_value(self.mid_vals[c])
self.range_thresh_adj_lst[c].set_value(self.delta_vals[c])
else:
for c in range(3):
self.mid_val_thresh_adj_lst[c].set_value(self.mid_vals[0])
self.range_thresh_adj_lst[c].set_value(self.delta_vals[0])
def select_callback(self, widget, data=None):
# Callback for [Select] color range btn:
image = self.image
drawable = pdb.gimp_image_get_active_drawable(image)
RGB_chans = (0, 1, 2)
color_1, color_2 = [], []
for c in RGB_chans:
mid_clr_real = self.mid_val_thresh_adj_lst[c].value
threshold_real = self.range_thresh_adj_lst[c].value
color_1 += [int(mid_clr_real - threshold_real)] # Append to list
color_2 += [int(mid_clr_real + threshold_real)] # Append to list
color_1 = tuple(color_1)
color_2 = tuple(color_2)
self.set_my_context() # Set our 'feathered' etc. options
self.try_undo_group_start(image)
self.select_color_range(image, self.select_mode, drawable, color_1, color_2)
self.try_undo_group_end(image)
pdb.gimp_context_set_defaults()
gimp.displays_flush()
def select_color_range(self, image, operation, drawable, color_1, color_2):
# Select composite RGB by a range between two colors in RGB space.
# What we're aiming for a version of gimp_image_select_color()
# that allows separate thresholds for each color channel.
# Implementation on a pixel by pixel level thru Python would
# run too slowly (and would be a lot of work!). So instead we
# accomplish the task by making multiple calls to
# gimp_image_select_color()
#
# To make a color selection with different thresholds for each
# color channel, we'll get the intersection of the separate
# selections from R, G, & B channels.
# We'll first have to convert our 'colors as end points' to
# a mid point color with thresholds that we can pass to
# gimp_image_select_color(). B/c _select_color() only accepts
# an int val for color target, a mid pt of say 132.5 will be
# rounded down. That means if the difference between end color
# vals is odd, the mid point will shift down so we'll also have
# to bump up the threshold so the max val will still be selected.
# This of course means our min val selected will be 1 too low
# so we'll have to subtract out a second selection for just that
# min val.
#
# Note on feathering and antialias:
# feather(sel1) intersect feather(sel1)
# != feather( sel1 intersect sel2 )
# so we do turn off feathering and antialiasing while
# constructing the RGB selection and then apply them to the
# final result.
#
# BTW, this routine works on grayscale drawables too.
#
# Convert our two end point colors into a mid pt clr and
# thresholds to use with gimp_image_select_color()
mid_color, min_color, thresholds = [], [], []
# Vals from each channel will be appended to above lists.
for c1, c2 in zip(color_1, color_2): # Each channel
min_c = min(int(c1), int(c2))
delta = abs(int(c2) - int(c1))
thresh = delta / 2 # integer div
if delta % 2: # if odd (i.e .5 mid pt)
min_c -= 1
thresh += 1
mid_color += [min_c + thresh]
else:
mid_color += [min_c + thresh]
min_c = -1
# will need to subtract selection for min
min_color += [min_c] # Append this chan
thresholds += [thresh]
mid_color = tuple(mid_color)
min_color = tuple(min_color)
antialias = pdb.gimp_context_get_antialias()
if antialias:
pdb.gimp_context_set_antialias(False)
feather = pdb.gimp_context_get_feather()
if feather:
pdb.gimp_context_set_feather(False)
feather_or_antialias = feather or antialias
# Determine whether our 1st channel operation has to be a
# REPLACE or an INTERSECT, the next two have to be INTERSECT
# of course. Save the current selection if necessary.
active_changed = False
if CHANNEL_OP_INTERSECT == operation and not feather_or_antialias:
chan_ops = [CHANNEL_OP_INTERSECT] * 3
else:
chan_ops = [CHANNEL_OP_REPLACE] + [CHANNEL_OP_INTERSECT] * 2
starting_sel = None
if CHANNEL_OP_REPLACE != operation:
if CHANNEL_OP_INTERSECT != operation or feather_or_antialias:
# We save the current selection if the mode is ADD or SUBTRACT
# or if mode is INTERSECT but feather or antialias is True
starting_sel = pdb.gimp_selection_save(image)
active_changed = True
# Create the intersection of the range selections for
# each RGB chan
sel_criteria = [SELECT_CRITERION_R,
SELECT_CRITERION_G,
SELECT_CRITERION_B]
if pdb.gimp_drawable_is_gray(drawable):
each_channel = range(1)
else:
each_channel = range(3)
for c in each_channel:
pdb.gimp_context_set_sample_criterion(sel_criteria[c])
pdb.gimp_context_set_sample_threshold_int(thresholds[c])
pdb.gimp_image_select_color(image, chan_ops[c],
drawable, mid_color)
if -1 < min_color[c]:
pdb.gimp_context_set_sample_threshold_int(0)
pdb.gimp_image_select_color(image, CHANNEL_OP_SUBTRACT,
drawable, min_color)
# Current selection is the intersection of each RGB chan
# selection; so integrate that with the old selection
# if necessary.
if antialias:
cur_sel = pdb.gimp_selection_save(image)
pdb.gimp_selection_none(image)
pdb.plug_in_antialias(image, cur_sel)
pdb.gimp_image_select_item(image, CHANNEL_OP_REPLACE,
cur_sel)
active_changed = True
if feather:
radius_x, radius_y = pdb.gimp_context_get_feather_radius()
pdb.gimp_selection_feather(image, radius_x)
if starting_sel is not None:
if CHANNEL_OP_ADD == operation or CHANNEL_OP_INTERSECT == operation:
pdb.gimp_image_select_item(image, operation, starting_sel)
elif CHANNEL_OP_SUBTRACT == operation:
sel_rgb = pdb.gimp_selection_save(image)
active_changed = True
pdb.gimp_image_select_item(image, CHANNEL_OP_REPLACE,
starting_sel)
pdb.gimp_image_select_item(image, CHANNEL_OP_SUBTRACT,
sel_rgb)
pdb.gimp_image_remove_channel(image, sel_rgb)
pdb.gimp_image_remove_channel(image, starting_sel)
if active_changed:
if pdb.gimp_item_is_channel(drawable):
pdb.gimp_image_set_active_channel(image, drawable)
else:
pdb.gimp_image_set_active_layer(image, drawable)
if antialias:
pdb.gimp_context_set_antialias(True)
if feather:
pdb.gimp_context_set_feather(True)
def try_undo_group_start(self, image):
# Another plugin could have disabled Undo's
self.undo_wasnt_enabled = not pdb.gimp_image_undo_is_enabled(image)
if self.undo_wasnt_enabled:
self.undo_enable_ok = pdb.gimp_image_undo_enable(image)
else:
self.undo_enable_ok = True
if self.undo_enable_ok:
pdb.gimp_image_undo_group_start(image)
def try_undo_group_end(self, image):
if self.undo_enable_ok:
pdb.gimp_image_undo_group_end(image)
def set_my_context(self):
for f in range(4):
self.context_funcs[f](self.my_context[f])
if pdb.gimp_context_get_feather():
radius = self.f_radius_thresh_adj.value
pdb.gimp_context_set_feather_radius(radius, radius)
def sel_mode_callback(self, widget, data):
# Callback for Selection Mode Radio btns:
if widget.get_active():
self.select_mode = data
def sel_ctxt_callback(self, widget, idx = 0):
# Callback for Selection Context Check btns:
self.my_context[idx] = widget.get_active()
if idx == self.context_feather_idx:
if self.my_context[idx]:
self.f_radius_box.show()
else:
self.f_radius_box.hide()
def color_range_selector(image, drawable) :
analyzer = analyzer_selector(image)
analyzer.build_dialog()
gtk.main()
return
# This is the plugin registration function
register(
"python_fu_analyze_select_color_range",
"Analyze & Select by Color Range",
"This tool finds Max & Min RGB vals within current selection " +
"and then allows you to select all pixels in current layer " +
"within that range; allows different thresholds for R, B & G",
"Donald Myron",
"Donald Myron",
"Febuary 2014",
"/Colors/Analyze & Select Color Range",
"RGB*, GRAY*",
[],
[],
color_range_selector,
)
main()