diff --git a/alertwildfire_live_recording.py b/alertwildfire_live_recording.py index 17636e7..9e35044 100644 --- a/alertwildfire_live_recording.py +++ b/alertwildfire_live_recording.py @@ -39,28 +39,33 @@ import db_manager import hashlib import random - - +import OCR +import dateutil +import itertools def build_name_with_metadata(image_base_name,metadata): """reformats image name to include positional metadata Args: image_base_name (str): original image name containing only camera name and timestamp - metadata (dict): individual camera metadata pulled from alertwildfire_API.get_individual_camera_info + metadata (dict): individual camera metadata pulled from alertwildfire_API.get_individual_camera_info or OCR from image Returns: imgname (str): name of image with positionalmetadata """ - - cameraName_chunk = image_base_name[:-23] - metadata_chunk = 'p'+str(metadata['position']['pan'])+'_t'+str(metadata['position']['tilt'])+'_z'+str(metadata['position']['zoom']) - timeStamp_chunk = image_base_name[-23:-4]+'__' + cameraName_chunk = metadata['name']+"__" + metadata_chunk = 'p'+str(metadata['pan'])+'_t'+str(metadata['tilt'])+'_z'+str(metadata['zoom']) + if metadata['date']: + timeStamp_chunk = metadata['date'].replace('/','-')+'T'+metadata['time'].replace(':',';').split('.')[0]+'__' + else: + timeStamp_chunk = image_base_name[-23:-4]+'__' fileTag=image_base_name[-4:] imgname = cameraName_chunk+timeStamp_chunk+metadata_chunk+fileTag return imgname + + def capture_and_record(googleServices, dbManager, outputDir, camera_name): """requests current image from camera and uploads it to cloud Args: @@ -77,7 +82,7 @@ def capture_and_record(googleServices, dbManager, outputDir, camera_name): imgPath = alertwildfire_API.request_current_image(outputDir, camera_name) pull2 = alertwildfire_API.get_individual_camera_info(camera_name) - if pull1['position'] == pull1['position']: + if pull1['position'] == pull2['position']: success = True else: pull1 = pull2 @@ -94,15 +99,88 @@ def capture_and_record(googleServices, dbManager, outputDir, camera_name): image_base_name = pathlib.PurePath(imgPath).name - image_name_with_metadata = build_name_with_metadata(image_base_name,pull1) + #implement the ocr + vals = OCR.pull_metadata("Axis", filename = imgPath ).split() + logging.warning('values read by OCR %s',vals) + if len(vals) == 0: + logging.warning('OCR Failed image recorded using motor information') + + metadata = {key:None for key in ["name", "date", "time","timeStamp","pan","tilt","zoom"]} + """ format + metadata = { + "name" : camera_name, + "date" : [elem for elem in vals if elem.count("/") == 2][0], + "time" : [elem for elem in vals if elem.count(":") == 2][0], + "timeStamp" : #unix timestamp + "pan" : float([elem for elem in vals if "X:" in elem][0][2:]), + "tilt" : float([elem for elem in vals if "Y:" in elem][0][2:]), + "zoom" : float([elem for elem in vals if "Z:" in elem][0][2:]), + }""" + """ + common error cases in OCR recognition of Axis metadata + X= x,x,x,x,x + Y= v, v,V,V + Z= z,7,2,2,2,2,2,2,z,2,2,2,2,2,2,2,2,2 + .= -,:,_,(" "),(" "),(" "),(" "),(" "),(" "),(" "),(" ") + := i, -,1,1,;,1,1,2,(" "),(" "),(" "),(" "),(" "),(" ") + -= ~,_,r,(","),7,r,7,7,(" "),V,v,r,7,r,(" "),(" ") + +=- + ... more cases exist for natural letters + Note, cannot use cases where numbers and chars are mixed because there are likely inverse cases natural to the meta data i.e. Z:->2: which looks is found in 12:12:42 + """ + cases = {"pan" :[''.join(elem) for elem in list(itertools.product(['X','x'],[':','.','_']))], + "tilt" :[''.join(elem) for elem in list(itertools.product(['Y','y','V','v'],[':','.','_']))], + "zoom" :[''.join(elem) for elem in list(itertools.product(['Z','z'],[':','.','_']))], +} + status = 'ocrworking' + try: + metadata["name"] = camera_name + metadata["date"] = [elem for elem in vals if elem.count("/") == 2][0] + metadata["time"] = [elem for elem in vals if elem.count(":") == 2][0] + dt = dateutil.parser.parse(metadata['date']+'T'+metadata['time'].split('.')[0]) + metadata["timeStamp"] = time.mktime(dt.timetuple()) + except Exception as e: + status ='ocrfailure' + + + for key in ["pan","tilt","zoom"]: + if status == 'ocrfailure': + break + for attempts in cases[key]: + try: + metadata[key] = float([elem for elem in vals if attempts in elem][0][2:]) + break + except Exception as e: + try: + metadata[key] = float([elem for elem in vals if attempts in elem][0][3:]) * (pull1['position'][key]/np.absolute(pull1['position'][key])) + break + except Exception as e: + logging.warning('OCR Failed @ %s, attempt %s',key,attempts) + if metadata[key] == None: + status ='ocrfailure' + + for key in metadata.keys(): + if metadata[key] == None: + status = 'ocrfailure' + if status == 'ocrfailure':# revert to use of metdata + logging.warning('OCR Failed image recorded using motor information') + metadata = { + "name" : camera_name, + "date" : None, + "time" : None, + "pan" : pull1['position']['pan'], + "tilt" : pull1['position']['tilt'], + "zoom" : pull1['position']['zoom'], + } + metadata["timeStamp"] = img_archive.parseFilename(image_base_name)['unixTime'] + + image_name_with_metadata = build_name_with_metadata(image_base_name,metadata) cloud_file_path = 'alert_archive/' + camera_name + '/' + image_name_with_metadata goog_helper.uploadBucketObject(googleServices["storage"], settings.archive_storage_bucket, cloud_file_path, imgPath) - #add to Database - timeStamp = img_archive.parseFilename(image_base_name)['unixTime'] - img_archive.addImageToArchiveDb(dbManager, camera_name, timeStamp, 'gs://'+settings.archive_storage_bucket, cloud_file_path, pull1['position']['pan'], pull1['position']['tilt'], pull1['position']['zoom'], md5) - + #add to Database + img_archive.addImageToArchiveDb(dbManager, camera_name, metadata["timeStamp"], 'gs://'+settings.archive_storage_bucket, cloud_file_path, metadata['pan'], metadata['tilt'], metadata['zoom'], md5) @@ -171,7 +249,7 @@ def test_System_response_time(googleServices, dbManager, trial_length = 10): def main(): - """directs the funtionality of the process ie start a cleanup, record all cameras on 2min refresh, record a subset of cameras, manage multiprocessed recording of cameras + """directs the funtionality of the process ie start a cleanup, record all cameras on (2min refresh/rotating,1min cadence/stationary, record a subset of cameras, manage multiprocessed recording of cameras Args: -c cleaning_threshold" (flt): time in hours to store data -o cameras_overide (str): list of specific cameras to watch diff --git a/lib/OCR.py b/lib/OCR.py new file mode 100644 index 0000000..fea8f5b --- /dev/null +++ b/lib/OCR.py @@ -0,0 +1,227 @@ + +# Copyright 2018 The Fuego Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + + +import sys +import os +fuegoRoot = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(fuegoRoot, 'lib')) +sys.path.insert(0, fuegoRoot) +import settings +settings.fuegoRoot = fuegoRoot +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.image import imread +from PIL import Image +import tempfile +import math +import logging +import string +import pytesseract +import time + +#generalized OCR will attempt to compare img against all characters inclusive of +#UTF-8,etc. As both camera networks restrict their outputs to ASCII this improves the +#efficiency and accuracy of the OCR script by defining expected allowed characters +char_whitelist = string.digits +char_whitelist += string.ascii_lowercase +char_whitelist += string.ascii_uppercase +char_whitelist += string.punctuation.replace("'","").replace('"','') + + +def load_image( infilename ) : + """loads an image file to an array + Args: + infilename: file path + Returns: + numpy array of image data + """ + im = imread(infilename) + return np.array(im) + +def save_image( npdata, outfilename ) : + """saves an image file from an array + Args: + npdata: (array) numpy array of image data + outfilename: (str)strfile path + Returns: + None + """ + outimg = Image.fromarray( npdata, "RGB" ) + outimg.save( outfilename, format='JPEG' ) + + +def ocr_crop(image,outputname = None,maxHeight=60): + """saves an image file from an array + Args: + image (str): image path + opt outputname (str): save crop to address + opt maxHeight (int): maximum height to search for metadata else default 60 + Returns: + npdata (array): numpy array of cropped image data + bottom (int): height of bottom of metadata from image bottom + top (int):height of top of metadata from image bottom + """ + + cushionRows = 4 + minLetterBrightness = int(.8*255) # % of max value of 255 + minLetterSize = 12 + + img = Image.open(image) + assert maxHeight < img.size[1] + try: + imgCroppedGray = img.crop((1, img.size[1] - maxHeight, 2*minLetterSize, img.size[1])).convert('L')#possibility to apply to hprwen under simuliar parameters + croppedArray = np.array(imgCroppedGray) + + except Exception as e: + logging.error('Error processing image: %s', str(e)) + return + + top = 0 + bottom = 0 + mode = 'find_dark_below_bottom' + for i in range(maxHeight): + row = maxHeight - i - 1 + maxVal = croppedArray[row].max() + # logging.warning('Mode = %s: Top %d, Bottom %d, Max val for row %d is %d', mode, top, bottom, row, maxVal) + if mode == 'find_dark_below_bottom': + if maxVal < minLetterBrightness: + mode = 'find_bottom' + elif mode == 'find_bottom': + if maxVal >= minLetterBrightness: + mode = 'find_top' + bottom = row + elif mode == 'find_top': + if maxVal < minLetterBrightness: + possibleTop = row + if bottom - possibleTop > minLetterSize: + top = possibleTop + break + + if not top or not bottom: + logging.error('Unable to locate metadata') + return + + # row is last row with letters, so row +1 is first without letters, and add cushionRows + bottom = min(img.size[1] - maxHeight + bottom + 1 + cushionRows, img.size[1]) + # row is first row without letters, so subtract cushionRows + top = max(img.size[1] - maxHeight + top - cushionRows, img.size[1] - maxHeight) + + logging.warning('Top = %d, bottom = %d', top, bottom) + imgOut = img.crop((0, top, img.size[0], bottom)) + img.close() + if outputname: + imgOut.save(outputname, format='JPEG') + npdata = np.array(imgOut) + return npdata, bottom, top + + +def cut_metadata(im, camera_type): + """locates and cuts the metadata tag from image + Args: + im (str) : filepath + camera_type (str): {'hpwren','Axis','unknown'} defined type of image to remove metadata from. + Returns: + metadatastrip (array): numpy array containing only the presumed metadata line + """ + + if camera_type == 'unknown':#needs update + logging.warning('unkown has not been implemented yet') + return + + if camera_type == 'Axis': + #output = im[:-4]+"_cutout"+im[-4:] + maxHeight=60 + metadatastrip, metabottom, metatop = ocr_crop(im,maxHeight=maxHeight) + + + return metadatastrip + if camera_type == 'hpwren':#uses first order central difference gradient in 1-D to determine edges of text + im = load_image( im ) + index = 10 + xview =10 + + while 30>index: + pt1up = np.sum(im[index-1,:xview,:]) + pt1down =np.sum(im[index+1,:xview,:]) + if np.abs(.5*pt1down-.5*pt1up)>160*xview:#(np.sum(im[index,:xview,:]) <1000) and (np.sum(im[index+1,:xview,:]) <1000): + index=math.ceil(index*1.5)#index+=3#add a buffer for lower than average chars like g,j,q,p... + break + index+=1 + metadatastrip = im[:index,:,:] + return metadatastrip + return None + + + + + + + + + + + +def ocr_core(filename=None, data=None): + """ + This function will handle the core OCR processing of images. + Args: + opt filename (str) : filepath + opt data (array): data + Returns: + text (str): string of OCR recognized data + """ + if filename: + text = pytesseract.image_to_string(load_image( filename ),config="-c tessedit_char_whitelist=%s_-." % char_whitelist) + elif type(data) == np.ndarray: + text = pytesseract.image_to_string(data,config="-c tessedit_char_whitelist=%s_-." % char_whitelist) + else: + logging.warning('Please feed in processable data to ocr_core of type filename or data') + return + return text + + + +def pull_metadata(camera_type,filename = None, save_location=False): + """ function to separate metadata from image + Args: + opt filename (str) : filepath + camera_type (str): {'hpwren','Axis','unknown'} defined type of image to remove metadata from. + opt save_location (str): filepath to save metadata strip to + Returns: + vals (list): list of OCR recognized data + """ + if not filename: + logging.warning('specify data location of data itself') + return + tic=time.time() + metadata = cut_metadata(filename,camera_type) + logging.warning('time to complete cropping: %s',time.time()-tic) + try: + tic=time.time() + vals = ocr_core(data = metadata) + logging.warning('time to complete OCR: %s',time.time()-tic) + except Exception as e: + vals = '' + if save_location: + save_image(metadata,save_location) + logging.warning('metadata strip saved to location, %s',save_location) + return vals + + + + + diff --git a/lib/test_OCR1.jpg b/lib/test_OCR1.jpg new file mode 100644 index 0000000..f0e08fb Binary files /dev/null and b/lib/test_OCR1.jpg differ diff --git a/lib/test_OCR2.jpg b/lib/test_OCR2.jpg new file mode 100644 index 0000000..2a0aff5 Binary files /dev/null and b/lib/test_OCR2.jpg differ diff --git a/lib/test_ocr.py b/lib/test_ocr.py new file mode 100644 index 0000000..34d234c --- /dev/null +++ b/lib/test_ocr.py @@ -0,0 +1,103 @@ +# Copyright 2018 The Fuego Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== + + +import sys +import os +fuegoRoot = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(fuegoRoot, 'lib')) +sys.path.insert(0, fuegoRoot) +import settings +settings.fuegoRoot = fuegoRoot +import numpy as np +import logging +import OCR + +assert os.path.isfile('./test_OCR1.jpg') +assert os.path.isfile('./test_OCR2.jpg') + +path = tempfile.TemporaryDirectory() +temporaryDir.name + assert os.path.isfile(imgPath) + shutil.rmtree(temporaryDir.name) + +def test_load_image(): + #load_image( infilename ) + testdata = OCR.load_image('test_OCR1.jpg') + assert type(metadatastrip) == type(np.array([])) + +def test_save_image(): + #save_image( npdata, outfilename ) + testdata = OCR.load_image('test_OCR1.jpg') + OCR.save_image( testdata, path+'test.jpg' ) + assert os.path.isfile(path+'/test.jpg') + os.remove(path+'/test.jpg') + assert not os.path.isfile(path+'/test.jpg') + +def test_ocr_crop(): + #ocr_crop(image,outputname = None,maxHeight=60) + metadatastrip, metabottom, metatop = OCR.ocr_crop('test_OCR1.jpg', outputname = path+'test.jpg',maxHeight=60) + assert type(metadatastrip)==type(np.array([])) + assert type(metabottom)==type(1) + assert type(metatop)==type(1) + assert metatop-metabottom<20 + assert os.path.isfile(path+'/test.jpg') + +def test_cut_metadata(): + #cut_metadata(im, camera_type) + metadata = OCR.cut_metadata('test_OCR1.jpg', 'Axis') + assert metadata.shape[0]<20 + metadata = OCR.cut_metadata('test_OCR2.jpg', 'hpwren') + assert metadata.shape[0]<20 + +def test_iden(filename,cam_type): + """test function to assess the capability of the metadata location and cropping + Args: + filename (str) : filepath + camera_type (str): {'hpwren','Axis','unknown'} defined type of image to remove metadata from. + Returns: + saved_file_name (str): name of cropped image + toc (flt): time taken to perform metadatacrop + """ + tic=time.time() + metadata = OCR.cut_metadata(filename,cam_type) + toc = time.time()-tic + saved_file_name = filename[:-4]+"_cutout"+filename[-4:] + OCR.save_image(metadata,saved_file_name) + + + logging.warning('time taken to cut metadata %s',toc) + return saved_file_name, toc + +def test_ocr_core(): + #ocr_core(filename=None, data=None) + testdata = OCR.load_image(path+'test.jpg') + testfile = path+'/test.jpg' + OCR.save_image( testdata, testfile) + text = OCR.ocr_core( data=testdata ) + assert type(text )==type('') + text = OCR.ocr_core(filename=testfile) + assert type(text )==type('') + os.remove(path+'/test.jpg') + assert not os.path.isfile(path+'/test.jpg') + +def test_pull_metadata(): + #pull_metadata(camera_type,filename = None, save_location=False) + testfile = 'test_OCR1.jpg' + vals = OCR.pull_metadata('Axis',filename = testfile, save_location=False) + assert type(vals)==type('') + + +