using System; using System.Collections.Generic; using System.Windows.Media; using System.Windows.Media.Imaging; namespace Character_Builder; /// /// Alpha-based analysis for auto-alignment: opaque bounding box + neck anchors /// (top-edge center for a headless body, bottom-edge center for a head). /// public static class AlphaTools { public class Analysis { public int W, H; public int MinX, MinY, MaxX, MaxY; public double TopCenterX; // center X of opaque pixels along the top band public double BottomCenterX; // center X of opaque pixels along the bottom band public int AxisX; // torso central axis (robust neck X; ignores raised arms) public int NeckTop; // topmost row of the central column (robust neck Y) public int BH => MaxY - MinY + 1; public int BW => MaxX - MinX + 1; } private static readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); private const byte AlphaThreshold = 24; public static Analysis? Analyze(string path) { if (_cache.TryGetValue(path, out var cached)) return cached; Analysis? a = null; try { a = Compute(path); } catch { a = null; } _cache[path] = a; return a; } private static Analysis? Compute(string path) { var src = AssetScanner.LoadPart(path); if (src == null) return null; BitmapSource conv = src.Format == PixelFormats.Bgra32 ? src : new FormatConvertedBitmap(src, PixelFormats.Bgra32, null, 0); int w = conv.PixelWidth, h = conv.PixelHeight; if (w <= 0 || h <= 0) return null; int stride = w * 4; var px = new byte[h * stride]; conv.CopyPixels(px, stride, 0); int minX = int.MaxValue, minY = int.MaxValue, maxX = -1, maxY = -1; for (int y = 0; y < h; y++) { int row = y * stride; for (int x = 0; x < w; x++) { if (px[row + x * 4 + 3] >= AlphaThreshold) { if (x < minX) minX = x; if (x > maxX) maxX = x; if (y < minY) minY = y; if (y > maxY) maxY = y; } } } if (maxX < 0) return null; // fully transparent int bh = maxY - minY + 1; int band = Math.Max(1, (int)(bh * 0.06)); double topSum = 0; long topCount = 0; for (int y = minY; y < Math.Min(h, minY + band); y++) { int row = y * stride; for (int x = minX; x <= maxX; x++) if (px[row + x * 4 + 3] >= AlphaThreshold) { topSum += x; topCount++; } } double bottomSum = 0; long bottomCount = 0; for (int y = Math.Max(0, maxY - band + 1); y <= maxY; y++) { int row = y * stride; for (int x = minX; x <= maxX; x++) if (px[row + x * 4 + 3] >= AlphaThreshold) { bottomSum += x; bottomCount++; } } // Torso central axis: median opaque column in the mid-lower body (waist/legs are centered // and free of raised arms), giving a neck-X that is stable across gesture poses. int ta0 = minY + (int)(bh * 0.40), ta1 = Math.Min(maxY, minY + (int)(bh * 0.85)); var colCount = new int[w]; long axisTotal = 0; for (int y = ta0; y <= ta1; y++) { int row = y * stride; for (int x = minX; x <= maxX; x++) if (px[row + x * 4 + 3] >= AlphaThreshold) { colCount[x]++; axisTotal++; } } int axisX = (minX + maxX) / 2; if (axisTotal > 0) { long half = axisTotal / 2, acc = 0; for (int x = 0; x < w; x++) { acc += colCount[x]; if (acc >= half) { axisX = x; break; } } } // Neck top: first row (from the top) whose central column — the opaque run around the // axis — is solid (>=15px). Skips raised hands/arms that sit above the real neck. int neckTop = minY; for (int y = minY; y < minY + (int)(bh * 0.5); y++) { if (CentralRunWidth(px, stride, y, w, axisX) >= 15) { neckTop = y; break; } } // Precise neck via the bare-skin neck stump: the head's neck and the body's neck stump are // the same anatomical part, so matching this locks the assembly. The stump is the central // skin column at the top of a headless body; detecting it here gives a neck centre/top that // is robust to raised arms, open jackets and asymmetric poses. Falls back to the alpha // estimate above when no skin stump is present. int bboxW = maxX - minX + 1, seedX = (minX + maxX) / 2; for (int y = minY; y < minY + (int)(bh * 0.5); y++) { var (lo, hi, rw) = CentralSkinRun(px, stride, y, w, seedX); if (rw >= 30 && rw <= bboxW / 2) { axisX = (lo + hi) / 2; neckTop = y; break; } } double cx = (minX + maxX) / 2.0; return new Analysis { W = w, H = h, MinX = minX, MinY = minY, MaxX = maxX, MaxY = maxY, AxisX = axisX, NeckTop = neckTop, TopCenterX = topCount > 0 ? topSum / topCount : cx, BottomCenterX = bottomCount > 0 ? bottomSum / bottomCount : cx, }; } /// Width of the opaque run that straddles on row y (0 if none). private static int CentralRunWidth(byte[] px, int stride, int y, int w, int axisX) { int row = y * stride, seed = -1; for (int d = 0; d <= 8 && seed < 0; d++) { if (axisX + d < w && px[row + (axisX + d) * 4 + 3] >= AlphaThreshold) seed = axisX + d; else if (axisX - d >= 0 && px[row + (axisX - d) * 4 + 3] >= AlphaThreshold) seed = axisX - d; } if (seed < 0) return 0; int lo = seed, hi = seed; while (lo - 1 >= 0 && px[row + (lo - 1) * 4 + 3] >= AlphaThreshold) lo--; while (hi + 1 < w && px[row + (hi + 1) * 4 + 3] >= AlphaThreshold) hi++; return hi - lo + 1; } private static bool IsSkin(byte b, byte g, byte r) => r > 150 && r >= g && g >= b - 8 && (r - b) >= 15 && (r - b) <= 130 && (g - b) <= 70; /// The bare-skin run straddling on row y (searches ±60px for the seed). private static (int lo, int hi, int w) CentralSkinRun(byte[] px, int stride, int y, int w, int cx) { int row = y * stride, seed = -1; for (int d = 0; d <= 60 && seed < 0; d++) { int xr = cx + d, xl = cx - d; if (xr < w && IsSkin(px[row + xr * 4], px[row + xr * 4 + 1], px[row + xr * 4 + 2])) seed = xr; else if (xl >= 0 && IsSkin(px[row + xl * 4], px[row + xl * 4 + 1], px[row + xl * 4 + 2])) seed = xl; } if (seed < 0) return (0, 0, 0); int lo = seed, hi = seed; while (lo - 1 >= 0 && IsSkin(px[row + (lo - 1) * 4], px[row + (lo - 1) * 4 + 1], px[row + (lo - 1) * 4 + 2])) lo--; while (hi + 1 < w && IsSkin(px[row + (hi + 1) * 4], px[row + (hi + 1) * 4 + 1], px[row + (hi + 1) * 4 + 2])) hi++; return (lo, hi, hi - lo + 1); } }