├── .gitignore ├── README.md ├── digit_reader.py ├── image_selection.py ├── requirements.txt ├── sample_files ├── test.mov └── test.png └── seven_segment_ocr.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # seven-segment-ocr 2 | A program and set of Python modules to parse digits from videos of seven segment displays. This program does not operate on a trained model--rather, it takes a universal approach by taking three line profiles to efficiently and effectively determine which digit is shown. 3 | 4 | :eyes: I'm actively revisiting this library after many years (when the core of this was written) to improve code quality, testing, features, architecture, and more as this starts to get organic traction. 5 | 6 | ## Usage 7 | 8 | Simply supply a path to a video of a seven segment display and an interval to sample the digits at and an output file will be generated (by default out.csv) with the recognized digits at each sample interval. Before proceeding, the program first presents the first frame of the video and you must click and drag rectangular selections around the digits in the video and then press "d" when done or "r" to redraw rectangles. The program will then go though and recognize and parse the digits at each interval. 9 | 10 | ```bash 11 | python seven_segment_ocr.py --video VIDEO_PATH --output OUTPUT_DATA_FILE --period SAMPLE_PERIOD 12 | ``` 13 | 14 | This will bring up the first frame of the video where you can draw rectangles around the digits you want to parse throughout the video. When you're done selecting as many digits as you'd like, press "d" and then enter. To redraw rectangles press "r" instead of "d". 15 | 16 | ## Modules 17 | 18 | * `image_selection.py`: Functions to present the user an image and returns retangular ROIs the user selects (specifically `getSelectionsFromImage(img)`) 19 | * `digit_reader.py`: Functions to parse all the seven segment display digits (specified by retangular ROIs) and return the recognized igits (see `read_digits(image, roiPoints)`). 20 | * `seven_segment_ocr.py`: The main entry point for the program is here. Given a video path and a sample interval (in seconds), it gets ROIs from the user and parses digits out of the video at every sample interval. 21 | -------------------------------------------------------------------------------- /digit_reader.py: -------------------------------------------------------------------------------- 1 | """ 2 | digit_reader.py 3 | Module to extract and read seven-segment display digits from an 4 | ROI in an image. 5 | @author: Suyash Kumar 6 | """ 7 | import cv2 8 | import os 9 | import time 10 | import warnings 11 | warnings.filterwarnings("ignore") # Filter matplotlib warnings 12 | import matplotlib 13 | matplotlib.use('TkAgg') 14 | import matplotlib.pyplot as plt 15 | 16 | """ 17 | Parses environment variables to int, sets 18 | to default if they don't exist yet 19 | """ 20 | def environ_read(eVar): 21 | if(eVar): 22 | eVar = int(eVar) 23 | else: 24 | eVar = 0 # Set to default 25 | 26 | # Read Environment Vars safely 27 | dev = environ_read(os.environ.get('DEV')) 28 | demo = environ_read(os.environ.get('DEMO')) 29 | 30 | # Number Mapping: 31 | mapping = { 32 | "0101000": 1, 33 | "0110111": 2, 34 | "0101111": 3, 35 | "1101010": 4, 36 | "1001111": 5, 37 | "1011111": 6, 38 | "1011011": 6, 39 | "0101100": 7, 40 | "1101100": 7, 41 | "1111111": 8, 42 | "1101110": 9, 43 | "1101111": 9, 44 | "1111101": 0 45 | } 46 | 47 | def cropImage(image, roi): 48 | #print roi 49 | clone = image.copy() 50 | retImg = clone[roi[0][1]:roi[1][1], roi[0][0]:roi[1][0]] 51 | return retImg 52 | 53 | def line_profile(image, profType, loc): 54 | height, width = image.shape[:2] 55 | if (profType == "h"): 56 | return image[int(round(height*loc)):int(round(height*loc))+1, 0:width] 57 | elif(profType == "v"): 58 | return image[0:height, int(round(0.5*width)):int(round(0.5*width)+1)] 59 | 60 | def getProcessStringHoriz(arr): 61 | firstHalf = check_high(arr[0:int(round(len(arr)/2))]) 62 | lastHalf = check_high(arr[int(round(len(arr)/2)):len(arr)]) 63 | return str(firstHalf)+str(lastHalf) 64 | 65 | def getProcessStringVert(arr): 66 | firstQuarter = check_high(arr[0:int(round(len(arr)/4))]) 67 | middleHalf = check_high(arr[int(round(1*len(arr)/4)):int(round(3*len(arr)/4))]) 68 | lastQuarter = check_high(arr[int(3*round(len(arr)/4)):len(arr)]) 69 | return str(firstQuarter)+str(middleHalf)+str(lastQuarter) 70 | 71 | 72 | def check_high(arraySlice, N=8, threshold=100): 73 | arraySlice = arraySlice[5:] 74 | numInRow = 0 75 | maxInRow = 0 76 | for x in arraySlice: 77 | if (x>=threshold): 78 | numInRow = numInRow + 1 79 | else: 80 | if numInRow > maxInRow: 81 | maxInRow = numInRow 82 | numInRow = 0 83 | if (maxInRow > N or numInRow >N): 84 | return 1 85 | else: 86 | return 0 87 | 88 | 89 | """ 90 | Returns digit given a cropped grayscale image of the 7 segments. 91 | In the image 0 is signal and 255 is background 92 | """ 93 | def resolve_digit(croppedImage): 94 | #L1Coord = [(round(height*.25),round(height*.25)+1), (0,width)] 95 | L1 = line_profile(croppedImage, "h", 0.25) 96 | L2 = line_profile(croppedImage, "h", 0.75) 97 | L3 = line_profile(croppedImage, "v", 0.5) 98 | L1Arr = [int(x) for x in L1[0]] 99 | L2Arr = [int(x) for x in L2[0]] 100 | L3Arr = [int(x) for x in L3] 101 | 102 | #cv2.imshow("orig",croppedImage) 103 | processString = getProcessStringHoriz(L1Arr)+getProcessStringHoriz(L2Arr)+getProcessStringVert(L3Arr) 104 | digit = mapping.get(processString) 105 | if (digit is None): 106 | print "Digit not recognized: " + processString 107 | cv2.imshow("orig", croppedImage) 108 | digit = input("What digit is this? Enter here: ") 109 | cv2.waitKey(0) 110 | #cv2.waitKey(0) 111 | if (dev): 112 | print processString 113 | print mapping[processString] 114 | # Show images and line profiles: 115 | cv2.imshow("L1", L1) 116 | cv2.imshow("L2", L2) 117 | cv2.imshow("L3",L3) 118 | cv2.imshow("orig",croppedImage) 119 | 120 | plt.figure(1) 121 | plt.subplot(311) 122 | plt.plot(L1Arr) 123 | plt.title("L1") 124 | plt.subplot(312) 125 | plt.plot(L2Arr) 126 | plt.title("L2") 127 | plt.subplot(313) 128 | plt.plot(L3Arr) 129 | plt.title("L3") 130 | plt.show() 131 | 132 | cv2.waitKey(0) 133 | return digit 134 | 135 | def read_digits(image, roiPoints): 136 | digits = [] 137 | for selection in xrange(0,len(roiPoints)/2): 138 | currentSel = image.copy() 139 | currentSel = cv2.cvtColor(currentSel, cv2.COLOR_BGR2GRAY) # Convert to grayscale 140 | currentSel = cropImage(currentSel, [roiPoints[selection*2], roiPoints[2*selection+1]]) 141 | digit=resolve_digit(currentSel) 142 | digits.append(digit) 143 | if (demo): 144 | cv2.imshow("demo",currentSel) 145 | print digit 146 | cv2.waitKey(0) 147 | return digits 148 | -------------------------------------------------------------------------------- /image_selection.py: -------------------------------------------------------------------------------- 1 | """ 2 | image_selection.py 3 | This module gives the user an image to specify selections 4 | in and returns those selections 5 | @author: Suyash Kumar 6 | """ 7 | import cv2 8 | import sys 9 | 10 | # Shared variable declarations 11 | refPts = [] 12 | image=1 13 | numSelected=0 14 | 15 | """ 16 | Called every time a click event is fired on the displayed 17 | image. Is used to record ROI selections from the user, and 18 | updates the image to place a bounding box highlighting the 19 | ROIs 20 | """ 21 | def click_handler(event, x, y, flags, pram): 22 | global refPts, image, numSelected 23 | 24 | if(event == cv2.EVENT_LBUTTONDOWN): 25 | if(len(refPts)==0): 26 | refPts = [(x,y)] 27 | else: 28 | refPts.append((x,y)) 29 | elif (event == cv2.EVENT_LBUTTONUP): 30 | refPts.append((x,y)) 31 | cv2.rectangle(image, refPts[numSelected*2], refPts[(numSelected*2)+1], (0,255,0), 2, lineType=8) 32 | cv2.imshow("image",image) 33 | numSelected = numSelected+1 34 | 35 | 36 | def getSelectionsFromImage(img): 37 | global image, refPts, numSelected 38 | image = img 39 | refPts = [] # Reinit refPts 40 | clone = image.copy() 41 | cv2.namedWindow("image") 42 | cv2.setMouseCallback("image", click_handler) 43 | cv2.imshow("image",image) 44 | while True: 45 | #cv2.imshow("image", image) 46 | key = cv2.waitKey(1) & 0xFF 47 | if (key == ord("d")): 48 | break; 49 | if (key == ord("r")): 50 | refPts = refPts[:len(refPts)-2] 51 | numSelected = numSelected-1 52 | image = clone.copy() 53 | if ((len(refPts) % 2) == 0): 54 | print len(refPts)/2 55 | print refPts 56 | for selection in range(0, len(refPts)/2): 57 | roi = clone[refPts[0+(selection*2)][1]:refPts[1+(selection*2)][1], refPts[0+(2*selection)][0]:refPts[1+(2*selection)][0]] 58 | cv2.imshow("ROI"+str(selection), roi) 59 | else: 60 | sys.exit("Selection Capture didn't get an even number of bounding points.") 61 | 62 | cv2.waitKey(0) 63 | cv2.destroyAllWindows() 64 | return refPts 65 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | matplotlib==2.2.3 2 | opencv-python==3.4.2.17 3 | 4 | -------------------------------------------------------------------------------- /sample_files/test.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyashkumar/seven-segment-ocr/f60715df496e69386d7f5c9686f7d98fdbdd013d/sample_files/test.mov -------------------------------------------------------------------------------- /sample_files/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suyashkumar/seven-segment-ocr/f60715df496e69386d7f5c9686f7d98fdbdd013d/sample_files/test.png -------------------------------------------------------------------------------- /seven_segment_ocr.py: -------------------------------------------------------------------------------- 1 | """ 2 | seven-segment-ocr.py 3 | Extracts numbers from a video of a seven segment display and 4 | saves them to an output file. 5 | @author: Suyash Kumar 6 | """ 7 | import argparse 8 | import cv2 9 | import image_selection 10 | import digit_reader 11 | 12 | """ 13 | Reads selected digits at intervals specified by samplePeriod from. 14 | the video specified by videoPath. Returns a list containing lists 15 | of numbers read at each period. 16 | """ 17 | def read_video_digits(videoPath, samplePeriod): 18 | video = cv2.VideoCapture(videoPath) # Open video 19 | success,image = video.read() # Get first frame 20 | selections=image_selection.getSelectionsFromImage(image) 21 | fps = video.get(cv2.CAP_PROP_FPS) # Get FPS 22 | frameInterval = int(round(float(fps) * float(samplePeriod))) 23 | totalFrames = video.get(cv2.CAP_PROP_FRAME_COUNT) 24 | 25 | digitMatrix = [] # Holds list of dgits from each sample 26 | 27 | for frameNumber in xrange(0,int(round(totalFrames/frameInterval))): 28 | video.set(cv2.CAP_PROP_POS_FRAMES, frameNumber*frameInterval) # Set frame to read next 29 | success,image = video.read() # Get frame 30 | digits = digit_reader.read_digits(image, selections) # Get digits 31 | digitMatrix.append(digits) 32 | 33 | video.release() 34 | return digitMatrix 35 | 36 | def to_csv(digitsReadArray, outputFileName): 37 | with open(outputFileName,'w') as f: 38 | for sample in digitsReadArray: 39 | f.write(str(sample[0])+"."+str(sample[1])+str(sample[2])) 40 | f.write("\n") 41 | def to_file(digitsReadArray, outputFileName): 42 | with open(outputFileName,'w') as f: 43 | for sample in digitsReadArray: 44 | for digit in sample: 45 | f.write(str(digit)) 46 | f.write("\n") 47 | 48 | 49 | if __name__=="__main__": 50 | # Set up argument parsing: 51 | parser = argparse.ArgumentParser() 52 | parser.add_argument('--video', help = "Input Video File") 53 | parser.add_argument('--output', help = "Output data file", default="out.csv") 54 | parser.add_argument('--config', help = "How to format the digit output file.", default="none") 55 | parser.add_argument('--period', help = "Period (in seconds) to sample the video at", default=1) 56 | 57 | args = parser.parse_args() 58 | digitsReadArray = read_video_digits(args.video, args.period) 59 | if (args.config == "drok"): 60 | # Output digit data in the A.BC drok power meter configuration 61 | to_csv(digitsReadArray, args.output) 62 | else: 63 | # Output digit data to file 64 | to_file(digitsReadArray, args.output) 65 | --------------------------------------------------------------------------------