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);
}
}