ModelTranslator: 新增 uv_preview.render_uv_png——PIL 绘 UV 线框 PNG
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8fe0a3f7ae
commit
a19e342bf4
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import uv_preview as up
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
class TestToPx(unittest.TestCase):
|
||||||
|
def test_origin_bottom_left_maps_to_bottom_left_pixel(self):
|
||||||
|
# UV(0,0) 原点在左下 -> 图像左下角(x=pad, y=size-pad)
|
||||||
|
x, y = up._to_px(0.0, 0.0, size=100, pad=10)
|
||||||
|
self.assertAlmostEqual(x, 10.0)
|
||||||
|
self.assertAlmostEqual(y, 90.0)
|
||||||
|
|
||||||
|
def test_top_right(self):
|
||||||
|
x, y = up._to_px(1.0, 1.0, size=100, pad=10)
|
||||||
|
self.assertAlmostEqual(x, 90.0)
|
||||||
|
self.assertAlmostEqual(y, 10.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRenderUvPng(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.tmp = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
def _out(self, name="uv.png"):
|
||||||
|
return os.path.join(self.tmp, name)
|
||||||
|
|
||||||
|
def test_writes_png_of_requested_size(self):
|
||||||
|
polys = [[[0.1, 0.1], [0.4, 0.1], [0.25, 0.4]],
|
||||||
|
[[0.6, 0.6], [0.9, 0.6], [0.9, 0.9], [0.6, 0.9]]]
|
||||||
|
out = up.render_uv_png(polys, self._out(), size=128)
|
||||||
|
self.assertTrue(os.path.isfile(out))
|
||||||
|
with Image.open(out) as im:
|
||||||
|
self.assertEqual(im.size, (128, 128))
|
||||||
|
self.assertEqual(im.format, "PNG")
|
||||||
|
|
||||||
|
def test_empty_polygons_still_writes_blank(self):
|
||||||
|
out = up.render_uv_png([], self._out("blank.png"), size=64)
|
||||||
|
self.assertTrue(os.path.isfile(out))
|
||||||
|
with Image.open(out) as im:
|
||||||
|
self.assertEqual(im.size, (64, 64))
|
||||||
|
|
||||||
|
def test_degenerate_polygon_skipped_no_crash(self):
|
||||||
|
out = up.render_uv_png([[[0.5, 0.5]]], self._out("deg.png"), size=64)
|
||||||
|
self.assertTrue(os.path.isfile(out))
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""UV 线框观察图渲染:把 UV 多边形(归一化 0-1 坐标)画成 PNG 供人工查阅。
|
||||||
|
在系统 Python(有 Pillow)中运行——Blender 自带 Python 无 PIL,故 UV 几何由
|
||||||
|
bl_decimate 导出为 JSON,本模块负责绘制。"""
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
|
def _to_px(u, v, size, pad):
|
||||||
|
"""UV(0-1, 原点左下) -> 像素(原点左上),含边距。"""
|
||||||
|
span = size - 2 * pad
|
||||||
|
x = pad + u * span
|
||||||
|
y = pad + (1.0 - v) * span # V 轴翻转:图像 y 向下
|
||||||
|
return x, y
|
||||||
|
|
||||||
|
|
||||||
|
def render_uv_png(polygons, out_png, size=1024, pad=8,
|
||||||
|
line=(30, 30, 30, 255), fill=(80, 140, 220, 64)):
|
||||||
|
"""polygons: [[[u,v], ...], ...] 每个多边形一组 UV 顶点。
|
||||||
|
白底 + 半透明填充 + 深色线框;写 PNG 到 out_png,返回 out_png。"""
|
||||||
|
base = Image.new("RGBA", (size, size), (255, 255, 255, 255))
|
||||||
|
overlay = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||||
|
draw = ImageDraw.Draw(overlay, "RGBA")
|
||||||
|
for poly in polygons:
|
||||||
|
pts = [_to_px(u, v, size, pad) for (u, v) in poly]
|
||||||
|
if len(pts) >= 3:
|
||||||
|
draw.polygon(pts, fill=fill)
|
||||||
|
for poly in polygons:
|
||||||
|
pts = [_to_px(u, v, size, pad) for (u, v) in poly]
|
||||||
|
if len(pts) >= 2:
|
||||||
|
draw.line(pts + pts[:1], fill=line, width=1)
|
||||||
|
Image.alpha_composite(base, overlay).convert("RGB").save(out_png, "PNG")
|
||||||
|
return out_png
|
||||||
Reference in New Issue
Block a user