Public
Edited
Jun 26, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function step1Gray(src, dst, data=null, do_desc=false) {
if (do_desc) {
return "Convert the image to grayscale, since following image processing steps require a grayscale image."
}
cv.cvtColor(src, dst, cv.COLOR_RGB2GRAY, 0)
}
Insert cell
function step2Thresh(src, dst, data=null, do_desc=false) {
let thresh = 10
if (do_desc) {
return `Threshold the image. Pixel values range from 0 (black) to 255 (white). This threshold sets every pixel to 0 if it is below the threshold (${255-thresh}), otherwise it sets it to 255. This step isolates the film strips from the background.`
}
cv.bitwise_not(src, dst)
cv.threshold(src, dst, thresh, 255, cv.THRESH_BINARY)
cv.bitwise_not(src, dst)
}
Insert cell
function step3Dilate(src, dst, data=null, do_desc=false) {
if (do_desc) {
return `Clean up the edges of the thresholded image by performing a "dilate" operation.`
}
const ksize = new cv.Size(3,3)
const kernel = cv.getStructuringElement(cv.MORPH_CROSS, ksize)
cv.dilate(src, dst, kernel)
kernel.delete()
}
Insert cell
function step4Contour(src, dst, conts, do_desc=false) {
if (do_desc) {
return "Find contours. Contours are islands of white pixels surrounded by black pixels or the image edge. Here they are highlighted in red. Our goal is to find the film perforations (perfs), which will allow us to find the image frames on the film. This step finds the perfs, but also finds some other contours that we will need to filter out."
}
const hier = new cv.Mat()
cv.findContours(src, conts, hier, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)

cv.cvtColor(src, dst, cv.COLOR_GRAY2RGB, 0)
cv.drawContours(dst, conts, -1, [255,0,0,0], dim('line_weight'), cv.LINE_AA)
hier.delete()
}
Insert cell
function step5Perfs(src, dst, conts, do_desc=false) {
if (do_desc) {
return "Select the contours for the film perforations. Since 110 film perforations are a standard size and aspect ratio, we can measure each contour and filter out contours that do not meet the metrics for film perforations. Perforations are highlighted in green."
}
let perfs = []
for (let i = 0; i < conts.size(); ++i) {
let cont = conts.get(i)
if (isPerf(cont)) {
perfs.push({
'com': getCOM(cont),
'cont': cont
})
}
}

let perf_conts = new cv.MatVector()
contsFromPerfs(perfs, perf_conts)
cv.drawContours(dst, perf_conts, -1, [0,255,0,0], dim('line_weight'), cv.LINE_AA)
perf_conts.delete()

return perfs
}
Insert cell
function step6StripPerfs(src, dst, strip_perfs, do_desc=false) {
if (do_desc) {
return "Sort the perfs by strip and within each strip. The contour finding algorithm finds contours in no particular order. To make our cropping program useful, it should not only find each frame but output the frames in the correct order. Here we use an algorithm that relies on the strips being reasonably (but not perfectly) straight to associate perfs with strips. Once the perfs are associated with strips it is easy to sort them left-to-right, and to sort strips top-to-bottom."
}
for (let i = 0; i < strip_perfs.length; ++i) {
let strip = strip_perfs[i]
for (let j = 0; j < strip.length; ++j) {
let p = strip[j]
let x_or = p['com'].x-dim('frame_height')
let y_or = p['com'].y+dim('frame_height')/4
cv.putText(dst, `Strip ${i}`, {x: x_or, y:y_or}, cv.FONT_HERSHEY_SIMPLEX, dim('font_size'), [0,255,0,0], dim('line_weight'), cv.LINE_AA)
cv.putText(dst, `Perf ${j}`, {x: x_or, y:y_or+dim('frame_height')/4}, cv.FONT_HERSHEY_SIMPLEX, dim('font_size'), [0,255,0,0], dim('line_weight'), cv.LINE_AA)
}
}
}
Insert cell
function step7StripFrames(src, dst, strip_perfs, do_desc=false) {
if (do_desc) {
return "Draw lines between adjacent perforations along each strip. The perf positions tell us the orientation of each strip, allowing us to find frames without requiring perfectly straight strips. Drawing lines between each strip (cyan) is the first step towards building the rectangle that will surround each image frame on each strip."
}
let strip_frames = []
for (let i = 0; i < strip_perfs.length; ++i) {
let strip = strip_perfs[i]
for (let j = 1; j < strip.length; ++j) {
let c0 = strip[j-1]['com']
let c1 = strip[j]['com']
cv.line(dst, c0, c1, [0,255,255,0], dim('line_weight'), cv.LINE_AA)
let perfline = subtract(c1, c0)

// for the first perf, make another frame before it
let frame = null
if (j == 1) {
frame = makeValidFrame(subtract(c0, perfline), c0)
strip_frames.push(frame)
}

// make the frame between these two perfs
frame = makeValidFrame(c0, c1)
strip_frames.push(frame)

// make another frame after the last perf
if (j == strip.length-1) {
frame = makeValidFrame(c1, add(c1, perfline))
strip_frames.push(frame)
}
}
}
return strip_frames
}
Insert cell
function step8DrawFrames(src, dst, strip_frame_conts, do_desc=false) {
if (do_desc) {
return "Draw frames, including one extra frame at each end of each strip. We don't want this algorithm to require film strips to be cut a certain way, or require the scan to contain the strip boundaries. So we automatically add frames to the beginning and end of each strip, if there is enough image area. This step assumes that strips occupy roughly the entire width of the image, so it could add extra frames if the image is too wide."
}
cv.drawContours(src, strip_frame_conts, -1, [0,255,0,255], dim('line_weight'), cv.LINE_AA)
}
Insert cell
function step9LabelFrames(src, dst, strip_frames, do_desc=false) {
if (do_desc) {
return "Number the frames. Since we previously sorted the strips and perfs, this is straightfoward."
}
for (let i = 0; i < strip_frames.length; ++i) {
let f_com = strip_frames[i]['com']
let text_or = {x: f_com.x-dim('frame_height')*1.2, y: f_com.y}
let y_offset = {x: 0, y: dim('frame_height')/2}
cv.putText(src, `Frame ${i+1}`, add(text_or, dot(y_offset, 3)), cv.FONT_HERSHEY_SIMPLEX, dim('font_size'), [0,255,0,0], dim('line_weight'), cv.LINE_AA)
}
}
Insert cell
function step10SuperimposeFrames(src, dst, strip_frames, do_desc=false) {
if (do_desc) {
return "Frames (green) superimposed onto the original image. Note how each green rectangle contains one image frame, and how the image frame numbers (green) correspond to the numbers on the film strip itself."
}
src.copyTo(dst)
let strip_frame_conts = new cv.MatVector()
contsFromPerfs(strip_frames, strip_frame_conts)
cv.drawContours(dst, strip_frame_conts, -1, [0,255,0,255], dim('line_weight'), cv.LINE_AA)
for (let i = 0; i < strip_frames.length; ++i) {
let f_com = strip_frames[i]['com']
let text_or = {x: f_com.x-dim('frame_height')/3, y: f_com.y}
let y_offset = {x: 0, y: dim('frame_height')/4}
cv.putText(dst, `${i+1}`, add(text_or, dot(y_offset, 3)), cv.FONT_HERSHEY_SIMPLEX, dim('font_size')/1.7, [0,255,0,255], dim('line_weight'), cv.LINE_AA)
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more