Can you copy a key from a photo? It turns out you can. I wanted to try it for myself and see if I could use my CNC milling machine to cut the key from a blank key purchased in a hardware store. The duplicated key worked perfectly on the first try. And yes, this guide is showing you how to break into my house.

Briefly

To duplicate the key, this guide will take you through each of the following steps:
  1. Calibrate the camera
  2. Photograph the key
  3. Trace the key outline
  4. Determine profile to be cut from blank key
  5. Create the toolpaths for the CNC machine.
  6. Cut the blank key

I used a Nikon D7000 camera with a Nikkor 16-85 mm lens set to 85 mm. After correcting lens distortion with OpenCV, I traced the key in Inkscape and exported a DXF drawing. SolidWorks and HSMWorks turned that drawing into machining toolpaths for an OmioCNC X8-2200 EPL mill.

Camera calibration

Camera calibration estimates how the camera and lens distort an image. I photographed a checkerboard at several distances and orientations, then used a Python script to find its corners and estimate the correction. Displaying the checkerboard on a computer screen worked for this experiment. The correction made little difference with this lens and setup, but can matter more with other lenses.

Photographing the key

Photo of key

Using the highest focal length I had access to (85mm) I took a photo of the target key with the smallest aperture setting on the camera. A high focal length was used to help minimise any lens distortion resulting from a wide field of view. The small aperture was used because it puts everything in focus and increases the exposure time. With a long exposure time I could move a light source around to reduce shadowing. An exposure time of about three seconds was used with the camera mounted on a tripod. The result is shown above.

Trace the key outline

The undistorted photo was imported into Inkscape and rescaled to match the dimensions of the actual key. In a way this was cheating as I measured the length of the target key using vernier callipers. If you don't have access to the physical key you could measure the blank key, or place a fiducial marker (such as a ruler) in the image at time of capture. The traced vector path was then exported from Inkscape as a DXF file for importing into SolidWorks.

Inkscape Screenshot

Determine Cutting Profile

Key template

This could be done either in the CAD package or Inkscape. For illustration purposes I've shown the cutting profile as created in Inkscape, but for creating the cutting profile this was done in SolidWorks. HSMWorks (a CAM package that runs inside SolidWorks) was used to generate the machining toolpaths. The first pass was a rough clearing operation to remove most of the material. The second pass was a contoured profile cut that left the desired contour.

Cut the blank key

To hold the key down, I cut its outline into a piece of MDF, with a recess beneath the area to be machined. I screwed a piece of wood over the top to hold the key in place. I had assumed that the blank and original key had the same dimensions. They didn't, which offset the cutting profile.

Key in CNC jig CNC cutting the key profile

Result

The new key's contour was smooth and closely followed the original. The alignment error is visible at the tip: the front key sticks out about a millimetre further. Despite that offset, it operated the lock, as the video shows.

Comparison of new key (dark blue) with original key (light blue)

Camera Calibration and Image Undistortion

The code used to calibrate the camera and undistort the photo of the key is given below. This code came straight from the OpenCV camera calibration tutorial so there is nothing special here. I have put it here purely for convenience.

import numpy as np
import cv2
import glob
import os
import sys

# Termination criteria
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)

# Prepare object points, like (0,0,0), (1,0,0), (2,0,0) ....,(6,5,0)
objp = np.zeros((6*8, 3), np.float32)
objp[:, :2] = np.mgrid[0:8, 0:6].T.reshape(-1, 2)

# Arrays to store object points and image points from all the images.
objpoints = []  # 3d point in real world space
imgpoints = []  # 2d points in image plane.

images = glob.glob('*.JPG')

for fname in images:
    print(fname)

for fname in images:
    img = cv2.imread(fname)
    print("Opened", fname)

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    print("Converted to grey")

    # Find the chess board corners
    ret, corners = cv2.findChessboardCorners(gray, (8, 6), None)

    # If found, add object points, image points (after refining them)
    if ret == True:
        print("Found corners")
        objpoints.append(objp)
        img1 = img.copy()
        img2 = img.copy()
        cv2.drawChessboardCorners(img1, (8, 6), corners, ret)
        cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
        cv2.drawChessboardCorners(img2, (8, 6), corners, ret)
        imgpoints.append(corners)
        cv2.imwrite('found/'+fname, img)
    else:
        cv2.imwrite('not_found/'+fname, img)
        print("Corners not found")

ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera(
    objpoints, imgpoints, gray.shape[::-1], None, None)

images = glob.glob('keys/*.JPG')

for fname in images:
    print(fname)

for fname in images:
    img = cv2.imread(fname)
    h, w = img.shape[:2]
    newcameramtx, roi = cv2.getOptimalNewCameraMatrix(
        mtx, dist, (w, h), 1, (w, h))

    # Undistort
    dst = cv2.undistort(img, mtx, dist, None, newcameramtx)

    # Crop the image
    x, y, w, h = roi
    dst = dst[y:y+h, x:x+w]
    cv2.imwrite(fname[:-4]+'_undistorted_shortest.JPG', dst)

    # Undistort
    mapx, mapy = cv2.initUndistortRectifyMap(
        mtx, dist, None, newcameramtx, (w, h), 5)
    dst = cv2.remap(img, mapx, mapy, cv2.INTER_LINEAR)

    # Crop the image
    x, y, w, h = roi
    dst = dst[y:y+h, x:x+w]
    cv2.imwrite(fname[:-4]+'_undistorted_remap.JPG', dst)

cv2.destroyAllWindows()