Update app.py
Browse files
app.py
CHANGED
@@ -9,12 +9,8 @@ import cv2
|
|
9 |
import base64
|
10 |
import httpx
|
11 |
import logging
|
12 |
-
import traceback
|
13 |
import boto3
|
14 |
import base64
|
15 |
-
import urllib.parse
|
16 |
-
from google.cloud import firestore
|
17 |
-
from google.oauth2 import service_account
|
18 |
|
19 |
app = FastAPI()
|
20 |
|
@@ -40,8 +36,277 @@ class ImageData(BaseModel):
|
|
40 |
class UnprocessedImage(BaseModel):
|
41 |
sheetID: str
|
42 |
s3Url: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
45 |
# async def failure(sheetid):
|
46 |
# async with httpx.AsyncClient(follow_redirects=True) as client:
|
47 |
# response = await client.post(
|
@@ -102,21 +367,6 @@ class UnprocessedImage(BaseModel):
|
|
102 |
# failure(image_data['sheetID'])
|
103 |
# raise HTTPException(status_code=500, detail="Error from external API.")
|
104 |
|
105 |
-
@app.post("/redirect")
|
106 |
-
async def redirect_to_google_apps_script(request: Request):
|
107 |
-
# Get the body of the incoming request
|
108 |
-
body = await request.body()
|
109 |
-
|
110 |
-
async with httpx.AsyncClient(timeout=300.0, follow_redirects=True) as client: # Follow redirects
|
111 |
-
response = await client.post(
|
112 |
-
'https://script.google.com/macros/s/AKfycbw9_31pMwQVhJPezJ9qlMRCAamQKRREnun-tfT_TnS5TOwOVcfdgU1Y9xTpAv6bZuiKOA/exec',
|
113 |
-
data=body
|
114 |
-
)
|
115 |
-
if response.status_code != 200:
|
116 |
-
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
117 |
-
raise HTTPException(status_code=500, detail="Error from external API.")
|
118 |
-
return response.json()
|
119 |
-
|
120 |
# @app.post("/save-to-s3")
|
121 |
# async def save_to_s3(request: Request, background_tasks: BackgroundTasks):
|
122 |
# image_data = await request.json()
|
@@ -335,256 +585,6 @@ async def redirect_to_google_apps_script(request: Request):
|
|
335 |
|
336 |
# finally:
|
337 |
# logging.info("File processing completed")
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
|
343 |
# else:
|
344 |
-
# raise HTTPException(status_code=400, detail="Email address not provided.")
|
345 |
-
appsScriptsURL = 'https://script.google.com/macros/s/AKfycbw9_31pMwQVhJPezJ9qlMRCAamQKRREnun-tfT_TnS5TOwOVcfdgU1Y9xTpAv6bZuiKOA/exec'
|
346 |
-
dezgo_api_key = 'DEZGO-AA84DEB300562DA089E56CEA6081AE7DB601EF2D3EE4C693910E1C25E59A4FF8023D4932'
|
347 |
-
|
348 |
-
dezgo_headers = {
|
349 |
-
'X-Dezgo-Key': dezgo_api_key,
|
350 |
-
}
|
351 |
-
|
352 |
-
# update the google sheet with failure
|
353 |
-
async def record_failure(s3_original_url, failed_task, sheetID):
|
354 |
-
body = {
|
355 |
-
"s3Url": s3_original_url,
|
356 |
-
"mode": "failed",
|
357 |
-
"message": failed_task,
|
358 |
-
"sheetID": sheetID,
|
359 |
-
}
|
360 |
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
361 |
-
response = await client.post(appsScriptsURL, json=body)
|
362 |
-
if response.status_code != 200:
|
363 |
-
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
364 |
-
raise HTTPException(status_code=500, detail="Error from external API.")
|
365 |
-
|
366 |
-
async def post_to_google_apps_script(appsScriptsURL, body, s3_orginal_url):
|
367 |
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
368 |
-
response = await client.post(appsScriptsURL, json=body)
|
369 |
-
if response.status_code != 200:
|
370 |
-
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
371 |
-
await record_failure(s3_orginal_url, 'Failed to queue processed image.', body.sheetID)
|
372 |
-
|
373 |
-
async def background_worker(sheeturl, image_contents, filename, content_type, appsScriptsURL):
|
374 |
-
sheetID = sheeturl.split('/')[-2]
|
375 |
-
image_data = {
|
376 |
-
'sheetID': sheetID,
|
377 |
-
'fileName': filename,
|
378 |
-
'mimeType': content_type
|
379 |
-
}
|
380 |
-
|
381 |
-
s3_client = boto3.client('s3')
|
382 |
-
bucket_name = 're-sheet'
|
383 |
-
object_name = sheetID + '/' + image_data['fileName']
|
384 |
-
|
385 |
-
s3response = s3_client.put_object(
|
386 |
-
Bucket=bucket_name,
|
387 |
-
Key=object_name,
|
388 |
-
Body=image_contents,
|
389 |
-
ContentType=image_data['mimeType']
|
390 |
-
)
|
391 |
-
cut_images = []
|
392 |
-
s3_orginal_url = f"https://{bucket_name}.s3.amazonaws.com/{object_name}"
|
393 |
-
masked_image = await detect_receipts(image_contents, s3_orginal_url, sheetID)
|
394 |
-
await cutting_image(masked_image, image_contents, cut_images, s3_orginal_url, sheetID)
|
395 |
-
|
396 |
-
queued = []
|
397 |
-
# save the processed image(s) to s3
|
398 |
-
if not cut_images:
|
399 |
-
await record_failure(s3_orginal_url, 'No valid receipts found.', sheetID)
|
400 |
-
return {"message": "No valid receipts found."}
|
401 |
-
else:
|
402 |
-
for i, base64_str in enumerate(cut_images, start=1):
|
403 |
-
image_bytes = base64.b64decode(base64_str)
|
404 |
-
object_name = f"{object_name}_processed_{i}.png"
|
405 |
-
response = s3_client.put_object(
|
406 |
-
Bucket=bucket_name,
|
407 |
-
Key=object_name,
|
408 |
-
Body=image_bytes,
|
409 |
-
ContentType='image/png'
|
410 |
-
)
|
411 |
-
status = response.get("ResponseMetadata", {}).get("HTTPStatusCode")
|
412 |
-
if status != 200:
|
413 |
-
await record_failure(s3_orginal_url, 'Failed to upload processed image to S3.', sheetID)
|
414 |
-
return {"message": "Failed to upload processed image to S3."}
|
415 |
-
# otherwise put it as queued
|
416 |
-
object_s3_url = f"https://{bucket_name}.s3.amazonaws.com/{object_name}"
|
417 |
-
queued.append(object_s3_url)
|
418 |
-
# queue the image to google apps script web app
|
419 |
-
body ={
|
420 |
-
"queued": queued,
|
421 |
-
"mode": "mobile",
|
422 |
-
"sheetID": sheetID,
|
423 |
-
}
|
424 |
-
|
425 |
-
print(body)
|
426 |
-
logging.info(body)
|
427 |
-
await post_to_google_apps_script(appsScriptsURL, body, s3_orginal_url)
|
428 |
-
# receive a sheetID and an image as multipart form data
|
429 |
-
@app.post('/mobile-process-receipts')
|
430 |
-
async def mobile_process_receipts(background_tasks: BackgroundTasks, sheeturl: str = Form(...), image: UploadFile = File(...)):
|
431 |
-
image_contents = await image.read()
|
432 |
-
uuid = str(uuid4())
|
433 |
-
timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
434 |
-
message = "영수기 AI가 데이터 분석 중입니다. 잠시만 기다려주세요."
|
435 |
-
data = [timestamp, uuid, message]
|
436 |
-
body = {
|
437 |
-
"sheetID": sheeturl.split('/')[-2],
|
438 |
-
"data": data,
|
439 |
-
"mode": "alert-user"
|
440 |
-
}
|
441 |
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
442 |
-
response = await client.post(appsScriptsURL, json=body)
|
443 |
-
if response.status_code != 200:
|
444 |
-
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
445 |
-
raise HTTPException(status_code=500, detail="Error from external API.")
|
446 |
-
|
447 |
-
background_tasks.add_task(background_worker, sheeturl, image_contents, uuid, image.content_type, appsScriptsURL)
|
448 |
-
|
449 |
-
return {"message": "Image processing completed."}
|
450 |
-
|
451 |
-
@app.post('/process-receipts')
|
452 |
-
async def mobile_process_receipts(background_tasks: BackgroundTasks, request: Request):
|
453 |
-
data = await request.json()
|
454 |
-
image_contents = base64.b64decode(data['base64Image'])
|
455 |
-
content_type = data['mimeType']
|
456 |
-
sheeturl = data['sheetID']
|
457 |
-
uuid = str(uuid4())
|
458 |
-
timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
459 |
-
message = "영수기 AI가 데이터 분석 중입니다. 잠시만 기다려주세요."
|
460 |
-
data = [timestamp, uuid, message]
|
461 |
-
body = {
|
462 |
-
"sheetID": sheeturl.split('/')[-2],
|
463 |
-
"data": data,
|
464 |
-
"mode": "alert-user"
|
465 |
-
}
|
466 |
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
467 |
-
response = await client.post(appsScriptsURL, json=body)
|
468 |
-
if response.status_code != 200:
|
469 |
-
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
470 |
-
raise HTTPException(status_code=500, detail="Error from external API.")
|
471 |
-
|
472 |
-
background_tasks.add_task(background_worker, sheeturl, image_contents, uuid, content_type, appsScriptsURL)
|
473 |
-
|
474 |
-
return {"message": "Image processing completed."}
|
475 |
-
|
476 |
-
# send the image to dezgo
|
477 |
-
async def detect_receipts(image_contents, s3_orginal_url, sheetID):
|
478 |
-
files = {
|
479 |
-
'image': ('filename', image_contents),
|
480 |
-
'mode': 'mask',
|
481 |
-
}
|
482 |
-
logging.info("Sending image to Dezgo API")
|
483 |
-
async with httpx.AsyncClient(timeout=timeout) as client:
|
484 |
-
response = await client.post('https://api.dezgo.com/remove-background', headers=dezgo_headers, files=files)
|
485 |
-
# if successful, pass the masked image to cutting_image function
|
486 |
-
if response.status_code == 200:
|
487 |
-
masked_image = response.content
|
488 |
-
return masked_image
|
489 |
-
else:
|
490 |
-
logging.error(f"Unexpected response from Dezgo API: {response.status_code} - {response.text}")
|
491 |
-
await record_failure(s3_orginal_url, 'Dezgo API failed.', sheetID)
|
492 |
-
raise HTTPException(status_code=500, detail="Error from external API.")
|
493 |
-
|
494 |
-
# receive the masked image and process it
|
495 |
-
async def cutting_image(masked_image, image_contents, cut_images, s3_orginal_url, sheetID):
|
496 |
-
# Convert the masked_image (which is in bytes format) to a numpy array.
|
497 |
-
# The dtype of the array is set to np.uint8 because images are usually 8-bit per channel.
|
498 |
-
nparr_masked = np.frombuffer(masked_image, np.uint8)
|
499 |
-
|
500 |
-
# Check if the size of the numpy array is 0, which means that the conversion failed.
|
501 |
-
if nparr_masked.size == 0:
|
502 |
-
# Log an error message indicating that the conversion failed.
|
503 |
-
logging.error("Failed to convert API response to numpy array.")
|
504 |
-
# Record the reason for the failure.
|
505 |
-
record_failure(s3_orginal_url, 'failed to convert API response to numpy array.', sheetID)
|
506 |
-
# Raise an HTTPException with a 500 status code (Internal Server Error) and a detail message.
|
507 |
-
raise HTTPException(status_code=500, detail="Error processing API response data.")
|
508 |
-
|
509 |
-
# Decode the numpy array to an image using OpenCV. The flag cv2.IMREAD_COLOR loads the image in color.
|
510 |
-
masked_image = cv2.imdecode(nparr_masked, cv2.IMREAD_COLOR)
|
511 |
-
|
512 |
-
# Check if the decoding failed, which would result in masked_image being None.
|
513 |
-
if masked_image is None:
|
514 |
-
# Log an error message indicating that the decoding failed.
|
515 |
-
logging.error("Failed to decode image data.")
|
516 |
-
# Record the reason for the failure.
|
517 |
-
record_failure(s3_orginal_url, 'failed to decode image data.', sheetID)
|
518 |
-
# Raise an HTTPException with a 500 status code (Internal Server Error) and a detail message.
|
519 |
-
raise HTTPException(status_code=500, detail="Error decoding image data.")
|
520 |
-
|
521 |
-
nparr_original = np.frombuffer(image_contents, np.uint8)
|
522 |
-
original_image = cv2.imdecode(nparr_original, cv2.IMREAD_COLOR)
|
523 |
-
|
524 |
-
original_aspect_ratio = original_image.shape[1] / original_image.shape[0]
|
525 |
-
masked_aspect_ratio = masked_image.shape[1] / masked_image.shape[0]
|
526 |
-
|
527 |
-
# if the image has been rotated by dezgo, rotate it back
|
528 |
-
if abs(original_aspect_ratio - masked_aspect_ratio) > 0.1:
|
529 |
-
# Rotate the masked image 90 degrees clockwise
|
530 |
-
masked_image = cv2.rotate(masked_image, cv2.ROTATE_90_CLOCKWISE)
|
531 |
-
|
532 |
-
original_image_shape = original_image.shape[:2]
|
533 |
-
|
534 |
-
resized_masked_image = cv2.resize(masked_image, (original_image_shape[1], original_image_shape[0]))
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
blurred_masked_image = cv2.GaussianBlur(resized_masked_image, (5, 5), 0)
|
539 |
-
_, thresh_masked_image = cv2.threshold(blurred_masked_image, 175, 255, cv2.THRESH_BINARY)
|
540 |
-
edges = cv2.Canny(thresh_masked_image, 75, 150)
|
541 |
-
contours, hierarchy = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
542 |
-
|
543 |
-
padding = 20
|
544 |
-
for contour in contours:
|
545 |
-
perimeter = cv2.arcLength(contour, True)
|
546 |
-
approximation = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
|
547 |
-
x, y, w, h = cv2.boundingRect(approximation)
|
548 |
-
|
549 |
-
# Add padding to the bounding box coordinates while ensuring they are within the image boundaries
|
550 |
-
x_pad = max(x - padding, 0)
|
551 |
-
y_pad = max(y - padding, 0)
|
552 |
-
w_pad = min(w + 2 * padding, original_image.shape[1] - x_pad)
|
553 |
-
h_pad = min(h + 2 * padding, original_image.shape[0] - y_pad)
|
554 |
-
|
555 |
-
# logging.info(f"Bounding box: x={x_pad}, y={y_pad}, w={w_pad}, h={h_pad}")
|
556 |
-
|
557 |
-
# Check if the area of the bounding rectangle is greater than 50px x 50px
|
558 |
-
if w_pad > 150 or h_pad > 150:
|
559 |
-
cropped_image = original_image[y_pad:y_pad + h_pad, x_pad:x_pad + w_pad]
|
560 |
-
_, img_encoded = cv2.imencode('.png', cropped_image)
|
561 |
-
base64_str = base64.b64encode(img_encoded).decode('utf-8')
|
562 |
-
cut_images.append(base64_str)
|
563 |
-
# -> failed or empty: send a response of image and sheetid to the google sheet
|
564 |
-
# save the processed image(s) to s3
|
565 |
-
# -> failed or empty: send a response of image and sheetid to the google sheet
|
566 |
-
# use google ocr to extract text from the image
|
567 |
-
# -> failed or empty: send a response of image and sheetid to the google sheet
|
568 |
-
# send the text to Openai
|
569 |
-
# -> failed or empty: send a response of image and sheetid to the google sheet
|
570 |
-
# send the response to the google sheet
|
571 |
-
|
572 |
-
@app.post('/test')
|
573 |
-
async def mobile_process_receipts(background_tasks: BackgroundTasks, sheeturl: str = Form(...), image: UploadFile = File(...)):
|
574 |
-
image_contents = await image.read()
|
575 |
-
uuid = str(uuid4())
|
576 |
-
timestamp = str(datetime.now().strftime("%Y-%m-%d-%H-%M-%S"))
|
577 |
-
message = "영수기가 데이터 분석 중입니다. 잠시만 기다려주세요."
|
578 |
-
data = [timestamp, uuid, message]
|
579 |
-
body = {
|
580 |
-
"sheetID": sheeturl.split('/')[-2],
|
581 |
-
"data": data,
|
582 |
-
"mode": "alert-user"
|
583 |
-
}
|
584 |
-
async with httpx.AsyncClient(follow_redirects=True) as client:
|
585 |
-
response = await client.post(appsScriptsURL, json=body)
|
586 |
-
if response.status_code != 200:
|
587 |
-
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
588 |
-
raise HTTPException(status_code=500, detail="Error from external API.")
|
589 |
-
background_tasks.add_task(background_worker, sheeturl, image_contents, image.filename, image.content_type, appsScriptsURL)
|
590 |
-
return {"message": "Image processing completed."}
|
|
|
9 |
import base64
|
10 |
import httpx
|
11 |
import logging
|
|
|
12 |
import boto3
|
13 |
import base64
|
|
|
|
|
|
|
14 |
|
15 |
app = FastAPI()
|
16 |
|
|
|
36 |
class UnprocessedImage(BaseModel):
|
37 |
sheetID: str
|
38 |
s3Url: str
|
39 |
+
|
40 |
+
@app.post("/redirect")
|
41 |
+
async def redirect_to_google_apps_script(request: Request):
|
42 |
+
# Get the body of the incoming request
|
43 |
+
body = await request.body()
|
44 |
+
|
45 |
+
async with httpx.AsyncClient(timeout=300.0, follow_redirects=True) as client: # Follow redirects
|
46 |
+
response = await client.post(
|
47 |
+
'https://script.google.com/macros/s/AKfycbw9_31pMwQVhJPezJ9qlMRCAamQKRREnun-tfT_TnS5TOwOVcfdgU1Y9xTpAv6bZuiKOA/exec',
|
48 |
+
data=body
|
49 |
+
)
|
50 |
+
if response.status_code != 200:
|
51 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
52 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
53 |
+
return response.json()
|
54 |
+
|
55 |
+
appsScriptsURL = 'https://script.google.com/macros/s/AKfycbw9_31pMwQVhJPezJ9qlMRCAamQKRREnun-tfT_TnS5TOwOVcfdgU1Y9xTpAv6bZuiKOA/exec'
|
56 |
+
dezgo_api_key = 'DEZGO-AA84DEB300562DA089E56CEA6081AE7DB601EF2D3EE4C693910E1C25E59A4FF8023D4932'
|
57 |
+
|
58 |
+
dezgo_headers = {
|
59 |
+
'X-Dezgo-Key': dezgo_api_key,
|
60 |
+
}
|
61 |
+
|
62 |
+
# update the google sheet with failure
|
63 |
+
async def record_failure(s3_original_url, failed_task, sheetID):
|
64 |
+
body = {
|
65 |
+
"s3Url": s3_original_url,
|
66 |
+
"mode": "failed",
|
67 |
+
"message": failed_task,
|
68 |
+
"sheetID": sheetID,
|
69 |
+
}
|
70 |
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
71 |
+
response = await client.post(appsScriptsURL, json=body)
|
72 |
+
if response.status_code != 200:
|
73 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
74 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
75 |
+
|
76 |
+
async def post_to_google_apps_script(appsScriptsURL, body, s3_orginal_url):
|
77 |
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
78 |
+
response = await client.post(appsScriptsURL, json=body)
|
79 |
+
if response.status_code != 200:
|
80 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
81 |
+
await record_failure(s3_orginal_url, 'Failed to queue processed image.', body.sheetID)
|
82 |
+
|
83 |
+
async def background_worker(sheeturl, image_contents, filename, content_type, appsScriptsURL):
|
84 |
+
sheetID = sheeturl.split('/')[-2]
|
85 |
+
image_data = {
|
86 |
+
'sheetID': sheetID,
|
87 |
+
'fileName': filename,
|
88 |
+
'mimeType': content_type
|
89 |
+
}
|
90 |
+
|
91 |
+
s3_client = boto3.client('s3')
|
92 |
+
bucket_name = 're-sheet'
|
93 |
+
object_name = sheetID + '/' + image_data['fileName']
|
94 |
+
cut_images = []
|
95 |
+
s3_orginal_url = f"https://{bucket_name}.s3.amazonaws.com/{object_name}"
|
96 |
+
masked_image = await detect_receipts(image_contents, s3_orginal_url, sheetID)
|
97 |
+
await cutting_image(masked_image, image_contents, cut_images, s3_orginal_url, sheetID)
|
98 |
|
99 |
+
queued = []
|
100 |
+
# save the processed image(s) to s3
|
101 |
+
if not cut_images:
|
102 |
+
await record_failure(s3_orginal_url, 'No valid receipts found.', sheetID)
|
103 |
+
return {"message": "No valid receipts found."}
|
104 |
+
else:
|
105 |
+
for i, base64_str in enumerate(cut_images, start=1):
|
106 |
+
image_bytes = base64.b64decode(base64_str)
|
107 |
+
object_name = f"{object_name}_processed_{i}.png"
|
108 |
+
response = s3_client.put_object(
|
109 |
+
Bucket=bucket_name,
|
110 |
+
Key=object_name,
|
111 |
+
Body=image_bytes,
|
112 |
+
ContentType='image/png'
|
113 |
+
)
|
114 |
+
status = response.get("ResponseMetadata", {}).get("HTTPStatusCode")
|
115 |
+
if status != 200:
|
116 |
+
await record_failure(s3_orginal_url, 'Failed to upload processed image to S3.', sheetID)
|
117 |
+
return {"message": "Failed to upload processed image to S3."}
|
118 |
+
# otherwise put it as queued
|
119 |
+
object_s3_url = f"https://{bucket_name}.s3.amazonaws.com/{object_name}"
|
120 |
+
queued.append(object_s3_url)
|
121 |
+
# queue the image to google apps script web app
|
122 |
+
body ={
|
123 |
+
"queued": queued,
|
124 |
+
"mode": "mobile",
|
125 |
+
"sheetID": sheetID,
|
126 |
+
}
|
127 |
+
|
128 |
+
print(body)
|
129 |
+
logging.info(body)
|
130 |
+
await post_to_google_apps_script(appsScriptsURL, body, s3_orginal_url)
|
131 |
+
return {"message": "Image processing completed."}
|
132 |
|
133 |
+
# receive a sheetID and an image as multipart form data
|
134 |
+
@app.post('/mobile-process-receipts')
|
135 |
+
async def mobile_process_receipts(background_tasks: BackgroundTasks, sheeturl: str = Form(...), image: UploadFile = File(...)):
|
136 |
+
image_contents = await image.read()
|
137 |
+
uuid = str(uuid4())
|
138 |
+
timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
139 |
+
sheetID = sheeturl.split('/')[-2]
|
140 |
+
|
141 |
+
message = "영수기 AI가 데이터 분석 중입니다. 잠시만 기다려주세요."
|
142 |
+
data = [timestamp, uuid, message]
|
143 |
+
body = {
|
144 |
+
"sheetID": sheetID,
|
145 |
+
"data": data,
|
146 |
+
"mode": "alert-user"
|
147 |
+
}
|
148 |
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
149 |
+
response = await client.post(appsScriptsURL, json=body)
|
150 |
+
if response.status_code != 200:
|
151 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
152 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
153 |
+
|
154 |
+
background_tasks.add_task(background_worker, sheeturl, image_contents, uuid, image.content_type, appsScriptsURL, )
|
155 |
+
|
156 |
+
return {"message": "Image processing completed."}
|
157 |
+
|
158 |
+
|
159 |
+
@app.post('/process-receipts')
|
160 |
+
async def mobile_process_receipts(background_tasks: BackgroundTasks, request: Request):
|
161 |
+
data = await request.json()
|
162 |
+
base64_str = data['base64Image']
|
163 |
+
prefix, base64_str = base64_str.split(',', 1) # Split on the first comma
|
164 |
+
image_contents = base64.b64decode(base64_str)
|
165 |
+
content_type = data['mimeType']
|
166 |
+
sheeturl = data['sheetID']
|
167 |
+
|
168 |
+
checkPhoto = {
|
169 |
+
'sheetID': sheeturl.split('/')[-2],
|
170 |
+
'mode': "check",
|
171 |
+
'photo': image_contents,
|
172 |
+
}
|
173 |
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
174 |
+
response = await client.post(appsScriptsURL, json=checkPhoto)
|
175 |
+
if response.status_code != 200:
|
176 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
177 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
178 |
+
if response.json()['status'] == 'invalid':
|
179 |
+
return {"message": "Invalid photo."}
|
180 |
+
|
181 |
+
|
182 |
+
uuid = str(uuid4())
|
183 |
+
timestamp = str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
184 |
+
message = "영수기 AI가 데이터 분석 중입니다. 잠시만 기다려주세요."
|
185 |
+
data = [timestamp, uuid, message]
|
186 |
+
body = {
|
187 |
+
"sheetID": sheeturl.split('/')[-2],
|
188 |
+
"data": data,
|
189 |
+
"mode": "alert-user"
|
190 |
+
}
|
191 |
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
192 |
+
response = await client.post(appsScriptsURL, json=body)
|
193 |
+
if response.status_code != 200:
|
194 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
195 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
196 |
+
|
197 |
+
background_tasks.add_task(background_worker, sheeturl, image_contents, uuid, content_type, appsScriptsURL)
|
198 |
+
|
199 |
+
return {"message": "Image processing completed."}
|
200 |
+
|
201 |
+
# send the image to dezgo
|
202 |
+
async def detect_receipts(image_contents, s3_orginal_url, sheetID):
|
203 |
+
files = {
|
204 |
+
'image': ('filename', image_contents),
|
205 |
+
'mode': 'mask',
|
206 |
+
}
|
207 |
+
logging.info("Sending image to Dezgo API")
|
208 |
+
async with httpx.AsyncClient(timeout=timeout) as client:
|
209 |
+
response = await client.post('https://api.dezgo.com/remove-background', headers=dezgo_headers, files=files)
|
210 |
+
# if successful, pass the masked image to cutting_image function
|
211 |
+
if response.status_code == 200:
|
212 |
+
masked_image = response.content
|
213 |
+
return masked_image
|
214 |
+
else:
|
215 |
+
logging.error(f"Unexpected response from Dezgo API: {response.status_code} - {response.text}")
|
216 |
+
await record_failure(s3_orginal_url, 'Dezgo API failed.', sheetID)
|
217 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
218 |
+
|
219 |
+
# receive the masked image and process it
|
220 |
+
async def cutting_image(masked_image, image_contents, cut_images, s3_orginal_url, sheetID):
|
221 |
+
# Convert the masked_image (which is in bytes format) to a numpy array.
|
222 |
+
# The dtype of the array is set to np.uint8 because images are usually 8-bit per channel.
|
223 |
+
nparr_masked = np.frombuffer(masked_image, np.uint8)
|
224 |
+
|
225 |
+
# Check if the size of the numpy array is 0, which means that the conversion failed.
|
226 |
+
if nparr_masked.size == 0:
|
227 |
+
# Log an error message indicating that the conversion failed.
|
228 |
+
logging.error("Failed to convert API response to numpy array.")
|
229 |
+
# Record the reason for the failure.
|
230 |
+
record_failure(s3_orginal_url, 'failed to convert API response to numpy array.', sheetID)
|
231 |
+
# Raise an HTTPException with a 500 status code (Internal Server Error) and a detail message.
|
232 |
+
raise HTTPException(status_code=500, detail="Error processing API response data.")
|
233 |
+
|
234 |
+
# Decode the numpy array to an image using OpenCV. The flag cv2.IMREAD_COLOR loads the image in color.
|
235 |
+
masked_image = cv2.imdecode(nparr_masked, cv2.IMREAD_COLOR)
|
236 |
+
|
237 |
+
# Check if the decoding failed, which would result in masked_image being None.
|
238 |
+
if masked_image is None:
|
239 |
+
# Log an error message indicating that the decoding failed.
|
240 |
+
logging.error("Failed to decode image data.")
|
241 |
+
# Record the reason for the failure.
|
242 |
+
record_failure(s3_orginal_url, 'failed to decode image data.', sheetID)
|
243 |
+
# Raise an HTTPException with a 500 status code (Internal Server Error) and a detail message.
|
244 |
+
raise HTTPException(status_code=500, detail="Error decoding image data.")
|
245 |
+
|
246 |
+
nparr_original = np.frombuffer(image_contents, np.uint8)
|
247 |
+
original_image = cv2.imdecode(nparr_original, cv2.IMREAD_COLOR)
|
248 |
+
|
249 |
+
original_aspect_ratio = original_image.shape[1] / original_image.shape[0]
|
250 |
+
masked_aspect_ratio = masked_image.shape[1] / masked_image.shape[0]
|
251 |
+
|
252 |
+
# if the image has been rotated by dezgo, rotate it back
|
253 |
+
if abs(original_aspect_ratio - masked_aspect_ratio) > 0.1:
|
254 |
+
# Rotate the masked image 90 degrees clockwise
|
255 |
+
masked_image = cv2.rotate(masked_image, cv2.ROTATE_90_CLOCKWISE)
|
256 |
+
|
257 |
+
original_image_shape = original_image.shape[:2]
|
258 |
+
|
259 |
+
resized_masked_image = cv2.resize(masked_image, (original_image_shape[1], original_image_shape[0]))
|
260 |
+
|
261 |
+
|
262 |
+
|
263 |
+
blurred_masked_image = cv2.GaussianBlur(resized_masked_image, (5, 5), 0)
|
264 |
+
_, thresh_masked_image = cv2.threshold(blurred_masked_image, 175, 255, cv2.THRESH_BINARY)
|
265 |
+
edges = cv2.Canny(thresh_masked_image, 75, 150)
|
266 |
+
contours, hierarchy = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
267 |
+
|
268 |
+
padding = 20
|
269 |
+
for contour in contours:
|
270 |
+
perimeter = cv2.arcLength(contour, True)
|
271 |
+
approximation = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
|
272 |
+
x, y, w, h = cv2.boundingRect(approximation)
|
273 |
+
|
274 |
+
# Add padding to the bounding box coordinates while ensuring they are within the image boundaries
|
275 |
+
x_pad = max(x - padding, 0)
|
276 |
+
y_pad = max(y - padding, 0)
|
277 |
+
w_pad = min(w + 2 * padding, original_image.shape[1] - x_pad)
|
278 |
+
h_pad = min(h + 2 * padding, original_image.shape[0] - y_pad)
|
279 |
+
|
280 |
+
# logging.info(f"Bounding box: x={x_pad}, y={y_pad}, w={w_pad}, h={h_pad}")
|
281 |
+
|
282 |
+
# Check if the area of the bounding rectangle is greater than 50px x 50px
|
283 |
+
if w_pad > 150 or h_pad > 150:
|
284 |
+
cropped_image = original_image[y_pad:y_pad + h_pad, x_pad:x_pad + w_pad]
|
285 |
+
_, img_encoded = cv2.imencode('.png', cropped_image)
|
286 |
+
base64_str = base64.b64encode(img_encoded).decode('utf-8')
|
287 |
+
cut_images.append(base64_str)
|
288 |
+
|
289 |
+
|
290 |
+
@app.post('/test')
|
291 |
+
async def mobile_process_receipts(background_tasks: BackgroundTasks, sheeturl: str = Form(...), image: UploadFile = File(...)):
|
292 |
+
image_contents = await image.read()
|
293 |
+
uuid = str(uuid4())
|
294 |
+
timestamp = str(datetime.now().strftime("%Y-%m-%d-%H-%M-%S"))
|
295 |
+
message = "영수기가 데이터 분석 중입니다. 잠시만 기다려주세요."
|
296 |
+
data = [timestamp, uuid, message]
|
297 |
+
body = {
|
298 |
+
"sheetID": sheeturl.split('/')[-2],
|
299 |
+
"data": data,
|
300 |
+
"mode": "alert-user"
|
301 |
+
}
|
302 |
+
async with httpx.AsyncClient(follow_redirects=True) as client:
|
303 |
+
response = await client.post(appsScriptsURL, json=body)
|
304 |
+
if response.status_code != 200:
|
305 |
+
logging.error(f"Unexpected response from Google Apps Script API: {response.status_code} - {response.text}")
|
306 |
+
raise HTTPException(status_code=500, detail="Error from external API.")
|
307 |
+
background_tasks.add_task(background_worker, sheeturl, image_contents, image.filename, image.content_type, appsScriptsURL)
|
308 |
+
return {"message": "Image processing completed."}
|
309 |
+
|
310 |
# async def failure(sheetid):
|
311 |
# async with httpx.AsyncClient(follow_redirects=True) as client:
|
312 |
# response = await client.post(
|
|
|
367 |
# failure(image_data['sheetID'])
|
368 |
# raise HTTPException(status_code=500, detail="Error from external API.")
|
369 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
370 |
# @app.post("/save-to-s3")
|
371 |
# async def save_to_s3(request: Request, background_tasks: BackgroundTasks):
|
372 |
# image_data = await request.json()
|
|
|
585 |
|
586 |
# finally:
|
587 |
# logging.info("File processing completed")
|
|
|
|
|
|
|
|
|
588 |
|
589 |
# else:
|
590 |
+
# raise HTTPException(status_code=400, detail="Email address not provided.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|