import numpy as np
from PIL import Image
from scipy.fftpack import dct

img = Image.open('/Users/user/puz/README.jpg').convert('YCbCr')
Y = np.asarray(img)[:, :, 0].astype(np.float64) - 128.0
H, W = Y.shape
bh, bw = H // 8, W // 8
print('image', Y.shape, 'blocks', bh, 'x', bw, '=', bh * bw)

blocks = Y[:bh*8, :bw*8].reshape(bh, 8, bw, 8).transpose(0, 2, 1, 3)
D = dct(dct(blocks, axis=2, norm='ortho'), axis=3, norm='ortho')
D = D.reshape(-1, 8, 8)

ZIG = [(0,0),(0,1),(1,0),(2,0),(1,1),(0,2),(0,3),(1,2),(2,1),(3,0),
       (4,0),(3,1),(2,2),(1,3),(0,4),(0,5),(1,4),(2,3),(3,2),(4,1)]

def render(bs):
    return ''.join(chr(b) if 32 <= b < 127 else ('\n' if b == 10 else '.') for b in bs)

for skip_dc in (1, 0):
    coeffs = ZIG[skip_dc:skip_dc+8]
    vals = np.stack([D[:, r, c] for (r, c) in coeffs], axis=1)
    bits = (vals > 0).astype(np.uint8)
    for invert in (0, 1):
        b = bits ^ invert
        for order, name in ((b, 'MSB'), (b[:, ::-1], 'LSB')):
            by = np.packbits(order, axis=1).ravel()
            print(f'--- skip_dc={skip_dc} invert={invert} {name} ---')
            print(repr(render(by[:96])))
