Initial Dansori character workspace
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
/// <summary>
|
||||
/// Alpha-based analysis for auto-alignment: opaque bounding box + neck anchors
|
||||
/// (top-edge center for a headless body, bottom-edge center for a head).
|
||||
/// </summary>
|
||||
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<string, Analysis?> _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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Width of the opaque run that straddles <paramref name="axisX"/> on row y (0 if none).</summary>
|
||||
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;
|
||||
|
||||
/// <summary>The bare-skin run straddling <paramref name="cx"/> on row y (searches ±60px for the seed).</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user