sclass G22CriticalPixelSearch is Steppable { // input settable IG22MasksHolder masksHolder; settable bool keepAllEntries; settable bool sortPixelsFirst = true; // internal int w, h; Iterator pixelIterator; // output new Best bestPixel; Entry bestEntry; Pt lastPixelLookedAt; Entry lastEntry; Entry[] pixelEntries; float[] pixelScores; int nPixelsTested; // stores all subsets of masks found if not null CompactHashSet subSets; *() {} *(IG22MasksHolder *masksHolder) {} class Entry { Pt pixel; // child mask holders for pixel dark or pixel bright //IG22MasksHolder darkChild, brightChild; gettable L darkMasks; gettable L brightMasks; G22GhostImage darkGhost; G22GhostImage brightGhost; simplyCached IG22MasksHolder darkHolder() { ret new G22HashedMasks(darkMasks, darkGhost); } simplyCached IG22MasksHolder brightHolder() { ret new G22HashedMasks(brightMasks, brightGhost); } void calcGhosts { if (l(darkMasks) < l(brightMasks)) { darkGhost = new G22GhostImage(lmapMethod image(darkMasks)); brightGhost = masksHolder().ghost().minus(darkGhost); } else { brightGhost = new G22GhostImage(lmapMethod image(brightMasks)); darkGhost = masksHolder().ghost().minus(brightGhost); } } toString { ret commaCombine( "Split at " + pixel, "dark=" + darkHolder().renderCountAndCertainty(), "bright=" + brightHolder().renderCountAndCertainty()); } } public bool step() { if (w == 0) { if (!masksHolder.canSplit()) false; meta-for w also as h { w = masksHolder.maskSize().w(); } pixelEntries = new Entry[w*h]; pixelScores = new float[w*h]; arrayfill(pixelScores, -1); pixelIterator = makePixelIterator(); } if (!pixelIterator.hasNext()) false; lastPixelLookedAt = pixelIterator.next(); testPixel(lastPixelLookedAt); true; } int idx(Pt p) { ret p.y*w+p.x; } void testPixel(Pt p) { double score; Entry e = null; ++nPixelsTested; // Find out if pixel is the same in all masks float ghostBrightness = masksHolder.ghost().getFloatPixel(p); if (ghostBrightness == 0 || ghostBrightness == 1) // Pixel is determined, not usable for splitting score = -2; else { // Pixel is not determined - let's make the 2 subsets e = new Entry; e.pixel = p; lastEntry = e; if (keepAllEntries) pixelEntries[idx(p)] = e; L masks = wideningListCast(masksHolder.masks()); int n = l(masks); // bit is 1 when pixel is bright byte[] brightBits = byteArrayForNBits(n); // calculate the split for i to n: if (masks.get(i).image().getBoolPixel(p)) setBit(brightBits, i); subSets?.add(new HashedByteArray(brightBits)); var splitMasks = filterAntiFilterByBitSet(masks, brightBits); e.brightMasks = splitMasks.a; e.darkMasks = splitMasks.b; e.calcGhosts(); score = scoreEntry(e); } pixelScores[idx(p)] = (float) score; if (score > 0 && bestPixel.put(p, score)) bestEntry = e; } swappable double scoreEntry(Entry e) { double c1 = e.darkGhost.certainty(); double c2 = e.brightGhost.certainty(); // ret avg(c1, c2); ret min(c1, c2); } double bestScore() { ret bestPixel.score(); } BWImage scoreImage() { double min = masksHolder.certainty(); double max = bestPixel.score(); ret bwImageFromFunction(w, h, (x, y) -> transformToZeroToOne(pixelScores[y*w+x], max, min)); } G22PixelSplitMasks splitMasksHolder() { if (!bestPixel.has()) null; ret new G22PixelSplitMasks() .splitPixel(bestPixel!) .darkHolder(bestEntry.darkHolder()) .brightHolder(bestEntry.brightHolder()); } IG22MasksHolder makeBestTree aka bestTree aka get() { stepAll(this); G22PixelSplitMasks split = splitMasksHolder(); if (split == null) ret masksHolder; split.transformSubHolders(subHolder -> new G22CriticalPixelSearch(subHolder).makeBestTree()); ret split; } void collectSubSets { subSets if null = new CompactHashSet; } Iterator makePixelIterator() { if (sortPixelsFirst) // try least certain pixels first ret iterator(pixelsByBrightness(masksHolder().ghost().certaintyImage())); else ret new WeightlessShuffledIterator(pixelsInImage(w, h)); } }