Last active
September 13, 2022 08:02
-
-
Save mxwell/a04869c05fc6b491e9dc to your computer and use it in GitHub Desktop.
Script to convert a picture into ITU-R BT.656 frame.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Great job, I will try it in my project and will give you feed back
Thank you