diff options
Diffstat (limited to 'plug-ins/common/hot.c')
-rw-r--r-- | plug-ins/common/hot.c | 782 |
1 files changed, 782 insertions, 0 deletions
diff --git a/plug-ins/common/hot.c b/plug-ins/common/hot.c new file mode 100644 index 0000000..8ed215e --- /dev/null +++ b/plug-ins/common/hot.c @@ -0,0 +1,782 @@ +/* + * GIMP - The GNU Image Manipulation Program + * Copyright (C) 1995 Spencer Kimball and Peter Mattis + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/* + * hot.c - Scan an image for pixels with RGB values that will give + * "unsafe" values of chrominance signal or composite signal + * amplitude when encoded into an NTSC or PAL color signal. + * (This happens for certain high-intensity high-saturation colors + * that are rare in real scenes, but can easily be present + * in synthetic images.) + * + * Such pixels can be flagged so the user may then choose other + * colors. Or, the offending pixels can be made "safe" + * in a manner that preserves hue. + * + * There are two reasonable ways to make a pixel "safe": + * We can reduce its intensity (luminance) while leaving + * hue and saturation the same. Or, we can reduce saturation + * while leaving hue and luminance the same. A #define selects + * which strategy to use. + * + * Note to the user: You must add your own read_pixel() and write_pixel() + * routines. You may have to modify pix_decode() and pix_encode(). + * MAXPIX, WID, and HGT are likely to need modification. + */ + +/* + * Originally written as "ikNTSC.c" by Alan Wm Paeth, + * University of Waterloo, August, 1985 + * Updated by Dave Martindale, Imax Systems Corp., December 1990 + */ + +/* + * Compile time options: + * + * + * CHROMA_LIM is the limit (in IRE units) of the overall + * chrominance amplitude; it should be 50 or perhaps + * very slightly higher. + * + * COMPOS_LIM is the maximum amplitude (in IRE units) allowed for + * the composite signal. A value of 100 is the maximum + * monochrome white, and is always safe. 120 is the absolute + * limit for NTSC broadcasting, since the transmitter's carrier + * goes to zero with 120 IRE input signal. Generally, 110 + * is a good compromise - it allows somewhat brighter colors + * than 100, while staying safely away from the hard limit. + */ + +#include "config.h" + +#include <string.h> + +#include <libgimp/gimp.h> +#include <libgimp/gimpui.h> + +#include "libgimp/stdplugins-intl.h" + + +#define PLUG_IN_PROC "plug-in-hot" +#define PLUG_IN_BINARY "hot" +#define PLUG_IN_ROLE "gimp-hot" + + +typedef struct +{ + gint32 image; + gint32 drawable; + gint32 mode; + gint32 action; + gint32 new_layerp; +} piArgs; + +typedef enum +{ + ACT_LREDUX, + ACT_SREDUX, + ACT_FLAG +} hotAction; + +typedef enum +{ + MODE_NTSC, + MODE_PAL +} hotModes; + +#define CHROMA_LIM 50.0 /* chroma amplitude limit */ +#define COMPOS_LIM 110.0 /* max IRE amplitude */ + +/* + * RGB to YIQ encoding matrix. + */ + +struct +{ + gdouble pedestal; + gdouble gamma; + gdouble code[3][3]; +} static mode[2] = { + { + 7.5, + 2.2, + { + { 0.2989, 0.5866, 0.1144 }, + { 0.5959, -0.2741, -0.3218 }, + { 0.2113, -0.5227, 0.3113 } + } + }, + { + 0.0, + 2.8, + { + { 0.2989, 0.5866, 0.1144 }, + { -0.1473, -0.2891, 0.4364 }, + { 0.6149, -0.5145, -0.1004 } + } + } +}; + + +#define SCALE 8192 /* scale factor: do floats with int math */ +#define MAXPIX 255 /* white value */ + +static gint tab[3][3][MAXPIX+1]; /* multiply lookup table */ +static gdouble chroma_lim; /* chroma limit */ +static gdouble compos_lim; /* composite amplitude limit */ +static glong ichroma_lim2; /* chroma limit squared (scaled integer) */ +static gint icompos_lim; /* composite amplitude limit (scaled integer) */ + +static void query (void); +static void run (const gchar *name, + gint nparam, + const GimpParam *param, + gint *nretvals, + GimpParam **retvals); + +static gboolean pluginCore (piArgs *argp); +static gboolean plugin_dialog (piArgs *argp); +static gboolean hotp (guint8 r, + guint8 g, + guint8 b); +static void build_tab (gint m); + +/* + * gc: apply the gamma correction specified for this video standard. + * inv_gc: inverse function of gc. + * + * These are generally just a call to pow(), but be careful! + * Future standards may use more complex functions. + * (e.g. SMPTE 240M's "electro-optic transfer characteristic"). + */ +#define gc(x,m) pow(x, 1.0 / mode[m].gamma) +#define inv_gc(x,m) pow(x, mode[m].gamma) + +/* + * pix_decode: decode an integer pixel value into a floating-point + * intensity in the range [0, 1]. + * + * pix_encode: encode a floating-point intensity into an integer + * pixel value. + * + * The code given here assumes simple linear encoding; you must change + * these routines if you use a different pixel encoding technique. + */ +#define pix_decode(v) ((double)v / (double)MAXPIX) +#define pix_encode(v) ((int)(v * (double)MAXPIX + 0.5)) + +const GimpPlugInInfo PLUG_IN_INFO = +{ + NULL, /* init_proc */ + NULL, /* quit_proc */ + query, /* query_proc */ + run, /* run_proc */ +}; + +MAIN () + +static void +query (void) +{ + static const GimpParamDef args[] = + { + { GIMP_PDB_INT32, "run-mode", "The run mode { RUN-INTERACTIVE (0), RUN-NONINTERACTIVE (1) }" }, + { GIMP_PDB_IMAGE, "image", "The Image" }, + { GIMP_PDB_DRAWABLE, "drawable", "The Drawable" }, + { GIMP_PDB_INT32, "mode", "Mode { NTSC (0), PAL (1) }" }, + { GIMP_PDB_INT32, "action", "The action to perform" }, + { GIMP_PDB_INT32, "new-layer", "Create a new layer { TRUE, FALSE }" } + }; + + gimp_install_procedure (PLUG_IN_PROC, + N_("Find and fix pixels that may be unsafely bright"), + "hot scans an image for pixels that will give unsave " + "values of chrominance or composite signale " + "amplitude when encoded into an NTSC or PAL signal. " + "Three actions can be performed on these ``hot'' " + "pixels. (0) reduce luminance, (1) reduce " + "saturation, or (2) Blacken.", + "Eric L. Hernes, Alan Wm Paeth", + "Eric L. Hernes", + "1997", + N_("_Hot..."), + "RGB", + GIMP_PLUGIN, + G_N_ELEMENTS (args), 0, + args, NULL); + + gimp_plugin_menu_register (PLUG_IN_PROC, "<Image>/Colors/Modify"); +} + +static void +run (const gchar *name, + gint nparam, + const GimpParam *param, + gint *nretvals, + GimpParam **retvals) +{ + static GimpParam rvals[1]; + piArgs args; + + *nretvals = 1; + *retvals = rvals; + + INIT_I18N (); + gegl_init (NULL, NULL); + + memset (&args, 0, sizeof (args)); + args.mode = -1; + + gimp_get_data (PLUG_IN_PROC, &args); + + args.image = param[1].data.d_image; + args.drawable = param[2].data.d_drawable; + + rvals[0].type = GIMP_PDB_STATUS; + rvals[0].data.d_status = GIMP_PDB_SUCCESS; + + switch (param[0].data.d_int32) + { + case GIMP_RUN_INTERACTIVE: + /* XXX: add code here for interactive running */ + if (args.mode == -1) + { + args.mode = MODE_NTSC; + args.action = ACT_LREDUX; + args.new_layerp = 1; + } + + if (plugin_dialog (&args)) + { + if (! pluginCore (&args)) + { + rvals[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + } + } + else + { + rvals[0].data.d_status = GIMP_PDB_CANCEL; + } + + gimp_set_data (PLUG_IN_PROC, &args, sizeof (args)); + break; + + case GIMP_RUN_NONINTERACTIVE: + /* XXX: add code here for non-interactive running */ + if (nparam != 6) + { + rvals[0].data.d_status = GIMP_PDB_CALLING_ERROR; + break; + } + args.mode = param[3].data.d_int32; + args.action = param[4].data.d_int32; + args.new_layerp = param[5].data.d_int32; + + if (! pluginCore (&args)) + { + rvals[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + break; + } + break; + + case GIMP_RUN_WITH_LAST_VALS: + /* XXX: add code here for last-values running */ + if (! pluginCore (&args)) + { + rvals[0].data.d_status = GIMP_PDB_EXECUTION_ERROR; + } + break; + } +} + +static gboolean +pluginCore (piArgs *argp) +{ + GeglBuffer *src_buffer; + GeglBuffer *dest_buffer; + const Babl *src_format; + const Babl *dest_format; + gint src_bpp; + gint dest_bpp; + gboolean success = TRUE; + gint nl = 0; + gint y, i; + gint Y, I, Q; + gint width, height; + gint sel_x1, sel_x2, sel_y1, sel_y2; + gint prog_interval; + guchar *src, *s, *dst, *d; + guchar r, prev_r=0, new_r=0; + guchar g, prev_g=0, new_g=0; + guchar b, prev_b=0, new_b=0; + gdouble fy, fc, t, scale; + gdouble pr, pg, pb; + gdouble py; + + width = gimp_drawable_width (argp->drawable); + height = gimp_drawable_height (argp->drawable); + + if (gimp_drawable_has_alpha (argp->drawable)) + src_format = babl_format ("R'G'B'A u8"); + else + src_format = babl_format ("R'G'B' u8"); + + dest_format = src_format; + + if (argp->new_layerp) + { + gchar name[40]; + const gchar *mode_names[] = + { + "ntsc", + "pal", + }; + const gchar *action_names[] = + { + "lum redux", + "sat redux", + "flag", + }; + + g_snprintf (name, sizeof (name), "hot mask (%s, %s)", + mode_names[argp->mode], + action_names[argp->action]); + + nl = gimp_layer_new (argp->image, name, width, height, + GIMP_RGBA_IMAGE, + 100, + gimp_image_get_default_new_layer_mode (argp->image)); + + gimp_drawable_fill (nl, GIMP_FILL_TRANSPARENT); + gimp_image_insert_layer (argp->image, nl, -1, 0); + + dest_format = babl_format ("R'G'B'A u8"); + } + + if (! gimp_drawable_mask_intersect (argp->drawable, + &sel_x1, &sel_y1, &width, &height)) + return success; + + src_bpp = babl_format_get_bytes_per_pixel (src_format); + dest_bpp = babl_format_get_bytes_per_pixel (dest_format); + + sel_x2 = sel_x1 + width; + sel_y2 = sel_y1 + height; + + src = g_new (guchar, width * height * src_bpp); + dst = g_new (guchar, width * height * dest_bpp); + + src_buffer = gimp_drawable_get_buffer (argp->drawable); + + if (argp->new_layerp) + { + dest_buffer = gimp_drawable_get_buffer (nl); + } + else + { + dest_buffer = gimp_drawable_get_shadow_buffer (argp->drawable); + } + + gegl_buffer_get (src_buffer, + GEGL_RECTANGLE (sel_x1, sel_y1, width, height), 1.0, + src_format, src, + GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE); + + s = src; + d = dst; + + build_tab (argp->mode); + + gimp_progress_init (_("Hot")); + prog_interval = height / 10; + + for (y = sel_y1; y < sel_y2; y++) + { + gint x; + + if (y % prog_interval == 0) + gimp_progress_update ((double) y / (double) (sel_y2 - sel_y1)); + + for (x = sel_x1; x < sel_x2; x++) + { + if (hotp (r = *(s + 0), g = *(s + 1), b = *(s + 2))) + { + if (argp->action == ACT_FLAG) + { + for (i = 0; i < 3; i++) + *d++ = 0; + s += 3; + if (src_bpp == 4) + *d++ = *s++; + else if (argp->new_layerp) + *d++ = 255; + } + else + { + /* + * Optimization: cache the last-computed hot pixel. + */ + if (r == prev_r && g == prev_g && b == prev_b) + { + *d++ = new_r; + *d++ = new_g; + *d++ = new_b; + s += 3; + if (src_bpp == 4) + *d++ = *s++; + else if (argp->new_layerp) + *d++ = 255; + } + else + { + Y = tab[0][0][r] + tab[0][1][g] + tab[0][2][b]; + I = tab[1][0][r] + tab[1][1][g] + tab[1][2][b]; + Q = tab[2][0][r] + tab[2][1][g] + tab[2][2][b]; + + prev_r = r; + prev_g = g; + prev_b = b; + /* + * Get Y and chroma amplitudes in floating point. + * + * If your C library doesn't have hypot(), just use + * hypot(a,b) = sqrt(a*a, b*b); + * + * Then extract linear (un-gamma-corrected) + * floating-point pixel RGB values. + */ + fy = (double)Y / (double)SCALE; + fc = hypot ((double) I / (double) SCALE, + (double) Q / (double) SCALE); + + pr = (double) pix_decode (r); + pg = (double) pix_decode (g); + pb = (double) pix_decode (b); + + /* + * Reducing overall pixel intensity by scaling R, + * G, and B reduces Y, I, and Q by the same factor. + * This changes luminance but not saturation, since + * saturation is determined by the chroma/luminance + * ratio. + * + * On the other hand, by linearly interpolating + * between the original pixel value and a grey + * pixel with the same luminance (R=G=B=Y), we + * change saturation without affecting luminance. + */ + if (argp->action == ACT_LREDUX) + { + /* + * Calculate a scale factor that will bring the pixel + * within both chroma and composite limits, if we scale + * luminance and chroma simultaneously. + * + * The calculated chrominance reduction applies + * to the gamma-corrected RGB values that are + * the input to the RGB-to-YIQ operation. + * Multiplying the original un-gamma-corrected + * pixel values by the scaling factor raised to + * the "gamma" power is equivalent, and avoids + * calling gc() and inv_gc() three times each. */ + scale = chroma_lim / fc; + t = compos_lim / (fy + fc); + if (t < scale) + scale = t; + scale = pow (scale, mode[argp->mode].gamma); + + r = (guint8) pix_encode (scale * pr); + g = (guint8) pix_encode (scale * pg); + b = (guint8) pix_encode (scale * pb); + } + else + { /* ACT_SREDUX hopefully */ + /* + * Calculate a scale factor that will bring the + * pixel within both chroma and composite + * limits, if we scale chroma while leaving + * luminance unchanged. + * + * We have to interpolate gamma-corrected RGB + * values, so we must convert from linear to + * gamma-corrected before interpolation and then + * back to linear afterwards. + */ + scale = chroma_lim / fc; + t = (compos_lim - fy) / fc; + if (t < scale) + scale = t; + + pr = gc (pr, argp->mode); + pg = gc (pg, argp->mode); + pb = gc (pb, argp->mode); + + py = pr * mode[argp->mode].code[0][0] + + pg * mode[argp->mode].code[0][1] + + pb * mode[argp->mode].code[0][2]; + + r = pix_encode (inv_gc (py + scale * (pr - py), + argp->mode)); + g = pix_encode (inv_gc (py + scale * (pg - py), + argp->mode)); + b = pix_encode (inv_gc (py + scale * (pb - py), + argp->mode)); + } + + *d++ = new_r = r; + *d++ = new_g = g; + *d++ = new_b = b; + + s += 3; + + if (src_bpp == 4) + *d++ = *s++; + else if (argp->new_layerp) + *d++ = 255; + } + } + } + else + { + if (! argp->new_layerp) + { + for (i = 0; i < src_bpp; i++) + *d++ = *s++; + } + else + { + s += src_bpp; + d += dest_bpp; + } + } + } + } + + gegl_buffer_set (dest_buffer, + GEGL_RECTANGLE (sel_x1, sel_y1, width, height), 0, + dest_format, dst, + GEGL_AUTO_ROWSTRIDE); + + gimp_progress_update (1.0); + + g_free (src); + g_free (dst); + + g_object_unref (src_buffer); + g_object_unref (dest_buffer); + + if (argp->new_layerp) + { + gimp_drawable_update (nl, sel_x1, sel_y1, width, height); + } + else + { + gimp_drawable_merge_shadow (argp->drawable, TRUE); + gimp_drawable_update (argp->drawable, sel_x1, sel_y1, width, height); + } + + gimp_displays_flush (); + + return success; +} + +static gboolean +plugin_dialog (piArgs *argp) +{ + GtkWidget *dlg; + GtkWidget *hbox; + GtkWidget *vbox; + GtkWidget *toggle; + GtkWidget *frame; + gboolean run; + + gimp_ui_init (PLUG_IN_BINARY, FALSE); + + dlg = gimp_dialog_new (_("Hot"), PLUG_IN_ROLE, + NULL, 0, + gimp_standard_help_func, PLUG_IN_PROC, + + _("_Cancel"), GTK_RESPONSE_CANCEL, + _("_OK"), GTK_RESPONSE_OK, + + NULL); + + gtk_dialog_set_alternative_button_order (GTK_DIALOG (dlg), + GTK_RESPONSE_OK, + GTK_RESPONSE_CANCEL, + -1); + + gimp_window_set_transient (GTK_WINDOW (dlg)); + + hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12); + gtk_container_set_border_width (GTK_CONTAINER (hbox), 12); + gtk_box_pack_start (GTK_BOX (gtk_dialog_get_content_area (GTK_DIALOG (dlg))), + hbox, TRUE, TRUE, 0); + gtk_widget_show (hbox); + + vbox = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12); + gtk_box_pack_start (GTK_BOX (hbox), vbox, TRUE, TRUE, 0); + gtk_widget_show (vbox); + + frame = gimp_int_radio_group_new (TRUE, _("Mode"), + G_CALLBACK (gimp_radio_button_update), + &argp->mode, argp->mode, + + "N_TSC", MODE_NTSC, NULL, + "_PAL", MODE_PAL, NULL, + + NULL); + + gtk_box_pack_start (GTK_BOX (vbox), frame, FALSE, FALSE, 0); + gtk_widget_show (frame); + + toggle = gtk_check_button_new_with_mnemonic (_("Create _new layer")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (toggle), argp->new_layerp); + gtk_box_pack_start (GTK_BOX (vbox), toggle, FALSE, FALSE, 0); + gtk_widget_show (toggle); + + g_signal_connect (toggle, "toggled", + G_CALLBACK (gimp_toggle_button_update), + &argp->new_layerp); + + frame = gimp_int_radio_group_new (TRUE, _("Action"), + G_CALLBACK (gimp_radio_button_update), + &argp->action, argp->action, + + _("Reduce _Luminance"), ACT_LREDUX, NULL, + _("Reduce _Saturation"), ACT_SREDUX, NULL, + _("_Blacken"), ACT_FLAG, NULL, + + NULL); + + gtk_box_pack_start (GTK_BOX (hbox), frame, FALSE, FALSE, 0); + gtk_widget_show (frame); + + gtk_widget_show (dlg); + + run = (gimp_dialog_run (GIMP_DIALOG (dlg)) == GTK_RESPONSE_OK); + + gtk_widget_destroy (dlg); + + return run; +} + +/* + * build_tab: Build multiply lookup table. + * + * For each possible pixel value, decode value into floating-point + * intensity. Then do gamma correction required by the video + * standard. Scale the result by our fixed-point scale factor. + * Then calculate 9 lookup table entries for this pixel value. + * + * We also calculate floating-point and scaled integer versions + * of our limits here. This prevents evaluating expressions every pixel + * when the compiler is too stupid to evaluate constant-valued + * floating-point expressions at compile time. + * + * For convenience, the limits are #defined using IRE units. + * We must convert them here into the units in which YIQ + * are measured. The conversion from IRE to internal units + * depends on the pedestal level in use, since as Y goes from + * 0 to 1, the signal goes from the pedestal level to 100 IRE. + * Chroma is always scaled to remain consistent with Y. + */ +static void +build_tab (int m) +{ + double f; + int pv; + + for (pv = 0; pv <= MAXPIX; pv++) + { + f = (double)SCALE * (double)gc((double)pix_decode(pv),m); + tab[0][0][pv] = (int)(f * mode[m].code[0][0] + 0.5); + tab[0][1][pv] = (int)(f * mode[m].code[0][1] + 0.5); + tab[0][2][pv] = (int)(f * mode[m].code[0][2] + 0.5); + tab[1][0][pv] = (int)(f * mode[m].code[1][0] + 0.5); + tab[1][1][pv] = (int)(f * mode[m].code[1][1] + 0.5); + tab[1][2][pv] = (int)(f * mode[m].code[1][2] + 0.5); + tab[2][0][pv] = (int)(f * mode[m].code[2][0] + 0.5); + tab[2][1][pv] = (int)(f * mode[m].code[2][1] + 0.5); + tab[2][2][pv] = (int)(f * mode[m].code[2][2] + 0.5); + } + + chroma_lim = (double)CHROMA_LIM / (100.0 - mode[m].pedestal); + compos_lim = ((double)COMPOS_LIM - mode[m].pedestal) / + (100.0 - mode[m].pedestal); + + ichroma_lim2 = (int)(chroma_lim * SCALE + 0.5); + ichroma_lim2 *= ichroma_lim2; + icompos_lim = (int)(compos_lim * SCALE + 0.5); +} + +static gboolean +hotp (guint8 r, + guint8 g, + guint8 b) +{ + int y, i, q; + long y2, c2; + + /* + * Pixel decoding, gamma correction, and matrix multiplication + * all done by lookup table. + * + * "i" and "q" are the two chrominance components; + * they are I and Q for NTSC. + * For PAL, "i" is U (scaled B-Y) and "q" is V (scaled R-Y). + * Since we only care about the length of the chroma vector, + * not its angle, we don't care which is which. + */ + y = tab[0][0][r] + tab[0][1][g] + tab[0][2][b]; + i = tab[1][0][r] + tab[1][1][g] + tab[1][2][b]; + q = tab[2][0][r] + tab[2][1][g] + tab[2][2][b]; + + /* + * Check to see if the chrominance vector is too long or the + * composite waveform amplitude is too large. + * + * Chrominance is too large if + * + * sqrt(i^2, q^2) > chroma_lim. + * + * The composite signal amplitude is too large if + * + * y + sqrt(i^2, q^2) > compos_lim. + * + * We avoid doing the sqrt by checking + * + * i^2 + q^2 > chroma_lim^2 + * and + * y + sqrt(i^2 + q^2) > compos_lim + * sqrt(i^2 + q^2) > compos_lim - y + * i^2 + q^2 > (compos_lim - y)^2 + * + */ + + c2 = (long)i * i + (long)q * q; + y2 = (long)icompos_lim - y; + y2 *= y2; + + if (c2 <= ichroma_lim2 && c2 <= y2) + { /* no problems */ + return FALSE; + } + + return TRUE; +} |