Exemplo n.º 1
0
    def testSeparableCodePath(self):

        # Make sure that there is no inordinate difference between the special optimized code
        # when rotation is 0 and the general code
        checker1 = GafferImage.Checkerboard()
        checker1["size"].setValue(imath.V2f(35, 121))
        checker1["transform"]["translate"].setValue(
            imath.V2f(214.849503, -199.025101))
        checker1["transform"]["scale"].setValue(
            imath.V2f(7.1413002, 0.640600026))

        checker2 = GafferImage.Checkerboard()
        checker2["size"].setValue(imath.V2f(35, 121))
        checker2["transform"]["translate"].setValue(
            imath.V2f(214.849503, -199.025101))
        checker2["transform"]["scale"].setValue(
            imath.V2f(7.1413002, 0.640600026))
        checker2["transform"]["rotate"].setValue(1e-6)

        diff = GafferImage.Merge()
        diff["in"][0].setInput(checker2["out"])
        diff["in"][1].setInput(checker1["out"])
        diff["operation"].setValue(GafferImage.Merge.Operation.Difference)

        stats = GafferImage.ImageStats()
        stats["in"].setInput(diff["out"])
        stats["area"].setValue(
            imath.Box2i(imath.V2i(0, 0), imath.V2i(1920, 1080)))

        self.assertGreater(stats["max"].getValue()[0],
                           0)  # Should produce some change
        self.assertLess(stats["max"].getValue()[0],
                        1e-4)  # But nothing visible
        self.assertLess(stats["average"].getValue()[0], 1e-6)
Exemplo n.º 2
0
    def testLayerAffectsChannelNames(self):

        c = GafferImage.Checkerboard()
        cs = GafferTest.CapturingSlot(c.plugDirtiedSignal())
        c["layer"].setValue("diffuse")

        self.assertTrue(c["out"]["channelNames"] in set([x[0] for x in cs]))
Exemplo n.º 3
0
    def testFormatDependencies(self):

        c = GafferImage.Checkerboard()

        self.assertEqual(
            c.affects(c["format"]["pixelAspect"]),
            [c["out"]["format"]],
        )

        self.assertEqual(c.affects(c["format"]["displayWindow"]["min"]["x"]),
                         [c["out"]["format"], c["out"]["dataWindow"]])

        self.assertEqual(
            c.affects(c["colorA"]["r"]),
            [c["out"]["channelData"]],
        )

        self.assertEqual(
            c.affects(c["colorB"]["r"]),
            [c["out"]["channelData"]],
        )

        self.assertEqual(
            c.affects(c["size"]["x"]),
            [c["out"]["channelData"]],
        )
Exemplo n.º 4
0
    def mergePerf(self, operation, mismatch):
        r = GafferImage.Checkerboard("Checkerboard")
        r["format"].setValue(GafferImage.Format(4096, 3112, 1.000))
        # Make the size of the checkerboard not a perfect multiple of tile size
        # in case we ever fix Checkerboard to notice when tiles are repeated
        # and return an identical hash ( which would invalidate this performance
        # test )
        r["size"].setValue(imath.V2f(64.01))

        alphaShuffle = GafferImage.Shuffle()
        alphaShuffle["in"].setInput(r["out"])
        alphaShuffle["channels"].addChild(
            GafferImage.Shuffle.ChannelPlug("A", "R"))

        transform = GafferImage.Offset()
        transform["in"].setInput(alphaShuffle["out"])
        if mismatch:
            transform["offset"].setValue(imath.V2i(4000, 3000))
        else:
            transform["offset"].setValue(imath.V2i(26, 42))

        merge = GafferImage.Merge()
        merge["operation"].setValue(operation)
        merge["in"][0].setInput(alphaShuffle["out"])
        merge["in"][1].setInput(transform["out"])

        # Precache upstream network, we're only interested in the performance of Merge
        GafferImageTest.processTiles(alphaShuffle["out"])
        GafferImageTest.processTiles(transform["out"])

        with GafferTest.TestRunner.PerformanceScope():
            GafferImageTest.processTiles(merge["out"])
Exemplo n.º 5
0
    def testGlobalConvenienceMethods(self):

        checker = GafferImage.Checkerboard()

        metadata = GafferImage.ImageMetadata()
        metadata["in"].setInput(checker["out"])
        metadata["metadata"].addChild(Gaffer.NameValuePlug("test", 10))

        self.assertEqual(metadata["out"].format(),
                         metadata["out"]["format"].getValue())
        self.assertEqual(metadata["out"].formatHash(),
                         metadata["out"]["format"].hash())

        self.assertEqual(metadata["out"].dataWindow(),
                         metadata["out"]["dataWindow"].getValue())
        self.assertEqual(metadata["out"].dataWindowHash(),
                         metadata["out"]["dataWindow"].hash())

        self.assertEqual(metadata["out"].channelNames(),
                         metadata["out"]["channelNames"].getValue())
        self.assertFalse(metadata["out"].channelNames().isSame(
            metadata["out"]["channelNames"].getValue(_copy=False)))
        self.assertTrue(metadata["out"].channelNames(_copy=False).isSame(
            metadata["out"]["channelNames"].getValue(_copy=False)))
        self.assertEqual(metadata["out"].channelNamesHash(),
                         metadata["out"]["channelNames"].hash())

        self.assertEqual(metadata["out"].metadata(),
                         metadata["out"]["metadata"].getValue())
        self.assertFalse(metadata["out"].metadata().isSame(
            metadata["out"]["metadata"].getValue(_copy=False)))
        self.assertTrue(metadata["out"].metadata(_copy=False).isSame(
            metadata["out"]["metadata"].getValue(_copy=False)))
        self.assertEqual(metadata["out"].metadataHash(),
                         metadata["out"]["metadata"].hash())
Exemplo n.º 6
0
    def testChannelNamesHash(self):

        c = GafferImage.Checkerboard()
        h1 = c["out"]["channelNames"].hash()
        c["colorA"].setValue(imath.Color4f(1, 0.5, 0.25, 1))
        h2 = c["out"]["channelNames"].hash()

        self.assertEqual(h1, h2)
Exemplo n.º 7
0
    def testEnableBehaviour(self):

        c = GafferImage.Checkerboard()
        self.assertTrue(c.enabledPlug().isSame(c["enabled"]))
        self.assertEqual(c.correspondingInput(c["out"]), None)
        self.assertEqual(c.correspondingInput(c["colorA"]), None)
        self.assertEqual(c.correspondingInput(c["colorB"]), None)
        self.assertEqual(c.correspondingInput(c["size"]), None)
        self.assertEqual(c.correspondingInput(c["format"]), None)
Exemplo n.º 8
0
    def testFormatHash(self):

        # Check that the data hash change when the format does.
        c = GafferImage.Checkerboard()
        c["format"].setValue(GafferImage.Format(2048, 1156, 1.))
        h1 = c["out"].channelData("R", imath.V2i(0)).hash()
        c["format"].setValue(GafferImage.Format(1920, 1080, 1.))
        h2 = c["out"].channelData("R", imath.V2i(0)).hash()
        self.assertEqual(h1, h2)
Exemplo n.º 9
0
    def testSerialisationWithZeroAlpha(self):

        s = Gaffer.ScriptNode()
        s["c"] = GafferImage.Checkerboard()
        s["c"]["colorA"].setValue(imath.Color4f(0, 1, 0, 0))

        s2 = Gaffer.ScriptNode()
        s2.execute(s.serialise())

        self.assertEqual(s2["c"]["colorA"].getValue(),
                         imath.Color4f(0, 1, 0, 0))
Exemplo n.º 10
0
    def testTranslationPerformance(self):

        checker = GafferImage.Checkerboard()
        checker["format"].setValue(GafferImage.Format(3000, 3000))

        transform = GafferImage.ImageTransform()
        transform["in"].setInput(checker["out"])
        transform["transform"]["translate"].setValue(imath.V2f(2.2))

        with GafferTest.TestRunner.PerformanceScope():
            GafferImageTest.processTiles(transform["out"])
Exemplo n.º 11
0
    def testUpsizingPerformance(self):

        checker = GafferImage.Checkerboard()
        checker["format"].setValue(GafferImage.Format(1000, 1000))

        transform = GafferImage.ImageTransform()
        transform["in"].setInput(checker["out"])
        transform["transform"]["scale"].setValue(imath.V2f(3))

        with GafferTest.TestRunner.PerformanceScope():
            GafferImageTest.processTiles(transform["out"])
Exemplo n.º 12
0
    def testRotationAndScalingPerformance(self):

        checker = GafferImage.Checkerboard()
        checker["format"].setValue(GafferImage.Format(3000, 3000))

        transform = GafferImage.ImageTransform()
        transform["in"].setInput(checker["out"])
        transform["transform"]["pivot"].setValue(imath.V2f(1500))
        transform["transform"]["rotate"].setValue(2.5)
        transform["transform"]["scale"].setValue(imath.V2f(0.75))

        with GafferTest.TestRunner.PerformanceScope():
            GafferImageTest.processTiles(transform["out"])
Exemplo n.º 13
0
    def testFilterWidth(self):

        checkerboard = GafferImage.Checkerboard()
        checkerboard["format"].setValue(GafferImage.Format(128, 128))
        checkerboard["size"]["x"].setValue(1)
        checkerboard["size"]["y"].setValue(1)
        checkerboard["transform"]["scale"]["x"].setValue(200)
        checkerboard["transform"]["scale"]["y"].setValue(200)

        sampler = GafferImage.ImageSampler()
        sampler["image"].setInput(checkerboard["out"])
        sampler["pixel"].setValue(imath.V2f(101, 54))
        self.assertAlmostEqual(checkerboard["colorB"].getValue().r,
                               sampler["color"].getValue().r,
                               delta=0.001)
Exemplo n.º 14
0
    def testConcatenationPerformance1(self):

        checker = GafferImage.Checkerboard()
        checker["format"].setValue(GafferImage.Format(3000, 3000))

        transform1 = GafferImage.ImageTransform("Transform1")
        transform1["in"].setInput(checker["out"])
        transform1["transform"]["pivot"].setValue(imath.V2f(1500))
        transform1["transform"]["rotate"].setValue(2.5)

        transform2 = GafferImage.ImageTransform("Transform2")
        transform2["in"].setInput(transform1["out"])
        transform2["transform"]["translate"].setValue(imath.V2f(10))

        with GafferTest.TestRunner.PerformanceScope():
            GafferImageTest.processTiles(transform2["out"])
Exemplo n.º 15
0
    def testMonitorParallelProcessTiles(self):

        numTilesX = 50
        numTilesY = 50

        c = GafferImage.Checkerboard()
        c["format"].setValue(
            GafferImage.Format(
                numTilesX * GafferImage.ImagePlug.tileSize(),
                numTilesY * GafferImage.ImagePlug.tileSize(),
            ))

        with Gaffer.PerformanceMonitor() as m:
            GafferImageTest.processTiles(c["out"])

        self.assertEqual(
            m.plugStatistics(c["out"]["channelData"]).computeCount,
            numTilesX * numTilesY * 4)
Exemplo n.º 16
0
    def testExpectedResult(self):

        checkerboard = GafferImage.Checkerboard()
        checkerboard["format"].setValue(GafferImage.Format(128, 128))
        checkerboard["size"]["x"].setValue(1)
        checkerboard["size"]["y"].setValue(1)
        checkerboard["transform"]["rotate"].setValue(45)
        checkerboard["transform"]["scale"]["x"].setValue(200)
        checkerboard["transform"]["scale"]["y"].setValue(10)

        reader = GafferImage.ImageReader()
        reader["fileName"].setValue(
            os.path.dirname(__file__) + "/images/GafferChecker.exr")

        self.assertImagesEqual(checkerboard["out"],
                               reader["out"],
                               ignoreMetadata=True,
                               maxDifference=0.001)
Exemplo n.º 17
0
    def testChannelData(self):

        checkerboard = GafferImage.Checkerboard()
        checkerboard["format"].setValue(
            GafferImage.Format(imath.Box2i(imath.V2i(0), imath.V2i(511)), 1))
        checkerboard["colorA"].setValue(imath.Color4f(0.1, 0.25, 0.5, 1))

        for i, channel in enumerate(["R", "G", "B", "A"]):
            channelData = checkerboard["out"].channelData(
                channel, imath.V2i(0))
            self.assertEqual(
                len(channelData), checkerboard["out"].tileSize() *
                checkerboard["out"].tileSize())

            expectedValue = checkerboard["colorA"][i].getValue()
            s = GafferImage.Sampler(
                checkerboard["out"], channel,
                checkerboard["out"]["dataWindow"].getValue())
            self.assertEqual(s.sample(12, 12), expectedValue)
            self.assertEqual(s.sample(72, 72), expectedValue)
Exemplo n.º 18
0
    def testValuesIdentical(self):

        checkerboard = GafferImage.Checkerboard()
        checkerboard["format"].setValue(GafferImage.Format(1024, 1024, 1))
        checkerboard["size"].setValue(imath.V2f(14))

        offset = GafferImage.Offset()
        offset["in"].setInput(checkerboard["out"])
        offset["offset"].setValue(imath.V2i(7))

        merge = GafferImage.Merge()
        merge["in"][0].setInput(checkerboard["out"])
        merge["in"][1].setInput(offset["out"])
        merge["operation"].setValue(GafferImage.Merge.Operation.Difference)

        imageStats = GafferImage.ImageStats()
        imageStats["in"].setInput(merge["out"])
        imageStats["area"].setValue(imath.Box2i(imath.V2i(7), imath.V2i(1024)))

        self.assertLess(imageStats["max"][0].getValue(), 2e-5)
        self.assertLess(imageStats["average"][0].getValue(), 5e-7)
Exemplo n.º 19
0
    def testFilterWidth(self):

        checkerboard = GafferImage.Checkerboard()
        checkerboard["format"].setValue(GafferImage.Format(128, 128))
        checkerboard["size"]["x"].setValue(1)
        checkerboard["size"]["y"].setValue(1)
        checkerboard["transform"]["scale"]["x"].setValue(200)
        checkerboard["transform"]["scale"]["y"].setValue(200)

        sampler = GafferImage.ImageSampler()
        sampler["image"].setInput(checkerboard["out"])

        # Pixels on the border could be classified as within the filter width due to floating point precision,
        # and could have floating point error
        sampler["pixel"].setValue(imath.V2f(101, 54))
        self.assertAlmostEqual(checkerboard["colorB"].getValue().r,
                               sampler["color"].getValue().r,
                               delta=0.0000001)

        # Pixels on the interior of a checker must be exact
        sampler["pixel"].setValue(imath.V2f(102, 54))
        self.assertEqual(checkerboard["colorB"].getValue().r,
                         sampler["color"].getValue().r)
Exemplo n.º 20
0
    def testDisabledAndNonConcatenating(self):

        checker = GafferImage.Checkerboard()
        checker["format"].setValue(GafferImage.Format(200, 200))

        t1 = GafferImage.ImageTransform()
        t1["in"].setInput(checker["out"])
        t1["transform"]["translate"]["x"].setValue(10)

        t2 = GafferImage.ImageTransform()
        t2["in"].setInput(t1["out"])
        t2["transform"]["translate"]["x"].setValue(10)

        t3 = GafferImage.ImageTransform()
        t3["in"].setInput(t2["out"])
        t3["transform"]["translate"]["x"].setValue(10)

        self.assertEqual(t3["out"]["dataWindow"].getValue().min().x, 30)

        t2["concatenate"].setValue(False)
        self.assertEqual(t3["out"]["dataWindow"].getValue().min().x, 30)

        t2["enabled"].setValue(False)
        self.assertEqual(t3["out"]["dataWindow"].getValue().min().x, 20)
Exemplo n.º 21
0
def nodeMenuCreateCommand(menu):

    checkerboard = GafferImage.Checkerboard()
    checkerboard["size"].gang()

    return checkerboard
Exemplo n.º 22
0
    def testConcatenation(self):

        # Identical transformation chains, but one
        # with concatenation broken by a Blur node.
        #
        #        checker
        #          |
        #    deleteChannels
        #          /\
        #         /  \
        #       tc1  t1
        #        |    |
        #       tc2  blur
        #             |
        #            t2

        checker = GafferImage.Checkerboard()
        checker["format"].setValue(GafferImage.Format(200, 200))

        deleteChannels = GafferImage.DeleteChannels()
        deleteChannels["in"].setInput(checker["out"])
        deleteChannels["channels"].setValue("A")

        tc1 = GafferImage.ImageTransform()
        tc1["in"].setInput(deleteChannels["out"])
        tc1["filter"].setValue("gaussian")

        tc2 = GafferImage.ImageTransform()
        tc2["in"].setInput(tc1["out"])
        tc2["filter"].setInput(tc1["filter"])

        t1 = GafferImage.ImageTransform()
        t1["in"].setInput(deleteChannels["out"])
        t1["transform"].setInput(tc1["transform"])
        t1["filter"].setInput(tc1["filter"])

        blur = GafferImage.Blur()
        blur["in"].setInput(t1["out"])

        t2 = GafferImage.ImageTransform()
        t2["in"].setInput(blur["out"])
        t2["transform"].setInput(tc2["transform"])
        t2["filter"].setInput(tc1["filter"])

        # The blur doesn't do anything except
        # break concatentation. Check that tc2
        # is practically identical to t2 for
        # a range of transforms.

        for i in range(0, 10):

            random.seed(i)

            translate1 = imath.V2f(random.uniform(-100, 100),
                                   random.uniform(-100, 100))
            rotate1 = random.uniform(-360, 360)
            scale1 = imath.V2f(random.uniform(-2, 2), random.uniform(-2, 2))

            tc1["transform"]["translate"].setValue(translate1)
            tc1["transform"]["rotate"].setValue(rotate1)
            tc1["transform"]["scale"].setValue(scale1)

            translate2 = imath.V2f(random.uniform(-100, 100),
                                   random.uniform(-100, 100))
            rotate2 = random.uniform(-360, 360)
            scale2 = imath.V2f(random.uniform(-2, 2), random.uniform(-2, 2))

            tc2["transform"]["translate"].setValue(translate2)
            tc2["transform"]["rotate"].setValue(rotate2)
            tc2["transform"]["scale"].setValue(scale2)

            # The `maxDifference` here is surprisingly high, but visual checks
            # show that it is legitimate : differences in filtering are that great.
            # The threshold is still significantly lower than the differences between
            # checker tiles, so does guarantee that tiles aren't getting out of alignment.
            self.assertImagesEqual(tc2["out"],
                                   t2["out"],
                                   maxDifference=0.11,
                                   ignoreDataWindow=True)
Exemplo n.º 23
0
    def testPassthroughs(self):

        ts = GafferImage.ImagePlug.tileSize()

        checkerboardB = GafferImage.Checkerboard()
        checkerboardB["format"]["displayWindow"].setValue(
            imath.Box2i(imath.V2i(0), imath.V2i(4096)))

        checkerboardA = GafferImage.Checkerboard()
        checkerboardA["format"]["displayWindow"].setValue(
            imath.Box2i(imath.V2i(0), imath.V2i(4096)))
        checkerboardA["size"].setValue(imath.V2f(5))

        cropB = GafferImage.Crop()
        cropB["in"].setInput(checkerboardB["out"])
        cropB["area"].setValue(
            imath.Box2i(imath.V2i(ts * 0.5), imath.V2i(ts * 4.5)))
        cropB["affectDisplayWindow"].setValue(False)

        cropA = GafferImage.Crop()
        cropA["in"].setInput(checkerboardA["out"])
        cropA["area"].setValue(
            imath.Box2i(imath.V2i(ts * 2.5), imath.V2i(ts * 6.5)))
        cropA["affectDisplayWindow"].setValue(False)

        merge = GafferImage.Merge()
        merge["in"][0].setInput(cropB["out"])
        merge["in"][1].setInput(cropA["out"])
        merge["operation"].setValue(8)

        sampleTileOrigins = {
            "insideBoth": imath.V2i(ts * 3, ts * 3),
            "outsideBoth": imath.V2i(ts * 5, ts),
            "outsideEdgeB": imath.V2i(ts, 0),
            "insideB": imath.V2i(ts, ts),
            "internalEdgeB": imath.V2i(ts * 4, ts),
            "internalEdgeA": imath.V2i(ts * 5, ts * 2),
            "insideA": imath.V2i(ts * 5, ts * 5),
            "outsideEdgeA": imath.V2i(ts * 6, ts * 5)
        }

        for opName, onlyA, onlyB in [("Atop", "black", "passB"),
                                     ("Divide", "operate", "black"),
                                     ("Out", "passA", "black"),
                                     ("Multiply", "black", "black"),
                                     ("Over", "passA", "passB"),
                                     ("Subtract", "passA", "operate"),
                                     ("Difference", "operate", "operate")]:
            op = getattr(GafferImage.Merge.Operation, opName)
            merge["operation"].setValue(op)

            results = {}
            for name, tileOrigin in sampleTileOrigins.items():
                # We want to check the value pass through code independently
                # of the hash passthrough code, which we can do by dropping
                # the value cached and evaluating values first
                Gaffer.ValuePlug.clearCache()

                with Gaffer.Context() as c:
                    c["image:tileOrigin"] = tileOrigin
                    c["image:channelName"] = "R"

                    data = merge["out"]["channelData"].getValue(_copy=False)
                    if data.isSame(
                            GafferImage.ImagePlug.blackTile(_copy=False)):
                        computeMode = "black"
                    elif data.isSame(
                            cropB["out"]["channelData"].getValue(_copy=False)):
                        computeMode = "passB"
                    elif data.isSame(
                            cropA["out"]["channelData"].getValue(_copy=False)):
                        computeMode = "passA"
                    else:
                        computeMode = "operate"

                    h = merge["out"]["channelData"].hash()
                    if h == GafferImage.ImagePlug.blackTile().hash():
                        hashMode = "black"
                    elif h == cropB["out"]["channelData"].hash():
                        hashMode = "passB"
                    elif h == cropA["out"]["channelData"].hash():
                        hashMode = "passA"
                    else:
                        hashMode = "operate"

                    self.assertEqual(hashMode, computeMode)

                    results[name] = hashMode

            self.assertEqual(results["insideBoth"], "operate")
            self.assertEqual(results["outsideBoth"], "black")
            self.assertEqual(results["outsideEdgeB"], onlyB)
            self.assertEqual(results["insideB"], onlyB)
            self.assertEqual(results["outsideEdgeA"], onlyA)
            self.assertEqual(results["insideA"], onlyA)

            if onlyA == "black" or onlyB == "black":
                self.assertEqual(results["internalEdgeB"], onlyB)
                self.assertEqual(results["internalEdgeA"], onlyA)
            else:
                self.assertEqual(results["internalEdgeB"], "operate")
                self.assertEqual(results["internalEdgeA"], "operate")
Exemplo n.º 24
0
    def runBoundaryCorrectness(self, scale):

        testMerge = GafferImage.Merge()
        subImageNodes = []
        for checkSize, col, bound in [
            (2, (0.672299981, 0.672299981, 0), ((11, 7), (61, 57))),
            (4, (0.972599983, 0.493499994, 1), ((9, 5), (59, 55))),
            (6, (0.310799986, 0.843800008, 1), ((0, 21), (1024, 41))),
            (8, (0.958999991, 0.672299981, 0.0296), ((22, 0), (42, 1024))),
            (10, (0.950900018, 0.0899000019, 0.235499993), ((7, 10), (47,
                                                                      50))),
        ]:
            checkerboard = GafferImage.Checkerboard()
            checkerboard["format"].setValue(
                GafferImage.Format(1024 * scale, 1024 * scale, 1.000))
            checkerboard["size"].setValue(imath.V2f(checkSize * scale))
            checkerboard["colorA"].setValue(
                imath.Color4f(0.1 * col[0], 0.1 * col[1], 0.1 * col[2], 0.3))
            checkerboard["colorB"].setValue(
                imath.Color4f(0.5 * col[0], 0.5 * col[1], 0.5 * col[2], 0.7))

            crop = GafferImage.Crop("Crop")
            crop["in"].setInput(checkerboard["out"])
            crop["area"].setValue(
                imath.Box2i(
                    imath.V2i(scale * bound[0][0], scale * bound[0][1]),
                    imath.V2i(scale * bound[1][0], scale * bound[1][1])))
            crop["affectDisplayWindow"].setValue(False)

            subImageNodes.append(checkerboard)
            subImageNodes.append(crop)

            testMerge["in"][-1].setInput(crop["out"])

        testMerge["expression"] = Gaffer.Expression()
        testMerge["expression"].setExpression(
            'parent["operation"] = context[ "loop:index" ]')

        inverseScale = GafferImage.ImageTransform()
        inverseScale["in"].setInput(testMerge["out"])
        inverseScale["filter"].setValue("box")
        inverseScale["transform"]["scale"].setValue(imath.V2f(1.0 / scale))

        crop1 = GafferImage.Crop()
        crop1["in"].setInput(inverseScale["out"])
        crop1["area"].setValue(imath.Box2i(imath.V2i(0, 0), imath.V2i(64, 64)))

        loopInit = GafferImage.Constant()
        loopInit["format"].setValue(GafferImage.Format(896, 64, 1.000))
        loopInit["color"].setValue(imath.Color4f(0))

        loopOffset = GafferImage.Offset()
        loopOffset["in"].setInput(crop1["out"])
        loopOffset["expression"] = Gaffer.Expression()
        loopOffset["expression"].setExpression(
            'parent["offset"]["x"] = 64 * context[ "loop:index" ]')

        loopMerge = GafferImage.Merge()
        loopMerge["in"][1].setInput(loopOffset["out"])

        loop = Gaffer.Loop()
        loop.setup(GafferImage.ImagePlug("in", ))
        loop["iterations"].setValue(14)
        loop["in"].setInput(loopInit["out"])
        loop["next"].setInput(loopMerge["out"])
        loopMerge["in"][0].setInput(loop["previous"])

        # Uncomment for debug
        #imageWriter = GafferImage.ImageWriter( "ImageWriter" )
        #imageWriter["in"].setInput( loop["out"] )
        #imageWriter['openexr']['dataType'].setValue( "float" )
        #imageWriter["fileName"].setValue( "/tmp/mergeBoundaries.exr" )
        #imageWriter.execute()

        reader = GafferImage.ImageReader()
        reader["fileName"].setValue(self.mergeBoundariesRefPath)

        self.assertImagesEqual(loop["out"],
                               reader["out"],
                               ignoreMetadata=True,
                               maxDifference=1e-5 if scale > 1 else 0)