In-Field Testing Using MISR
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

161 lines
6.5 KiB

#!/usr/bin/env python3
import argparse
import os
import random
import subprocess
import tempfile
from PIL import Image, ImageChops, ImageStat
# ---------------------------------------------------------------------------
# Image generation
# ---------------------------------------------------------------------------
def generate_plasma(width: int, height: int, rng: random.Random, scale: int) -> Image.Image:
"""
Multi-octave noise with 1/f-like spectrum (similar to ImageMagick plasma).
Each octave is a tiny random image bilinearly upsampled to full size;
amplitudes halve each octave so low frequencies dominate.
`scale` is the coarsest feature size in pixels (absolute, image-size-
independent). Larger values → smoother image; 1 → pure pixel noise.
"""
accum = [[0.0, 0.0, 0.0] for _ in range(width * height)]
total = 0.0
amplitude = 1.0
# Clamp so we never generate patches larger than the image itself
octave_scale = min(scale, max(width, height))
while octave_scale >= 1:
sw = max(2, width // octave_scale + 2)
sh = max(2, height // octave_scale + 2)
patch = bytes(rng.randint(0, 255) for _ in range(sw * sh * 3))
layer = Image.frombytes('RGB', (sw, sh), patch).resize(
(width, height), Image.BILINEAR)
for i, rgb in enumerate(layer.get_flattened_data()): # type: ignore[attr-defined]
for c in range(3):
accum[i][c] += amplitude * rgb[c]
total += amplitude
amplitude *= 0.5
octave_scale //= 2
pixels = bytes(
min(255, max(0, round(accum[i][c] / total)))
for i in range(width * height)
for c in range(3)
)
return Image.frombytes('RGB', (width, height), pixels)
# ---------------------------------------------------------------------------
# Simulation
# ---------------------------------------------------------------------------
def run_simulation(jpeg_path, ppm_path, vvp_path):
"""
Decode jpeg_path into ppm_path with vvp sim.vvp and return the result as a PIL Image.
"""
assert os.path.exists(vvp_path), "sim.vvp not found — run 'make sim.vvp' first"
result = subprocess.run(
['vvp', vvp_path, f'+infile={jpeg_path}', f'+outfile={ppm_path}'],
capture_output=True, text=True,
)
combined = result.stdout + result.stderr
assert result.returncode == 0, f"simulation exited with code {result.returncode}:\n{combined}"
assert 'ERROR:' not in combined, f"simulation reported error:\n{combined}"
assert os.path.exists(ppm_path) and os.path.getsize(ppm_path) > 0, f"simulation produced no output:\n{combined}"
return _read_ppm(ppm_path)
def _read_ppm(ppm_path):
"""Parse a P6 binary PPM file into a PIL Image."""
with open(ppm_path, 'rb') as f:
data = f.read()
# Header written by testbench: "P6\n<w> <h>\n255\n"
pos = 0
lines = []
while len(lines) < 3:
nl = data.index(b'\n', pos)
lines.append(data[pos:nl].decode().strip())
pos = nl + 1
assert lines[0] == 'P6', f"unexpected PPM magic {lines[0]!r}"
w, h = map(int, lines[1].split())
expected = w * h * 3
actual = len(data) - pos
assert actual == expected, f"PPM pixel data is {actual} bytes, expected {expected} ({w}x{h})"
return Image.frombytes('RGB', (w, h), data[pos:])
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(
description='Run jpeg_core testbench with randomly generated images.')
parser.add_argument('--width', type=int, default=32, help='Image width in pixels (default: 32)')
parser.add_argument('--height', type=int, default=32, help='Image height in pixels (default: 32)')
parser.add_argument('--subsampling', action='store_true',
help='Use 4:2:0 chroma subsampling (default: 4:4:4)')
parser.add_argument('--seed', type=int, default=42,
help='Random seed (default: 42)')
parser.add_argument('--quality', type=int, default=85,
help='JPEG quality 1-95 (default 85)')
parser.add_argument('--scale', type=int, default=32,
help='Plasma coarsest feature size in pixels (default 32)')
parser.add_argument('--save-input', metavar='PATH', help='Save generated JPEG to this path')
parser.add_argument('--save-output', metavar='PATH', help='Save RTL output PPM to this path')
parser.add_argument('--vvp-path', metavar='PATH', default='sim.vvp',
help='Path to compiled simulation (default: sim.vvp)')
args = parser.parse_args()
mcu, subsampling_str = (16, '4:2:0') if args.subsampling else (8, '4:4:4')
assert args.width % mcu == 0, f"width {args.width} must be a multiple of {mcu} for {subsampling_str}"
assert args.height % mcu == 0, f"height {args.height} must be a multiple of {mcu} for {subsampling_str}"
print(f"input: width={args.width} height={args.height} YCbCr={subsampling_str} q={args.quality} seed={args.seed}")
jpeg_path = args.save_input
if jpeg_path is None:
fd, jpeg_path = tempfile.mkstemp(suffix='.jpg', prefix='jpeg_core_tb_infile_')
os.close(fd)
ppm_path = args.save_output
if ppm_path is None:
fd, ppm_path = tempfile.mkstemp(suffix='.jpg', prefix='jpeg_core_tb_outfile_')
os.close(fd)
try:
img_input = generate_plasma(args.width, args.height, random.Random(args.seed), args.scale)
img_input.save(jpeg_path, format='JPEG', quality=args.quality, subsampling=2 if args.subsampling else 0)
img_input = Image.open(jpeg_path).convert('RGB') # re-open to decode saved jpeg image
img_output = run_simulation(jpeg_path, ppm_path, args.vvp_path)
assert img_output.size == img_input.size, f"dimension mismatch: img_input={img_input.width}x{img_input.height} img_output={img_output.width}x{img_output.height}"
stat = ImageStat.Stat(ImageChops.difference(img_output, img_input))
per_channel = {
ch: {'max': stat.extrema[i][1], 'mean': stat.mean[i]}
for i, ch in enumerate(['R', 'G', 'B'])
}
overall_max = max(v['max'] for v in per_channel.values())
overall_mean = sum(v['mean'] for v in per_channel.values()) / 3
print(f"result: max_err={overall_max} mean_err={overall_mean:.2f}")
finally:
if args.save_input is None:
os.unlink(jpeg_path)
if __name__ == '__main__':
main()