120 lines
7.4 KiB
Python
120 lines
7.4 KiB
Python
import copy
|
|
import json
|
|
from pathlib import Path
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import numpy as np
|
|
from PIL import Image
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import hd_terrain_cleanup as c
|
|
|
|
class TerrainCleanupTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.palette = np.zeros((256, 3), np.uint8)
|
|
for i in range(32): self.palette[i] = [4*i]*3
|
|
self.palette[60:64] = [[56,52,4],[40,40,0],[24,24,0],[12,12,0]]
|
|
self.palette[200:208] = [[85,40,36],[81,36,32],[73,32,28],[69,28,24],[60,24,20],[56,16,16],[40,12,8],[20,0,0]]
|
|
self.palette[231] = [81,60,52]
|
|
self.p = copy.deepcopy(c.DEFAULTS); self.p['output_side'] = 64
|
|
|
|
def test_constant_parent_has_no_retained_texture(self):
|
|
source = np.full((4,4),63,np.uint8)
|
|
out, base, labels, m = c.reconstruct(source,self.palette,np.full((64,64,3),180,np.uint8),self.p)
|
|
np.testing.assert_array_equal(out,base)
|
|
self.assertEqual(m['pixels_changed_from_Source_guided_base'],0)
|
|
np.testing.assert_array_equal(out,np.broadcast_to(self.palette[63],out.shape))
|
|
|
|
def test_parent_colour_and_constant_lighting_are_not_carried(self):
|
|
source = np.full((4,4),63,np.uint8)
|
|
a = np.full((64,64,3),[200,5,50],np.uint8)
|
|
b = np.full((64,64,3),[10,240,70],np.uint8)
|
|
np.testing.assert_array_equal(c.reconstruct(source,self.palette,a,self.p)[0],c.reconstruct(source,self.palette,b,self.p)[0])
|
|
|
|
def test_fine_generated_detail_is_retained_but_bounded(self):
|
|
yy,xx = np.indices((64,64)); raw = np.repeat((120+40*np.sin(xx*1.9)+20*np.sin(yy*2.1))[...,None],3,2).clip(0,255).astype(np.uint8)
|
|
out,base,labels,m = c.reconstruct(np.full((4,4),202,np.uint8),self.palette,raw,self.p)
|
|
self.assertGreater(m['pixels_changed_from_Source_guided_base'],1500)
|
|
self.assertGreater(m['mean_absolute_generated_residual'],0.5)
|
|
self.assertLessEqual(np.abs(out.astype(float)-base).max(),3)
|
|
self.assertLessEqual(m['maximum_retained_luma_residual'],2.5)
|
|
|
|
def test_exact_family_geometry_and_thin_L_are_preserved(self):
|
|
source = np.array([[27,27,231,63],[27,27,231,63],[27,27,231,231],[27,27,27,27]],np.uint8)
|
|
raw = np.random.default_rng(9).integers(0,256,(80,80,3),dtype=np.uint8)
|
|
out,base,labels,m = c.reconstruct(source,self.palette,raw,self.p)
|
|
expected = c.material_families(c.scale_nx(source,4))
|
|
np.testing.assert_array_equal(labels,np.repeat(np.repeat(expected,4,0),4,1))
|
|
self.assertTrue(np.all(out[labels==0].max(axis=1)-out[labels==0].min(axis=1)==0))
|
|
self.assertTrue(np.all(out[labels==1,2] <= 2))
|
|
self.assertTrue(np.all(out[labels==3,0] > out[labels==3,1]))
|
|
|
|
def test_smooth_source_guidance_is_not_nearest_source_copy(self):
|
|
source = np.array([[200,200,205,205]]*4,np.uint8)
|
|
out,base,_,m = c.reconstruct(source,self.palette,np.full((64,64,3),128,np.uint8),self.p)
|
|
nearest=np.repeat(np.repeat(self.palette[source],16,0),16,1)
|
|
self.assertGreater(np.any(base!=nearest,axis=2).sum(),0)
|
|
self.assertGreater(len(np.unique(base.reshape(-1,3),axis=0)),2)
|
|
|
|
def test_deterministic_pixels_and_geometry(self):
|
|
src=np.array([[27,27,27,27],[27,63,202,27],[27,231,231,27],[27,27,27,27]],np.uint8)
|
|
raw=np.random.default_rng(7).integers(0,256,(61,61,3),dtype=np.uint8)
|
|
a=c.reconstruct(src,self.palette,raw,self.p);b=c.reconstruct(src,self.palette,raw,self.p)
|
|
for x,y in zip(a[:3],b[:3]):np.testing.assert_array_equal(x,y)
|
|
self.assertEqual(a[3],b[3])
|
|
|
|
def test_scale4_is_two_scale2_and_non_nearest(self):
|
|
src=np.array([[1,1,2],[1,2,2],[3,3,2]],np.uint8)
|
|
np.testing.assert_array_equal(c.scale_nx(src,4),c.scale_nx(c.scale_nx(src,2),2))
|
|
self.assertFalse(np.array_equal(c.scale_nx(src,2),np.repeat(np.repeat(src,2,0),2,1)))
|
|
|
|
def test_decode_rejects_reserved_and_malformed_source(self):
|
|
good=struct.pack('<5i',1,1,0,2,2)+bytes([27,63,202,231])
|
|
self.assertEqual(c.decode_source(good).shape,(2,2))
|
|
for bad in [good[:-1],good[:20]+bytes([27,63,240,231]),struct.pack('<5i',2,1,0,2,2)+good[20:]]:
|
|
with self.assertRaises(ValueError):c.decode_source(bad)
|
|
|
|
def test_rejects_unknown_family_and_unsafe_parameters(self):
|
|
with self.assertRaises(ValueError):c.material_families(np.array([[100]],np.uint8))
|
|
for name,value in [('output_side',True),('output_side',15),('source_sigma',float('nan')),('residual_sigma_coarse',0.1)]:
|
|
p=copy.deepcopy(self.p);p[name]=value
|
|
with self.assertRaises(ValueError):c.reconstruct(np.full((4,4),63,np.uint8),self.palette,np.zeros((64,64,3),np.uint8),p)
|
|
|
|
def test_stable_reader_rejects_symlinks(self):
|
|
with tempfile.TemporaryDirectory()as d:
|
|
p=Path(d);(p/'a').write_bytes(b'fixed');(p/'link').symlink_to(p/'a')
|
|
with self.assertRaises(OSError):c.read_regular(p/'link')
|
|
|
|
def test_actual_cli_lineage_and_bad_crop_stops_before_mkdir(self):
|
|
import hashlib
|
|
with tempfile.TemporaryDirectory()as d:
|
|
p=Path(d);src=p/'source.pic';src.write_bytes(struct.pack('<5i',1,1,0,4,4)+bytes([63]*16))
|
|
pal=p/'palette';pal.write_bytes(bytes([0,0,0])*60+bytes([14,13,1,10,10,0,6,6,0,3,3,0])+bytes([0,0,0])*(256-64))
|
|
raw=p/'parent.png';yy,xx=np.indices((64,64));Image.fromarray(np.repeat((80+(xx%2)*80)[...,None],3,2).astype(np.uint8)).save(raw)
|
|
def binding(q):return dict(path=str(q),sha256=hashlib.sha256(q.read_bytes()).hexdigest())
|
|
recipe=dict(method=c.METHOD,palette=binding(pal),parameters=self.p,frames=[dict(asset='20400',Source=binding(src),parent_raw=binding(raw),crop=[0,0,64,64])])
|
|
rp=p/'recipe.json';rp.write_text(json.dumps(recipe));out=p/'result';c.main(['--recipe',str(rp),'--output',str(out)])
|
|
report=json.loads((out/'DERIVED-LINEAGE-AND-ACTUAL-RECONSTRUCTION-METRICS.json').read_text());row=report['frames'][0]
|
|
self.assertEqual(row['output']['sha256'],hashlib.sha256(Path(row['output']['path']).read_bytes()).hexdigest())
|
|
self.assertEqual(row['parent_raw']['sha256'],binding(raw)['sha256'])
|
|
self.assertIn('DERIVED',row['lineage'])
|
|
with Image.open(row['output']['path']) as saved:
|
|
self.assertEqual(saved.size,(64,64))
|
|
self.assertEqual(report['execution']['status'],'success')
|
|
self.assertEqual(report['execution']['source_sha256'],{'20400':binding(src)['sha256']})
|
|
self.assertEqual(report['execution']['output_sha256'],{'20400':row['output']['sha256']})
|
|
self.assertEqual(report['execution']['parent_raw_sha256'],[binding(raw)['sha256']])
|
|
portable=copy.deepcopy(recipe)
|
|
portable['palette']['path']=pal.name
|
|
portable['frames'][0]['Source']['path']=src.name
|
|
portable['frames'][0]['parent_raw']['path']=raw.name
|
|
portable_path=p/'portable.json';portable_path.write_text(json.dumps(portable))
|
|
moved=p/'relative-result';c.main(['--recipe',str(portable_path),'--output',str(moved)])
|
|
self.assertEqual((moved/Path(row['output']['path']).name).read_bytes(),Path(row['output']['path']).read_bytes())
|
|
recipe['frames'][0]['crop']=[1,0,64,64];rp.write_text(json.dumps(recipe));bad=p/'bad'
|
|
with self.assertRaises(ValueError):c.main(['--recipe',str(rp),'--output',str(bad)])
|
|
self.assertFalse(bad.exists())
|
|
|
|
if __name__ == '__main__':unittest.main()
|