Leptonica  1.82.0
Image processing and image analysis suite
edge.c
Go to the documentation of this file.
1 /*====================================================================*
2  - Copyright (C) 2001 Leptonica. All rights reserved.
3  -
4  - Redistribution and use in source and binary forms, with or without
5  - modification, are permitted provided that the following conditions
6  - are met:
7  - 1. Redistributions of source code must retain the above copyright
8  - notice, this list of conditions and the following disclaimer.
9  - 2. Redistributions in binary form must reproduce the above
10  - copyright notice, this list of conditions and the following
11  - disclaimer in the documentation and/or other materials
12  - provided with the distribution.
13  -
14  - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15  - ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16  - LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17  - A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ANY
18  - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19  - EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20  - PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21  - PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22  - OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
23  - NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24  - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  *====================================================================*/
26 
61 #ifdef HAVE_CONFIG_H
62 #include <config_auto.h>
63 #endif /* HAVE_CONFIG_H */
64 
65 #include "allheaders.h"
66 
67 /*----------------------------------------------------------------------*
68  * Sobel edge detecting filter *
69  *----------------------------------------------------------------------*/
93 PIX *
95  l_int32 orientflag)
96 {
97 l_int32 w, h, d, i, j, wplt, wpld, gx, gy, vald;
98 l_int32 val1, val2, val3, val4, val5, val6, val7, val8, val9;
99 l_uint32 *datat, *linet, *datad, *lined;
100 PIX *pixt, *pixd;
101 
102  PROCNAME("pixSobelEdgeFilter");
103 
104  if (!pixs)
105  return (PIX *)ERROR_PTR("pixs not defined", procName, NULL);
106  pixGetDimensions(pixs, &w, &h, &d);
107  if (d != 8)
108  return (PIX *)ERROR_PTR("pixs not 8 bpp", procName, NULL);
109  if (orientflag != L_HORIZONTAL_EDGES && orientflag != L_VERTICAL_EDGES &&
110  orientflag != L_ALL_EDGES)
111  return (PIX *)ERROR_PTR("invalid orientflag", procName, NULL);
112 
113  /* Add 1 pixel (mirrored) to each side of the image. */
114  if ((pixt = pixAddMirroredBorder(pixs, 1, 1, 1, 1)) == NULL)
115  return (PIX *)ERROR_PTR("pixt not made", procName, NULL);
116 
117  /* Compute filter output at each location. */
118  pixd = pixCreateTemplate(pixs);
119  datat = pixGetData(pixt);
120  wplt = pixGetWpl(pixt);
121  datad = pixGetData(pixd);
122  wpld = pixGetWpl(pixd);
123  for (i = 0; i < h; i++) {
124  linet = datat + i * wplt;
125  lined = datad + i * wpld;
126  for (j = 0; j < w; j++) {
127  if (j == 0) { /* start a new row */
128  val1 = GET_DATA_BYTE(linet, j);
129  val2 = GET_DATA_BYTE(linet + wplt, j);
130  val3 = GET_DATA_BYTE(linet + 2 * wplt, j);
131  val4 = GET_DATA_BYTE(linet, j + 1);
132  val5 = GET_DATA_BYTE(linet + wplt, j + 1);
133  val6 = GET_DATA_BYTE(linet + 2 * wplt, j + 1);
134  val7 = GET_DATA_BYTE(linet, j + 2);
135  val8 = GET_DATA_BYTE(linet + wplt, j + 2);
136  val9 = GET_DATA_BYTE(linet + 2 * wplt, j + 2);
137  } else { /* shift right by 1 pixel; update incrementally */
138  val1 = val4;
139  val2 = val5;
140  val3 = val6;
141  val4 = val7;
142  val5 = val8;
143  val6 = val9;
144  val7 = GET_DATA_BYTE(linet, j + 2);
145  val8 = GET_DATA_BYTE(linet + wplt, j + 2);
146  val9 = GET_DATA_BYTE(linet + 2 * wplt, j + 2);
147  }
148  if (orientflag == L_HORIZONTAL_EDGES)
149  vald = L_ABS(val1 + 2 * val4 + val7
150  - val3 - 2 * val6 - val9) >> 3;
151  else if (orientflag == L_VERTICAL_EDGES)
152  vald = L_ABS(val1 + 2 * val2 + val3 - val7
153  - 2 * val8 - val9) >> 3;
154  else { /* L_ALL_EDGES */
155  gx = L_ABS(val1 + 2 * val2 + val3 - val7
156  - 2 * val8 - val9) >> 3;
157  gy = L_ABS(val1 + 2 * val4 + val7
158  - val3 - 2 * val6 - val9) >> 3;
159  vald = L_MIN(255, gx + gy);
160  }
161  SET_DATA_BYTE(lined, j, vald);
162  }
163  }
164 
165  pixDestroy(&pixt);
166  return pixd;
167 }
168 
169 
170 /*----------------------------------------------------------------------*
171  * Two-sided edge gradient filter *
172  *----------------------------------------------------------------------*/
201 PIX *
203  l_int32 orientflag)
204 {
205 l_int32 w, h, d, i, j, wpls, wpld;
206 l_int32 cval, rval, bval, val, lgrad, rgrad, tgrad, bgrad;
207 l_uint32 *datas, *lines, *datad, *lined;
208 PIX *pixd;
209 
210  PROCNAME("pixTwoSidedEdgeFilter");
211 
212  if (!pixs)
213  return (PIX *)ERROR_PTR("pixs not defined", procName, NULL);
214  pixGetDimensions(pixs, &w, &h, &d);
215  if (d != 8)
216  return (PIX *)ERROR_PTR("pixs not 8 bpp", procName, NULL);
217  if (orientflag != L_HORIZONTAL_EDGES && orientflag != L_VERTICAL_EDGES)
218  return (PIX *)ERROR_PTR("invalid orientflag", procName, NULL);
219 
220  pixd = pixCreateTemplate(pixs);
221  datas = pixGetData(pixs);
222  wpls = pixGetWpl(pixs);
223  datad = pixGetData(pixd);
224  wpld = pixGetWpl(pixd);
225  if (orientflag == L_VERTICAL_EDGES) {
226  for (i = 0; i < h; i++) {
227  lines = datas + i * wpls;
228  lined = datad + i * wpld;
229  cval = GET_DATA_BYTE(lines, 1);
230  lgrad = cval - GET_DATA_BYTE(lines, 0);
231  for (j = 1; j < w - 1; j++) {
232  rval = GET_DATA_BYTE(lines, j + 1);
233  rgrad = rval - cval;
234  if (lgrad * rgrad > 0) {
235  if (lgrad < 0)
236  val = -L_MAX(lgrad, rgrad);
237  else
238  val = L_MIN(lgrad, rgrad);
239  SET_DATA_BYTE(lined, j, val);
240  }
241  lgrad = rgrad;
242  cval = rval;
243  }
244  }
245  }
246  else { /* L_HORIZONTAL_EDGES) */
247  for (j = 0; j < w; j++) {
248  lines = datas + wpls;
249  cval = GET_DATA_BYTE(lines, j); /* for line 1 */
250  tgrad = cval - GET_DATA_BYTE(datas, j);
251  for (i = 1; i < h - 1; i++) {
252  lines += wpls; /* for line i + 1 */
253  lined = datad + i * wpld;
254  bval = GET_DATA_BYTE(lines, j);
255  bgrad = bval - cval;
256  if (tgrad * bgrad > 0) {
257  if (tgrad < 0)
258  val = -L_MAX(tgrad, bgrad);
259  else
260  val = L_MIN(tgrad, bgrad);
261  SET_DATA_BYTE(lined, j, val);
262  }
263  tgrad = bgrad;
264  cval = bval;
265  }
266  }
267  }
268 
269  return pixd;
270 }
271 
272 
273 /*----------------------------------------------------------------------*
274  * Measurement of edge smoothness *
275  *----------------------------------------------------------------------*/
311 l_ok
313  l_int32 side,
314  l_int32 minjump,
315  l_int32 minreversal,
316  l_float32 *pjpl,
317  l_float32 *pjspl,
318  l_float32 *prpl,
319  const char *debugfile)
320 {
321 l_int32 i, n, val, nval, diff, njumps, jumpsum, nreversal;
322 NUMA *na, *nae;
323 
324  PROCNAME("pixMeasureEdgeSmoothness");
325 
326  if (pjpl) *pjpl = 0.0;
327  if (pjspl) *pjspl = 0.0;
328  if (prpl) *prpl = 0.0;
329  if (!pjpl && !pjspl && !prpl && !debugfile)
330  return ERROR_INT("no output requested", procName, 1);
331  if (!pixs || pixGetDepth(pixs) != 1)
332  return ERROR_INT("pixs not defined or not 1 bpp", procName, 1);
333  if (side != L_FROM_LEFT && side != L_FROM_RIGHT &&
334  side != L_FROM_TOP && side != L_FROM_BOT)
335  return ERROR_INT("invalid side", procName, 1);
336  if (minjump < 1)
337  return ERROR_INT("invalid minjump; must be >= 1", procName, 1);
338  if (minreversal < 1)
339  return ERROR_INT("invalid minreversal; must be >= 1", procName, 1);
340 
341  if ((na = pixGetEdgeProfile(pixs, side, debugfile)) == NULL)
342  return ERROR_INT("edge profile not made", procName, 1);
343  if ((n = numaGetCount(na)) < 2) {
344  numaDestroy(&na);
345  return 0;
346  }
347 
348  if (pjpl || pjspl) {
349  jumpsum = 0;
350  njumps = 0;
351  numaGetIValue(na, 0, &val);
352  for (i = 1; i < n; i++) {
353  numaGetIValue(na, i, &nval);
354  diff = L_ABS(nval - val);
355  if (diff >= minjump) {
356  njumps++;
357  jumpsum += diff;
358  }
359  val = nval;
360  }
361  if (pjpl)
362  *pjpl = (l_float32)njumps / (l_float32)(n - 1);
363  if (pjspl)
364  *pjspl = (l_float32)jumpsum / (l_float32)(n - 1);
365  }
366 
367  if (prpl) {
368  nae = numaFindExtrema(na, minreversal, NULL);
369  nreversal = numaGetCount(nae) - 1;
370  *prpl = (l_float32)nreversal / (l_float32)(n - 1);
371  numaDestroy(&nae);
372  }
373 
374  numaDestroy(&na);
375  return 0;
376 }
377 
378 
388 NUMA *
390  l_int32 side,
391  const char *debugfile)
392 {
393 l_int32 x, y, w, h, loc, index, ival;
394 l_uint32 val;
395 NUMA *na;
396 PIX *pixt;
397 PIXCMAP *cmap;
398 
399  PROCNAME("pixGetEdgeProfile");
400 
401  if (!pixs || pixGetDepth(pixs) != 1)
402  return (NUMA *)ERROR_PTR("pixs undefined or not 1 bpp", procName, NULL);
403  if (side != L_FROM_LEFT && side != L_FROM_RIGHT &&
404  side != L_FROM_TOP && side != L_FROM_BOT)
405  return (NUMA *)ERROR_PTR("invalid side", procName, NULL);
406 
407  pixGetDimensions(pixs, &w, &h, NULL);
408  if (side == L_FROM_LEFT || side == L_FROM_RIGHT)
409  na = numaCreate(h);
410  else
411  na = numaCreate(w);
412  if (side == L_FROM_LEFT) {
413  pixGetLastOffPixelInRun(pixs, 0, 0, L_FROM_LEFT, &loc);
414  loc = (loc == w - 1) ? 0 : loc + 1; /* back to the left edge */
415  numaAddNumber(na, loc);
416  for (y = 1; y < h; y++) {
417  pixGetPixel(pixs, loc, y, &val);
418  if (val == 1) {
419  pixGetLastOnPixelInRun(pixs, loc, y, L_FROM_RIGHT, &loc);
420  } else {
421  pixGetLastOffPixelInRun(pixs, loc, y, L_FROM_LEFT, &loc);
422  loc = (loc == w - 1) ? 0 : loc + 1;
423  }
424  numaAddNumber(na, loc);
425  }
426  }
427  else if (side == L_FROM_RIGHT) {
428  pixGetLastOffPixelInRun(pixs, w - 1, 0, L_FROM_RIGHT, &loc);
429  loc = (loc == 0) ? w - 1 : loc - 1; /* back to the right edge */
430  numaAddNumber(na, loc);
431  for (y = 1; y < h; y++) {
432  pixGetPixel(pixs, loc, y, &val);
433  if (val == 1) {
434  pixGetLastOnPixelInRun(pixs, loc, y, L_FROM_LEFT, &loc);
435  } else {
436  pixGetLastOffPixelInRun(pixs, loc, y, L_FROM_RIGHT, &loc);
437  loc = (loc == 0) ? w - 1 : loc - 1;
438  }
439  numaAddNumber(na, loc);
440  }
441  }
442  else if (side == L_FROM_TOP) {
443  pixGetLastOffPixelInRun(pixs, 0, 0, L_FROM_TOP, &loc);
444  loc = (loc == h - 1) ? 0 : loc + 1; /* back to the top edge */
445  numaAddNumber(na, loc);
446  for (x = 1; x < w; x++) {
447  pixGetPixel(pixs, x, loc, &val);
448  if (val == 1) {
449  pixGetLastOnPixelInRun(pixs, x, loc, L_FROM_BOT, &loc);
450  } else {
451  pixGetLastOffPixelInRun(pixs, x, loc, L_FROM_TOP, &loc);
452  loc = (loc == h - 1) ? 0 : loc + 1;
453  }
454  numaAddNumber(na, loc);
455  }
456  }
457  else { /* side == L_FROM_BOT */
458  pixGetLastOffPixelInRun(pixs, 0, h - 1, L_FROM_BOT, &loc);
459  loc = (loc == 0) ? h - 1 : loc - 1; /* back to the bottom edge */
460  numaAddNumber(na, loc);
461  for (x = 1; x < w; x++) {
462  pixGetPixel(pixs, x, loc, &val);
463  if (val == 1) {
464  pixGetLastOnPixelInRun(pixs, x, loc, L_FROM_TOP, &loc);
465  } else {
466  pixGetLastOffPixelInRun(pixs, x, loc, L_FROM_BOT, &loc);
467  loc = (loc == 0) ? h - 1 : loc - 1;
468  }
469  numaAddNumber(na, loc);
470  }
471  }
472 
473  if (debugfile) {
474  pixt = pixConvertTo8(pixs, TRUE);
475  cmap = pixGetColormap(pixt);
476  pixcmapAddColor(cmap, 255, 0, 0);
477  index = pixcmapGetCount(cmap) - 1;
478  if (side == L_FROM_LEFT || side == L_FROM_RIGHT) {
479  for (y = 0; y < h; y++) {
480  numaGetIValue(na, y, &ival);
481  pixSetPixel(pixt, ival, y, index);
482  }
483  } else { /* L_FROM_TOP or L_FROM_BOT */
484  for (x = 0; x < w; x++) {
485  numaGetIValue(na, x, &ival);
486  pixSetPixel(pixt, x, ival, index);
487  }
488  }
489  pixWrite(debugfile, pixt, IFF_PNG);
490  pixDestroy(&pixt);
491  }
492 
493  return na;
494 }
495 
496 
497 /*
498  * \brief pixGetLastOffPixelInRun()
499  *
500  * \param[in] pixs 1 bpp
501  * \param[in] x, y starting location
502  * \param[in] direction L_FROM_LEFT, L_FROM_RIGHT, L_FROM_TOP, L_FROM_BOT
503  * \param[out] ploc location in scan direction coordinate
504  * of last OFF pixel found
505  * \return 0 if OK, 1 on error
506  *
507  * <pre>
508  * Notes:
509  * (1) Search starts from the pixel at (x, y), which is OFF.
510  * (2) It returns the location in the scan direction of the last
511  * pixel in the current run that is OFF.
512  * (3) The interface for these pixel run functions is cleaner when
513  * you ask for the last pixel in the current run, rather than the
514  * first pixel of opposite polarity that is found, because the
515  * current run may go to the edge of the image, in which case
516  * no pixel of opposite polarity is found.
517  * </pre>
518  */
519 l_ok
520 pixGetLastOffPixelInRun(PIX *pixs,
521  l_int32 x,
522  l_int32 y,
523  l_int32 direction,
524  l_int32 *ploc)
525 {
526 l_int32 loc, w, h;
527 l_uint32 val;
528 
529  PROCNAME("pixGetLastOffPixelInRun");
530 
531  if (!ploc)
532  return ERROR_INT("&loc not defined", procName, 1);
533  *ploc = 0;
534  if (!pixs || pixGetDepth(pixs) != 1)
535  return ERROR_INT("pixs undefined or not 1 bpp", procName, 1);
536  if (direction != L_FROM_LEFT && direction != L_FROM_RIGHT &&
537  direction != L_FROM_TOP && direction != L_FROM_BOT)
538  return ERROR_INT("invalid side", procName, 1);
539 
540  pixGetDimensions(pixs, &w, &h, NULL);
541  if (direction == L_FROM_LEFT) {
542  for (loc = x; loc < w; loc++) {
543  pixGetPixel(pixs, loc, y, &val);
544  if (val == 1)
545  break;
546  }
547  *ploc = loc - 1;
548  } else if (direction == L_FROM_RIGHT) {
549  for (loc = x; loc >= 0; loc--) {
550  pixGetPixel(pixs, loc, y, &val);
551  if (val == 1)
552  break;
553  }
554  *ploc = loc + 1;
555  }
556  else if (direction == L_FROM_TOP) {
557  for (loc = y; loc < h; loc++) {
558  pixGetPixel(pixs, x, loc, &val);
559  if (val == 1)
560  break;
561  }
562  *ploc = loc - 1;
563  }
564  else if (direction == L_FROM_BOT) {
565  for (loc = y; loc >= 0; loc--) {
566  pixGetPixel(pixs, x, loc, &val);
567  if (val == 1)
568  break;
569  }
570  *ploc = loc + 1;
571  }
572  return 0;
573 }
574 
575 
576 /*
577  * \brief pixGetLastOnPixelInRun()
578  *
579  * \param[in] pixs 1 bpp
580  * \param[in] x, y starting location
581  * \param[in] direction L_FROM_LEFT, L_FROM_RIGHT, L_FROM_TOP, L_FROM_BOT
582  * \param[out] ploc location in scan direction coordinate
583  * of first ON pixel found
584  * \return 0 if OK, 1 on error
585  *
586  * <pre>
587  * Notes:
588  * (1) Search starts from the pixel at (x, y), which is ON.
589  * (2) It returns the location in the scan direction of the last
590  * pixel in the current run that is ON.
591  * </pre>
592  */
593 l_int32
594 pixGetLastOnPixelInRun(PIX *pixs,
595  l_int32 x,
596  l_int32 y,
597  l_int32 direction,
598  l_int32 *ploc)
599 {
600 l_int32 loc, w, h;
601 l_uint32 val;
602 
603  PROCNAME("pixLastOnPixelInRun");
604 
605  if (!ploc)
606  return ERROR_INT("&loc not defined", procName, 1);
607  *ploc = 0;
608  if (!pixs || pixGetDepth(pixs) != 1)
609  return ERROR_INT("pixs undefined or not 1 bpp", procName, 1);
610  if (direction != L_FROM_LEFT && direction != L_FROM_RIGHT &&
611  direction != L_FROM_TOP && direction != L_FROM_BOT)
612  return ERROR_INT("invalid side", procName, 1);
613 
614  pixGetDimensions(pixs, &w, &h, NULL);
615  if (direction == L_FROM_LEFT) {
616  for (loc = x; loc < w; loc++) {
617  pixGetPixel(pixs, loc, y, &val);
618  if (val == 0)
619  break;
620  }
621  *ploc = loc - 1;
622  } else if (direction == L_FROM_RIGHT) {
623  for (loc = x; loc >= 0; loc--) {
624  pixGetPixel(pixs, loc, y, &val);
625  if (val == 0)
626  break;
627  }
628  *ploc = loc + 1;
629  }
630  else if (direction == L_FROM_TOP) {
631  for (loc = y; loc < h; loc++) {
632  pixGetPixel(pixs, x, loc, &val);
633  if (val == 0)
634  break;
635  }
636  *ploc = loc - 1;
637  }
638  else if (direction == L_FROM_BOT) {
639  for (loc = y; loc >= 0; loc--) {
640  pixGetPixel(pixs, x, loc, &val);
641  if (val == 0)
642  break;
643  }
644  *ploc = loc + 1;
645  }
646  return 0;
647 }
NUMA * pixGetEdgeProfile(PIX *pixs, l_int32 side, const char *debugfile)
pixGetEdgeProfile()
Definition: edge.c:389
l_int32 h
Definition: dewarp.h:164
NUMA * numaFindExtrema(NUMA *nas, l_float32 delta, NUMA **pnav)
numaFindExtrema()
Definition: numafunc2.c:2550
PIX * pixCreateTemplate(const PIX *pixs)
pixCreateTemplate()
Definition: pix1.c:383
l_ok numaAddNumber(NUMA *na, l_float32 val)
numaAddNumber()
Definition: numabasic.c:478
PIX * pixConvertTo8(PIX *pixs, l_int32 cmapflag)
pixConvertTo8()
Definition: pixconv.c:3133
PIX * pixAddMirroredBorder(PIX *pixs, l_int32 left, l_int32 right, l_int32 top, l_int32 bot)
pixAddMirroredBorder()
Definition: pix2.c:2101
NUMA * numaCreate(l_int32 n)
numaCreate()
Definition: numabasic.c:194
l_uint32 * pixGetData(PIX *pix)
pixGetData()
Definition: pix1.c:1763
PIX * pixTwoSidedEdgeFilter(PIX *pixs, l_int32 orientflag)
pixTwoSidedEdgeFilter()
Definition: edge.c:202
struct Pix * pixs
Definition: dewarp.h:154
l_ok numaGetIValue(NUMA *na, l_int32 index, l_int32 *pival)
numaGetIValue()
Definition: numabasic.c:754
Definition: array.h:70
l_int32 numaGetCount(NUMA *na)
numaGetCount()
Definition: numabasic.c:658
l_ok pixSetPixel(PIX *pix, l_int32 x, l_int32 y, l_uint32 val)
pixSetPixel()
Definition: pix2.c:263
#define SET_DATA_BYTE(pdata, n, val)
Definition: arrayaccess.h:198
#define GET_DATA_BYTE(pdata, n)
Definition: arrayaccess.h:188
void pixDestroy(PIX **ppix)
pixDestroy()
Definition: pix1.c:621
void numaDestroy(NUMA **pna)
numaDestroy()
Definition: numabasic.c:366
l_ok pixGetPixel(PIX *pix, l_int32 x, l_int32 y, l_uint32 *pval)
pixGetPixel()
Definition: pix2.c:190
l_ok pixGetDimensions(const PIX *pix, l_int32 *pw, l_int32 *ph, l_int32 *pd)
pixGetDimensions()
Definition: pix1.c:1113
l_ok pixMeasureEdgeSmoothness(PIX *pixs, l_int32 side, l_int32 minjump, l_int32 minreversal, l_float32 *pjpl, l_float32 *pjspl, l_float32 *prpl, const char *debugfile)
pixMeasureEdgeSmoothness()
Definition: edge.c:312
Definition: pix.h:138
PIX * pixSobelEdgeFilter(PIX *pixs, l_int32 orientflag)
pixSobelEdgeFilter()
Definition: edge.c:94
l_int32 pixcmapGetCount(const PIXCMAP *cmap)
pixcmapGetCount()
Definition: colormap.c:708
l_int32 w
Definition: dewarp.h:163
l_ok pixcmapAddColor(PIXCMAP *cmap, l_int32 rval, l_int32 gval, l_int32 bval)
pixcmapAddColor()
Definition: colormap.c:414