#include "toonz/Naa2TlvConverter.h" #include "toonz/tcenterlinevectorizer.h" #include "toonz/stage.h" #include "tpixelutils.h" #include "tpalette.h" #include #include #include #include QString RegionInfo::getTypeString() const { switch (type) { case RegionInfo::Background: return "Background"; case RegionInfo::Ink: return "Ink"; case RegionInfo::ThinInk: return "ThinInk"; case RegionInfo::MainInk: return "Main ink"; case RegionInfo::SyntheticInk: return "SyntheticInk"; case RegionInfo::Paint: return "Paint"; case RegionInfo::LargePaint: return "LargePaint"; case RegionInfo::SmallPaint: return "SmallPaint"; case RegionInfo::Unused: return "Unused"; case RegionInfo::Unknown: return "Unknown"; default: return "??????"; } } Naa2TlvConverter::Naa2TlvConverter() : MaxColorCount(1000) , m_regionRas(0) , m_borderRas(0) , m_dotRas(0) , m_syntheticInkRas(0) , m_inkThickness(0) , m_palette(0) , m_valid(false) {} Naa2TlvConverter::~Naa2TlvConverter() { delete m_regionRas; delete m_borderRas; delete m_dotRas; delete m_syntheticInkRas; if (m_palette) m_palette->release(); } void Naa2TlvConverter::setPalette(TPalette *palette) { if (m_palette != palette) { if (palette) palette->addRef(); if (m_palette) m_palette->release(); m_palette = palette; } } void Naa2TlvConverter::process(const TRaster32P &srcRas) { m_valid = false; setSourceImage(srcRas); if (!m_valid) return; separateRegions(); computeLinks(); findRegionBorders(); erodeRegions(); findLargePaints(); findBackgroundRegions(); findMainInks(); // computeMainInkThickness(); findThinInks(); measureThickness(); // findPaints(); findPaints2(); findThinPaints(); findSuspectInks(); addBorderInks(); } //----------------------------------------------------------------------------- // Input: srcRas (full color image) // Output: // m_regionRas : pixel => region index // m_regions : region index => RegionInfo // // Note: delete m_borderRas void Naa2TlvConverter::setSourceImage(const TRaster32P &srcRas) { int lx = srcRas->getSize().lx; int ly = srcRas->getSize().ly; m_colors.clear(); m_regions.clear(); delete m_regionRas; m_regionRas = new WorkRaster(lx, ly); delete m_borderRas; m_borderRas = 0; delete m_dotRas; m_dotRas = 0; delete m_syntheticInkRas; m_syntheticInkRas = 0; QMap colorTable; for (int y = 0; y < ly; y++) { TPixel32 *srcScanLine = srcRas->pixels(y); unsigned short *regionScanLine = m_regionRas->pixels(y); for (int x = 0; x < lx; x++) { TPixel32 srcPix = overPixOnWhite(srcScanLine[x]); QMap::ConstIterator it = colorTable.find(srcPix); if (it == colorTable.end()) { // found new color (and therefore new region) RegionInfo r; // add new color r.colorIndex = m_colors.count(); m_colors.append(srcPix); r.pixelCount = 1; // add new region int regionIndex = m_regions.count(); m_regions.append(r); // update raster and colorTable regionScanLine[x] = regionIndex; colorTable.insert(srcPix, regionIndex); if (m_colors.count() > MaxColorCount) { return; } } else { // already defined color int regionIndex = it.value(); regionScanLine[x] = regionIndex; m_regions[regionIndex].pixelCount++; } } } m_valid = true; } //----------------------------------------------------------------------------- // color-based regions => connection-based regions (i.e. split unconnected // region parts) // note: rebuild m_regions void Naa2TlvConverter::separateRegions() { if (!m_regionRas) return; const int lx = m_regionRas->getLx(); const int ly = m_regionRas->getLy(); const int wrap = lx; // we assume that m_regions contains almost no information (except: // m_regions[i].colorIndex == i) m_regions.clear(); WorkRaster wb(lx, ly); // work buffer QVector regionMap; // wb pixels => region indices QList freeRegions; for (int y = 0; y < ly; y++) { unsigned short *waScanLine = m_regionRas->pixels(y); int *wbScanLine = wb.pixels(y); for (int x = 0; x < lx; x++) { int c = waScanLine[x]; int cUp = y > 0 ? waScanLine[x - wrap] : -1; int cLeft = x > 0 ? waScanLine[x - 1] : -1; int cUpLeft = x > 0 && y > 0 ? waScanLine[x - wrap - 1] : -1; if (c != cUp && c != cLeft && c != cUpLeft) { // no connection: create a new region Q_ASSERT(0 <= c && c < m_colors.count()); RegionInfo region; region.colorIndex = c; int regionIndex; if (!freeRegions.empty()) { // use a previously discarded region regionIndex = freeRegions.back(); freeRegions.pop_back(); m_regions[regionIndex] = region; } else { // create a new region regionIndex = m_regions.count(); m_regions.append(region); } wbScanLine[x] = regionMap.count(); regionMap.append(regionIndex); } else { // at least one connection if (c == cUpLeft) wbScanLine[x] = wbScanLine[x - wrap - 1]; else if (c == cUp) wbScanLine[x] = wbScanLine[x - wrap]; else { Q_ASSERT(c == cLeft); wbScanLine[x] = wbScanLine[x - 1]; } } // merge if needed if (cUp == cLeft && cUp >= 0 && regionMap[wbScanLine[x - 1]] != regionMap[wbScanLine[x - wrap]]) { // merge int pixelToDiscard = wbScanLine[x - 1]; int pixelToKeep = wbScanLine[x - wrap]; Q_ASSERT(pixelToDiscard != pixelToKeep); int regionToDiscard = regionMap[pixelToDiscard]; int regionToKeep = regionMap[pixelToKeep]; Q_ASSERT(regionToDiscard != regionToKeep); Q_ASSERT(m_regions[regionToDiscard].type != RegionInfo::Unused); Q_ASSERT(m_regions[regionToKeep].type != RegionInfo::Unused); for (int i = 0; i < regionMap.count(); i++) if (regionMap[i] == regionToDiscard) regionMap[i] = regionToKeep; // wbScanLine[x] = pixelToKeep; m_regions[regionToDiscard].type = RegionInfo::Unused; freeRegions.append(regionToDiscard); } } } // fill gaps for (;;) { // just remove topmost Unused regions if (!m_regions.empty() && m_regions.back().type == RegionInfo::Unused) { m_regions.pop_back(); int k = m_regions.count(); freeRegions.removeAll(k); } else if (!freeRegions.empty()) { // last region is used, but there is at least a free region with lower // index: move it there int regionToDiscard = m_regions.count() - 1; int regionToKeep = freeRegions.back(); freeRegions.pop_back(); Q_ASSERT(m_regions[regionToKeep].type == RegionInfo::Unused); Q_ASSERT(m_regions[regionToDiscard].type != RegionInfo::Unused); Q_ASSERT(regionToKeep < regionToDiscard); m_regions[regionToKeep] = m_regions[regionToDiscard]; // pop the region to discard m_regions.pop_back(); // update map for (int i = 0; i < regionMap.count(); i++) if (regionMap[i] == regionToDiscard) regionMap[i] = regionToKeep; } else break; } Q_ASSERT(freeRegions.empty()); for (int i = 0; i < m_regions.count(); i++) m_regions[i].pixelCount = 0; // wb => m_regionRas for (int y = 0; y < ly; y++) { unsigned short *regionScanLine = m_regionRas->pixels(y); int *wbScanLine = wb.pixels(y); for (int x = 0; x < lx; x++) { int regionIndex = regionMap[wbScanLine[x]]; regionScanLine[x] = (unsigned short)regionIndex; m_regions[regionIndex].pixelCount++; } } } //----------------------------------------------------------------------------- // compute adjacency relationships // update: m_regions[].links // outer regions are connected to the "pseudo-region" -1 // note: check orthogonal directions only // compute region.perimeter void Naa2TlvConverter::computeLinks() { if (!m_regionRas || m_regions.empty()) return; const int lx = m_regionRas->getLx(); const int ly = m_regionRas->getLy(); const int wrap = lx; unsigned short *waScanLine; int x; for (int i = 0; i < m_regions.count(); i++) m_regions[i].perimeter = 0; for (int y = 0; y < ly; y++) { waScanLine = m_regionRas->pixels(y); for (x = 0; x < lx; x++) { int c = waScanLine[x]; int cUp = y > 0 ? waScanLine[x - wrap] : -1; int cLeft = x > 0 ? waScanLine[x - 1] : -1; if (c != cUp) { m_regions[c].touchRegion(cUp); m_regions[c].perimeter++; if (cUp >= 0) { m_regions[cUp].touchRegion(c); m_regions[cUp].perimeter++; } } if (c != cLeft) { m_regions[c].touchRegion(cLeft); m_regions[c].perimeter++; if (cLeft >= 0) { m_regions[cLeft].touchRegion(c); m_regions[cLeft].perimeter++; } } } m_regions[waScanLine[lx - 1]].touchRegion(-1); } waScanLine = m_regionRas->pixels(ly - 1); for (x = 0; x < lx; x++) { m_regions[waScanLine[x]].touchRegion(-1); } } //----------------------------------------------------------------------------- // find background regions : (almost) white // void Naa2TlvConverter::findBackgroundRegions() { if (!m_regionRas || m_regions.empty()) return; int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); // find the whitest color int bgColorIndex = -1; int maxV = 0; for (int i = 0; i < m_colors.count(); i++) { TPixel color = m_colors.at(i); int v = color.r + color.g + color.b; int a = qMin(color.r, qMin(color.g, color.b)); if (a < 230) continue; if (v > maxV) { bgColorIndex = i; maxV = v; } } if (bgColorIndex < 0) { // no white found. no bg region return; } for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.colorIndex == bgColorIndex) region.type = RegionInfo::Background; } } //----------------------------------------------------------------------------- // start computing m_borderRas : 1 for boundary pixels, 0 for internal pixels void Naa2TlvConverter::findRegionBorders() { if (!m_regionRas) return; int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); delete m_borderRas; m_borderRas = new WorkRaster(lx, ly); static const int dd[][2] = {{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}}; // find region-region boundaries for (int y = 0; y < ly; y++) { unsigned short *workScanLine = m_regionRas->pixels(y); unsigned char *borderScanLine = m_borderRas->pixels(y); for (int x = 0; x < lx; x++) { int k = workScanLine[x]; Q_ASSERT(0 <= k && k < m_regions.count()); int isBoundary = 0; for (int j = 0; j < 8; j++) { int x1 = x + dd[j][0], y1 = y + dd[j][1]; if (0 <= x1 && x1 < lx && 0 <= y1 && y1 < ly) { int k1 = m_regionRas->pixels(y1)[x1]; if (k1 != k) { isBoundary = 1; break; } } } borderScanLine[x] = isBoundary; } } } //----------------------------------------------------------------------------- // update m_borders, adding 2,3... for internal pixels close to boundary // update m_regions[i].boundaries (histogram : boundaries[k] is the number of // pixels belonging to that region with m_border[pix] == k) void Naa2TlvConverter::erodeRegions() { QTime clock; clock.start(); if (!m_regionRas || !m_borderRas) return; int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); static const int dd[][2] = {{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}}; for (int iter = 0; iter < 10; iter++) { for (int y = 0; y < ly; y++) { unsigned short *workScanLine = m_regionRas->pixels(y); unsigned char *borderScanLine = m_borderRas->pixels(y); for (int x = 0; x < lx; x++) { if (borderScanLine[x] != iter + 1) continue; int c = workScanLine[x]; for (int j = 0; j < 8; j++) { int x1 = x + dd[j][0], y1 = y + dd[j][1]; if (!(0 <= x1 && x1 < lx && 0 <= y1 && y1 < ly)) continue; int c1 = m_regionRas->pixels(y1)[x1]; int b1 = m_borderRas->pixels(y1)[x1]; if (c1 == c && b1 == 0) m_borderRas->pixels(y1)[x1] = iter + 2; } } } } for (int i = 0; i < m_regions.count(); i++) m_regions[i].boundaries.clear(); for (int y = 0; y < ly; y++) { unsigned short *workScanLine = m_regionRas->pixels(y); unsigned char *borderScanLine = m_borderRas->pixels(y); for (int x = 0; x < lx; x++) { int c = workScanLine[x]; int b = borderScanLine[x]; RegionInfo &r = m_regions[c]; QList &bs = r.boundaries; while (bs.count() <= b) bs.append(0); bs[b]++; if (b == bs.count() - 1) r.pos = QPoint(x, y); if (r.x0 > r.x1) { r.x0 = r.x1 = x; r.y0 = r.y1 = y; } else { if (x < r.x0) r.x0 = x; else if (x > r.x1) r.x1 = x; if (y < r.y0) r.y0 = y; else if (y > r.y1) r.y1 = y; } } } qDebug() << "Erode regions. time = " << clock.elapsed(); } //----------------------------------------------------------------------------- // main inks touch background regions and don't have large interior void Naa2TlvConverter::findMainInks() { for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; if (region.isBackground() || region.type == RegionInfo::LargePaint || region.boundaries[0] > 0) continue; double ap2 = 100000.0 * (double)region.pixelCount / pow((double)region.perimeter, 2); if (ap2 > 100) continue; foreach (int c, region.links.keys()) { if (c >= 0 && (m_regions[c].isBackground() || m_regions[c].type == RegionInfo::LargePaint)) { int strength = region.links[c]; if (strength > 50) { m_regions[i].type = RegionInfo::MainInk; break; } } } } } //----------------------------------------------------------------------------- // compute the average thickness of the main ink region void Naa2TlvConverter::computeMainInkThickness() { m_inkThickness = 0.0; int largestArea = 0; for (int i = 0; i < m_regions.count(); i++) { if (m_regions[i].type != RegionInfo::MainInk) continue; if (m_regions[i].pixelCount < largestArea) continue; largestArea = m_regions[i].pixelCount; if (i == 55) { int x = 123; } QList &bs = m_regions[i].boundaries; Q_ASSERT(bs[1] > 0); int perimeter = m_regions[i].perimeter; int area = bs[1]; for (int j = 2; j < bs.count() && bs[j] * 2 > bs[1]; j++) area += bs[j]; if (perimeter > 0) { m_inkThickness = 2 * (double)area / (double)perimeter; } } } //----------------------------------------------------------------------------- void Naa2TlvConverter::findLargePaints() { if (!m_regionRas || !m_borderRas || m_regions.empty()) return; QSet largePaintColors; for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; QList &bs = region.boundaries; if (bs[0] > 0) { region.type = RegionInfo::LargePaint; largePaintColors.insert(region.colorIndex); } } for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; if (largePaintColors.contains(region.colorIndex)) region.type = RegionInfo::LargePaint; } } //----------------------------------------------------------------------------- void Naa2TlvConverter::findThinInks() { if (!m_regionRas || !m_borderRas || m_regions.empty()) return; for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; QList &bs = region.boundaries; if (bs.count() == 2) region.type = RegionInfo::ThinInk; else if (bs.count() == 3) { continue; if (bs[2] * 5 < bs[1]) region.type = RegionInfo::ThinInk; } else { continue; int k = 1; int s = 0; while (k < bs.count() && s * 100 < region.pixelCount * 90) s += bs[k++]; if (region.pixelCount > 100 && k <= 3) region.type = RegionInfo::ThinInk; // era Ink per qualche ragione } } } //----------------------------------------------------------------------------- void Naa2TlvConverter::findPaints() { for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; foreach (int c, m_regions[i].links.keys()) { if (c >= 0 && m_regions[c].isInk()) { m_regions[i].type = RegionInfo::Paint; break; } } } } //----------------------------------------------------------------------------- void Naa2TlvConverter::findPaints2() { double avThickness = 0.0; int m = 0; for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type == RegionInfo::MainInk) { avThickness += region.thickness * region.pixelCount; m += region.pixelCount; } } if (m > 0) { avThickness = avThickness / m; } else { avThickness = 1.5; // this should never happen } for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; if (region.thickness > 0.0) { if (region.thickness < 1.2 * avThickness) region.type = RegionInfo::Ink; else region.type = RegionInfo::Paint; } } } //----------------------------------------------------------------------------- void Naa2TlvConverter::findThinPaints() { int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); QList regions; for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (!region.isInk()) continue; if (region.type == RegionInfo::MainInk) continue; int inkBoundary = 0; QMap::ConstIterator it; for (it = region.links.begin(); it != region.links.end(); ++it) { int c = it.key(); int linkStrength = it.value(); // number of contact points if (c >= 0 && m_regions[c].isInk()) inkBoundary += linkStrength; } region.inkBoundary = inkBoundary; if (inkBoundary * 100 > region.perimeter * 80) regions.append(i); } foreach (int c, regions) m_regions[c].type = RegionInfo::SmallPaint; } //----------------------------------------------------------------------------- void Naa2TlvConverter::findSuspectInks() { QMultiMap paintColorTable; for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (0 == (region.type & RegionInfo::Paint)) continue; TPixel32 color = m_colors[region.colorIndex]; if (color == TPixel32(0, 0, 0)) { int x = 1234; continue; } TUINT32 rawColor = *(TUINT32 *)&color; paintColorTable.insert(rawColor, i); } int count = 0; for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.isInk() && region.links.size() == 2) { int ra = region.links.keys().at(0), rb = region.links.keys().at(1); if (ra >= 0 && rb >= 0) { if (!m_regions[ra].isInk()) qSwap(ra, rb); if (m_regions[ra].isInk() && !m_regions[rb].isInk()) { int sa = region.links[ra]; int sb = region.links[rb]; if (sa > sb) { region.type = RegionInfo::Paint; count++; continue; } } } } if (region.type != RegionInfo::ThinInk) continue; TUINT32 rawColor = *(TUINT32 *)&m_colors[region.colorIndex]; if (paintColorTable.contains(rawColor)) { region.type = RegionInfo::Unknown; count++; } } for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.isInk() && 10 <= region.pixelCount && region.pixelCount < 100) { int lx = region.x1 - region.x0 + 1; int ly = region.y1 - region.y0 + 1; int d = qMax(lx, ly); if (qMin(lx, ly) * 2 > qMax(lx, ly) && region.pixelCount > d * d / 2) { region.type = RegionInfo::Paint; } } if (region.type == RegionInfo::Paint || region.type == RegionInfo::Unknown) { bool isInk = false; if (region.boundaries.at(0) == 0) { if (region.boundaries.count() == 2) isInk = true; else if (region.boundaries.count() == 3) { int b1 = region.boundaries.at(1); int b2 = region.boundaries.at(2); if (b1 * 2 < b2) isInk = true; } } if (isInk) region.type = RegionInfo::Ink; } } } //----------------------------------------------------------------------------- // void Naa2TlvConverter::assignColorTypes() { if (!m_regionRas || !m_borderRas || m_regions.empty()) return; int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; if (region.type != RegionInfo::Unknown) continue; QList &bs = region.boundaries; if (bs[0] > 0) { region.type = RegionInfo::LargePaint; } else { int b = 0; for (int j = 1; j <= 2 && j < bs.count(); j++) b += bs[j]; if (region.pixelCount > 200 && (region.pixelCount - b) * 10 < region.pixelCount) { region.type = RegionInfo::ThinInk; } } } } //----------------------------------------------------------------------------- void Naa2TlvConverter::addBorderInks() // add syntethic inks: lines between two // adjacent fill-regions { int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); static const int dd[][2] = {{0, -1}, {-1, 0}, {1, 0}, {0, 1}, {1, 1}, {-1, -1}, {-1, 1}, {1, -1}}; m_syntheticInkRas = new WorkRaster(lx, ly); for (int i = 0; i < lx * ly; i++) m_syntheticInkRas->pixels(0)[i] = 0; // calculate brightness of all colors in order to decide the region on which // the border to be added QList colorsBrightness; QVector::iterator c; for (c = m_colors.begin(); c != m_colors.end(); ++c) colorsBrightness.append((int)(*c).r * 30 + (int)(*c).g * 59 + (int)(*c).b * 11); int borderInkColorIndex = m_colors.count(); m_colors.append(TPixel32(255, 0, 0)); RegionInfo borderInkRegion; borderInkRegion.type = RegionInfo::SyntheticInk; borderInkRegion.colorIndex = borderInkColorIndex; int borderInkRegionIndex = m_regions.count(); m_regions.append(borderInkRegion); for (int y = 0; y < ly; y++) { unsigned short *workScanLine = m_regionRas->pixels(y); unsigned char *borderScanLine = m_borderRas->pixels(y); for (int x = 0; x < lx; x++) { int c = workScanLine[x]; if (borderScanLine[x] != 1) continue; // consider border pixel only if (0 == (m_regions[c].type & RegionInfo::Paint) && m_regions[c].type != RegionInfo::Unknown) continue; // consider paint pixels only // is touching a different no-ink pixel? bool touchesOtherRegion = false; for (int j = 0; j < 8; j++) { int x1 = x + dd[j][0], y1 = y + dd[j][1]; if (0 <= x1 && x1 < lx && 0 <= y1 && y1 < ly) { int c1 = m_regionRas->pixels(y1)[x1]; if (m_regions[c1].type == RegionInfo::Background) { touchesOtherRegion = true; break; } else if (m_regions[c1].type == RegionInfo::Unknown || (m_regions[c1].type & RegionInfo::Paint) != 0) { // OLD: note: we consider only regions with a lower index, to avoid // to create double border strokes // NEW: we put syntetic ink pixels in larger regions // UPDATE 2017/5/12 : we put ink on darker style if (colorsBrightness[m_regions[c1].colorIndex] > colorsBrightness[m_regions[c].colorIndex]) { touchesOtherRegion = true; break; } } } } if (touchesOtherRegion) { m_syntheticInkRas->pixels(y)[x] = 1; } } } } //----------------------------------------------------------------------------- void Naa2TlvConverter::measureThickness() { QTime timer; timer.start(); if (!m_regionRas || !m_borderRas) return; unsigned short *regionBuffer = m_regionRas->pixels(); unsigned char *borderBuffer = m_borderRas->pixels(); int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); if (!m_dotRas || m_dotRas->getLx() != lx || m_dotRas->getLy() != ly) { delete m_dotRas; m_dotRas = new WorkRaster(lx, ly); } memset(m_dotRas->pixels(), 0, lx * ly); // for(int i=0;ipixels()[i]=0; unsigned char *dotBuffer = m_dotRas->pixels(); for (int y = 0; y < ly; y++) { for (int x = 0; x < lx; x++) { if (borderBuffer[y * lx + x] != 1) continue; if (dotBuffer[y * lx + x] != 0) continue; int regionId = regionBuffer[y * lx + x]; RegionInfo ®ion = m_regions[regionId]; int type = region.type; if (type == RegionInfo::Background || type == RegionInfo::LargePaint || type == RegionInfo::ThinInk) continue; int thickness = measureThickness(x, y); if (thickness < 1) continue; QMap::Iterator it = region.thicknessHistogram.find(thickness); if (it != region.thicknessHistogram.end()) it.value() += 1; else region.thicknessHistogram.insert(thickness, 1); } } for (int i = 0; i < m_regions.count(); i++) { RegionInfo ®ion = m_regions[i]; int type = region.type; if (type == RegionInfo::Background || type == RegionInfo::LargePaint || type == RegionInfo::ThinInk) continue; int thicknessCount = 0; double thickness = 0.0; for (QMap::Iterator it = region.thicknessHistogram.begin(); it != region.thicknessHistogram.end(); ++it) { thicknessCount += it.value(); thickness += it.key() * it.value(); } if (thicknessCount > 0) { thickness *= 1.0 / thicknessCount; region.thickness = thickness; } } qDebug() << "measure thickness. time=" << timer.elapsed(); } //----------------------------------------------------------------------------- int Naa2TlvConverter::measureThickness(int x0, int y0) { if (!m_regionRas || !m_borderRas || !m_dotRas) return -1; unsigned short *regionBuffer = m_regionRas->pixels(); unsigned char *borderBuffer = m_borderRas->pixels(); unsigned char *dotBuffer = m_dotRas->pixels(); int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); int k0 = lx * y0 + x0; // no needs to worry about image limits if (x0 - 1 < 0 || x0 + 1 >= lx || y0 - 1 < 0 || y0 + 1 >= ly) return -1; // we must start from a border if (borderBuffer[k0] != 1) return -1; // far from other measures if (dotBuffer[k0] != 0) return -1; int regionId = regionBuffer[k0]; RegionInfo &info = m_regions[regionId]; // directions const int dd[8] = {1, lx + 1, lx, lx - 1, -1, -1 - lx, -lx, 1 - lx}; // a is a direction index; a : inside; a+1 : outside int a = 0; while (a < 8 && !(regionBuffer[k0 + dd[a]] == regionId && regionBuffer[k0 + dd[(a + 1) % 8]] != regionId)) a++; if (a == 8) { // k0 is an isolated point or (strange!) an intern point qDebug() << "Isolated point or intern point"; return -1; } int ka = k0 + dd[a]; int b = (a + 2) % 8; while (regionBuffer[k0 + dd[b]] != regionId) b = (b + 1) % 8; // a..b = boundaries int kb = k0 + dd[b]; if (a == b || ((b + 1) % 8) == a) return -1; // k0 is a corner dotBuffer[k0] = 1; dotBuffer[ka] = 2; dotBuffer[kb] = 3; int c = (b + 1) % 8; bool isThin = false; while (c != a) { if (regionBuffer[k0 + dd[c]] != regionId) { isThin = true; break; } c = (c + 1) % 8; } if (isThin) { // stroke larga un pixel, con paint da una parte e dall'altra. return 1; } int baseLength = 3; int d = b; int oldk = k0; int k = kb; int lastd2 = 0; for (int i = 0; i < baseLength; i++) { int x = k % lx; int y = k / lx; if (x <= 1 || x >= lx - 1 || y <= 1 || y >= ly - 1) break; // just to be sure int d2 = (x - x0) * (x - x0) + (y - y0) * (y - y0); if (d2 <= lastd2) break; lastd2 = d2; int d1 = (d + 4) % 8; d1 = (d1 + 1) % 8; while (regionBuffer[k + dd[d1]] != regionId) d1 = (d1 + 1) % 8; Q_ASSERT(regionBuffer[k + dd[d1]] == regionId); oldk = k; k = k + dd[d1]; d = d1; dotBuffer[k] = 4; } // punto ad un estremo (lungo il confine) int x1 = k % lx; int y1 = k / lx; d = a; oldk = k0; k = ka; lastd2 = 0; for (int i = 0; i < baseLength; i++) { int x = k % lx; int y = k / lx; if (x <= 1 || x >= lx - 1 || y <= 1 || y >= ly - 1) break; // just to be sure int d2 = (x - x0) * (x - x0) + (y - y0) * (y - y0); if (d2 <= lastd2) break; lastd2 = d2; int d1 = (d + 4) % 8; d1 = (d1 + 7) % 8; while (regionBuffer[k + dd[d1]] != regionId) d1 = (d1 + 7) % 8; Q_ASSERT(regionBuffer[k + dd[d1]] == regionId); oldk = k; k = k + dd[d1]; d = d1; dotBuffer[k] = 5; } // punto all'estremo opposto (lungo il confine) int x2 = k % lx; int y2 = k / lx; int dx = y2 - y1, dy = x1 - x2; int delta2 = dx * dx + dy * dy; if (delta2 < baseLength * baseLength * 3) return -1; int adx = abs(dx), ady = abs(dy); int sgx = dx > 0 ? 1 : -1, sgy = dy > 0 ? 1 : -1; int thickness = 1; const int maxThickness = 64; for (; thickness < maxThickness; thickness++) { int x, y; if (adx > ady) { x = x0 + thickness * sgx; y = y0 + sgy * (thickness * ady + adx / 2) / adx; } else { y = y0 + thickness * sgy; x = x0 + sgx * (thickness * adx + ady / 2) / ady; } k = y * lx + x; if (0 <= x && x < lx && 0 <= y && y < ly && regionBuffer[k] == regionId) dotBuffer[k] = 6; else break; } return thickness; } //----------------------------------------------------------------------------- TToonzImageP Naa2TlvConverter::makeTlv(bool transparentSyntheticInks, QList &usedStyleIds, double dpi) { if (!m_valid || m_colors.empty() || m_regions.empty() || !m_regionRas) return TToonzImageP(); int lx = m_regionRas->getLx(); int ly = m_regionRas->getLy(); TPalette *palette = m_palette; if (!palette) palette = new TPalette(); TRasterCM32P ras(lx, ly); QList styleIds; for (int i = 0; i < m_colors.count() - 1; i++) { TPixel32 color = m_colors.at(i); int styleId = palette->getClosestStyle(color); TColorStyle *cs = styleId < 0 ? 0 : palette->getStyle(styleId); if (cs) { if (cs->getMainColor() != color) cs = 0; } if (cs == 0) { styleId = palette->addStyle(color); palette->getPage(0)->addStyle(styleId); cs = palette->getStyle(styleId); } styleIds.append(styleId); if (!usedStyleIds.contains(styleId)) usedStyleIds.append(styleId); } styleIds.append(0); // syntetic ink // int synteticInkStyleId = palette->getPage(0)->addStyle(TPixel32(0,0,0,0)); // styleIds.append(synteticInkStyleId); for (int y = 0; y < ly; y++) { unsigned short *workScanLine = m_regionRas->pixels(y); TPixelCM32 *outScanLine = ras->pixels(y); for (int x = 0; x < lx; x++) { int c = workScanLine[x]; Q_ASSERT(0 <= c && c < m_regions.count()); int color = m_regions[c].colorIndex; Q_ASSERT(0 <= color && color < styleIds.count()); int styleId = styleIds.at(color); RegionInfo::Type type = m_regions.at(c).type; if (type == RegionInfo::Background) outScanLine[x] = TPixelCM32(); else if (type & RegionInfo::Ink) outScanLine[x] = TPixelCM32(styleId, 0, 0); else if (m_syntheticInkRas->pixels(y)[x] == 1) outScanLine[x] = TPixelCM32(transparentSyntheticInks ? 0 : styleId, 0, 0); else outScanLine[x] = TPixelCM32(0, styleId, 255); } } struct locals { static bool compare(const QPair &p1, const QPair &p2) { return p1.second > p2.second; } static void addPaint(QList> &list, const int paintId) { if (paintId == 0) return; for (int i = 0; i < list.size(); i++) if (list[i].first == paintId) { list[i].second += 1; return; } list.append(QPair(paintId, 1)); } }; // locals // Here, expand paint area under the solid ink pixel in order to prevent the // gap appears when the "Add Antialiasing" option is activated. TRasterCM32P copiedRas = ras->clone(); copiedRas->lock(); for (int y = 0; y < ly; y++) { TPixelCM32 *topScanLine = copiedRas->pixels((y == 0) ? y : y - 1); TPixelCM32 *middleScanLine = copiedRas->pixels(y); TPixelCM32 *bottomScanLine = copiedRas->pixels((y == ly - 1) ? y : y + 1); TPixelCM32 *outScanLine = ras->pixels(y); for (int x = 0; x < lx; x++) { if (!middleScanLine[x].isPureInk() || middleScanLine[x].getPaint() != 0) continue; int prev_x = (x == 0) ? x : x - 1; int next_x = (x == lx - 1) ? x : x + 1; QList> neighborPaints; locals::addPaint(neighborPaints, topScanLine[prev_x].getPaint()); locals::addPaint(neighborPaints, topScanLine[x].getPaint()); locals::addPaint(neighborPaints, topScanLine[next_x].getPaint()); locals::addPaint(neighborPaints, middleScanLine[prev_x].getPaint()); locals::addPaint(neighborPaints, middleScanLine[next_x].getPaint()); locals::addPaint(neighborPaints, bottomScanLine[prev_x].getPaint()); locals::addPaint(neighborPaints, bottomScanLine[x].getPaint()); locals::addPaint(neighborPaints, bottomScanLine[next_x].getPaint()); qSort(neighborPaints.begin(), neighborPaints.end(), locals::compare); if (!neighborPaints.isEmpty()) outScanLine[x].setPaint(neighborPaints[0].first); } } copiedRas->unlock(); TToonzImageP ti = new TToonzImage(ras, ras->getBounds()); ti->setPalette(palette); if (dpi > 0.0) // for now, accept only square pixel ti->setDpi(dpi, dpi); else ti->setDpi(72, 72); return ti; } //----------------------------------------------------------------------------- TVectorImageP Naa2TlvConverter::vectorize(const TToonzImageP &ti) { CenterlineConfiguration conf; if (!ti) return TVectorImageP(); TPalette *palette = ti->getPalette(); VectorizerCore vc; TAffine dpiAff; double factor = Stage::inch; double dpix = factor / 72, dpiy = factor / 72; ti->getDpi(dpix, dpiy); TPointD center = ti->getRaster()->getCenterD(); if (dpix != 0.0 && dpiy != 0.0) dpiAff = TScale(factor / dpix, factor / dpiy); factor = norm(dpiAff * TPointD(1, 0)); conf.m_affine = dpiAff * TTranslation(-center); conf.m_thickScale = factor; conf.m_leaveUnpainted = false; conf.m_makeFrame = true; conf.m_penalty = 0.0; conf.m_despeckling = 0; TImageP img(ti.getPointer()); TVectorImageP vi = vc.vectorize(img, conf, palette); vi->setPalette(palette); return vi; } //----------------------------------------------------------------------------- void Naa2TlvConverter::removeUnusedStyles(const QList &styleIds) { // Remove unused styles from input palette if (!m_palette) return; for (int p = m_palette->getPageCount() - 1; p >= 0; p--) { TPalette::Page *page = m_palette->getPage(p); for (int s = page->getStyleCount() - 1; s >= 0; s--) { int styleId = page->getStyleId(s); if (styleId == -1) continue; // check if the style is used or not if (!styleIds.contains(styleId)) page->removeStyle(s); } // erase empty page if (page->getStyleCount() == 0) m_palette->erasePage(p); } }