Thursday, May 16, 2024
HomePythonWriting a SAM Coupé SCREEN$ Converter in Python

Writing a SAM Coupé SCREEN$ Converter in Python


The SAM Coupé was a British 8 bit dwelling pc that was pitched as a successor to the ZX Spectrum, that includes improved graphics and sound and better processor pace.

The SAM Coupé’s high-color MODE4 may handle 256×192 decision graphics, with 16 colours from a selection of 128. Every pixel could be set individually, somewhat than utilizing PEN/PAPER attributes as on the Spectrum. However there’s extra. The SAM additionally helps line interrupts which permit palette entries to be modified on specific scan traces: a single palette entry can show a number of colours.

The limitation that coloration can solely be modified per line means it is probably not helpful for video games, or different shifting graphics. However it does mean you can use a totally separate palette for “off display screen” parts like panels. For static photographs, resembling images, it is extra helpful – assuming that the distribution of coloration within the picture is favorable.

Demonstration SCREEN$ have been an everyday characteristic of SAM Coupé disk magazines, however interrupts have been hardly ever used for the reason that placement needed to be completed manually.
Now we’re residing sooner or later, I needed to have a crack at automating line interrupts to squeeze out as many colours as doable & let the SAM showcase it is capabilities.

If you happen to simply need the converter, you will get it right here. It’s written in Python, utilizing Pillow for picture coloration conversion.

First a fast take a look at the SAM Coupé display screen modes to see what we’re coping with.

Sam Coupe Display screen Modes

There are 4 display screen modes on the SAM Coupé.

  • MODE 1 is the ZX Spectrum suitable mode, with 8×8 blocks which may comprise 2 colours PAPER (background) and PEN (foreground). The framebuffer in MODE 1 is non-linear, in that line 1 is adopted by line 8.
  • MODE 2 additionally makes use of attributes, with PAPER and PEN, however the cells are 8×1 pixels and the framebuffer is linear. This MODE wasn’t used an amazing deal, however was the quickest mode on the SAM because of Spectrum-compatibility delays in MODE 1.
  • MODE 3 is excessive decision, with double the X pixels however solely 4 colors — making it good for studying textual content.
  • MODE 4 is the excessive coloration mode, with 256×192 and unbiased coloring of each pixel from a palette of 16. Most video games/software program used this mode.
Mode Dimensions Framebuffer bpp Colours Measurement Notes
4 256×192 linear 4 16 24 KB Excessive coloration
3 512×192 linear 2 4 24 KB Excessive decision
2 256×192 linear 1 16 12 KB Coloration attributes for every 8×1 block
1 256×192 non-linear 1 16 6.75KB Coloration attributes for every 8×8 block; matches ZX Spectrum

Most SAM Coupe SCREEN$ have been in MODE 4, so that is what we’ll be focusing on. It will be comparatively simple to help MODE 3 on prime of this.

The SCREEN$ format

The format itself is pretty easy, consisting of the next bytes.

Bytes Content material
24576 Pixel information, Mode 4 4bpp: 1 byte=2 pixels; Mode 3 2bpp: 1 byte = 4 pixels
16 Mode 4 Palette A
4 Mode 3 Palette A retailer
16 Mode 4 Palette B
4 Mode 3 Palette B retailer
Variable Line interrupts 4 bytes per interrupt (see beneath)
1 FF termination byte

In MODE 4 the pixel information is 4bbp, that’s 1 byte = 2 pixels (16 doable colours). To deal with this we are able to create our picture as 16 colours and bit-shift the values earlier than packing adjoining pixels right into a single byte.

Palette A & B

As proven within the desk above the SAM really helps two simultaneous palettes (right here marked A & B). These are full palettes that are alternated between, by default 3 instances per second, to create flashing results. The whole palette is switched, however you possibly can choose to solely change a single coloration. The speed of flashing is configurable with:

fundamental

POKE &5A08, <worth>

The <worth> is the time between swaps of alternate palettes, in 50ths of a second. That is solely
typically helpful for creating flashing cursor results . For changing to SAM SCREEN$ we’ll be ignoring this and simply duplicating the palette.

The exporter helps palette flash for GIF export.

MODE 3 Retailer

When switching between MODE 3 and MODE 4. The palettes of MODE 3 & 4 are separate, however palette operations on the identical CLUT. When altering mode 4 colours are apart to a short lived retailer, and changed when switching again. These values are additionally saved when saving SCREEN$ information (see “retailer” entries above), so you possibly can exchange the MODE 3 palette by loading a MODE 4 display screen. It is a bit odd.

We are able to ignore this for our conversions and simply write a default set of bytes.

Interrupts

Interrupts outline places on the display screen the place a given palette entry (0-15) adjustments to a unique coloration from the 128 coloration system palette. They’re encoded with 4 bytes per interrupt, with a number of interrupts appended one after one other.

Bytes Content material
1 Y place, saved as 172-y (see beneath)
1 Coloration to vary
1 Palette A
1 Palette B

Interrupt coordinates set from BASIC are calculated from -18 as much as 172 on the prime of the display screen. The plot vary in BASIC is definitely 0..173, however interrupts cannot have an effect on the primary pixel (which is sensible, since that is dealt with by way of the primary palette).

When saved within the file, line interrupts are saved as 172-y. For instance, a line interrupt at 150 is saved within the file as 22. The road interrupt nearest the highest of the display screen (1st row down, interrupt place 173) could be saved as 172-172=0.

This sounds difficult, however really signifies that to get our interrupt Y byte we are able to simply subtract 1 from the Y coordinate within the picture.

Changing Picture to SCREEN$

We now have all the data we have to convert a picture right into a SCREEN$ format. The difficult bit (and what takes a lot of the work) is optimising the position of the interrupts to maximise the variety of colours within the picture.

Pre-processing

Processing is completed utilizing Pillow package deal for Python. Enter photographs are resized and cropped to suit, utilizing utilizing the ImageOps.match() methodology, with centering.

python

SAM_COUPE_MODE4 = (256, 192, 16)
WIDTH, HEIGHT, MAX_COLORS = SAM_COUPE_MODE4

im = Picture.open(fn)

# Resize with crop to suit.
im = ImageOps.match(im, (WIDTH, HEIGHT), Picture.ANTIALIAS, 0, (0.5, 0.5))

If the above crop is dangerous, you possibly can alter it by pre-cropping/sizing the picture beforehand. There is not the choice to shrink with out cropping as any border space would waste a palette entry to fill the clean area.

Interrupts

That is the majority of the method for producing optimized photographs: the optimize methodology is proven beneath — this exhibits the excessive stage steps taken to succeed in optimum variety of colours utilizing interrupts to compress colours.

python

def optimize(im, max_colors, total_n_colors):
    """
    Makes an attempt to optimize the variety of colours within the display screen utilizing interrupts. The
    result's a dictionary of coloration areas, keyed by coloration quantity
    """
    optimal_n_colors = max_colors
    optimal_color_regions = {}
    optimal_total_interrupts = 0

    for n_colors in vary(max_colors, total_n_colors+1):
        # Determine coloration areas.
        color_regions = calculate_color_regions(im, n_colors)

        # Compress non-overlapping colours collectively.
        color_regions = compress_non_overlapping(color_regions)

        # Simplify our coloration areas.
        color_regions = simplify(color_regions)

        total_colors = len(color_regions)

        # Calculate dwelling many interrupts we're utilizing, size drop preliminary.
        _, interrupts = split_initial_colors(color_regions)
        total_interrupts = n_interrupts(interrupts)

        print("- making an attempt %d colours, with interrupts makes use of %d colours & %d interrupts" % (n_colors, total_colors, total_interrupts))

        if total_colors <= max_colors and total_interrupts <= MAX_INTERRUPTS:
            optimal_n_colors = n_colors
            optimal_color_regions = color_regions
            optimal_total_interrupts = total_interrupts
            proceed
        break

    print("Optimized to %d colours with %d interrupts (utilizing %d palette slots)" % (optimal_n_colors, optimal_total_interrupts, len(optimal_color_regions)))
    return optimal_n_colors, optimal_color_regions

The tactic accepts the picture to compress, a max_colors argument, which is the variety of colours supported by the display screen mode (16). This can be a decrease certain, the minimal variety of colours we should always be capable of get within the picture. The argument total_n_colors incorporates the whole variety of colours within the picture, capped at 128 — the variety of colours within the SAM palette. That is the higher certain, the utmost variety of colours we are able to use. If the total_n_colors < 16 we’ll skip optimization.

Every optimization spherical is as follows –

  • calculate_color_regions generates a dictionary of coloration areas within the picture. Every area is a (begin, finish) tuple of y positions within the picture the place a specific coloration is discovered. Every coloration will often have many blocks.
  • compress_non_overlapping takes colours with few blocks and tries to mix them with different colours with no overlapping areas: transitions between colours might be dealt with by interrupts
  • simplify takes the ensuing coloration areas and tries to simplify them additional, grouping blocks again with their very own colours if they will after which combining adjoining blocks
  • total_colors the size of the color_regions is now the variety of colours used
  • split_initial_colors removes the primary block, to get complete variety of interrupts

The compress_non_overlapping algorithm makes no effort to seek out the finest compression of areas – I experimented with this a bit and it simply explodes the variety of interrupts for little actual achieve in picture high quality.

The optimization course of is brute drive – step ahead, enhance the variety of colours by 1 and carry out the optimization steps above. If the variety of colours > 16 we have gone too far: we return the final profitable end result, with colours <= 16.

SAM Coupé Palette

As soon as we’ve the colours for the picture we map the picture over to the SAM Coupé palette. Each pixel within the picture should have a worth between 0-15 — pixels for colours managed by interrupts are mapped to their “dad or mum” coloration. Lastly, all the colours are mapped throughout from their RGB values to the closest SAM palette quantity equal.

That is sub-optimal, for the reason that selection of colours ought to actually learn by the colours obtainable. However I could not discover a approach to get Pillow to quantize to a hard and fast palette with out dithering.

The mapping is completed by calculating the space in RGB area for every coloration to every coloration within the SAM 128 coloration palette, utilizing the same old RGB coloration distance algorithm.

python

def convert_image_to_sam_palette(picture, colours=16):
    new_palette = []
    rgb = picture.getpalette()[:colors*3]
    for r, g, b in zip(rgb[::3], rgb[1::3], rgb[2::3]):

        def distance_to_color(o):
            return distance(o, (r, g, b))

        spalette = sorted(SAM_PALETTE, key=distance_to_color)
        new_palette.append(spalette[0])

    palette = [c for i in new_palette for c in i]
    picture.putpalette(palette)
    return picture

Packing bits

Now our picture incorporates pixels of values 0-15 we are able to pack the bits and export the info. we are able to iterate by way of the flattened information in steps of two, and pack right into a single byte:

python

pixels = np.array(image16)

image_data = []
pixel_data = pixels.flatten()
# Generate bytestream and palette; pack to 2 pixels/byte.
for a, b in zip(pixel_data[::2], pixel_data[1::2]):
    byte = (a << 4) | b
    image_data.append(byte)

image_data = bytearray(image_data)

The operation a << 4 shifts the bits of integer a left by 4, so 15 (00001111) turns into 240 (11110000), whereas | ORs the end result with b. If a = 0100 and b = 0011 the end result could be 01000011 with each values packed right into a single byte.

Writing the SCREEN$

Lastly, the picture information is written out, together with the palette information and line interrupts.

python

        # Extra 4 bytes 0, 17, 34, 127; mode 3 short-term retailer.
        bytes4 = b'x00x11x22x7F'

        with open(outfile, 'wb') as f:
            f.write(image_data)
            # Write palette.
            f.write(palette)

            # Write further bytes (4 bytes, 2nd palette, 4 bytes)
            f.write(bytes4)
            f.write(palette)
            f.write(bytes4)

            # Write line interrupts
            f.write(interrupts)

            # Write remaining byte.
            f.write(b'xff')

To truly view the end result, I like to recommend the SAM Coupé Superior Disk Supervisor.

You possibly can see the supply code for the img2sam converter on Github.

Examples

Beneath are some instance photographs, transformed from PNG/JPG supply photographs to SAM Coupé MODE 4 SCREEN$ and
then again into PNGs for show. The palette of every picture is restricted to the SAM Coupé’s 128
colours and colours are modified utilizing interrupts.

PoolPool 16 colours, no interrupts

PoolPool 24 colours, 12 interrupts (examine gradients)

This picture pair exhibits the impact on line interrupts on a picture with out dither. The separation between the in a different way coloured pool balls makes this candidate.

LeiaLeia 26 colours, 15 interrupts

TullyTully 22 colours, 15 interrupts

The separation between the helmet (blue, yellow elements) and horizontal line within the background
make this work out properly. Similar for the second picture of Tully beneath.

IslaIsla 18 colours, 6 interrupts

Tully (2)Tully (2) 18 colours, 5 interrupts

DanaDana 17 colours, 2 interrupts

Numerous photographs that do not compress effectively as a result of the identical shades are used all through the picture. That is made worse by the conversion to the SAM’s restricted palette of 128.

InterstellarInterstellar 17 colours, 3 interrupts

Blade RunnerBlade Runner 16 colours (11 used), 18 interrupts

This final picture does not handle to squeeze greater than 16 colours out of the picture,
however does scale back the variety of colours used for these 16 to only 11.
This offers you 5 spare colours so as to add one thing else to the picture.

Changing SCREEN$ to Picture

Included within the scrimage package deal is the sam2img converter, which is able to take a SAM MODE4 SCREEN$ and convert it to a picture. The conversion course of respects interrupts and when exporting to GIF will export flashing palettes as animations.

The pictures above have been all created utilizing sam2img on SCREEN$ created with img2sam. The next two GIFs are examples of export from SAM Coupe SCREEN$ with flashing palettes.

Flashing paletteFlashing palette

Flashing paletteFlashing palette and flashing Line interrupts

You possibly can see the supply code for the sam2img converter on Github.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments