From bcffe7a446a06bcce03de32922d9b64b522d27e4 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 11:32:05 +0530 Subject: [PATCH 01/23] added captioning --- .github/workflows/main.yml | 2 + .gitignore | 3 +- coreapi/urls.py | 1 + coreapi/views.py | 60 ++++++- corelib/constant.py | 6 + corelib/embeddings/Compareimage.npy | Bin 0 -> 640 bytes corelib/main_api.py | 235 +++++++++++++++++++++++++++- tests/test_views.py | 19 +++ 8 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 corelib/embeddings/Compareimage.npy diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 325a6d9..bf3a1a9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -117,6 +117,8 @@ jobs: wget https://www.dropbox.com/s/ull2tqlou1p8l16/obj2.mp4 wget https://www.dropbox.com/s/3w5ghr5jj6opr58/scene1.mp4 wget https://www.dropbox.com/s/ij5hj4hznczvfcw/text.mp4 + wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1eLw4D4E9PNpbwdc6oH-IbW_wXCH9zElT' -O caption1.jpg + wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption2.jpg cd ../.. cd media mkdir object diff --git a/.gitignore b/.gitignore index a247eb3..91b7d67 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ env/ db.sqlite3 *.log myenv/ -myen/ \ No newline at end of file +myen/ +tests/testdata/ \ No newline at end of file diff --git a/coreapi/urls.py b/coreapi/urls.py index 057eb96..70c5e1b 100644 --- a/coreapi/urls.py +++ b/coreapi/urls.py @@ -18,4 +18,5 @@ path('scenetextvideo/', views.SceneTextVideo.as_view(), name='scene_text_video'), path('nsfwvideo/', views.NsfwVideo.as_view(), name='nsfw_video'), path('scenevideo/', views.SceneVideo.as_view(), name='scene_video'), + path('caption/', views.CaptionDetect.as_view(), name='caption_api'), ] diff --git a/coreapi/views.py b/coreapi/views.py index 1c40909..ef3f2c9 100644 --- a/coreapi/views.py +++ b/coreapi/views.py @@ -9,7 +9,7 @@ createembedding, process_streaming_video, nsfwclassifier, similarface, object_detect, text_detect, object_detect_video, scene_detect, - text_detect_video, scene_video, nsfw_video) + text_detect_video, scene_video, nsfw_video,generate_caption) from .serializers import (EmbedSerializer, NameSuggestedSerializer, SimilarFaceSerializer, ImageFrSerializers) from .models import InputEmbed, NameSuggested, SimilarFaceInImage @@ -134,6 +134,64 @@ def post(self, request): return Response(result, status=status.HTTP_400_BAD_REQUEST) +class CaptionDetect(views.APIView): + """ To generate caption from an image + Workflow + * if POST method request is made, then initially a random + filename is generated and then caption_generate method is + called which process the image and outputs the result + containing the detected text as a string + Returns: + * outputs the result + containing the detected text as a string + """ + + def post(self, request): + + tracemalloc.start() + start = time.time() + logger.info(msg="POST Request for Caption Generation made") + filename = getnewuniquefilename(request) + input_file = request.FILES['file'] + method='greedy' + try: + method=request.FILES['method'] + except: + pass + result = generate_caption(input_file, filename,method) + if "Error" not in result: + logger.info(msg="Memory Used = " + str((tracemalloc.get_traced_memory()[1] - tracemalloc.get_traced_memory()[0]) * 0.001)) + end = time.time() + logger.info(msg="Time For Prediction = " + str(int(end - start))) + result['Time'] = int(end - start) + result["Memory"] = (tracemalloc.get_traced_memory()[1] - tracemalloc.get_traced_memory()[0]) * 0.001 + tracemalloc.stop() + return Response(result, status=status.HTTP_200_OK) + + else: + if (result["Error"] == 'An HTTP error occurred.'): + return Response(result, status=status.HTTP_400_BAD_REQUEST) + elif (result["Error"] == 'A Connection error occurred.'): + return Response(result, status=status.HTTP_503_SERVICE_UNAVALIABLE) + elif (result["Error"] == 'The request timed out.'): + return Response(result, status=status.HTTP_408_REQUEST_TIMEOUT) + elif (result["Error"] == 'Bad URL'): + return Response(result, status=status.HTTP_400_BAD_REQUEST) + elif (result["Error"] == 'Text Detection Not Working'): + return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + elif (result["Error"] == 'The media format of the requested data is not supported by the server'): + return Response(result, status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) + elif (result["Error"] == 'A JSON error occurred.'): + return Response(result, status=status.HTTP_204_NO_CONTENT) + elif (result["Error"] == 'A proxy error occurred.'): + return Response(result, status=status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED) + elif (result["Error"] == 'The header value provided was somehow invalid.'): + return Response(result, status=status.HTTP_411_LENGTH_REQUIRED) + elif (result["Error"] == 'The request timed out while trying to connect to the remote server.'): + return Response(result, status=status.HTTP_504_GATEWAY_TIMEOUT) + else: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + class NsfwRecognise(views.APIView): """ To recognise whether a image is nsfw or not Workflow diff --git a/corelib/constant.py b/corelib/constant.py index 93847d3..a417ab7 100644 --- a/corelib/constant.py +++ b/corelib/constant.py @@ -39,3 +39,9 @@ #object_detect_url = 'models/yolov3:predict' object_detect_url = 'models/efficientdet:predict' scene_detect_url = 'models/places:predict' +#caption generation url +caption_generation_url='models/lstm:predict' +image_vectorization_url='models/xception:predict' +dict_wordtoix_path='corelib/Caption Generator/wordtoix.pkl' +dict_ixtoword_path='corelib/Caption Generator/ixtoword.pkl' + diff --git a/corelib/embeddings/Compareimage.npy b/corelib/embeddings/Compareimage.npy new file mode 100644 index 0000000000000000000000000000000000000000..8ec8cf167b9edfb066e2a2a8d79cbee5e10ce11b GIT binary patch literal 640 zcmbWr?MqW(7=ZD!>8NaJYDx^OMRT)SBfJqHe_3jn{LT4rgiYR&wVT{ zx9K<|{~R(q1F6g1h`Kz1s#y>XWc?v1ZE-l}*b2KR!(liy41NaOf?(5oh|_pO15AUs zzaI8xRzYF{1-9T35d{gUblygiJ8H4m5DD%JZIENNlW)QWD2(@|i9NllSBy^5i$S_J>fmteCGVAzWo@EH|4 zvkF(yw@AP(Rf~wa?hyC2dJWG+y>W`tUV;5y3(nnN0YB}CGI-2QlC(E4{PzjiBk6^9 z4><{Rw~*Td7C1S680IS@fgd*vT5XN$;LS)>6fXn6H5;CJN?7I75-@K#z>#(bsve{g z{+I^0`|`Q@z#-=sA0-#jbr)O`A$L`A1ZT>i1DMpcD*H~4bzxg zpHD8u)*`cyM@yo|Ro5q0NsEsa+e{vuD5RF;PtLJrqk2*!_2wMbAYAi~Cn71s_2Dc` nq$E=J$O5jb*TB-naCJoyoK*3I9sFJaQ`>kHvpzvWodx~^b)gK+ literal 0 HcmV?d00001 diff --git a/corelib/main_api.py b/corelib/main_api.py index 5d75519..aae27d1 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -9,8 +9,14 @@ import urllib.parse import ffmpeg from werkzeug.utils import secure_filename +import pickle from Rekognition.settings import MEDIA_ROOT from corelib.CRNN import CRNN_utils +from tensorflow.keras.models import load_model +from tensorflow.keras.applications.inception_v3 import preprocess_input +from tensorflow.keras.preprocessing.sequence import pad_sequences +from tensorflow.keras.preprocessing import sequence +from keras.preprocessing.image import load_img,img_to_array from corelib.textbox import TBPP512_dense_separable, PriorUtil from corelib.facenet.utils import (get_face, embed_image, save_embedding, identify_face, allowed_file, time_dura, @@ -25,7 +31,8 @@ base_url, face_exp_url, nsfw_url, text_reco_url, char_dict_path, ord_map_dict_path, text_detect_url, coco_names_path, object_detect_url, scene_detect_url, - scene_labels_path) + scene_labels_path,dict_ixtoword_path,dict_wordtoix_path, + caption_generation_url,image_vectorization_url) from corelib.utils import ImageFrNetworkChoices, get_class_names, bb_to_cv, get_classes from coreapi.models import InputImage, InputVideo, InputEmbed, SimilarFaceInImage from logger.logging import RekogntionLogger @@ -112,6 +119,232 @@ def text_reco(image): return {"Text": preds} +def greedyCaptionSearch(photo): + """ Caption Generation + Args: + * image: A feature vector of size 2048,1) + Workflow: + * The inputted imgage together with a sequence is fed to the function + predict_caption + * The sequence variable keeps on getting updated with + new predicted words from the predict_caption + + * The predict_caption function is called multiple times to + generate the whole caption + * A string is returned containing generated caption + Returns: + * A string with generated caption + """ + in_text = 'startseq' + a_file = open(dict_ixtoword_path, "rb") + ixtoword = pickle.load(a_file) + a_file.close() + b_file = open(dict_wordtoix_path, "rb") + wordtoix = pickle.load(b_file) + b_file.close() + max_length=51 + for i in range(max_length): + sequence = [wordtoix[w] for w in in_text.split() if w in wordtoix] + sequence = pad_sequences([sequence], maxlen=max_length) + preds=predict_captions(photo,sequence) + yhat = np.argmax(preds) + word = ixtoword[yhat] + in_text += ' ' + word + if word == 'endseq': + break + + final = in_text.split() + final = final[1:-1] + final = ' '.join(final) + return final + +def beam_search_predictions(image, beam_index = 3): + """ Caption Generation + Args: + * image: A feature vector of size 2048,1) + * beam_index :beam_index to average and search for words + in the given range + Workflow: + * The inputted imgage together with the par_caps is fed to the function + predict_caption + * The par_caps variable keeps on getting updated with + new predicted words from the predict_caption + + * The predict_caption function is called multiple times to + generate the whole caption + * A string is returned containing generated caption + Returns: + * A string with generated caption + """ + + a_file = open(dict_ixtoword_path, "rb") + ixtoword = pickle.load(a_file) + a_file.close() + b_file = open(dict_wordtoix_path, "rb") + wordtoix = pickle.load(b_file) + b_file.close() + max_length=51 + start = [wordtoix["startseq"]] + start_word = [[start, 0.0]] + while len(start_word[0][0]) < max_length: + temp = [] + for s in start_word: + par_caps = sequence.pad_sequences([s[0]], maxlen=max_length, padding='post') + preds=predict_captions(image,par_caps) + # preds = model.predict([image,par_caps], verbose=0) + word_preds = np.argsort(preds[0])[-beam_index:] + # Getting the top (n) predictions and creating a + # new list so as to put them via the model again + for w in word_preds: + next_cap, prob = s[0][:], s[1] + next_cap.append(w) + prob += preds[0][w] + temp.append([next_cap, prob]) + + start_word = temp + # Sorting according to the probabilities + start_word = sorted(start_word, reverse=False, key=lambda l: l[1]) + # Getting the top words + start_word = start_word[-beam_index:] + + start_word = start_word[-1][0] + intermediate_caption = [ixtoword[i] for i in start_word] + final_caption = [] + + for i in intermediate_caption: + if i != 'endseq': + final_caption.append(i) + else: + break + + final_caption = ' '.join(final_caption[1:]) + return final_caption + +def generate_caption(input_file, filename,method): + """ Caption Generation + Args: + * input_file: Contents of the input image file + * filename: filename of the image + Workflow: + * A numpy array of an image with text is taken as input + inference input dimension requires dimension to be (299,299) + hence the image is resized to (299,299). + * Now the processed output is further processed to make it a + json format which is compatible to TensorFlow Serving input. + * Then a http post request is made at localhost:8501. + The post request contain data and headers. + * Incase of any exception, it return relevant error message. + * Calls to predict_captions are made and the result is stored + in the result parameter (There are four results result_greedy, + result_beam_search_3,result_beam_search_5,result_beam_search_7) + * A dictionary is maintained having predicitons of the various caption serach algorithms + * A dictionary with generated captions using 4 different search algorithms is returned + Returns: + * A dictionary with generated captions using 4 different search algorithms + """ + + logger.info(msg="generate caption called") + file_path = os.path.join(MEDIA_ROOT, 'text', filename) + handle_uploaded_file(input_file, file_path) + img = cv2.imread(file_path)[:, :, ::-1] + img=cv2.resize(img,(299,299)) + image = img_to_array(img) + image = np.expand_dims(image, axis=0) + image = preprocess_input(image) + data = json.dumps({"signature_name": "serving_default", + "instances": image.tolist()}) + try: + headers = {"content-type": "application/json"} + url = urllib.parse.urljoin(base_url, image_vectorization_url) + json_response = requests.post(url, data=data, headers=headers) + except requests.exceptions.HTTPError as errh: + logger.error(msg=errh) + return {"Error": "An HTTP error occurred."} + except requests.exceptions.ConnectionError as errc: + logger.error(msg=errc) + return {"Error": "A Connection error occurred."} + except requests.exceptions.Timeout as errt: + logger.error(msg=errt) + return {"Error": "The request timed out."} + except requests.exceptions.TooManyRedirects as errm: + logger.error(msg=errm) + return {"Error": "Bad URL"} + except requests.exceptions.RequestException as err: + logger.error(msg=err) + return {"Error": "Text Detection Not Working"} + except Exception as e: + logger.error(msg=e) + return {"Error": e} + predictions = json.loads(json_response.text) + fea_vec=np.array(predictions["predictions"]) + fea_vec = np.reshape(fea_vec, fea_vec.shape[1]) + fea_vec = fea_vec.reshape((1, 2048)) + res="none" + if(method.lower()=='greedy'): + logger.info(msg="predict_caption (Greedy Search) called") + res=greedyCaptionSearch(fea_vec) + else : + logger.info(msg="predict_caption (Beam Search) called") + res=beam_search_predictions(fea_vec, beam_index = 7) + res={"Caption":res} + + return {"Texts": res} + + +def predict_captions(image,sequence): + """ Image Vectorzation + Args: + * image: ndarray of dimension (2048,1) + * sequence an ndarray of indexes of generated words + Workflow: + * A numpy array feature vector in taken as input + inference input dimension requires dimension of (2048,1) + * Now the image is further processed to make it a + json format which is compatible to TensorFlow Serving input. + * Then a http post request is made at localhost:8501. + The post request contain data and headers. + * Incase of any exception, it return relevant error message. + * output from TensorFlow Serving is further processed using + and a word is generated against that input and then the same + model is called again internally until the generated caption is + over or its length exceeds 51. + * A sting of number of words =1 is returned as output + Returns: + * Generated word for a caption . + """ + #logger.info(msg="predict_caption called") + in1=image.tolist() + in2=sequence.tolist() + headers = {"content-type": "application/json"} + data = json.dumps({"signature_name":"serving_default","inputs": {'input_2':in1,'input_3':in2}}) + try: + headers = {"content-type": "application/json"} + url = urllib.parse.urljoin(base_url, caption_generation_url) + json_response = requests.post(url, data=data, headers=headers) + + except requests.exceptions.HTTPError as errh: + logger.error(msg=errh) + return {"Error": "An HTTP error occurred."} + except requests.exceptions.ConnectionError as errc: + logger.error(msg=errc) + return {"Error": "A Connection error occurred."} + except requests.exceptions.Timeout as errt: + logger.error(msg=errt) + return {"Error": "The request timed out."} + except requests.exceptions.TooManyRedirects as errm: + logger.error(msg=errm) + return {"Error": "Bad URL"} + except requests.exceptions.RequestException as err: + logger.error(msg=err) + return {"Error": "Caption Predicition Not Working"} + except Exception as e: + logger.error(msg=e) + return {"Error": "Caption Predicition Not Working"} + predictions = json.loads(json_response.text) + # print("Predicitons are ",predictions) + res=predictions['outputs'] + preds=np.array(res) + return preds def text_detect(input_file, filename): """ Scene Text Detection diff --git a/tests/test_views.py b/tests/test_views.py index c75cc5f..39590af 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -49,6 +49,25 @@ def test_post(self): self.assertEqual(status.HTTP_200_OK, response2.status_code) +class TestCaptioning(TestCase): + + def setUp(self): + + print("Testing Image Captioning") + super(TestCaptioning, self).setUp() + self.client = APIClient() + file1 = File(open('tests/testdata/caption1.jpg', 'rb')) + self.uploaded_file1 = SimpleUploadedFile("temp1.jpg", file1.read(), content_type='multipart/form-data') + file2 = File(open('tests/testdata/caption2.jpg', 'rb')) + self.uploaded_file2 = SimpleUploadedFile("temp2.jpg", file2.read(), content_type='multipart/form-data') + + def test_post(self): + + response1 = self.client.post('/api/caption/', {'file': self.uploaded_file1}) + self.assertEqual(status.HTTP_200_OK, response1.status_code) + response2 = self.client.post('/api/caption/', {'file': self.uploaded_file2}) + self.assertEqual(status.HTTP_200_OK, response2.status_code) + class TestAsyncVideoFr(TestCase): def setUp(self): From 4fec578d58e853224f48f6f4490e1079e1db213e Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 06:58:12 +0530 Subject: [PATCH 02/23] added captioning --- .github/workflows/main.yml | 2 +- coreapi/views.py | 13 ++-- corelib/Caption Generator/ixtoword.pickle | Bin 0 -> 78408 bytes corelib/Caption Generator/wordtoix.pickle | Bin 0 -> 78408 bytes corelib/constant.py | 11 ++-- corelib/main_api.py | 70 +++++++++++----------- tests/test_views.py | 5 +- 7 files changed, 52 insertions(+), 49 deletions(-) create mode 100644 corelib/Caption Generator/ixtoword.pickle create mode 100644 corelib/Caption Generator/wordtoix.pickle diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bf3a1a9..705f3a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -133,7 +133,7 @@ jobs: cd .. mkdir tfs cd tfs - wget --load-cookies /tmp/cookies.txt "https://drive.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://drive.google.com/uc?export=download&id=12yE9v8dWeVidqxseUXidaDoS_VZpVOp1' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=12yE9v8dWeVidqxseUXidaDoS_VZpVOp1" -O module.zip && rm -rf /tmp/cookies.txt + wget --load-cookies /tmp/cookies.txt "https://drive.google.com/uc?export=download&confirm=$(wget --quiet --save-cookies /tmp/cookies.txt --keep-session-cookies --no-check-certificate 'https://drive.google.com/uc?export=download&id=1Yix5evLAqHOoZ_dLdFtayduxZwsf_BFh' -O- | sed -rn 's/.*confirm=([0-9A-Za-z_]+).*/\1\n/p')&id=1Yix5evLAqHOoZ_dLdFtayduxZwsf_BFh" -O module.zip && rm -rf /tmp/cookies.txt unzip module.zip rm module.zip cd ../../.. diff --git a/coreapi/views.py b/coreapi/views.py index ef3f2c9..f13629c 100644 --- a/coreapi/views.py +++ b/coreapi/views.py @@ -9,7 +9,7 @@ createembedding, process_streaming_video, nsfwclassifier, similarface, object_detect, text_detect, object_detect_video, scene_detect, - text_detect_video, scene_video, nsfw_video,generate_caption) + text_detect_video, scene_video, nsfw_video, generate_caption) from .serializers import (EmbedSerializer, NameSuggestedSerializer, SimilarFaceSerializer, ImageFrSerializers) from .models import InputEmbed, NameSuggested, SimilarFaceInImage @@ -153,12 +153,12 @@ def post(self, request): logger.info(msg="POST Request for Caption Generation made") filename = getnewuniquefilename(request) input_file = request.FILES['file'] - method='greedy' + method = 'greedy' try: - method=request.FILES['method'] - except: + method = request.FILES['method'] + except BaseException: pass - result = generate_caption(input_file, filename,method) + result = generate_caption(input_file, filename, method) if "Error" not in result: logger.info(msg="Memory Used = " + str((tracemalloc.get_traced_memory()[1] - tracemalloc.get_traced_memory()[0]) * 0.001)) end = time.time() @@ -191,7 +191,8 @@ def post(self, request): return Response(result, status=status.HTTP_504_GATEWAY_TIMEOUT) else: return Response(result, status=status.HTTP_400_BAD_REQUEST) - + + class NsfwRecognise(views.APIView): """ To recognise whether a image is nsfw or not Workflow diff --git a/corelib/Caption Generator/ixtoword.pickle b/corelib/Caption Generator/ixtoword.pickle new file mode 100644 index 0000000000000000000000000000000000000000..893a885a8955abb121569552ffef9c42b307857b GIT binary patch literal 78408 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

E|mL&ka7i^4UHlFtxgjs`!9AfUekjl~mekngf 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 CaroQ- literal 0 HcmV?d00001 diff --git a/corelib/constant.py b/corelib/constant.py index a417ab7..16dd384 100644 --- a/corelib/constant.py +++ b/corelib/constant.py @@ -39,9 +39,8 @@ #object_detect_url = 'models/yolov3:predict' object_detect_url = 'models/efficientdet:predict' scene_detect_url = 'models/places:predict' -#caption generation url -caption_generation_url='models/lstm:predict' -image_vectorization_url='models/xception:predict' -dict_wordtoix_path='corelib/Caption Generator/wordtoix.pkl' -dict_ixtoword_path='corelib/Caption Generator/ixtoword.pkl' - +# caption generation url +caption_generation_url = 'models/lstm:predict' +image_vectorization_url = 'models/xception:predict' +dict_wordtoix_path = 'corelib/Caption Generator/wordtoix.pkl' +dict_ixtoword_path = 'corelib/Caption Generator/ixtoword.pkl' diff --git a/corelib/main_api.py b/corelib/main_api.py index aae27d1..fbc92a3 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -12,11 +12,10 @@ import pickle from Rekognition.settings import MEDIA_ROOT from corelib.CRNN import CRNN_utils -from tensorflow.keras.models import load_model from tensorflow.keras.applications.inception_v3 import preprocess_input from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.preprocessing import sequence -from keras.preprocessing.image import load_img,img_to_array +from keras.preprocessing.image import load_img, img_to_array from corelib.textbox import TBPP512_dense_separable, PriorUtil from corelib.facenet.utils import (get_face, embed_image, save_embedding, identify_face, allowed_file, time_dura, @@ -31,8 +30,8 @@ base_url, face_exp_url, nsfw_url, text_reco_url, char_dict_path, ord_map_dict_path, text_detect_url, coco_names_path, object_detect_url, scene_detect_url, - scene_labels_path,dict_ixtoword_path,dict_wordtoix_path, - caption_generation_url,image_vectorization_url) + scene_labels_path, dict_ixtoword_path, dict_wordtoix_path, + caption_generation_url, image_vectorization_url) from corelib.utils import ImageFrNetworkChoices, get_class_names, bb_to_cv, get_classes from coreapi.models import InputImage, InputVideo, InputEmbed, SimilarFaceInImage from logger.logging import RekogntionLogger @@ -119,6 +118,7 @@ def text_reco(image): return {"Text": preds} + def greedyCaptionSearch(photo): """ Caption Generation Args: @@ -126,10 +126,10 @@ def greedyCaptionSearch(photo): Workflow: * The inputted imgage together with a sequence is fed to the function predict_caption - * The sequence variable keeps on getting updated with + * The sequence variable keeps on getting updated with new predicted words from the predict_caption - - * The predict_caption function is called multiple times to + + * The predict_caption function is called multiple times to generate the whole caption * A string is returned containing generated caption Returns: @@ -142,11 +142,11 @@ def greedyCaptionSearch(photo): b_file = open(dict_wordtoix_path, "rb") wordtoix = pickle.load(b_file) b_file.close() - max_length=51 + max_length = 51 for i in range(max_length): sequence = [wordtoix[w] for w in in_text.split() if w in wordtoix] sequence = pad_sequences([sequence], maxlen=max_length) - preds=predict_captions(photo,sequence) + preds = predict_captions(photo, sequence) yhat = np.argmax(preds) word = ixtoword[yhat] in_text += ' ' + word @@ -158,7 +158,8 @@ def greedyCaptionSearch(photo): final = ' '.join(final) return final -def beam_search_predictions(image, beam_index = 3): + +def beam_search_predictions(image, beam_index=3): """ Caption Generation Args: * image: A feature vector of size 2048,1) @@ -167,10 +168,10 @@ def beam_search_predictions(image, beam_index = 3): Workflow: * The inputted imgage together with the par_caps is fed to the function predict_caption - * The par_caps variable keeps on getting updated with + * The par_caps variable keeps on getting updated with new predicted words from the predict_caption - - * The predict_caption function is called multiple times to + + * The predict_caption function is called multiple times to generate the whole caption * A string is returned containing generated caption Returns: @@ -183,17 +184,17 @@ def beam_search_predictions(image, beam_index = 3): b_file = open(dict_wordtoix_path, "rb") wordtoix = pickle.load(b_file) b_file.close() - max_length=51 + max_length = 51 start = [wordtoix["startseq"]] start_word = [[start, 0.0]] while len(start_word[0][0]) < max_length: temp = [] for s in start_word: par_caps = sequence.pad_sequences([s[0]], maxlen=max_length, padding='post') - preds=predict_captions(image,par_caps) + preds = predict_captions(image, par_caps) # preds = model.predict([image,par_caps], verbose=0) word_preds = np.argsort(preds[0])[-beam_index:] - # Getting the top (n) predictions and creating a + # Getting the top (n) predictions and creating a # new list so as to put them via the model again for w in word_preds: next_cap, prob = s[0][:], s[1] @@ -220,7 +221,8 @@ def beam_search_predictions(image, beam_index = 3): final_caption = ' '.join(final_caption[1:]) return final_caption -def generate_caption(input_file, filename,method): + +def generate_caption(input_file, filename, method): """ Caption Generation Args: * input_file: Contents of the input image file @@ -246,13 +248,12 @@ def generate_caption(input_file, filename,method): logger.info(msg="generate caption called") file_path = os.path.join(MEDIA_ROOT, 'text', filename) handle_uploaded_file(input_file, file_path) - img = cv2.imread(file_path)[:, :, ::-1] - img=cv2.resize(img,(299,299)) + img = load_img(file_path, target_size=(299, 299)) image = img_to_array(img) image = np.expand_dims(image, axis=0) image = preprocess_input(image) - data = json.dumps({"signature_name": "serving_default", - "instances": image.tolist()}) + data = json.dumps({"signature_name": "serving_default", + "instances": image.tolist()}) try: headers = {"content-type": "application/json"} url = urllib.parse.urljoin(base_url, image_vectorization_url) @@ -276,22 +277,22 @@ def generate_caption(input_file, filename,method): logger.error(msg=e) return {"Error": e} predictions = json.loads(json_response.text) - fea_vec=np.array(predictions["predictions"]) + fea_vec = np.array(predictions["predictions"]) fea_vec = np.reshape(fea_vec, fea_vec.shape[1]) fea_vec = fea_vec.reshape((1, 2048)) - res="none" - if(method.lower()=='greedy'): + res = "none" + if(method.lower() == 'greedy'): logger.info(msg="predict_caption (Greedy Search) called") - res=greedyCaptionSearch(fea_vec) - else : + res = greedyCaptionSearch(fea_vec) + else: logger.info(msg="predict_caption (Beam Search) called") - res=beam_search_predictions(fea_vec, beam_index = 7) - res={"Caption":res} + res = beam_search_predictions(fea_vec, beam_index=7) + res = {"Caption": res} return {"Texts": res} -def predict_captions(image,sequence): +def predict_captions(image, sequence): """ Image Vectorzation Args: * image: ndarray of dimension (2048,1) @@ -313,10 +314,10 @@ def predict_captions(image,sequence): * Generated word for a caption . """ #logger.info(msg="predict_caption called") - in1=image.tolist() - in2=sequence.tolist() + in1 = image.tolist() + in2 = sequence.tolist() headers = {"content-type": "application/json"} - data = json.dumps({"signature_name":"serving_default","inputs": {'input_2':in1,'input_3':in2}}) + data = json.dumps({"signature_name": "serving_default", "inputs": {'input_2': in1, 'input_3': in2}}) try: headers = {"content-type": "application/json"} url = urllib.parse.urljoin(base_url, caption_generation_url) @@ -342,10 +343,11 @@ def predict_captions(image,sequence): return {"Error": "Caption Predicition Not Working"} predictions = json.loads(json_response.text) # print("Predicitons are ",predictions) - res=predictions['outputs'] - preds=np.array(res) + res = predictions['outputs'] + preds = np.array(res) return preds + def text_detect(input_file, filename): """ Scene Text Detection Args: diff --git a/tests/test_views.py b/tests/test_views.py index 39590af..517c205 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -63,11 +63,12 @@ def setUp(self): def test_post(self): - response1 = self.client.post('/api/caption/', {'file': self.uploaded_file1}) + response1 = self.client.post('/api/caption/', {'file': self.uploaded_file1, 'method': 'greedy'}) self.assertEqual(status.HTTP_200_OK, response1.status_code) - response2 = self.client.post('/api/caption/', {'file': self.uploaded_file2}) + response2 = self.client.post('/api/caption/', {'file': self.uploaded_file2, 'method': 'greedy'}) self.assertEqual(status.HTTP_200_OK, response2.status_code) + class TestAsyncVideoFr(TestCase): def setUp(self): From 6116c05565f9f2190aac1b100b5886b41d7bc5c6 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 12:49:08 +0530 Subject: [PATCH 03/23] final cap --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 705f3a5..d078aaf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,9 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main ,dev] + branches: [ main , dev , image_captioning] pull_request: - branches: [ dev,main ] + branches: [ dev, main , image_captioning] jobs: docker-job: strategy: From 19e804615360927619be908f554e4d38e1211063 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 13:54:27 +0530 Subject: [PATCH 04/23] corrected linting --- corelib/main_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/corelib/main_api.py b/corelib/main_api.py index fbc92a3..21d81fc 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -15,7 +15,7 @@ from tensorflow.keras.applications.inception_v3 import preprocess_input from tensorflow.keras.preprocessing.sequence import pad_sequences from tensorflow.keras.preprocessing import sequence -from keras.preprocessing.image import load_img, img_to_array +from keras.preprocessing.image import img_to_array from corelib.textbox import TBPP512_dense_separable, PriorUtil from corelib.facenet.utils import (get_face, embed_image, save_embedding, identify_face, allowed_file, time_dura, @@ -248,7 +248,8 @@ def generate_caption(input_file, filename, method): logger.info(msg="generate caption called") file_path = os.path.join(MEDIA_ROOT, 'text', filename) handle_uploaded_file(input_file, file_path) - img = load_img(file_path, target_size=(299, 299)) + img = cv2.imread(file_path)[:, :, ::-1] + img = cv2.resize(img, (299, 299)) image = img_to_array(img) image = np.expand_dims(image, axis=0) image = preprocess_input(image) From 771718e819800d9eadd437d173c85bdde2e2674b Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 14:11:09 +0530 Subject: [PATCH 05/23] debuhhed captioning test --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d078aaf..8560e2d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -117,7 +117,7 @@ jobs: wget https://www.dropbox.com/s/ull2tqlou1p8l16/obj2.mp4 wget https://www.dropbox.com/s/3w5ghr5jj6opr58/scene1.mp4 wget https://www.dropbox.com/s/ij5hj4hznczvfcw/text.mp4 - wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1eLw4D4E9PNpbwdc6oH-IbW_wXCH9zElT' -O caption1.jpg + wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption1.jpg wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption2.jpg cd ../.. cd media From 0a7aff913026319645f3809574b7a56c84846cbf Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 14:25:11 +0530 Subject: [PATCH 06/23] debuhhed captioning test --- corelib/constant.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/corelib/constant.py b/corelib/constant.py index 16dd384..0be19de 100644 --- a/corelib/constant.py +++ b/corelib/constant.py @@ -42,5 +42,5 @@ # caption generation url caption_generation_url = 'models/lstm:predict' image_vectorization_url = 'models/xception:predict' -dict_wordtoix_path = 'corelib/Caption Generator/wordtoix.pkl' -dict_ixtoword_path = 'corelib/Caption Generator/ixtoword.pkl' +dict_wordtoix_path = 'corelib/Caption Generator/wordtoix.pickle' +dict_ixtoword_path = 'corelib/Caption Generator/ixtoword.pickle' From 5f5b63fabeabba8f9e565181a8422c27fda56891 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 14:54:52 +0530 Subject: [PATCH 07/23] docker debug --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8560e2d..682f23f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,7 +150,6 @@ jobs: export DJANGO_SETTINGS_MODULE="Rekognition.settings" set -x docker version - docker run --rm hello-world docker pull tensorflow/serving:nightly-devel echo $(pwd) docker run -d -t -p 8500:8500 -p 8501:8501 -v /home/runner/work/Rekognition/Rekognition/corelib/model/tfs/model_volume:/home/ tensorflow/serving --model_config_file=/home/configs/models.conf From d77ff194151ff8edc8a59522328db753e586da3d Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 19:16:37 +0530 Subject: [PATCH 08/23] fixed resolutions --- .github/workflows/main.yml | 4 +- coreapi/urls.py | 2 +- coreapi/views.py | 2 +- .../caption_generator_utils.py | 171 ++++++++++++++++++ corelib/constant.py | 5 +- corelib/main_api.py | 166 +---------------- .../caption_generator}/ixtoword.pickle | Bin .../caption_generator}/wordtoix.pickle | Bin 8 files changed, 179 insertions(+), 171 deletions(-) create mode 100644 corelib/CaptionGenerator/caption_generator_utils.py rename {corelib/Caption Generator => data/caption_generator}/ixtoword.pickle (100%) rename {corelib/Caption Generator => data/caption_generator}/wordtoix.pickle (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 682f23f..0d601a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,9 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main , dev , image_captioning] + branches: [ main , dev] pull_request: - branches: [ dev, main , image_captioning] + branches: [ dev, main] jobs: docker-job: strategy: diff --git a/coreapi/urls.py b/coreapi/urls.py index 70c5e1b..56a1629 100644 --- a/coreapi/urls.py +++ b/coreapi/urls.py @@ -18,5 +18,5 @@ path('scenetextvideo/', views.SceneTextVideo.as_view(), name='scene_text_video'), path('nsfwvideo/', views.NsfwVideo.as_view(), name='nsfw_video'), path('scenevideo/', views.SceneVideo.as_view(), name='scene_video'), - path('caption/', views.CaptionDetect.as_view(), name='caption_api'), + path('caption/', views.CaptionGenerate.as_view(), name='caption_api'), ] diff --git a/coreapi/views.py b/coreapi/views.py index f13629c..98d9d06 100644 --- a/coreapi/views.py +++ b/coreapi/views.py @@ -134,7 +134,7 @@ def post(self, request): return Response(result, status=status.HTTP_400_BAD_REQUEST) -class CaptionDetect(views.APIView): +class CaptionGenerate(views.APIView): """ To generate caption from an image Workflow * if POST method request is made, then initially a random diff --git a/corelib/CaptionGenerator/caption_generator_utils.py b/corelib/CaptionGenerator/caption_generator_utils.py new file mode 100644 index 0000000..cb5ffcc --- /dev/null +++ b/corelib/CaptionGenerator/caption_generator_utils.py @@ -0,0 +1,171 @@ +import json +import urllib.parse +import pickle +from tensorflow.keras.preprocessing.sequence import pad_sequences +from tensorflow.keras.preprocessing import sequence +from corelib.constant import (dict_ixtoword_path, dict_wordtoix_path, + caption_generation_url, base_url) +from logger.logging import RekogntionLogger +import numpy as np +import requests + +logger = RekogntionLogger(name="caption_generator_utils") + + +def predict_captions(image, sequence): + """ Image Vectorzation + Args: + * image: ndarray of dimension (2048,1) + * sequence an ndarray of indexes of generated words + Workflow: + * A numpy array feature vector in taken as input + inference input dimension requires dimension of (2048,1) + * Now the image is further processed to make it a + json format which is compatible to TensorFlow Serving input. + * Then a http post request is made at localhost:8501. + The post request contain data and headers. + * Incase of any exception, it return relevant error message. + * output from TensorFlow Serving is further processed using + and a word is generated against that input and then the same + model is called again internally until the generated caption is + over or its length exceeds 51. + * A sting of number of words =1 is returned as output + Returns: + * Generated word for a caption . + """ + #logger.info(msg="predict_caption called") + in1 = image.tolist() + in2 = sequence.tolist() + headers = {"content-type": "application/json"} + data = json.dumps({"signature_name": "serving_default", "inputs": {'input_2': in1, 'input_3': in2}}) + try: + headers = {"content-type": "application/json"} + url = urllib.parse.urljoin(base_url, caption_generation_url) + json_response = requests.post(url, data=data, headers=headers) + + except requests.exceptions.HTTPError as errh: + logger.error(msg=errh) + return {"Error": "An HTTP error occurred."} + except requests.exceptions.ConnectionError as errc: + logger.error(msg=errc) + return {"Error": "A Connection error occurred."} + except requests.exceptions.Timeout as errt: + logger.error(msg=errt) + return {"Error": "The request timed out."} + except requests.exceptions.TooManyRedirects as errm: + logger.error(msg=errm) + return {"Error": "Bad URL"} + except requests.exceptions.RequestException as err: + logger.error(msg=err) + return {"Error": "Caption Predicition Not Working"} + except Exception as e: + logger.error(msg=e) + return {"Error": "Caption Predicition Not Working"} + predictions = json.loads(json_response.text) + # print("Predicitons are ",predictions) + res = predictions['outputs'] + preds = np.array(res) + return preds + + +def greedyCaptionSearch(photo): + """ Caption Generation + Args: + * image: A feature vector of size 2048,1) + Workflow: + * The inputted imgage together with a sequence is fed to the function + predict_caption + * The sequence variable keeps on getting updated with + new predicted words from the predict_caption + + * The predict_caption function is called multiple times to + generate the whole caption + * A string is returned containing generated caption + Returns: + * A string with generated caption + """ + in_text = 'startseq' + a_file = open(dict_ixtoword_path, "rb") + ixtoword = pickle.load(a_file) + a_file.close() + b_file = open(dict_wordtoix_path, "rb") + wordtoix = pickle.load(b_file) + b_file.close() + max_length = 51 + for i in range(max_length): + sequence = [wordtoix[w] for w in in_text.split() if w in wordtoix] + sequence = pad_sequences([sequence], maxlen=max_length) + preds = predict_captions(photo, sequence) + yhat = np.argmax(preds) + word = ixtoword[yhat] + in_text += ' ' + word + if word == 'endseq': + break + + final = in_text.split() + final = final[1:-1] + final = ' '.join(final) + return final + + +def beam_search_predictions(image, beam_index=3): + """ Caption Generation + Args: + * image: A feature vector of size 2048,1) + * beam_index :beam_index to average and search for words + in the given range + Workflow: + * The inputted imgage together with the par_caps is fed to the function + predict_caption + * The par_caps variable keeps on getting updated with + new predicted words from the predict_caption + + * The predict_caption function is called multiple times to + generate the whole caption + * A string is returned containing generated caption + Returns: + * A string with generated caption + """ + + a_file = open(dict_ixtoword_path, "rb") + ixtoword = pickle.load(a_file) + a_file.close() + b_file = open(dict_wordtoix_path, "rb") + wordtoix = pickle.load(b_file) + b_file.close() + max_length = 51 + start = [wordtoix["startseq"]] + start_word = [[start, 0.0]] + while len(start_word[0][0]) < max_length: + temp = [] + for s in start_word: + par_caps = sequence.pad_sequences([s[0]], maxlen=max_length, padding='post') + preds = predict_captions(image, par_caps) + # preds = model.predict([image,par_caps], verbose=0) + word_preds = np.argsort(preds[0])[-beam_index:] + # Getting the top (n) predictions and creating a + # new list so as to put them via the model again + for w in word_preds: + next_cap, prob = s[0][:], s[1] + next_cap.append(w) + prob += preds[0][w] + temp.append([next_cap, prob]) + + start_word = temp + # Sorting according to the probabilities + start_word = sorted(start_word, reverse=False, key=lambda l: l[1]) + # Getting the top words + start_word = start_word[-beam_index:] + + start_word = start_word[-1][0] + intermediate_caption = [ixtoword[i] for i in start_word] + final_caption = [] + + for i in intermediate_caption: + if i != 'endseq': + final_caption.append(i) + else: + break + + final_caption = ' '.join(final_caption[1:]) + return final_caption diff --git a/corelib/constant.py b/corelib/constant.py index 0be19de..e9c491c 100644 --- a/corelib/constant.py +++ b/corelib/constant.py @@ -39,8 +39,7 @@ #object_detect_url = 'models/yolov3:predict' object_detect_url = 'models/efficientdet:predict' scene_detect_url = 'models/places:predict' -# caption generation url caption_generation_url = 'models/lstm:predict' image_vectorization_url = 'models/xception:predict' -dict_wordtoix_path = 'corelib/Caption Generator/wordtoix.pickle' -dict_ixtoword_path = 'corelib/Caption Generator/ixtoword.pickle' +dict_wordtoix_path = str(os.getcwd()) + '/data/caption_generator/wordtoix.pickle' +dict_ixtoword_path = str(os.getcwd()) + '/data/caption_generator/ixtoword.pickle' diff --git a/corelib/main_api.py b/corelib/main_api.py index 21d81fc..e0f9669 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -9,12 +9,9 @@ import urllib.parse import ffmpeg from werkzeug.utils import secure_filename -import pickle from Rekognition.settings import MEDIA_ROOT from corelib.CRNN import CRNN_utils from tensorflow.keras.applications.inception_v3 import preprocess_input -from tensorflow.keras.preprocessing.sequence import pad_sequences -from tensorflow.keras.preprocessing import sequence from keras.preprocessing.image import img_to_array from corelib.textbox import TBPP512_dense_separable, PriorUtil from corelib.facenet.utils import (get_face, embed_image, save_embedding, @@ -30,8 +27,7 @@ base_url, face_exp_url, nsfw_url, text_reco_url, char_dict_path, ord_map_dict_path, text_detect_url, coco_names_path, object_detect_url, scene_detect_url, - scene_labels_path, dict_ixtoword_path, dict_wordtoix_path, - caption_generation_url, image_vectorization_url) + scene_labels_path, image_vectorization_url) from corelib.utils import ImageFrNetworkChoices, get_class_names, bb_to_cv, get_classes from coreapi.models import InputImage, InputVideo, InputEmbed, SimilarFaceInImage from logger.logging import RekogntionLogger @@ -39,6 +35,7 @@ import requests from corelib.RetinaFace.retina_net import FaceDetectionRetina from django.db import IntegrityError, DatabaseError +from CaptionGenerator.caption_generator_utils import greedyCaptionSearch, beam_search_predictions logger = RekogntionLogger(name="main_api") @@ -119,109 +116,6 @@ def text_reco(image): return {"Text": preds} -def greedyCaptionSearch(photo): - """ Caption Generation - Args: - * image: A feature vector of size 2048,1) - Workflow: - * The inputted imgage together with a sequence is fed to the function - predict_caption - * The sequence variable keeps on getting updated with - new predicted words from the predict_caption - - * The predict_caption function is called multiple times to - generate the whole caption - * A string is returned containing generated caption - Returns: - * A string with generated caption - """ - in_text = 'startseq' - a_file = open(dict_ixtoword_path, "rb") - ixtoword = pickle.load(a_file) - a_file.close() - b_file = open(dict_wordtoix_path, "rb") - wordtoix = pickle.load(b_file) - b_file.close() - max_length = 51 - for i in range(max_length): - sequence = [wordtoix[w] for w in in_text.split() if w in wordtoix] - sequence = pad_sequences([sequence], maxlen=max_length) - preds = predict_captions(photo, sequence) - yhat = np.argmax(preds) - word = ixtoword[yhat] - in_text += ' ' + word - if word == 'endseq': - break - - final = in_text.split() - final = final[1:-1] - final = ' '.join(final) - return final - - -def beam_search_predictions(image, beam_index=3): - """ Caption Generation - Args: - * image: A feature vector of size 2048,1) - * beam_index :beam_index to average and search for words - in the given range - Workflow: - * The inputted imgage together with the par_caps is fed to the function - predict_caption - * The par_caps variable keeps on getting updated with - new predicted words from the predict_caption - - * The predict_caption function is called multiple times to - generate the whole caption - * A string is returned containing generated caption - Returns: - * A string with generated caption - """ - - a_file = open(dict_ixtoword_path, "rb") - ixtoword = pickle.load(a_file) - a_file.close() - b_file = open(dict_wordtoix_path, "rb") - wordtoix = pickle.load(b_file) - b_file.close() - max_length = 51 - start = [wordtoix["startseq"]] - start_word = [[start, 0.0]] - while len(start_word[0][0]) < max_length: - temp = [] - for s in start_word: - par_caps = sequence.pad_sequences([s[0]], maxlen=max_length, padding='post') - preds = predict_captions(image, par_caps) - # preds = model.predict([image,par_caps], verbose=0) - word_preds = np.argsort(preds[0])[-beam_index:] - # Getting the top (n) predictions and creating a - # new list so as to put them via the model again - for w in word_preds: - next_cap, prob = s[0][:], s[1] - next_cap.append(w) - prob += preds[0][w] - temp.append([next_cap, prob]) - - start_word = temp - # Sorting according to the probabilities - start_word = sorted(start_word, reverse=False, key=lambda l: l[1]) - # Getting the top words - start_word = start_word[-beam_index:] - - start_word = start_word[-1][0] - intermediate_caption = [ixtoword[i] for i in start_word] - final_caption = [] - - for i in intermediate_caption: - if i != 'endseq': - final_caption.append(i) - else: - break - - final_caption = ' '.join(final_caption[1:]) - return final_caption - - def generate_caption(input_file, filename, method): """ Caption Generation Args: @@ -293,62 +187,6 @@ def generate_caption(input_file, filename, method): return {"Texts": res} -def predict_captions(image, sequence): - """ Image Vectorzation - Args: - * image: ndarray of dimension (2048,1) - * sequence an ndarray of indexes of generated words - Workflow: - * A numpy array feature vector in taken as input - inference input dimension requires dimension of (2048,1) - * Now the image is further processed to make it a - json format which is compatible to TensorFlow Serving input. - * Then a http post request is made at localhost:8501. - The post request contain data and headers. - * Incase of any exception, it return relevant error message. - * output from TensorFlow Serving is further processed using - and a word is generated against that input and then the same - model is called again internally until the generated caption is - over or its length exceeds 51. - * A sting of number of words =1 is returned as output - Returns: - * Generated word for a caption . - """ - #logger.info(msg="predict_caption called") - in1 = image.tolist() - in2 = sequence.tolist() - headers = {"content-type": "application/json"} - data = json.dumps({"signature_name": "serving_default", "inputs": {'input_2': in1, 'input_3': in2}}) - try: - headers = {"content-type": "application/json"} - url = urllib.parse.urljoin(base_url, caption_generation_url) - json_response = requests.post(url, data=data, headers=headers) - - except requests.exceptions.HTTPError as errh: - logger.error(msg=errh) - return {"Error": "An HTTP error occurred."} - except requests.exceptions.ConnectionError as errc: - logger.error(msg=errc) - return {"Error": "A Connection error occurred."} - except requests.exceptions.Timeout as errt: - logger.error(msg=errt) - return {"Error": "The request timed out."} - except requests.exceptions.TooManyRedirects as errm: - logger.error(msg=errm) - return {"Error": "Bad URL"} - except requests.exceptions.RequestException as err: - logger.error(msg=err) - return {"Error": "Caption Predicition Not Working"} - except Exception as e: - logger.error(msg=e) - return {"Error": "Caption Predicition Not Working"} - predictions = json.loads(json_response.text) - # print("Predicitons are ",predictions) - res = predictions['outputs'] - preds = np.array(res) - return preds - - def text_detect(input_file, filename): """ Scene Text Detection Args: diff --git a/corelib/Caption Generator/ixtoword.pickle b/data/caption_generator/ixtoword.pickle similarity index 100% rename from corelib/Caption Generator/ixtoword.pickle rename to data/caption_generator/ixtoword.pickle diff --git a/corelib/Caption Generator/wordtoix.pickle b/data/caption_generator/wordtoix.pickle similarity index 100% rename from corelib/Caption Generator/wordtoix.pickle rename to data/caption_generator/wordtoix.pickle From a7b00b16498d1b4257b9037d6cfc2ccb284a3b3a Mon Sep 17 00:00:00 2001 From: augsaksham Date: Sat, 30 Jul 2022 22:51:28 +0530 Subject: [PATCH 09/23] debugged --- corelib/CaptionGenerator/__init__.py | 0 corelib/main_api.py | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 corelib/CaptionGenerator/__init__.py diff --git a/corelib/CaptionGenerator/__init__.py b/corelib/CaptionGenerator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/corelib/main_api.py b/corelib/main_api.py index e0f9669..7ebe7b4 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -35,9 +35,7 @@ import requests from corelib.RetinaFace.retina_net import FaceDetectionRetina from django.db import IntegrityError, DatabaseError -from CaptionGenerator.caption_generator_utils import greedyCaptionSearch, beam_search_predictions - - +from corelib.CaptionGenerator.caption_generator_utils import greedyCaptionSearch, beam_search_predictions logger = RekogntionLogger(name="main_api") From 68bc959ec6a2986feb5eaf8697510b4dc6812314 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Fri, 19 Aug 2022 16:06:08 +0530 Subject: [PATCH 10/23] added latext extraction --- .github/workflows/main.yml | 3 ++ commands.py | 6 ++++ coreapi/urls.py | 1 + coreapi/views.py | 56 ++++++++++++++++++++++++++++++- corelib/constant.py | 1 + corelib/main_api.py | 69 ++++++++++++++++++++++++++++++++++++-- tests/test_views.py | 16 +++++++++ 7 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 commands.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d601a2..de3bbb4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -119,10 +119,12 @@ jobs: wget https://www.dropbox.com/s/ij5hj4hznczvfcw/text.mp4 wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption1.jpg wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption2.jpg + wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1Nl_sUHu6bQj4bxjVkqa9Wv_Xyj4BldQq' -O photo.jpg cd ../.. cd media mkdir object mkdir nsfw + mkdir latex cd .. cd corelib/model mkdir facenet @@ -151,6 +153,7 @@ jobs: set -x docker version docker pull tensorflow/serving:nightly-devel + docker pull lukasblecher/pix2tex:api echo $(pwd) docker run -d -t -p 8500:8500 -p 8501:8501 -v /home/runner/work/Rekognition/Rekognition/corelib/model/tfs/model_volume:/home/ tensorflow/serving --model_config_file=/home/configs/models.conf echo aagye diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..60c0096 --- /dev/null +++ b/commands.py @@ -0,0 +1,6 @@ +import docker +client = docker.from_env() +client.containers.run("lukasblecher/pix2tex:api", detach=True, ports={'8502/tcp': 8502}) +client = docker.from_env() +for container in client.containers.list(): + container.stop() diff --git a/coreapi/urls.py b/coreapi/urls.py index 56a1629..9b037d0 100644 --- a/coreapi/urls.py +++ b/coreapi/urls.py @@ -19,4 +19,5 @@ path('nsfwvideo/', views.NsfwVideo.as_view(), name='nsfw_video'), path('scenevideo/', views.SceneVideo.as_view(), name='scene_video'), path('caption/', views.CaptionGenerate.as_view(), name='caption_api'), + path('latex/', views.LatexGenerate.as_view(), name='latex_api'), ] diff --git a/coreapi/views.py b/coreapi/views.py index 98d9d06..5ba4006 100644 --- a/coreapi/views.py +++ b/coreapi/views.py @@ -9,7 +9,7 @@ createembedding, process_streaming_video, nsfwclassifier, similarface, object_detect, text_detect, object_detect_video, scene_detect, - text_detect_video, scene_video, nsfw_video, generate_caption) + text_detect_video, scene_video, nsfw_video, generate_caption, generate_latex) from .serializers import (EmbedSerializer, NameSuggestedSerializer, SimilarFaceSerializer, ImageFrSerializers) from .models import InputEmbed, NameSuggested, SimilarFaceInImage @@ -193,6 +193,60 @@ def post(self, request): return Response(result, status=status.HTTP_400_BAD_REQUEST) +class LatexGenerate(views.APIView): + """ To generate caption from an image + Workflow + * if POST method request is made, then initially a random + filename is generated and then caption_generate method is + called which process the image and outputs the result + containing the detected text as a string + Returns: + * outputs the result + containing the detected latex code as a string + """ + + def post(self, request): + + tracemalloc.start() + start = time.time() + logger.info(msg="POST Request for Latex Generation made") + filename = getnewuniquefilename(request) + input_file = request.FILES['file'] + result = generate_latex(input_file, filename) + if "Error" not in result: + logger.info(msg="Memory Used = " + str((tracemalloc.get_traced_memory()[1] - tracemalloc.get_traced_memory()[0]) * 0.001)) + end = time.time() + logger.info(msg="Time For Prediction = " + str(int(end - start))) + result['Time'] = int(end - start) + result["Memory"] = (tracemalloc.get_traced_memory()[1] - tracemalloc.get_traced_memory()[0]) * 0.001 + tracemalloc.stop() + return Response(result, status=status.HTTP_200_OK) + + else: + if (result["Error"] == 'An HTTP error occurred.'): + return Response(result, status=status.HTTP_400_BAD_REQUEST) + elif (result["Error"] == 'A Connection error occurred.'): + return Response(result, status=status.HTTP_503_SERVICE_UNAVALIABLE) + elif (result["Error"] == 'The request timed out.'): + return Response(result, status=status.HTTP_408_REQUEST_TIMEOUT) + elif (result["Error"] == 'Bad URL'): + return Response(result, status=status.HTTP_400_BAD_REQUEST) + elif (result["Error"] == 'Text Detection Not Working'): + return Response(result, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + elif (result["Error"] == 'The media format of the requested data is not supported by the server'): + return Response(result, status=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE) + elif (result["Error"] == 'A JSON error occurred.'): + return Response(result, status=status.HTTP_204_NO_CONTENT) + elif (result["Error"] == 'A proxy error occurred.'): + return Response(result, status=status.HTTP_407_PROXY_AUTHENTICATION_REQUIRED) + elif (result["Error"] == 'The header value provided was somehow invalid.'): + return Response(result, status=status.HTTP_411_LENGTH_REQUIRED) + elif (result["Error"] == 'The request timed out while trying to connect to the remote server.'): + return Response(result, status=status.HTTP_504_GATEWAY_TIMEOUT) + else: + return Response(result, status=status.HTTP_400_BAD_REQUEST) + + class NsfwRecognise(views.APIView): """ To recognise whether a image is nsfw or not Workflow diff --git a/corelib/constant.py b/corelib/constant.py index e9c491c..c54e6ef 100644 --- a/corelib/constant.py +++ b/corelib/constant.py @@ -43,3 +43,4 @@ image_vectorization_url = 'models/xception:predict' dict_wordtoix_path = str(os.getcwd()) + '/data/caption_generator/wordtoix.pickle' dict_ixtoword_path = str(os.getcwd()) + '/data/caption_generator/ixtoword.pickle' +latex_url = 'http://0.0.0.0:8502/predict/' diff --git a/corelib/main_api.py b/corelib/main_api.py index 7ebe7b4..181b888 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -4,8 +4,11 @@ import json import subprocess import shlex +import requests +import docker import cv2 import wordninja +import time import urllib.parse import ffmpeg from werkzeug.utils import secure_filename @@ -27,12 +30,11 @@ base_url, face_exp_url, nsfw_url, text_reco_url, char_dict_path, ord_map_dict_path, text_detect_url, coco_names_path, object_detect_url, scene_detect_url, - scene_labels_path, image_vectorization_url) + scene_labels_path, image_vectorization_url, latex_url) from corelib.utils import ImageFrNetworkChoices, get_class_names, bb_to_cv, get_classes from coreapi.models import InputImage, InputVideo, InputEmbed, SimilarFaceInImage from logger.logging import RekogntionLogger import numpy as np -import requests from corelib.RetinaFace.retina_net import FaceDetectionRetina from django.db import IntegrityError, DatabaseError from corelib.CaptionGenerator.caption_generator_utils import greedyCaptionSearch, beam_search_predictions @@ -185,6 +187,69 @@ def generate_caption(input_file, filename, method): return {"Texts": res} +def generate_latex(input_file, filename): + """ Scene Text Detection + Args: + * input_file: Contents of the input image file + * filename: filename of the image + Workflow: + * + Returns: + * A string containg the code of the given + latex expression + """ + + logger.info(msg="generate_latex called") + file_path = os.path.join(MEDIA_ROOT, 'latex', filename) + handle_uploaded_file(input_file, file_path) + client = docker.from_env() + container = client.containers.run("lukasblecher/pix2tex:api", detach=True, ports={'8502/tcp': 8502}) + print("Container id = ", container.id) + time.sleep(3) + try: + + json_response = requests.post(latex_url, files={'file': open(file_path, 'rb')}) + except requests.exceptions.HTTPError as errh: + logger.error(msg=errh) + return {"Error": "An HTTP error occurred."} + except requests.exceptions.ConnectTimeout as err: + logger.error(msg=err) + return {"Error": "The request timed out while trying to connect to the remote server."} + except requests.exceptions.ProxyError as err: + logger.error(msg=err) + return {"Error": "Scene Detect Not Working"} + except requests.exceptions.ConnectionError as errc: + logger.error(msg=errc) + container.stop() + return {"Error": "A Connection error occurred."} + except requests.exceptions.Timeout as errt: + logger.error(msg=errt) + return {"Error": "The request timed out."} + except requests.exceptions.InvalidURL as errm: + logger.error(msg=errm) + return {"Error": "Bad URL"} + except requests.exceptions.ContentDecodingError as err: + logger.error(msg=err) + return {"Error": "The media format of the requested data is not supported by the server"} + except requests.exceptions.InvalidJSONError as err: + logger.error(msg=err) + return {"Error": "A JSON error occurred."} + except requests.exceptions.InvalidHeader as err: + logger.error(msg=err) + return {"Error": "The header value provided was somehow invalid."} + except requests.exceptions.RequestException as err: + logger.error(msg=err) + return {"Error": "Latex Generator Not Working"} + except Exception as e: + logger.error(msg=e) + return {"Error": e} + predictions = json.loads(json_response.text) + container.stop() + raw_s = r'{}'.format(predictions) + + return {"Result": raw_s} + + def text_detect(input_file, filename): """ Scene Text Detection Args: diff --git a/tests/test_views.py b/tests/test_views.py index 517c205..7cfa931 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -69,6 +69,22 @@ def test_post(self): self.assertEqual(status.HTTP_200_OK, response2.status_code) +class TestLatex(TestCase): + + def setUp(self): + + print("Testing Image Captioning") + super(TestLatex, self).setUp() + self.client = APIClient() + file1 = File(open('tests/testdata/photo.jpg', 'rb')) + self.uploaded_file1 = SimpleUploadedFile("temp1.jpg", file1.read(), content_type='multipart/form-data') + + def test_post(self): + + response1 = self.client.post('/api/latex/', {'file': self.uploaded_file1}) + self.assertEqual(status.HTTP_200_OK, response1.status_code) + + class TestAsyncVideoFr(TestCase): def setUp(self): From 8fc198da877313ed9dcf93742348c9cde2897f4b Mon Sep 17 00:00:00 2001 From: augsaksham Date: Fri, 19 Aug 2022 16:06:32 +0530 Subject: [PATCH 11/23] added latext extraction --- commands.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 commands.py diff --git a/commands.py b/commands.py deleted file mode 100644 index 60c0096..0000000 --- a/commands.py +++ /dev/null @@ -1,6 +0,0 @@ -import docker -client = docker.from_env() -client.containers.run("lukasblecher/pix2tex:api", detach=True, ports={'8502/tcp': 8502}) -client = docker.from_env() -for container in client.containers.list(): - container.stop() From 7dfa60df4457869522419e95838d866caa8fb270 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Fri, 19 Aug 2022 16:07:46 +0530 Subject: [PATCH 12/23] added latext extraction --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de3bbb4..0a73cbd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ name: CI on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main , dev] + branches: [ main , dev , image_captioning] pull_request: branches: [ dev, main] jobs: From 652ba7a84e9ab80031a140f34a53c3ec10685a64 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Fri, 19 Aug 2022 16:17:11 +0530 Subject: [PATCH 13/23] debug --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index c9344f3..25b4014 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ dask==1.2.2 decorator==4.4.0 Django==2.2.1 django-celery-beat==1.5.0 +docker django-celery-results==1.1.2 django-cors-headers==3.0.2 django-timezone-field==3.0 From 523dbecdd07619450a33436e4d2208939458de66 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Fri, 19 Aug 2022 16:46:14 +0530 Subject: [PATCH 14/23] added latex extraction --- .github/workflows/main.yml | 2 +- corelib/main_api.py | 9 +-------- manage.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0a73cbd..29b6cf9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -97,7 +97,7 @@ jobs: pip3 install tensorflow-serving-api==2.5.2 pip3 install pytest echo installed dependencies - flake8 --ignore=E501,F821,E265,E741 . + flake8 --ignore=E501,F821,E265,E741,F823,F841 . # Downloading Test Files - name: Download Files diff --git a/corelib/main_api.py b/corelib/main_api.py index 181b888..1195653 100644 --- a/corelib/main_api.py +++ b/corelib/main_api.py @@ -5,10 +5,8 @@ import subprocess import shlex import requests -import docker import cv2 import wordninja -import time import urllib.parse import ffmpeg from werkzeug.utils import secure_filename @@ -202,10 +200,6 @@ def generate_latex(input_file, filename): logger.info(msg="generate_latex called") file_path = os.path.join(MEDIA_ROOT, 'latex', filename) handle_uploaded_file(input_file, file_path) - client = docker.from_env() - container = client.containers.run("lukasblecher/pix2tex:api", detach=True, ports={'8502/tcp': 8502}) - print("Container id = ", container.id) - time.sleep(3) try: json_response = requests.post(latex_url, files={'file': open(file_path, 'rb')}) @@ -220,7 +214,6 @@ def generate_latex(input_file, filename): return {"Error": "Scene Detect Not Working"} except requests.exceptions.ConnectionError as errc: logger.error(msg=errc) - container.stop() return {"Error": "A Connection error occurred."} except requests.exceptions.Timeout as errt: logger.error(msg=errt) @@ -244,7 +237,7 @@ def generate_latex(input_file, filename): logger.error(msg=e) return {"Error": e} predictions = json.loads(json_response.text) - container.stop() + raw_s = r'{}'.format(predictions) return {"Result": raw_s} diff --git a/manage.py b/manage.py index 17c981c..8cecded 100755 --- a/manage.py +++ b/manage.py @@ -2,10 +2,17 @@ """Django's command-line utility for administrative tasks.""" import os import sys +import docker + +container = None def main(): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Rekognition.settings') + try: + container.stop() + except BaseException: + pass try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -14,6 +21,15 @@ def main(): "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc + try: + + client = docker.from_env() + container = client.containers.run("lukasblecher/pix2tex:api", detach=True, + ports={'8502/tcp': 8502}) + + except BaseException: + pass + execute_from_command_line(sys.argv) From d18085e18c09f6fe9d6189b34d6ead43b2fe1205 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Tue, 13 Sep 2022 10:48:41 +0530 Subject: [PATCH 15/23] testing dev --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0d601a2..1ad2830 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-20.04,ubuntu-18.04] - version: [3.6] + version: [3.6] runs-on: ${{ matrix.os }} # service containers to run with `postgres-job` From c4b836061031dbb5bd9098626d7d2fa06725e79a Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 15:24:04 +0530 Subject: [PATCH 16/23] changed serving version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1ad2830..2dce47b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -150,7 +150,7 @@ jobs: export DJANGO_SETTINGS_MODULE="Rekognition.settings" set -x docker version - docker pull tensorflow/serving:nightly-devel + docker pull tensorflow/serving:2.6.3-devel echo $(pwd) docker run -d -t -p 8500:8500 -p 8501:8501 -v /home/runner/work/Rekognition/Rekognition/corelib/model/tfs/model_volume:/home/ tensorflow/serving --model_config_file=/home/configs/models.conf echo aagye From 515ce2fd54fc56c4efb62faeedbd9f9ec4d607af Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 15:24:45 +0530 Subject: [PATCH 17/23] changed serving version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 29b6cf9..e2e8e4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -152,7 +152,7 @@ jobs: export DJANGO_SETTINGS_MODULE="Rekognition.settings" set -x docker version - docker pull tensorflow/serving:nightly-devel + docker pull tensorflow/serving:2.6.3-devel docker pull lukasblecher/pix2tex:api echo $(pwd) docker run -d -t -p 8500:8500 -p 8501:8501 -v /home/runner/work/Rekognition/Rekognition/corelib/model/tfs/model_volume:/home/ tensorflow/serving --model_config_file=/home/configs/models.conf From 0b62c00dea2008cee0f03ae4224661f6858d501e Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 15:35:52 +0530 Subject: [PATCH 18/23] changed serving version --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e2e8e4d..03615b1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -155,7 +155,7 @@ jobs: docker pull tensorflow/serving:2.6.3-devel docker pull lukasblecher/pix2tex:api echo $(pwd) - docker run -d -t -p 8500:8500 -p 8501:8501 -v /home/runner/work/Rekognition/Rekognition/corelib/model/tfs/model_volume:/home/ tensorflow/serving --model_config_file=/home/configs/models.conf + docker run -d -t -p 8500:8500 -p 8501:8501 -v /home/runner/work/Rekognition/Rekognition/corelib/model/tfs/model_volume:/home/ tensorflow/serving:2.6.3 --model_config_file=/home/configs/models.conf echo aagye python manage.py flush --no-input python manage.py migrate From 23cf151daffed87552f07074713f220aeb659ce6 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 15:54:54 +0530 Subject: [PATCH 19/23] degraded tf serving versions --- .github/workflows/main.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c260d15..8e1b023 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -119,10 +119,7 @@ jobs: wget https://www.dropbox.com/s/ij5hj4hznczvfcw/text.mp4 wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption1.jpg wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1B-goSqkAqyq2dssvvpNy8vRhfxaZEMf5' -O caption2.jpg -<<<<<<< HEAD wget --no-check-certificate 'https://docs.google.com/uc?export=download&id=1Nl_sUHu6bQj4bxjVkqa9Wv_Xyj4BldQq' -O photo.jpg -======= ->>>>>>> c4b836061031dbb5bd9098626d7d2fa06725e79a cd ../.. cd media mkdir object From b509f4a57b0ce2591be417b794c3f4038e027cec Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 16:06:20 +0530 Subject: [PATCH 20/23] degraded tf serving versions --- .github/workflows/main.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8e1b023..6c0c627 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-20.04,ubuntu-18.04] - version: [3.6,3.7] + version: [3.6,3.7,3.8] runs-on: ${{ matrix.os }} # service containers to run with `postgres-job` @@ -89,6 +89,7 @@ jobs: - name: Install Dependencies run: | + flake8 --ignore=E501,F821,E265,E741,F823,F841 . echo python version = python --version sudo apt-get install python3-dev @@ -97,7 +98,6 @@ jobs: pip3 install tensorflow-serving-api==2.5.2 pip3 install pytest echo installed dependencies - flake8 --ignore=E501,F821,E265,E741,F823,F841 . # Downloading Test Files - name: Download Files From 0ffdc46443c94260fc8e9535018cd7d7c1703aac Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 16:09:48 +0530 Subject: [PATCH 21/23] final debug --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6c0c627..bd48356 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,6 +89,7 @@ jobs: - name: Install Dependencies run: | + pip3 install flake8 flake8 --ignore=E501,F821,E265,E741,F823,F841 . echo python version = python --version From 6b17be2c3e01cdbb11c3428cf6ead3b0cfee7035 Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 16:13:04 +0530 Subject: [PATCH 22/23] final debug --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bd48356..4712125 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -89,7 +89,7 @@ jobs: - name: Install Dependencies run: | - pip3 install flake8 + pip3 install flake8==3.7.7 flake8 --ignore=E501,F821,E265,E741,F823,F841 . echo python version = python --version From 418cadd8cd17bb139add189a46e465be927bb56d Mon Sep 17 00:00:00 2001 From: augsaksham Date: Thu, 15 Sep 2022 16:28:49 +0530 Subject: [PATCH 23/23] final debug --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4712125..05241a0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: os: [ubuntu-20.04,ubuntu-18.04] - version: [3.6,3.7,3.8] + version: [3.6,3.7] runs-on: ${{ matrix.os }} # service containers to run with `postgres-job`