204 lines
12 KiB
Python
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()
|