| /* | |
| * Copyright (c) 2017, 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code 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 | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| import java.awt.BasicStroke; | |
| import java.awt.Color; | |
| import java.awt.Graphics2D; | |
| import java.awt.RenderingHints; | |
| import java.awt.Stroke; | |
| import java.awt.Shape; | |
| import java.awt.geom.CubicCurve2D; | |
| import java.awt.geom.Ellipse2D; | |
| import java.awt.geom.Line2D; | |
| import java.awt.geom.Path2D; | |
| import java.awt.geom.PathIterator; | |
| import java.awt.geom.QuadCurve2D; | |
| import java.awt.image.BufferedImage; | |
| import java.awt.image.DataBufferInt; | |
| import java.io.File; | |
| import java.io.FileOutputStream; | |
| import java.io.IOException; | |
| import java.util.Arrays; | |
| import java.util.Iterator; | |
| import java.util.Locale; | |
| import java.util.Random; | |
| import java.util.concurrent.atomic.AtomicBoolean; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.logging.Handler; | |
| import java.util.logging.LogRecord; | |
| import java.util.logging.Logger; | |
| import javax.imageio.IIOImage; | |
| import javax.imageio.ImageIO; | |
| import javax.imageio.ImageWriteParam; | |
| import javax.imageio.ImageWriter; | |
| import javax.imageio.stream.ImageOutputStream; | |
| /* | |
| * @test id=Poly | |
| * @bug 8191814 | |
| * @summary Runs the test with "-poly" option | |
| * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly | |
| */ | |
| /* | |
| * @test id=PolyDoDash | |
| * @bug 8191814 | |
| * @summary Runs the test with "-poly -doDash" options | |
| * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -poly -doDash | |
| */ | |
| /* | |
| * @test id=Cubic | |
| * @bug 8191814 | |
| * @summary Runs the test with "-cubic" option | |
| * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic | |
| */ | |
| /* | |
| * @test id=CubicDoDash | |
| * @bug 8191814 | |
| * @summary Runs the test with "-cubic -doDash" options | |
| * @run main/othervm/timeout=300 -Dsun.java2d.renderer=sun.java2d.marlin.DMarlinRenderingEngine ClipShapeTest -cubic -doDash | |
| */ | |
| /** | |
| * Verifies that Marlin rendering generates the same images with and without | |
| * clipping optimization with all possible stroke (cap/join) and/or dashes or | |
| * fill modes (EO rules) for paths made of either 9 lines, 4 quads, 2 cubics | |
| * (random). | |
| * <p> | |
| * Note: Use the argument {@code -slow} to run more intensive tests (too much | |
| * time). | |
| */ | |
| public final class ClipShapeTest { | |
| // test options: | |
| static int NUM_TESTS; | |
| // shape settings: | |
| static ShapeMode SHAPE_MODE; | |
| static boolean USE_DASHES; | |
| static boolean USE_VAR_STROKE; | |
| static int THRESHOLD_DELTA; | |
| static long THRESHOLD_NBPIX; | |
| // constants: | |
| static final boolean DO_FAIL = Boolean.valueOf(System.getProperty("ClipShapeTest.fail", "true")); | |
| static final boolean TEST_STROKER = true; | |
| static final boolean TEST_FILLER = true; | |
| static final boolean SUBDIVIDE_CURVE = true; | |
| static final double SUBDIVIDE_LEN_TH = 50.0; | |
| static final boolean TRACE_SUBDIVIDE_CURVE = false; | |
| static final int TESTW = 100; | |
| static final int TESTH = 100; | |
| // dump path on console: | |
| static final boolean DUMP_SHAPE = true; | |
| static final boolean SHOW_DETAILS = false; // disabled | |
| static final boolean SHOW_OUTLINE = true; | |
| static final boolean SHOW_POINTS = true; | |
| static final boolean SHOW_INFO = false; | |
| static final int MAX_SHOW_FRAMES = 10; | |
| static final int MAX_SAVE_FRAMES = 100; | |
| // use fixed seed to reproduce always same polygons between tests | |
| static final boolean FIXED_SEED = true; | |
| static final double RAND_SCALE = 3.0; | |
| static final double RANDW = TESTW * RAND_SCALE; | |
| static final double OFFW = (TESTW - RANDW) / 2.0; | |
| static final double RANDH = TESTH * RAND_SCALE; | |
| static final double OFFH = (TESTH - RANDH) / 2.0; | |
| static enum ShapeMode { | |
| TWO_CUBICS, | |
| FOUR_QUADS, | |
| FIVE_LINE_POLYS, | |
| NINE_LINE_POLYS, | |
| FIFTY_LINE_POLYS, | |
| MIXED | |
| } | |
| static final long SEED = 1666133789L; | |
| // Fixed seed to avoid any difference between runs: | |
| static final Random RANDOM = new Random(SEED); | |
| static final File OUTPUT_DIR = new File("."); | |
| static final AtomicBoolean isMarlin = new AtomicBoolean(); | |
| static final AtomicBoolean isClipRuntime = new AtomicBoolean(); | |
| static { | |
| Locale.setDefault(Locale.US); | |
| // FIRST: Get Marlin runtime state from its log: | |
| // initialize j.u.l Looger: | |
| final Logger log = Logger.getLogger("sun.java2d.marlin"); | |
| log.addHandler(new Handler() { | |
| @Override | |
| public void publish(LogRecord record) { | |
| final String msg = record.getMessage(); | |
| if (msg != null) { | |
| // last space to avoid matching other settings: | |
| if (msg.startsWith("sun.java2d.renderer ")) { | |
| isMarlin.set(msg.contains("DMarlinRenderingEngine")); | |
| } | |
| if (msg.startsWith("sun.java2d.renderer.clip.runtime.enable")) { | |
| isClipRuntime.set(msg.contains("true")); | |
| } | |
| } | |
| final Throwable th = record.getThrown(); | |
| // detect any Throwable: | |
| if (th != null) { | |
| System.out.println("Test failed:\n" + record.getMessage()); | |
| th.printStackTrace(System.out); | |
| throw new RuntimeException("Test failed: ", th); | |
| } | |
| } | |
| @Override | |
| public void flush() { | |
| } | |
| @Override | |
| public void close() throws SecurityException { | |
| } | |
| }); | |
| // enable Marlin logging & internal checks: | |
| System.setProperty("sun.java2d.renderer.log", "true"); | |
| System.setProperty("sun.java2d.renderer.useLogger", "true"); | |
| // disable static clipping setting: | |
| System.setProperty("sun.java2d.renderer.clip", "false"); | |
| System.setProperty("sun.java2d.renderer.clip.runtime.enable", "true"); | |
| // enable subdivider: | |
| System.setProperty("sun.java2d.renderer.clip.subdivider", "true"); | |
| // disable min length check: always subdivide curves at clip edges | |
| System.setProperty("sun.java2d.renderer.clip.subdivider.minLength", "-1"); | |
| // If any curve, increase curve accuracy: | |
| // curve length max error: | |
| System.setProperty("sun.java2d.renderer.curve_len_err", "1e-4"); | |
| // cubic min/max error: | |
| System.setProperty("sun.java2d.renderer.cubic_dec_d2", "1e-3"); | |
| System.setProperty("sun.java2d.renderer.cubic_inc_d1", "1e-4"); | |
| // quad max error: | |
| System.setProperty("sun.java2d.renderer.quad_dec_d2", "5e-4"); | |
| } | |
| private static void resetOptions() { | |
| NUM_TESTS = Integer.getInteger("ClipShapeTest.numTests", 5000); | |
| // shape settings: | |
| SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; | |
| USE_DASHES = false; | |
| USE_VAR_STROKE = false; | |
| } | |
| /** | |
| * Test | |
| * @param args | |
| */ | |
| public static void main(String[] args) { | |
| { | |
| // Bootstrap: init Renderer now: | |
| final BufferedImage img = newImage(TESTW, TESTH); | |
| final Graphics2D g2d = initialize(img, null); | |
| try { | |
| paintShape(new Line2D.Double(0,0,100,100), g2d, true, false); | |
| } finally { | |
| g2d.dispose(); | |
| } | |
| if (!isMarlin.get()) { | |
| throw new RuntimeException("Marlin renderer not used at runtime !"); | |
| } | |
| if (!isClipRuntime.get()) { | |
| throw new RuntimeException("Marlin clipping not enabled at runtime !"); | |
| } | |
| } | |
| System.out.println("---------------------------------------"); | |
| System.out.println("ClipShapeTest: image = " + TESTW + " x " + TESTH); | |
| resetOptions(); | |
| boolean runSlowTests = false; | |
| for (String arg : args) { | |
| if ("-slow".equals(arg)) { | |
| runSlowTests = true; | |
| } else if ("-doDash".equals(arg)) { | |
| USE_DASHES = true; | |
| } else if ("-doVarStroke".equals(arg)) { | |
| USE_VAR_STROKE = true; | |
| } else { | |
| // shape mode: | |
| if (arg.equalsIgnoreCase("-poly")) { | |
| SHAPE_MODE = ShapeMode.NINE_LINE_POLYS; | |
| } else if (arg.equalsIgnoreCase("-bigpoly")) { | |
| SHAPE_MODE = ShapeMode.FIFTY_LINE_POLYS; | |
| } else if (arg.equalsIgnoreCase("-quad")) { | |
| SHAPE_MODE = ShapeMode.FOUR_QUADS; | |
| } else if (arg.equalsIgnoreCase("-cubic")) { | |
| SHAPE_MODE = ShapeMode.TWO_CUBICS; | |
| } else if (arg.equalsIgnoreCase("-mixed")) { | |
| SHAPE_MODE = ShapeMode.MIXED; | |
| } | |
| } | |
| } | |
| System.out.println("Shape mode: " + SHAPE_MODE); | |
| // adjust image comparison thresholds: | |
| switch (SHAPE_MODE) { | |
| case TWO_CUBICS: | |
| // Define uncertainty for curves: | |
| THRESHOLD_DELTA = 32; | |
| THRESHOLD_NBPIX = (USE_DASHES) ? 50 : 200; | |
| if (SUBDIVIDE_CURVE) { | |
| THRESHOLD_NBPIX = 4; | |
| } | |
| break; | |
| case FOUR_QUADS: | |
| case MIXED: | |
| // Define uncertainty for quads: | |
| // curve subdivision causes curves to be smaller | |
| // then curve offsets are different (more accurate) | |
| THRESHOLD_DELTA = 64; | |
| THRESHOLD_NBPIX = (USE_DASHES) ? 40 : 420; | |
| if (SUBDIVIDE_CURVE) { | |
| THRESHOLD_NBPIX = 10; | |
| } | |
| break; | |
| default: | |
| // Define uncertainty for lines: | |
| // float variant have higher uncertainty | |
| THRESHOLD_DELTA = 2; | |
| THRESHOLD_NBPIX = (USE_DASHES) ? 6 : 0; | |
| } | |
| // Visual inspection (low threshold): | |
| // THRESHOLD_NBPIX = 2; | |
| System.out.println("THRESHOLD_DELTA: " + THRESHOLD_DELTA); | |
| System.out.println("THRESHOLD_NBPIX: " + THRESHOLD_NBPIX); | |
| if (runSlowTests) { | |
| NUM_TESTS = 10000; // or 100000 (very slow) | |
| USE_VAR_STROKE = true; | |
| } | |
| System.out.println("NUM_TESTS: " + NUM_TESTS); | |
| if (USE_DASHES) { | |
| System.out.println("USE_DASHES: enabled."); | |
| } | |
| if (USE_VAR_STROKE) { | |
| System.out.println("USE_VAR_STROKE: enabled."); | |
| } | |
| if (!DO_FAIL) { | |
| System.out.println("DO_FAIL: disabled."); | |
| } | |
| System.out.println("---------------------------------------"); | |
| final DiffContext allCtx = new DiffContext("All Test setups"); | |
| final DiffContext allWorstCtx = new DiffContext("Worst(All Test setups)"); | |
| int failures = 0; | |
| final long start = System.nanoTime(); | |
| try { | |
| if (TEST_STROKER) { | |
| final float[][] dashArrays = (USE_DASHES) ? | |
| // small | |
| // new float[][]{new float[]{1f, 2f}} | |
| // normal | |
| new float[][]{new float[]{13f, 7f}} | |
| // large (prime) | |
| // new float[][]{new float[]{41f, 7f}} | |
| // none | |
| : new float[][]{null}; | |
| System.out.println("dashes: " + Arrays.deepToString(dashArrays)); | |
| final float[] strokeWidths = (USE_VAR_STROKE) | |
| ? new float[5] : | |
| new float[]{10f}; | |
| int nsw = 0; | |
| if (USE_VAR_STROKE) { | |
| for (float width = 0.25f; width < 110f; width *= 5f) { | |
| strokeWidths[nsw++] = width; | |
| } | |
| } else { | |
| nsw = 1; | |
| } | |
| System.out.println("stroke widths: " + Arrays.toString(strokeWidths)); | |
| // Stroker tests: | |
| for (int w = 0; w < nsw; w++) { | |
| final float width = strokeWidths[w]; | |
| for (float[] dashes : dashArrays) { | |
| for (int cap = 0; cap <= 2; cap++) { | |
| for (int join = 0; join <= 2; join++) { | |
| failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, width, cap, join, dashes)); | |
| failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, width, cap, join, dashes)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (TEST_FILLER) { | |
| // Filler tests: | |
| failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_NON_ZERO)); | |
| failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_NON_ZERO)); | |
| failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, false, Path2D.WIND_EVEN_ODD)); | |
| failures += paintPaths(allCtx, allWorstCtx, new TestSetup(SHAPE_MODE, true, Path2D.WIND_EVEN_ODD)); | |
| } | |
| } catch (IOException ioe) { | |
| throw new RuntimeException(ioe); | |
| } | |
| System.out.println("main: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); | |
| allWorstCtx.dump(); | |
| allCtx.dump(); | |
| if (DO_FAIL && (failures != 0)) { | |
| throw new RuntimeException("Clip test failures : " + failures); | |
| } | |
| } | |
| static int paintPaths(final DiffContext allCtx, final DiffContext allWorstCtx, final TestSetup ts) throws IOException { | |
| final long start = System.nanoTime(); | |
| if (FIXED_SEED) { | |
| // Reset seed for random numbers: | |
| RANDOM.setSeed(SEED); | |
| } | |
| System.out.println("paintPaths: " + NUM_TESTS | |
| + " paths (" + SHAPE_MODE + ") - setup: " + ts); | |
| final boolean fill = !ts.isStroke(); | |
| final Path2D p2d = new Path2D.Double(ts.windingRule); | |
| final Stroke stroke = (!fill) ? createStroke(ts) : null; | |
| final BufferedImage imgOn = newImage(TESTW, TESTH); | |
| final Graphics2D g2dOn = initialize(imgOn, stroke); | |
| final BufferedImage imgOff = newImage(TESTW, TESTH); | |
| final Graphics2D g2dOff = initialize(imgOff, stroke); | |
| final BufferedImage imgDiff = newImage(TESTW, TESTH); | |
| final DiffContext testSetupCtx = new DiffContext("Test setup"); | |
| final DiffContext testWorstCtx = new DiffContext("Worst"); | |
| final DiffContext testWorstThCtx = new DiffContext("Worst(>threshold)"); | |
| int nd = 0; | |
| try { | |
| final DiffContext testCtx = new DiffContext("Test"); | |
| final DiffContext testThCtx = new DiffContext("Test(>threshold)"); | |
| BufferedImage diffImage; | |
| for (int n = 0; n < NUM_TESTS; n++) { | |
| genShape(p2d, ts); | |
| // Runtime clip setting OFF: | |
| paintShape(p2d, g2dOff, fill, false); | |
| // Runtime clip setting ON: | |
| paintShape(p2d, g2dOn, fill, true); | |
| /* compute image difference if possible */ | |
| diffImage = computeDiffImage(testCtx, testThCtx, imgOn, imgOff, imgDiff); | |
| // Worst (total) | |
| if (testCtx.isDiff()) { | |
| if (testWorstCtx.isWorse(testCtx, false)) { | |
| testWorstCtx.set(testCtx); | |
| } | |
| if (testWorstThCtx.isWorse(testCtx, true)) { | |
| testWorstThCtx.set(testCtx); | |
| } | |
| // accumulate data: | |
| testSetupCtx.add(testCtx); | |
| } | |
| if (diffImage != null) { | |
| nd++; | |
| testThCtx.dump(); | |
| testCtx.dump(); | |
| if (nd < MAX_SHOW_FRAMES) { | |
| if (SHOW_DETAILS) { | |
| paintShapeDetails(g2dOff, p2d); | |
| paintShapeDetails(g2dOn, p2d); | |
| } | |
| if (nd < MAX_SAVE_FRAMES) { | |
| if (DUMP_SHAPE) { | |
| dumpShape(p2d); | |
| } | |
| final String testName = "Setup_" + ts.id + "_test_" + n; | |
| saveImage(imgOff, OUTPUT_DIR, testName + "-off.png"); | |
| saveImage(imgOn, OUTPUT_DIR, testName + "-on.png"); | |
| saveImage(imgDiff, OUTPUT_DIR, testName + "-diff.png"); | |
| } | |
| } | |
| } | |
| } | |
| } finally { | |
| g2dOff.dispose(); | |
| g2dOn.dispose(); | |
| if (nd != 0) { | |
| System.out.println("paintPaths: " + NUM_TESTS + " paths - " | |
| + "Number of differences = " + nd | |
| + " ratio = " + (100f * nd) / NUM_TESTS + " %"); | |
| } | |
| if (testWorstCtx.isDiff()) { | |
| testWorstCtx.dump(); | |
| if (testWorstThCtx.isDiff() && testWorstThCtx.histPix.sum != testWorstCtx.histPix.sum) { | |
| testWorstThCtx.dump(); | |
| } | |
| if (allWorstCtx.isWorse(testWorstThCtx, true)) { | |
| allWorstCtx.set(testWorstThCtx); | |
| } | |
| } | |
| testSetupCtx.dump(); | |
| // accumulate data: | |
| allCtx.add(testSetupCtx); | |
| } | |
| System.out.println("paintPaths: duration= " + (1e-6 * (System.nanoTime() - start)) + " ms."); | |
| return nd; | |
| } | |
| private static void paintShape(final Shape p2d, final Graphics2D g2d, | |
| final boolean fill, final boolean clip) { | |
| reset(g2d); | |
| setClip(g2d, clip); | |
| if (fill) { | |
| g2d.fill(p2d); | |
| } else { | |
| g2d.draw(p2d); | |
| } | |
| } | |
| private static Graphics2D initialize(final BufferedImage img, | |
| final Stroke s) { | |
| final Graphics2D g2d = (Graphics2D) img.getGraphics(); | |
| g2d.setRenderingHint(RenderingHints.KEY_RENDERING, | |
| RenderingHints.VALUE_RENDER_QUALITY); | |
| g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, | |
| // Test normalize: | |
| // RenderingHints.VALUE_STROKE_NORMALIZE | |
| RenderingHints.VALUE_STROKE_PURE | |
| ); | |
| if (s != null) { | |
| g2d.setStroke(s); | |
| } | |
| g2d.setColor(Color.BLACK); | |
| return g2d; | |
| } | |
| private static void reset(final Graphics2D g2d) { | |
| // Disable antialiasing: | |
| g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, | |
| RenderingHints.VALUE_ANTIALIAS_OFF); | |
| g2d.setBackground(Color.WHITE); | |
| g2d.clearRect(0, 0, TESTW, TESTH); | |
| } | |
| private static void setClip(final Graphics2D g2d, final boolean clip) { | |
| // Enable antialiasing: | |
| g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, | |
| RenderingHints.VALUE_ANTIALIAS_ON); | |
| // Enable or Disable clipping: | |
| System.setProperty("sun.java2d.renderer.clip.runtime", (clip) ? "true" : "false"); | |
| } | |
| static void genShape(final Path2D p2d, final TestSetup ts) { | |
| p2d.reset(); | |
| /* | |
| Test closed path: | |
| 0: moveTo + (draw)To + closePath | |
| 1: (draw)To + closePath (closePath + (draw)To sequence) | |
| */ | |
| final int end = (ts.closed) ? 2 : 1; | |
| final double[] in = new double[8]; | |
| double sx0 = 0.0, sy0 = 0.0, x0 = 0.0, y0 = 0.0; | |
| for (int p = 0; p < end; p++) { | |
| if (p <= 0) { | |
| x0 = randX(); y0 = randY(); | |
| p2d.moveTo(x0, y0); | |
| sx0 = x0; sy0 = y0; | |
| } | |
| switch (ts.shapeMode) { | |
| case MIXED: | |
| case FIVE_LINE_POLYS: | |
| case NINE_LINE_POLYS: | |
| case FIFTY_LINE_POLYS: | |
| p2d.lineTo(randX(), randY()); | |
| p2d.lineTo(randX(), randY()); | |
| p2d.lineTo(randX(), randY()); | |
| p2d.lineTo(randX(), randY()); | |
| x0 = randX(); y0 = randY(); | |
| p2d.lineTo(x0, y0); | |
| if (ts.shapeMode == ShapeMode.FIVE_LINE_POLYS) { | |
| // And an implicit close makes 5 lines | |
| break; | |
| } | |
| p2d.lineTo(randX(), randY()); | |
| p2d.lineTo(randX(), randY()); | |
| p2d.lineTo(randX(), randY()); | |
| x0 = randX(); y0 = randY(); | |
| p2d.lineTo(x0, y0); | |
| if (ts.shapeMode == ShapeMode.NINE_LINE_POLYS) { | |
| // And an implicit close makes 9 lines | |
| break; | |
| } | |
| if (ts.shapeMode == ShapeMode.FIFTY_LINE_POLYS) { | |
| for (int i = 0; i < 41; i++) { | |
| x0 = randX(); y0 = randY(); | |
| p2d.lineTo(x0, y0); | |
| } | |
| // And an implicit close makes 50 lines | |
| break; | |
| } | |
| case TWO_CUBICS: | |
| if (SUBDIVIDE_CURVE) { | |
| in[0] = x0; in[1] = y0; | |
| in[2] = randX(); in[3] = randY(); | |
| in[4] = randX(); in[5] = randY(); | |
| x0 = randX(); y0 = randY(); | |
| in[6] = x0; in[7] = y0; | |
| subdivide(p2d, 8, in); | |
| in[0] = x0; in[1] = y0; | |
| in[2] = randX(); in[3] = randY(); | |
| in[4] = randX(); in[5] = randY(); | |
| x0 = randX(); y0 = randY(); | |
| in[6] = x0; in[7] = y0; | |
| subdivide(p2d, 8, in); | |
| } else { | |
| x0 = randX(); y0 = randY(); | |
| p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0); | |
| x0 = randX(); y0 = randY(); | |
| p2d.curveTo(randX(), randY(), randX(), randY(), x0, y0); | |
| } | |
| if (ts.shapeMode == ShapeMode.TWO_CUBICS) { | |
| break; | |
| } | |
| case FOUR_QUADS: | |
| if (SUBDIVIDE_CURVE) { | |
| in[0] = x0; in[1] = y0; | |
| in[2] = randX(); in[3] = randY(); | |
| x0 = randX(); y0 = randY(); | |
| in[4] = x0; in[5] = y0; | |
| subdivide(p2d, 6, in); | |
| in[0] = x0; in[1] = y0; | |
| in[2] = randX(); in[3] = randY(); | |
| x0 = randX(); y0 = randY(); | |
| in[4] = x0; in[5] = y0; | |
| subdivide(p2d, 6, in); | |
| in[0] = x0; in[1] = y0; | |
| in[2] = randX(); in[3] = randY(); | |
| x0 = randX(); y0 = randY(); | |
| in[4] = x0; in[5] = y0; | |
| subdivide(p2d, 6, in); | |
| in[0] = x0; in[1] = y0; | |
| in[2] = randX(); in[3] = randY(); | |
| x0 = randX(); y0 = randY(); | |
| in[4] = x0; in[5] = y0; | |
| subdivide(p2d, 6, in); | |
| } else { | |
| x0 = randX(); y0 = randY(); | |
| p2d.quadTo(randX(), randY(), x0, y0); | |
| x0 = randX(); y0 = randY(); | |
| p2d.quadTo(randX(), randY(), x0, y0); | |
| x0 = randX(); y0 = randY(); | |
| p2d.quadTo(randX(), randY(), x0, y0); | |
| x0 = randX(); y0 = randY(); | |
| p2d.quadTo(randX(), randY(), x0, y0); | |
| } | |
| if (ts.shapeMode == ShapeMode.FOUR_QUADS) { | |
| break; | |
| } | |
| default: | |
| } | |
| if (ts.closed) { | |
| p2d.closePath(); | |
| x0 = sx0; y0 = sy0; | |
| } | |
| } | |
| } | |
| static final int SUBDIVIDE_LIMIT = 5; | |
| static final double[][] SUBDIVIDE_CURVES = new double[SUBDIVIDE_LIMIT + 1][]; | |
| static { | |
| for (int i = 0, n = 1; i < SUBDIVIDE_LIMIT; i++, n *= 2) { | |
| SUBDIVIDE_CURVES[i] = new double[8 * n]; | |
| } | |
| } | |
| static void subdivide(final Path2D p2d, final int type, final double[] in) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("subdivide: " + Arrays.toString(Arrays.copyOf(in, type))); | |
| } | |
| double curveLen = ((type == 8) | |
| ? curvelen(in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7]) | |
| : quadlen(in[0], in[1], in[2], in[3], in[4], in[5])); | |
| if (curveLen > SUBDIVIDE_LEN_TH) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("curvelen: " + curveLen); | |
| } | |
| System.arraycopy(in, 0, SUBDIVIDE_CURVES[0], 0, 8); | |
| int level = 0; | |
| while (curveLen >= SUBDIVIDE_LEN_TH) { | |
| level++; | |
| curveLen /= 2.0; | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("curvelen: " + curveLen); | |
| } | |
| } | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("level: " + level); | |
| } | |
| if (level > SUBDIVIDE_LIMIT) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("max level reached : " + level); | |
| } | |
| level = SUBDIVIDE_LIMIT; | |
| } | |
| for (int l = 0; l < level; l++) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("level: " + l); | |
| } | |
| double[] src = SUBDIVIDE_CURVES[l]; | |
| double[] dst = SUBDIVIDE_CURVES[l + 1]; | |
| for (int i = 0, j = 0; i < src.length; i += 8, j += 16) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("subdivide: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type))); | |
| } | |
| if (type == 8) { | |
| CubicCurve2D.subdivide(src, i, dst, j, dst, j + 8); | |
| } else { | |
| QuadCurve2D.subdivide(src, i, dst, j, dst, j + 8); | |
| } | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("left: " + Arrays.toString(Arrays.copyOfRange(dst, j, j + type))); | |
| System.out.println("right: " + Arrays.toString(Arrays.copyOfRange(dst, j + 8, j + 8 + type))); | |
| } | |
| } | |
| } | |
| // Emit curves at last level: | |
| double[] src = SUBDIVIDE_CURVES[level]; | |
| double len = 0.0; | |
| for (int i = 0; i < src.length; i += 8) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("curve: " + Arrays.toString(Arrays.copyOfRange(src, i, i + type))); | |
| } | |
| if (type == 8) { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| len += curvelen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]); | |
| } | |
| p2d.curveTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5], src[i + 6], src[i + 7]); | |
| } else { | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| len += quadlen(src[i + 0], src[i + 1], src[i + 2], src[i + 3], src[i + 4], src[i + 5]); | |
| } | |
| p2d.quadTo(src[i + 2], src[i + 3], src[i + 4], src[i + 5]); | |
| } | |
| } | |
| if (TRACE_SUBDIVIDE_CURVE) { | |
| System.out.println("curveLen (final) = " + len); | |
| } | |
| } else { | |
| if (type == 8) { | |
| p2d.curveTo(in[2], in[3], in[4], in[5], in[6], in[7]); | |
| } else { | |
| p2d.quadTo(in[2], in[3], in[4], in[5]); | |
| } | |
| } | |
| } | |
| static final float POINT_RADIUS = 2f; | |
| static final float LINE_WIDTH = 1f; | |
| static final Stroke OUTLINE_STROKE = new BasicStroke(LINE_WIDTH); | |
| static final int COLOR_ALPHA = 128; | |
| static final Color COLOR_MOVETO = new Color(255, 0, 0, COLOR_ALPHA); | |
| static final Color COLOR_LINETO_ODD = new Color(0, 0, 255, COLOR_ALPHA); | |
| static final Color COLOR_LINETO_EVEN = new Color(0, 255, 0, COLOR_ALPHA); | |
| static final Ellipse2D.Float ELL_POINT = new Ellipse2D.Float(); | |
| private static void paintShapeDetails(final Graphics2D g2d, final Shape shape) { | |
| final Stroke oldStroke = g2d.getStroke(); | |
| final Color oldColor = g2d.getColor(); | |
| setClip(g2d, false); | |
| if (SHOW_OUTLINE) { | |
| g2d.setStroke(OUTLINE_STROKE); | |
| g2d.setColor(COLOR_LINETO_ODD); | |
| g2d.draw(shape); | |
| } | |
| final float[] coords = new float[6]; | |
| float px, py; | |
| int nMove = 0; | |
| int nLine = 0; | |
| int n = 0; | |
| for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { | |
| int type = it.currentSegment(coords); | |
| switch (type) { | |
| case PathIterator.SEG_MOVETO: | |
| if (SHOW_POINTS) { | |
| g2d.setColor(COLOR_MOVETO); | |
| } | |
| break; | |
| case PathIterator.SEG_LINETO: | |
| case PathIterator.SEG_QUADTO: | |
| case PathIterator.SEG_CUBICTO: | |
| if (SHOW_POINTS) { | |
| g2d.setColor((nLine % 2 == 0) ? COLOR_LINETO_ODD : COLOR_LINETO_EVEN); | |
| } | |
| nLine++; | |
| break; | |
| case PathIterator.SEG_CLOSE: | |
| continue; | |
| default: | |
| System.out.println("unsupported segment type= " + type); | |
| continue; | |
| } | |
| px = coords[0]; | |
| py = coords[1]; | |
| if (SHOW_INFO) { | |
| System.out.println("point[" + (n++) + "|seg=" + type + "]: " + px + " " + py); | |
| } | |
| if (SHOW_POINTS) { | |
| ELL_POINT.setFrame(px - POINT_RADIUS, py - POINT_RADIUS, | |
| POINT_RADIUS * 2f, POINT_RADIUS * 2f); | |
| g2d.fill(ELL_POINT); | |
| } | |
| } | |
| if (SHOW_INFO) { | |
| System.out.println("Path moveTo=" + nMove + ", lineTo=" + nLine); | |
| System.out.println("--------------------------------------------------"); | |
| } | |
| g2d.setStroke(oldStroke); | |
| g2d.setColor(oldColor); | |
| } | |
| private static void dumpShape(final Shape shape) { | |
| final float[] coords = new float[6]; | |
| for (final PathIterator it = shape.getPathIterator(null); !it.isDone(); it.next()) { | |
| final int type = it.currentSegment(coords); | |
| switch (type) { | |
| case PathIterator.SEG_MOVETO: | |
| System.out.println("p2d.moveTo(" + coords[0] + ", " + coords[1] + ");"); | |
| break; | |
| case PathIterator.SEG_LINETO: | |
| System.out.println("p2d.lineTo(" + coords[0] + ", " + coords[1] + ");"); | |
| break; | |
| case PathIterator.SEG_QUADTO: | |
| System.out.println("p2d.quadTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ");"); | |
| break; | |
| case PathIterator.SEG_CUBICTO: | |
| System.out.println("p2d.curveTo(" + coords[0] + ", " + coords[1] + ", " + coords[2] + ", " + coords[3] + ", " + coords[4] + ", " + coords[5] + ");"); | |
| break; | |
| case PathIterator.SEG_CLOSE: | |
| System.out.println("p2d.closePath();"); | |
| break; | |
| default: | |
| System.out.println("// Unsupported segment type= " + type); | |
| } | |
| } | |
| System.out.println("--------------------------------------------------"); | |
| } | |
| static double randX() { | |
| return RANDOM.nextDouble() * RANDW + OFFW; | |
| } | |
| static double randY() { | |
| return RANDOM.nextDouble() * RANDH + OFFH; | |
| } | |
| private static BasicStroke createStroke(final TestSetup ts) { | |
| return new BasicStroke(ts.strokeWidth, ts.strokeCap, ts.strokeJoin, 10.0f, ts.dashes, 0.0f); | |
| } | |
| private final static class TestSetup { | |
| static final AtomicInteger COUNT = new AtomicInteger(); | |
| final int id; | |
| final ShapeMode shapeMode; | |
| final boolean closed; | |
| // stroke | |
| final float strokeWidth; | |
| final int strokeCap; | |
| final int strokeJoin; | |
| final float[] dashes; | |
| // fill | |
| final int windingRule; | |
| TestSetup(ShapeMode shapeMode, final boolean closed, | |
| final float strokeWidth, final int strokeCap, final int strokeJoin, final float[] dashes) { | |
| this.id = COUNT.incrementAndGet(); | |
| this.shapeMode = shapeMode; | |
| this.closed = closed; | |
| this.strokeWidth = strokeWidth; | |
| this.strokeCap = strokeCap; | |
| this.strokeJoin = strokeJoin; | |
| this.dashes = dashes; | |
| this.windingRule = Path2D.WIND_NON_ZERO; | |
| } | |
| TestSetup(ShapeMode shapeMode, final boolean closed, final int windingRule) { | |
| this.id = COUNT.incrementAndGet(); | |
| this.shapeMode = shapeMode; | |
| this.closed = closed; | |
| this.strokeWidth = 0f; | |
| this.strokeCap = this.strokeJoin = -1; // invalid | |
| this.dashes = null; | |
| this.windingRule = windingRule; | |
| } | |
| boolean isStroke() { | |
| return this.strokeWidth > 0f; | |
| } | |
| @Override | |
| public String toString() { | |
| if (isStroke()) { | |
| return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed | |
| + ", strokeWidth=" + strokeWidth + ", strokeCap=" + getCap(strokeCap) + ", strokeJoin=" + getJoin(strokeJoin) | |
| + ((dashes != null) ? ", dashes: " + Arrays.toString(dashes) : "") | |
| + '}'; | |
| } | |
| return "TestSetup{id=" + id + ", shapeMode=" + shapeMode + ", closed=" + closed | |
| + ", fill" | |
| + ", windingRule=" + getWindingRule(windingRule) + '}'; | |
| } | |
| private static String getCap(final int cap) { | |
| switch (cap) { | |
| case BasicStroke.CAP_BUTT: | |
| return "CAP_BUTT"; | |
| case BasicStroke.CAP_ROUND: | |
| return "CAP_ROUND"; | |
| case BasicStroke.CAP_SQUARE: | |
| return "CAP_SQUARE"; | |
| default: | |
| return ""; | |
| } | |
| } | |
| private static String getJoin(final int join) { | |
| switch (join) { | |
| case BasicStroke.JOIN_MITER: | |
| return "JOIN_MITER"; | |
| case BasicStroke.JOIN_ROUND: | |
| return "JOIN_ROUND"; | |
| case BasicStroke.JOIN_BEVEL: | |
| return "JOIN_BEVEL"; | |
| default: | |
| return ""; | |
| } | |
| } | |
| private static String getWindingRule(final int rule) { | |
| switch (rule) { | |
| case PathIterator.WIND_EVEN_ODD: | |
| return "WIND_EVEN_ODD"; | |
| case PathIterator.WIND_NON_ZERO: | |
| return "WIND_NON_ZERO"; | |
| default: | |
| return ""; | |
| } | |
| } | |
| } | |
| // --- utilities --- | |
| private static final int DCM_ALPHA_MASK = 0xff000000; | |
| public static BufferedImage newImage(final int w, final int h) { | |
| return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB_PRE); | |
| } | |
| public static BufferedImage computeDiffImage(final DiffContext testCtx, | |
| final DiffContext testThCtx, | |
| final BufferedImage tstImage, | |
| final BufferedImage refImage, | |
| final BufferedImage diffImage) { | |
| final int[] aRefPix = ((DataBufferInt) refImage.getRaster().getDataBuffer()).getData(); | |
| final int[] aTstPix = ((DataBufferInt) tstImage.getRaster().getDataBuffer()).getData(); | |
| final int[] aDifPix = ((DataBufferInt) diffImage.getRaster().getDataBuffer()).getData(); | |
| // reset diff contexts: | |
| testCtx.reset(); | |
| testThCtx.reset(); | |
| int ref, tst, dg, v; | |
| for (int i = 0, len = aRefPix.length; i < len; i++) { | |
| ref = aRefPix[i]; | |
| tst = aTstPix[i]; | |
| // grayscale diff: | |
| dg = (r(ref) + g(ref) + b(ref)) - (r(tst) + g(tst) + b(tst)); | |
| // max difference on grayscale values: | |
| v = (int) Math.ceil(Math.abs(dg / 3.0)); | |
| if (v <= THRESHOLD_DELTA) { | |
| aDifPix[i] = 0; | |
| } else { | |
| aDifPix[i] = toInt(v, v, v); | |
| testThCtx.add(v); | |
| } | |
| if (v != 0) { | |
| testCtx.add(v); | |
| } | |
| } | |
| testCtx.addNbPix(testThCtx.histPix.count); | |
| if (!testThCtx.isDiff() || (testThCtx.histPix.count <= THRESHOLD_NBPIX)) { | |
| return null; | |
| } | |
| return diffImage; | |
| } | |
| static void saveImage(final BufferedImage image, final File resDirectory, final String imageFileName) throws IOException { | |
| final Iterator<ImageWriter> itWriters = ImageIO.getImageWritersByFormatName("PNG"); | |
| if (itWriters.hasNext()) { | |
| final ImageWriter writer = itWriters.next(); | |
| final ImageWriteParam writerParams = writer.getDefaultWriteParam(); | |
| writerParams.setProgressiveMode(ImageWriteParam.MODE_DISABLED); | |
| final File imgFile = new File(resDirectory, imageFileName); | |
| if (!imgFile.exists() || imgFile.canWrite()) { | |
| System.out.println("saveImage: saving image as PNG [" + imgFile + "]..."); | |
| imgFile.delete(); | |
| // disable cache in temporary files: | |
| ImageIO.setUseCache(false); | |
| final long start = System.nanoTime(); | |
| // PNG uses already buffering: | |
| final ImageOutputStream imgOutStream = ImageIO.createImageOutputStream(new FileOutputStream(imgFile)); | |
| writer.setOutput(imgOutStream); | |
| try { | |
| writer.write(null, new IIOImage(image, null, null), writerParams); | |
| } finally { | |
| imgOutStream.close(); | |
| final long time = System.nanoTime() - start; | |
| System.out.println("saveImage: duration= " + (time / 1000000l) + " ms."); | |
| } | |
| } | |
| } | |
| } | |
| static int r(final int v) { | |
| return (v >> 16 & 0xff); | |
| } | |
| static int g(final int v) { | |
| return (v >> 8 & 0xff); | |
| } | |
| static int b(final int v) { | |
| return (v & 0xff); | |
| } | |
| static int clamp127(final int v) { | |
| return (v < 128) ? (v > -127 ? (v + 127) : 0) : 255; | |
| } | |
| static int toInt(final int r, final int g, final int b) { | |
| return DCM_ALPHA_MASK | (r << 16) | (g << 8) | b; | |
| } | |
| /* stats */ | |
| static class StatInteger { | |
| public final String name; | |
| public long count = 0l; | |
| public long sum = 0l; | |
| public long min = Integer.MAX_VALUE; | |
| public long max = Integer.MIN_VALUE; | |
| StatInteger(String name) { | |
| this.name = name; | |
| } | |
| void reset() { | |
| count = 0l; | |
| sum = 0l; | |
| min = Integer.MAX_VALUE; | |
| max = Integer.MIN_VALUE; | |
| } | |
| void add(int val) { | |
| count++; | |
| sum += val; | |
| if (val < min) { | |
| min = val; | |
| } | |
| if (val > max) { | |
| max = val; | |
| } | |
| } | |
| void add(long val) { | |
| count++; | |
| sum += val; | |
| if (val < min) { | |
| min = val; | |
| } | |
| if (val > max) { | |
| max = val; | |
| } | |
| } | |
| void add(StatInteger stat) { | |
| count += stat.count; | |
| sum += stat.sum; | |
| if (stat.min < min) { | |
| min = stat.min; | |
| } | |
| if (stat.max > max) { | |
| max = stat.max; | |
| } | |
| } | |
| public final double average() { | |
| return ((double) sum) / count; | |
| } | |
| @Override | |
| public String toString() { | |
| final StringBuilder sb = new StringBuilder(128); | |
| toString(sb); | |
| return sb.toString(); | |
| } | |
| public final StringBuilder toString(final StringBuilder sb) { | |
| sb.append(name).append("[n: ").append(count); | |
| sb.append("] "); | |
| if (count != 0) { | |
| sb.append("sum: ").append(sum).append(" avg: ").append(trimTo3Digits(average())); | |
| sb.append(" [").append(min).append(" | ").append(max).append("]"); | |
| } | |
| return sb; | |
| } | |
| } | |
| final static class Histogram extends StatInteger { | |
| static final int BUCKET = 2; | |
| static final int MAX = 20; | |
| static final int LAST = MAX - 1; | |
| static final int[] STEPS = new int[MAX]; | |
| static final int BUCKET_TH; | |
| static { | |
| STEPS[0] = 0; | |
| STEPS[1] = 1; | |
| for (int i = 2; i < MAX; i++) { | |
| STEPS[i] = STEPS[i - 1] * BUCKET; | |
| } | |
| // System.out.println("Histogram.STEPS = " + Arrays.toString(STEPS)); | |
| if (THRESHOLD_DELTA % 2 != 0) { | |
| throw new IllegalStateException("THRESHOLD_DELTA must be odd"); | |
| } | |
| BUCKET_TH = bucket(THRESHOLD_DELTA); | |
| } | |
| static int bucket(int val) { | |
| for (int i = 1; i < MAX; i++) { | |
| if (val < STEPS[i]) { | |
| return i - 1; | |
| } | |
| } | |
| return LAST; | |
| } | |
| private final StatInteger[] stats = new StatInteger[MAX]; | |
| public Histogram(String name) { | |
| super(name); | |
| for (int i = 0; i < MAX; i++) { | |
| stats[i] = new StatInteger(String.format("%5s .. %5s", STEPS[i], ((i + 1 < MAX) ? STEPS[i + 1] : "~"))); | |
| } | |
| } | |
| @Override | |
| final void reset() { | |
| super.reset(); | |
| for (int i = 0; i < MAX; i++) { | |
| stats[i].reset(); | |
| } | |
| } | |
| @Override | |
| final void add(int val) { | |
| super.add(val); | |
| stats[bucket(val)].add(val); | |
| } | |
| @Override | |
| final void add(long val) { | |
| add((int) val); | |
| } | |
| void add(Histogram hist) { | |
| super.add(hist); | |
| for (int i = 0; i < MAX; i++) { | |
| stats[i].add(hist.stats[i]); | |
| } | |
| } | |
| boolean isWorse(Histogram hist, boolean useTh) { | |
| boolean worst = false; | |
| if (!useTh && (hist.sum > sum)) { | |
| worst = true; | |
| } else { | |
| long sumLoc = 0l; | |
| long sumHist = 0l; | |
| // use running sum: | |
| for (int i = MAX - 1; i >= BUCKET_TH; i--) { | |
| sumLoc += stats[i].sum; | |
| sumHist += hist.stats[i].sum; | |
| } | |
| if (sumHist > sumLoc) { | |
| worst = true; | |
| } | |
| } | |
| /* | |
| System.out.println("running sum worst:"); | |
| System.out.println("this ? " + toString()); | |
| System.out.println("worst ? " + hist.toString()); | |
| */ | |
| return worst; | |
| } | |
| @Override | |
| public final String toString() { | |
| final StringBuilder sb = new StringBuilder(2048); | |
| super.toString(sb).append(" { "); | |
| for (int i = 0; i < MAX; i++) { | |
| if (stats[i].count != 0l) { | |
| sb.append("\n ").append(stats[i].toString()); | |
| } | |
| } | |
| return sb.append(" }").toString(); | |
| } | |
| } | |
| /** | |
| * Adjust the given double value to keep only 3 decimal digits | |
| * @param value value to adjust | |
| * @return double value with only 3 decimal digits | |
| */ | |
| static double trimTo3Digits(final double value) { | |
| return ((long) (1e3d * value)) / 1e3d; | |
| } | |
| static final class DiffContext { | |
| public final Histogram histPix; | |
| public final StatInteger nbPix; | |
| DiffContext(String name) { | |
| histPix = new Histogram("Diff Pixels [" + name + "]"); | |
| nbPix = new StatInteger("NbPixels [" + name + "]"); | |
| } | |
| void reset() { | |
| histPix.reset(); | |
| nbPix.reset(); | |
| } | |
| void dump() { | |
| if (isDiff()) { | |
| System.out.println("Differences [" + histPix.name + "]:\n" | |
| + ((nbPix.count != 0) ? (nbPix.toString() + "\n") : "") | |
| + histPix.toString() | |
| ); | |
| } else { | |
| System.out.println("No difference for [" + histPix.name + "]."); | |
| } | |
| } | |
| void add(int val) { | |
| histPix.add(val); | |
| } | |
| void add(DiffContext ctx) { | |
| histPix.add(ctx.histPix); | |
| if (ctx.nbPix.count != 0L) { | |
| nbPix.add(ctx.nbPix); | |
| } | |
| } | |
| void addNbPix(long val) { | |
| if (val != 0L) { | |
| nbPix.add(val); | |
| } | |
| } | |
| void set(DiffContext ctx) { | |
| reset(); | |
| add(ctx); | |
| } | |
| boolean isWorse(DiffContext ctx, boolean useTh) { | |
| return histPix.isWorse(ctx.histPix, useTh); | |
| } | |
| boolean isDiff() { | |
| return histPix.sum != 0l; | |
| } | |
| } | |
| static double linelen(final double x0, final double y0, | |
| final double x1, final double y1) | |
| { | |
| final double dx = x1 - x0; | |
| final double dy = y1 - y0; | |
| return Math.sqrt(dx * dx + dy * dy); | |
| } | |
| static double quadlen(final double x0, final double y0, | |
| final double x1, final double y1, | |
| final double x2, final double y2) | |
| { | |
| return (linelen(x0, y0, x1, y1) | |
| + linelen(x1, y1, x2, y2) | |
| + linelen(x0, y0, x2, y2)) / 2.0d; | |
| } | |
| static double curvelen(final double x0, final double y0, | |
| final double x1, final double y1, | |
| final double x2, final double y2, | |
| final double x3, final double y3) | |
| { | |
| return (linelen(x0, y0, x1, y1) | |
| + linelen(x1, y1, x2, y2) | |
| + linelen(x2, y2, x3, y3) | |
| + linelen(x0, y0, x3, y3)) / 2.0d; | |
| } | |
| } |