ModelTranslator: adaptive ray distance pure functions
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
25f84f8557
commit
3ae981e40d
@@ -9,6 +9,9 @@ import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
DEFAULT_RAY_PCT = 0.02 # 自动射线距离 = 低模包围盒对角线 * 2%
|
||||
RAY_SAFETY = 1.5 # 自适应射线安全系数(分离 P99 × 此值)
|
||||
RAY_LOWER_PCT = 0.0005 # 射线下限 = 包围盒对角线 * 0.05%
|
||||
RAY_UPPER_PCT = 0.01 # 射线上限 = 包围盒对角线 * 1%
|
||||
BBOX_TOL = 0.10 # 高低模包围盒尺寸相对差警告阈值
|
||||
|
||||
# (输出名, bake type, EMIT 来源的 Principled 输入, 目标图颜色空间)
|
||||
@@ -26,6 +29,34 @@ def estimate_ray_distance(bbox_dims, pct=DEFAULT_RAY_PCT):
|
||||
return math.sqrt(sum(d * d for d in bbox_dims)) * pct
|
||||
|
||||
|
||||
def percentile(values, q):
|
||||
"""插值分位数。空列表或 q∉[0,1] 抛 ValueError。"""
|
||||
if not values:
|
||||
raise ValueError("percentile 需要至少一个值")
|
||||
if not 0.0 <= q <= 1.0:
|
||||
raise ValueError("分位数 q 必须在 [0,1]")
|
||||
ordered = sorted(float(v) for v in values)
|
||||
pos = (len(ordered) - 1) * q
|
||||
lo = int(math.floor(pos))
|
||||
hi = int(math.ceil(pos))
|
||||
if lo == hi:
|
||||
return ordered[lo]
|
||||
return ordered[lo] + (ordered[hi] - ordered[lo]) * (pos - lo)
|
||||
|
||||
|
||||
def adaptive_ray_distance(distances, bbox_dims):
|
||||
"""按低↔高分离距离定射线:P99×RAY_SAFETY,夹在对角线 [0.05%,1%]。
|
||||
返回 {distance_p99, value, capped};包围盒退化抛 ValueError。"""
|
||||
diagonal = math.sqrt(sum(float(d) * float(d) for d in bbox_dims))
|
||||
if diagonal <= 0.0:
|
||||
raise ValueError("低模包围盒对角线必须为正")
|
||||
p99 = percentile(distances, 0.99)
|
||||
lower = diagonal * RAY_LOWER_PCT
|
||||
upper = diagonal * RAY_UPPER_PCT
|
||||
raw = max(p99 * RAY_SAFETY, lower)
|
||||
return {"distance_p99": p99, "value": min(raw, upper), "capped": raw > upper}
|
||||
|
||||
|
||||
def output_stem(low_name):
|
||||
"""低模名去掉 _low 后缀作为输出前缀(对齐工具3纯网格贴图命名约定)。"""
|
||||
return low_name[:-4] if low_name.endswith("_low") else low_name
|
||||
|
||||
@@ -43,5 +43,40 @@ class TestBboxMismatch(unittest.TestCase):
|
||||
self.assertFalse(bb.bbox_mismatch((1.0, 0.0, 3.0), (1.0, 0.0, 3.0)))
|
||||
|
||||
|
||||
class TestPercentile(unittest.TestCase):
|
||||
def test_interpolates(self):
|
||||
self.assertAlmostEqual(bb.percentile([0.0, 10.0], 0.25), 2.5)
|
||||
|
||||
def test_rejects_empty(self):
|
||||
with self.assertRaises(ValueError):
|
||||
bb.percentile([], 0.99)
|
||||
|
||||
def test_rejects_bad_q(self):
|
||||
with self.assertRaises(ValueError):
|
||||
bb.percentile([1.0], 1.1)
|
||||
|
||||
|
||||
class TestAdaptiveRayDistance(unittest.TestCase):
|
||||
def test_p99_with_safety(self):
|
||||
r = bb.adaptive_ray_distance([0.002] * 20, (1.0, 0.0, 0.0))
|
||||
self.assertAlmostEqual(r["distance_p99"], 0.002)
|
||||
self.assertAlmostEqual(r["value"], 0.003) # 0.002*1.5,未夹
|
||||
self.assertFalse(r["capped"])
|
||||
|
||||
def test_lower_floor(self):
|
||||
r = bb.adaptive_ray_distance([0.0], (1.0, 0.0, 0.0))
|
||||
self.assertAlmostEqual(r["value"], 0.0005) # 下限 0.05%
|
||||
self.assertFalse(r["capped"])
|
||||
|
||||
def test_upper_cap(self):
|
||||
r = bb.adaptive_ray_distance([0.02], (1.0, 0.0, 0.0))
|
||||
self.assertAlmostEqual(r["value"], 0.01) # 夹到 1% 上限
|
||||
self.assertTrue(r["capped"])
|
||||
|
||||
def test_zero_bbox_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
bb.adaptive_ray_distance([0.01], (0.0, 0.0, 0.0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user