Generating MAME Bezel Layouts with Python

Eric Mulvihill
4 min readDec 8, 2017

--

I spent a few hours tweaking a nice little script to help me generate MAME bezel layouts for existing artwork more quickly using Python.

In case you don’t know what a MAME bezel is used for, take a look. Here’s our old friend Galaga, without any fancy effects or artwork:

Yeah “it’s Galaga” but it doesn’t really look like it did in the arcade. It’s sterile and boring. Here’s the same game, but with bezel artwork and some HLSL effects added:

Now that looks like the Galaga I remember!

Getting the HLSL effects in there is a whole different topic. But I have found hundreds, nay thousands, of these bezel artworks “around”. The problem is that most of them don’t come with MAME layout files, or they’re not correct.

The MAME .LAY File

The layout file is just a piece of XML that positions the emulated screen within the artwork in the correct location. It looks like this:

<mamelayout version="2">     
<element name="bezel">
<image file="galaga_bezel.png"/>
</element>
<view name="Bezel Artwork">
<screen index="0">
<bounds x="970" y="736" width="2040" height="2720"/>
</screen>
<bezel element="bezel">
<bounds x="0" y="0" width="4000" height="3713"/>
</bezel>
</view>
</mamelayout>

You can see that if you have hundreds of games, figuring out these coordinates is going to be, well, not fun or fast.

How does this script work to save me time?

You will edit the script to point it at your directory full of multiple Bezel image files. When the script runs, it will pop up each image in turn, and let you draw a rectangle where the screen is located within the image. Then hit the space bar, or any other key. It will generate the Layout XML and move that, along with the bezel art, into a folder named for the game. Then all you do is drop it into the MAME/artwork folder and presto, you have Bezel. I went through 400 bezels while listening to some trippy ambient music and it was quite relaxing.

OK Chatty Cathy, Where’s the Damn Script?

You can find it in my Git repository, along with a README. If you improve this script, please consider sending me a pull request.

It’s a small enough script so I will show it to you here. It requires Python 3.6.1+ with Numpy 1.13.1 & OpenCV 3.3.1+contrib as dependencies.

# import the necessary packages
import os
import numpy as np
import cv2
import csv
import shutil
dirName = "./arcade-bezel-overlays/"

MAX_DISPLAY_H = 1920
MAX_DISPLAY_V = 1080

xy1 = ()
xy2 = ()
bezelImage = np.array([])
gameName = ""
aspect = 1.0
mouseIsDown = False


def drawRect(img, xy_1, xy_2):
if xy_1 != () and xy_2 != ():
cv2.rectangle(img, xy_1, xy_2, (0, 255, 0), 2)


def makeTemplate(name, img, xy_1, xy_2):
width = img.shape[1]
height = img.shape[0]

template = f'''<!-- {name}.lay -->
<mamelayout version="2">
<element name="bezel">
<image file="{name}.png" />
</element>
<view name="Bezel Artwork">
<bezel element="bezel">
<bounds left="0" top="0" right="{width}" bottom="{height}" />
</bezel>
<screen index="0">
<bounds left="{xy_1[0]}" top="{xy_1[1]}" right="{xy_2[0]}" bottom="{xy_2[1]}" />
</screen>
</view>
</mamelayout>'''
return template


def parseAllGames():
games = {}
with open('resolutions.csv') as csvfile:
reader = csv.DictReader(csvfile, delimiter=";")
for row in reader:
games[str(row["game_name"])] = row
return games


def click_and_move(event, x, y, flags, param):
global xy1, xy2, mouseIsDown, gameName, bezelImage, aspect

if event == cv2.EVENT_LBUTTONDOWN:
xy1 = (x, y)
xy2 = ()
mouseIsDown = True

elif event == cv2.EVENT_LBUTTONUP:
xy2 = (x, y)
mouseIsDown = False

elif mouseIsDown and event == cv2.EVENT_MOUSEMOVE:
xa = int(xy1[0] + aspect * (y - xy1[1]))
# Hold down SHIFT to constrain to game aspect
xy2 = (xa, y) if flags & cv2.EVENT_FLAG_SHIFTKEY else (x, y)
# draw a rectangle around the region of interest
imgCopy = bezelImage.copy()
drawRect(imgCopy, xy1, xy2)
cv2.imshow(gameName, imgCopy)


def estimateRect(game, img):
global aspect

gw = float(game["video_x"])
gh = float(game["video_y"])
aspect = gw / gh

width = img.shape[1]
height = img.shape[0]

top = 0.025 * height
bottom = 0.975 * height

vspan = bottom - top
hspan = aspect * vspan

left = 0.5 * (width - hspan)
right = width - 0.5 * (width - hspan)

return (int(left), int(top)), (int(right), int(bottom))


def mainLoop():
global xy1, xy2, bezelImage, gameName

all_games = parseAllGames()

ld = os.listdir(dirName)
for filename in ld:
# print("opening " + dirName + filename)
gameName = str(os.path.splitext(filename)[0])
if gameName in all_games:
game = all_games[gameName]
bezelImage = cv2.imread(dirName + filename)
cv2.namedWindow(gameName, cv2.WINDOW_NORMAL)

width = min(MAX_DISPLAY_H, bezelImage.shape[1])
height = min(MAX_DISPLAY_V, bezelImage.shape[0])
cv2.resizeWindow(gameName, width, height)

xy1, xy2 = estimateRect(game, bezelImage)

imgCopy = bezelImage.copy()
drawRect(imgCopy, xy1, xy2)
cv2.imshow(gameName, imgCopy)

cv2.setMouseCallback(gameName, click_and_move)
cv2.waitKey(0)
template = makeTemplate(gameName, bezelImage, xy1, xy2)

os.makedirs(f'out/{gameName}', 0o777, True)
f = open(f'out/{gameName}/default.lay', 'w')
f.write(template)
f.close()
# shutil.copyfile(dirName + filename, f'out/{gameName}/{filename}')
shutil.move(dirName + filename, f'out/{gameName}/{filename}')
cv2.destroyAllWindows()
else:
print(f'WARNING: Could not locate {gameName} in list of all known MAME entries.')


mainLoop()

In Summary

I hope you find this useful, and please let me know what you think!

--

--

Eric Mulvihill

A software engineer who is interested in advances in computing, AI, automation and robotics.