Files
RustRaptor/tools/hd_grating_gray_cleanup.py
T

204 lines
12 KiB
Python

"""DERIVED gray-cast correction, preserving generated luminance structure.
Bounded to the two reviewed grating compositions. Source supplies only an exact
semantic mask and ONE aggregate gray color per frame, never a painted SourceNx
field. This tool does not approve art, create a runtime policy or modify inputs.
"""
from __future__ import annotations
import argparse, ctypes, errno, hashlib, io, json, os, shutil, stat, struct, sys, tempfile
from pathlib import Path, PurePosixPath
import numpy as np
from PIL import Image, __version__ as PILLOW_VERSION
METHOD = 'generated-grating-semantic-gray-cohort-mean-cast-only-v1'
REGIONS = {'203c6': [21,3,31,31], '203c7': [2,0,31,12]}
MAX_BYTES = 32*1024*1024
MAX_PIXELS = 4_194_304
MAX_SHIFT = 32.0
LUMA = np.asarray([0.2126,0.7152,0.0722], np.float64)
def sha(b): return hashlib.sha256(b).hexdigest()
def read_regular(root, relative, limit=MAX_BYTES):
"""Every component, including the root, is walked with stable no-follow FDs."""
if type(relative)is not str:raise ValueError('string relative path required')
p=PurePosixPath(relative)
if (p.is_absolute() or not p.parts
or '..'in p.parts or p.as_posix()!=relative):
raise ValueError('normalized contained relative path required')
base=Path(root).absolute();fd=os.open('/',os.O_RDONLY|os.O_DIRECTORY|os.O_NONBLOCK)
try:
for name in list(base.parts[1:])+list(p.parts[:-1]):
newer=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=fd)
os.close(fd);fd=newer
f=os.open(p.parts[-1],os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=fd)
try:
before=os.fstat(f)
if not stat.S_ISREG(before.st_mode)or before.st_size>limit:
raise ValueError('bounded regular input required')
with os.fdopen(f,'rb',closefd=False)as stream:body=stream.read(limit+1)
after=os.fstat(f)
identity=lambda q:(q.st_dev,q.st_ino,q.st_size,q.st_mtime_ns,q.st_ctime_ns)
if identity(before)!=identity(after)or len(body)!=before.st_size or len(body)>limit:
raise ValueError('input changed during bounded read')
finally:os.close(f)
finally:os.close(fd)
return body
def read_bound(root,binding,limit=MAX_BYTES):
if (type(binding)is not dict or set(binding)!={'path','sha256'}
or type(binding['sha256'])is not str or len(binding['sha256'])!=64
or any(c not in '0123456789abcdef'for c in binding['sha256'])):
raise ValueError('exact relative path/sha256 binding required')
body=read_regular(root,binding['path'],limit)
if sha(body)!=binding['sha256']:raise ValueError('input SHA mismatch')
return body
def source_indices(body):
if len(body)!=1044 or struct.unpack('<5i',body[:20])!=(1,1,0,32,32):
raise ValueError('exact complete uncompressed 32x32 Source required')
a=np.frombuffer(body[20:],np.uint8).reshape(32,32).copy()
if np.any(a>=240):raise ValueError('only opaque ordinary Source supported')
return a
def palette_rgb(body):
if len(body)!=768 or max(body)>63:raise ValueError('authentic six-bit palette required')
a=np.frombuffer(body,np.uint8).astype(np.uint16).reshape(256,3)
return ((a<<2)|(a>>4)).astype(np.uint8)
def semantic_mask(asset, source):
if asset not in REGIONS:raise ValueError('unreviewed grating asset')
x0,y0,x1,y1=REGIONS[asset];m=np.zeros((32,32),bool)
m[y0:y1,x0:x1]=(source[y0:y1,x0:x1]>=11)&(source[y0:y1,x0:x1]<=30)
if not m.any():raise ValueError('no authentic gray grating material')
return m
def selected_digest(mask):
return sha(json.dumps(sorted([[int(x),int(y)]for y,x in np.argwhere(mask)]),
separators=(',',':')).encode())
def validate_recipe(r):
if (type(r)is not dict or set(r)!={'format_version','method','palette','parent_raw','frames'}
or type(r['format_version'])is not int or r['format_version']!=1 or r['method']!=METHOD
or type(r['frames'])is not list or len(r['frames'])!=2):
raise ValueError('exact reviewed two-frame recipe required')
if any(type(f)is not dict for f in r['frames']):raise ValueError('frame objects required')
if [f.get('asset')for f in r['frames']]!=['203c6','203c7']:
raise ValueError('ordered complete pair required')
for f in r['frames']:
if (set(f)!={'asset','Source','selected_cells_sha256'}
or type(f['selected_cells_sha256'])is not str
or len(f['selected_cells_sha256'])!=64
or any(c not in '0123456789abcdef'for c in f['selected_cells_sha256'])):
raise ValueError('unknown frame field or selector digest')
def prepare_inputs(r, root):
validate_recipe(r)
return (palette_rgb(read_bound(root,r['palette'],768)),
read_bound(root,r['parent_raw']),
[(f,source_indices(read_bound(root,f['Source'],1044)))for f in r['frames']])
def derive(palette, raw_bytes, frames):
"""Pure verified-bytes operation. Never reopen source or parent filenames."""
with Image.open(io.BytesIO(raw_bytes)) as opened:
if (opened.format!='PNG' or opened.mode not in ('RGB','RGBA') or opened.width!=2*opened.height
or not 128<=opened.height<=2048 or opened.width*opened.height>MAX_PIXELS):
raise ValueError('bounded exact 2x1 PNG parent required')
raw=np.asarray(opened.convert('RGBA')).copy()
if np.any(raw[:,:,3]!=255):raise ValueError('no alpha repair/hidden RGB allowed')
size=raw.shape[0];output=[]
for column,(f,src)in enumerate(frames):
mask=semantic_mask(f['asset'],src)
if selected_digest(mask)!=f['selected_cells_sha256']:raise ValueError('Source cohort digest mismatch')
parent=raw[:,column*size:(column+1)*size,:]
# A raw 3x3 luminance witness must lie wholly in the same gray cohort.
coordinates=((np.arange(size)*2+1)*32)//(size*2)
mapped=mask[coordinates[:,None],coordinates[None,:]]
luma=parent[:,:,:3].astype(np.float64)@LUMA
eligible=mapped[1:-1,1:-1].copy();low=luma[1:-1,1:-1].copy();high=low.copy()
for dy,dx in [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]:
yy=slice(1+dy,size-1+dy);xx=slice(1+dx,size-1+dx)
eligible &= mapped[yy,xx];low=np.minimum(low,luma[yy,xx]);high=np.maximum(high,luma[yy,xx])
witnesses=int(np.count_nonzero(eligible&(high-low>=2.0)))
if witnesses==0:raise ValueError('no authentic raw same-material fine luminance witness')
baseline=np.asarray(Image.fromarray(parent).resize((128,128),Image.Resampling.LANCZOS)).copy()
area=np.repeat(np.repeat(mask,4,0),4,1);rgb=baseline[:,:,:3].astype(np.float64)
generated_luma=rgb@LUMA;anchor=palette[src[mask]].astype(np.float64).mean(axis=0)
if anchor.max()-anchor.min()>8:raise ValueError('Source cohort is not near-neutral gray')
anchor_luma=float(anchor@LUMA);shift=anchor_luma-float(generated_luma[area].mean())
if abs(shift)>MAX_SHIFT:raise ValueError('gray mean correction exceeds fixed32 cap')
proposed=(generated_luma[area]+shift)[:,None]+(anchor-anchor_luma)[None,:]
if np.any(proposed<0)or np.any(proposed>255):raise ValueError('clipping forbidden')
result=baseline.copy();result[:,:,:3][area]=np.rint(proposed).astype(np.uint8)
if not np.array_equal(result[~area],baseline[~area]):raise AssertionError('outside changed')
transformed_luma=result[:,:,:3].astype(np.float64)@LUMA
error=float(np.abs(transformed_luma[area]-(generated_luma[area]+shift)).max())
if error>0.5000001:raise AssertionError('generated luminance structure lost')
metrics=dict(asset=f['asset'],semantic_Source_cells=int(mask.sum()),corrected_output_pixels=int(area.sum()),
unmodified_resized_parent_pixels=int((~area).sum()),Source_aggregate_RGB=anchor.tolist(),
generated_luma_mean=float(generated_luma[area].mean()),constant_gray_mean_offset=shift,
raw_same_cohort_3x3_luma_witnesses=witnesses,max_luma_structure_rounding_error=error,
generated_luma_variance=float(generated_luma[area].var()),
output_luma_variance=float(transformed_luma[area].var()),
exact_Original_pixel_copies=0,outside_resized_parent_exact=True,
selected_cells_sha256=selected_digest(mask),output_scale=4)
output.append((f['asset'],result,metrics))
return output
def publish_no_replace(stage,target):
if os.name=='nt':os.rename(stage,target);return
if not sys.platform.startswith('linux'):raise OSError(errno.ENOTSUP,'atomic publication unsupported')
libc=ctypes.CDLL(None,use_errno=True)
if not hasattr(libc,'renameat2'):raise OSError(errno.ENOTSUP,'renameat2 unavailable')
fn=libc.renameat2;fn.argtypes=[ctypes.c_int,ctypes.c_char_p,ctypes.c_int,ctypes.c_char_p,ctypes.c_uint]
fn.restype=ctypes.c_int
if fn(-100,os.fsencode(stage),-100,os.fsencode(target),1)!=0:
code=ctypes.get_errno();raise OSError(code,os.strerror(code),str(target))
def run(recipe_relative,input_root,output):
root=Path(input_root).absolute();rel=PurePosixPath(recipe_relative)
if rel.is_absolute()or '..'in rel.parts or not rel.parts:raise ValueError('relative recipe required')
# Recipe bytes use the same no-follow reader after a separately bound hash.
body=read_regular(root,rel.as_posix(),1_000_000)
r=json.loads(body);prepared=prepare_inputs(r,root);result=derive(*prepared)
output=Path(output).absolute()
if output.exists()or output.is_symlink()or not output.parent.is_dir():raise ValueError('fresh output directory required')
tool_path=Path(__file__).absolute();tool_bytes=read_regular(tool_path.parent,tool_path.name);outputs={};pngs={}
for asset,rgba,_ in result:
stream=io.BytesIO();Image.fromarray(rgba).save(stream,format='PNG');pngs[asset]=stream.getvalue();outputs[asset]=sha(pngs[asset])
execution=dict(status='success',method=METHOD,recipe_sha256=sha(body),tool_sha256=sha(tool_bytes),
source_sha256={f['asset']:f['Source']['sha256']for f in r['frames']},
parent_raw_sha256=[r['parent_raw']['sha256']],output_sha256=outputs)
receipt=dict(status='DERIVED_NOT_ART_APPROVAL',execution=execution,frames=[m for _,_,m in result],
runtime=dict(python=sys.version,numpy=np.__version__,Pillow=PILLOW_VERSION),
limits='Source owns semantic geometry and one aggregate gray anchor only; generated raw owns every luminance difference. No per-cell Source RGB copying, flat fallback, new pixels outside resized-parent selection, or quality approval. Resized-parent baseline uses Pillow Lanczos3; equality to old Rust raw-bank resampling is NOT claimed.')
stage=Path(tempfile.mkdtemp(prefix='.'+output.name+'.stage-',dir=output.parent))
try:
for asset in pngs:(stage/(asset+'-DERIVED-graycast-v1.png')).write_bytes(pngs[asset])
(stage/'execution-receipt.json').write_text(json.dumps(receipt,indent=2)+'\n')
if read_bound(root,{'path':rel.as_posix(),'sha256':sha(body)},1_000_000)!=body:raise ValueError('recipe changed')
prepare_inputs(r,root) # Reauthenticate originals; no second render.
if read_regular(tool_path.parent,tool_path.name)!=tool_bytes:raise ValueError('tool changed')
publish_no_replace(stage,output)
except BaseException:shutil.rmtree(stage,ignore_errors=True);raise
return receipt
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('--recipe',required=True)
p.add_argument('--input-root',required=True);p.add_argument('--output',required=True);a=p.parse_args()
run(a.recipe,a.input_root,a.output)
if __name__=='__main__':main()