Skip to content

Instantly share code, notes, and snippets.

@mxwell
Last active September 13, 2022 08:02
Show Gist options
  • Save mxwell/a04869c05fc6b491e9dc to your computer and use it in GitHub Desktop.
Save mxwell/a04869c05fc6b491e9dc to your computer and use it in GitHub Desktop.
Script to convert a picture into ITU-R BT.656 frame.
#! /usr/bin/python
###############################################################################
# Script converts an arbitrary picture into a single frame,
# compliant with ITU-R BT.656-5, see:
#
# http://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.656-5-200712-I!!PDF-E.pdf
#
# Tested on Ubuntu 12.04, Python 2.7.3
###############################################################################
from PIL import Image
import sys
THE_FIRST_LINE_ON_TOP = False
LINES, ACTIVE_LINES, TOTAL_SAMPLES = 625, 576, 864
ACTIVE_SAMPLES = 720
BLANKING_SAMPLES = TOTAL_SAMPLES - ACTIVE_SAMPLES - 2 * 2
PROTECTION_BITS = [0x0, 0xD, 0xB, 0x6, 0x7, 0xA, 0xC, 0x1]
FILLER = [0x80, 0x10]
def is_field_blanking(line):
if LINES == 625:
return line < 23 or (311 <= line < 336) or (624 <= line)
else:
return line < 20 or (264 <= line < 283)
def field_id(line):
if LINES == 625:
return 0 if line < 313 else 1
else:
return 0 if (4 <= line < 266) else 1
def get_protection_bits(field, vblank, hor):
index = hor | (vblank << 1) | (field << 2)
return PROTECTION_BITS[index]
def make_marker(line, hor):
field = field_id(line)
vblank = 1 if is_field_blanking(line) else 0
marker = ((1 << 7) | (field << 6) | (vblank << 5) | (hor << 4) |
get_protection_bits(field, vblank, hor))
return [0xFF, 0x00, 0x00, marker]
def make_sav(line):
return make_marker(line, 0)
def make_eav(line):
return make_marker(line, 1)
# converts digital RGB (components in 0-255) to
# ITU BT.601 digital Y'CbCr (Y in 16-235, Cb/Cr in 16-240)
#
# equations are from http://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
def rgb_to_ycbcr(rgb):
red, green, blue = rgb
luminance = 16 + (65.738 * red + 129.057 * green + 25.064 * blue) / 256
chrome_blue = 128 + (-37.945 * red - 74.494 * green + 112.439 * blue) / 256
chrome_red = 128 + (112.439 * red - 94.154 * green - 18.285 * blue) / 256
return (luminance, chrome_blue, chrome_red)
# returns a tuple - (y, cb, cr)
# @row in [1, ACTIVE_LINES]
# @column in [1, ACTIVE_SAMPLES]
def get_sample(row, column, image):
image_x = min(image.max_x, int((column - 1) * image.column_ratio))
image_y = min(image.max_y, int((row - 1) * image.row_ratio))
data = image.getpixel((image_x, image_y))
return rgb_to_ycbcr(data)
# order inside of a line: EAV, blank, SAV, active or blank
def insert_blanking_line(line):
result = []
result += make_eav(line)
result += FILLER * BLANKING_SAMPLES
result += make_sav(line)
result += FILLER * ACTIVE_SAMPLES
# check length
if len(result) != TOTAL_SAMPLES * 2:
print "Error in function 'insert_blanking_line'"
exit(1)
return result
def insert_active_line(line, active_line, image):
result = []
result += make_eav(line)
result += FILLER * BLANKING_SAMPLES
result += make_sav(line)
for sample in range(1, ACTIVE_SAMPLES + 1, 2):
luma0, chroma_b0, chroma_r0 = get_sample(active_line, sample, image)
luma1, chroma_b1, chroma_r1 = get_sample(active_line, sample + 1, image)
chroma_blue = (chroma_b0 + chroma_b1) / 2
chroma_red = (chroma_r0 + chroma_r1) / 2
if not (15 < luma0 < 236 and 15 < luma1 < 236
and 15 < chroma_blue < 241 and 15 < chroma_red < 241):
print "Error in function 'insert_active_line'"
exit(1)
result += [chroma_blue, luma0, chroma_red, luma1]
if len(result) != TOTAL_SAMPLES * 2:
print "Error in function 'insert_active_line'"
exit(1)
return result
def open_image(image_name):
try:
image = Image.open(image_name)
except IOError:
print "Cannot open image"
exit(1)
print "Image size %dx%d" % image.size
image.max_x = image.size[0] - 1
image.max_y = image.size[1] - 1
if image.size != (ACTIVE_SAMPLES, ACTIVE_LINES):
print (" image will be scaled to fit %dx%d" %
(ACTIVE_SAMPLES, ACTIVE_LINES))
image.column_ratio = float(image.size[0]) / ACTIVE_SAMPLES
image.row_ratio = float(image.size[1]) / ACTIVE_LINES
return image
def write_frame_as_text(frame, name, as_hex):
try:
output = open(name, 'wt')
except IOError:
print "Cannot open output file"
exit(1)
if as_hex:
sample_format = '%02X'
else:
sample_format = '%d'
lines = ['\n'.join((sample_format % sample) for sample in line) for line in frame]
output.writelines('\n'.join(lines))
print " bytes as decimal values are placed on separate lines"
output.close()
print "Frame is written as plain text file: " + name
def get_actual_line(active_line):
half = ACTIVE_LINES / 2
choice = active_line <= half
index = active_line if choice else (active_line - half)
if not THE_FIRST_LINE_ON_TOP:
choice = not choice
if choice:
return index * 2 - 1
else:
return index * 2
def main():
if len(sys.argv) < 2:
print "Usage: %s <picture> [<output>]" % sys.argv[0]
exit(0)
image = open_image(sys.argv[1])
frame = []
active_line = 1
previous_was_blank = True
for line in range(1, LINES + 1):
blank_line = is_field_blanking(line)
actual_line = -1
if not blank_line:
actual_line = get_actual_line(active_line)
if previous_was_blank:
print "\tline %d is actual line #%d" % (line, actual_line)
previous_was_blank = blank_line
if blank_line:
frame += [insert_blanking_line(line)]
else:
frame += [insert_active_line(line, actual_line, image)]
active_line += 1
print("Frame is generated: %d rows of %d bytes" %
(len(frame), len(frame[0])))
outname = sys.argv[1] + ".frame"
if len(sys.argv) > 2:
outname = sys.argv[2]
write_frame_as_text(frame, outname, False)
if __name__ == '__main__':
main()
@ABADY1000
Copy link

Great job, I will try it in my project and will give you feed back

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment