Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Perspective Transformation Refactor a lot (but not all) JS style to Python style code deprecate weird features #3

Merged
merged 1 commit into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion golden_frame/__init__.py

This file was deleted.

Binary file removed golden_frame/assets/big_frame.jpg
Binary file not shown.
4 changes: 0 additions & 4 deletions golden_frame/assets/big_frame.json

This file was deleted.

4 changes: 0 additions & 4 deletions golden_frame/assets/golden_frame.json

This file was deleted.

Binary file removed golden_frame/assets/golden_frame.png
Binary file not shown.
9 changes: 9 additions & 0 deletions golden_frame/assets/json/obamium_portrait.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Obama Portrait",
"pos": [
[133, 29],
[835, 26],
[640, 786],
[119, 786]
]
}
10 changes: 10 additions & 0 deletions golden_frame/assets/json/vladdy_daddy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "Vladimir Putin does not #StandWithUkraine but I do",
"pos": [
[173, 459],
[363, 469],
[365, 797],
[175, 789]
]

}
9 changes: 9 additions & 0 deletions golden_frame/assets/json/wessuwan.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "ท้าวเวสสุวรรณ",
"pos": [
[77, 505],
[164, 502],
[173, 615],
[64, 620]
]
}
4 changes: 0 additions & 4 deletions golden_frame/assets/obamium_portrait.json

This file was deleted.

Binary file removed golden_frame/assets/plaingoldenframe.jpg
Binary file not shown.
5 changes: 0 additions & 5 deletions golden_frame/assets/plaingoldenframe.json

This file was deleted.

4 changes: 0 additions & 4 deletions golden_frame/assets/vladdy_daddy.json

This file was deleted.

4 changes: 0 additions & 4 deletions golden_frame/assets/wessuwan.json

This file was deleted.

33 changes: 12 additions & 21 deletions golden_frame/cli.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
#!/usr/bin/env python3

# pylint: disable=import-error

import os
import typer

if os.environ.get("DEBUG") is None:
from golden_frame.lib import PosOptions, buildFromPreset, listFrames
else:
print("[DEBUG] Only use this mode for local development")
from lib import PosOptions, buildFromPreset, listFrames
from golden_frame.lib import build_from_preset, listFrames

app = typer.Typer()


@app.command()
def build(frame_name: str, input: str, output="output.png",
pos=PosOptions.CENTER, res=720):
"""Build the Golden Frame
def build(
frame_name: str,
input: str,
output: str = "output.png",
res: int = 720
):
"""
Build the Golden Frame

Args:

frame_name (str): Name of Template Frame

input (str): Location of your Image

output (str, optional): Output Location. Defaults to "output.png".

pos (0 | 1 | 2, optional): Position Type CENTER = 0, START = 1, END = 2. Defaults to CENTER.

res (int, optional): Minimum size of image
res (int, optional): Minimum size of image. Defaults to 720.
"""
buildFromPreset(frame_name, input, output, int(pos), int(res))
build_from_preset(frame_name, input, output, res)


@app.command()
def list():
"""Print List of Available Frames
"""
"""Print List of Available Frames"""
print(listFrames())


Expand Down
146 changes: 55 additions & 91 deletions golden_frame/lib.py
Original file line number Diff line number Diff line change
@@ -1,111 +1,82 @@
# pylint: disable=no-member,misplaced-bare-raise

from typing import Dict, List, Tuple, Union
import json
import os
from enum import Enum

import cv2
import numpy as np
import pkg_resources

ASSET_PATH = pkg_resources.resource_filename("golden_frame", "assets")

ASSET_PATH = os.path.join("golden_frame", "assets")

if os.environ.get("DEBUG") is not None:
ASSET_PATH = "./golden_frame/assets"


class CropOptions(Enum):
CROP = 0
PRESERVE = 1


class PosOptions:
CENTER = 0
START = 1
END = 2


def getPos(x: Union[int, float], dx: Union[int, float], opt) -> Tuple[int, int]:
if opt == PosOptions.START:
return 0, round(x)
if opt == PosOptions.CENTER:
return round(dx), round(x+dx)
if opt == PosOptions.END:
return round(2*dx), round(2*dx+x)

raise Exception("opt is not valid PosOptions")


def resizeImage(
pic: np.ndarray,
size: List[int],
posOption=PosOptions.CENTER
) -> np.ndarray:
py, px = pic.shape[0:2]

ratio = max(size[0]/px, size[1]/py)

resized = cv2.resize(pic, dsize=(
round(px*ratio), round(py*ratio)))
def build_frame(
source_image: np.ndarray,
frame_image: np.ndarray,
frame_marks: List[List[int]],
res = 720
)->np.ndarray:

# Scale Frame to match size requirements
# - Scale to match height
# - Calculate change of x point and y point to shift position mark on frame
og_height, og_width = frame_image.shape[0:2]
frame_image = cv2.resize(frame_image, (frame_image.shape[1]*res//frame_image.shape[0], res))

ysize, xsize = resized.shape[0:2]

dx = (xsize - size[0])/2
dy = (ysize - size[1])/2
# Calculate change of x point and y point to shift position mark on frame
for mark in frame_marks:
mark[0] = mark[0]*frame_image.shape[1]//og_width
mark[1] = mark[1]*frame_image.shape[0]//og_height


sx, ex = getPos(size[0], dx, posOption)
sy, ey = getPos(size[1], dy, posOption)

return resized[sy:ey, sx:ex]
# Calculate width and height of the destination frame & Resize Source Image
# Convert frame_marks to np.float32
frame_marks = np.float32(frame_marks)
width = int(max(np.linalg.norm(frame_marks[0] - frame_marks[1]), np.linalg.norm(frame_marks[2] - frame_marks[3])))
height = int(max(np.linalg.norm(frame_marks[0] - frame_marks[3]), np.linalg.norm(frame_marks[1] - frame_marks[2])))
source_image = cv2.resize(source_image, (width, height))


# Perform Perspective Transformation
# - Define source points
# - Calculate the perspective transformation matrix
# - Apply the perspective transformation
src_pts = np.float32([[0, 0], [width, 0], [width, height], [0, height]])
M = cv2.getPerspectiveTransform(src_pts, np.float32(frame_marks))
warped = cv2.warpPerspective(source_image, M, (frame_image.shape[1], frame_image.shape[0]))

def scaleImage(img: np.ndarray, res: int) -> Tuple[np.ndarray, float, float]:
ly, lx = img.shape[0:2]

ratio = res / min(ly, lx, res)
# Create a mask and inverse mask of the warped image
mask = cv2.warpPerspective(np.ones_like(source_image) * 255, M, (frame_image.shape[1], frame_image.shape[0]))
inv_mask = cv2.bitwise_not(mask)

resized = cv2.resize(img, dsize=(round(lx*ratio), round(ly*ratio)))

return (resized, ratio, ratio)
# Blend the images
result = cv2.bitwise_and(frame_image, inv_mask)
result = cv2.add(result, warped)


return result

def buildGoldenFrame(
frame: np.ndarray,
picture: np.ndarray,
pos: List[int],
posOption=PosOptions.CENTER,
res=720
) -> np.ndarray:
# Scale frame to minimum size
frame, ratiox, ratioy = scaleImage(frame, res)

resized = resizeImage(
picture,
[round((pos[2] - pos[0]) * ratiox),
round((pos[3] - pos[1]) * ratioy)],
posOption)
xstart, ystart = (round(pos[0] * ratiox), round(pos[1] * ratioy))
ysize, xsize = resized.shape[0:2]

# Why can't you use same axis system?
frame[ystart:ystart+ysize, xstart:xstart+xsize] = resized

return frame


def loadConfig(name: str) -> Dict:
with open(".".join(name.split(".")[:-1]) + ".json") as f:
def load_config(frame: str):
with open(os.path.join(ASSET_PATH, "json", "{}.json".format(frame)), "r") as f:
return json.load(f)



def buildFromPreset(
frame: str, image: str, out: str, opt=PosOptions.CENTER, res=720):
def build_from_preset(
frame: str, image: str, out: str, res=720):
frame = f"{ASSET_PATH}/{frame}"

try:
frameimg = cv2.imread(frame)
if not frameimg.data:
frame_image = cv2.imread(frame)
if not frame_image.data:
raise
except:
print(
Expand All @@ -114,38 +85,31 @@ def buildFromPreset(
return

try:
inputimg = cv2.imread(image)
if not inputimg.data:
input_image = cv2.imread(image)
if not input_image.data:
raise
except:
print(f"ERROR: Cannot Read Input Image {image}!")
return

cfg = loadConfig(frame)["pos"]
outim = buildGoldenFrame(frameimg, inputimg,
list(int(k) for k in cfg.split(",")), opt, res)

cfg = load_config(frame)["pos"]
outim = build_frame(input_image, frame_image, cfg, res)
try:
cv2.imwrite(out, outim)
except:
print(f"Error writing Image!")


def listFrames() -> str:
def list_frames() -> str:
import os
items = list(filter(lambda x: not x.endswith(
".json"), os.listdir(ASSET_PATH)))
".json"), os.listdir(os.path.join(ASSET_PATH, "json"))))

text = f"There are {len(items)} frames available.\n"

for item in items:
cfg = loadConfig(f"{ASSET_PATH}/{item}")
cfg = load_config(item.replace(".json", ""))
text += f"\n{item} : {cfg['name']}"

return text


# if __name__ == "__main__":
# buildFromPreset(
# "big_frame.jpg", "example/zhongxina_before.jpg", "output.png",
# PosOptions.CENTER, 720)
27 changes: 14 additions & 13 deletions server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os
import cv2
import numpy as np
from golden_frame.lib import buildGoldenFrame, listFrames, ASSET_PATH, loadConfig, PosOptions
from golden_frame.lib import build_frame, list_frames, ASSET_PATH, load_config
from datetime import datetime

from flask import Flask, request, Response
Expand All @@ -31,19 +31,20 @@ def line_to_json(line: str):
}


def build_golden_frame(frame_name: str, input_img: np.ndarray):
frame_path = f"{ASSET_PATH}/{frame_name}"
frameimg = cv2.imread(frame_path)
cfg = loadConfig(frame_path)["pos"]

out_image = buildGoldenFrame(frame=frameimg, picture=input_img, pos=list(
int(k) for k in cfg.split(",")), posOption=PosOptions.CENTER)

def build_golden_frame(frame_name: str, input_image: np.ndarray):
frame_path = os.path.join(ASSET_PATH, frame_name)
frame_image = cv2.imread(frame_path)

out_image = build_frame(
source_image = input_image,
frame_image = frame_image,
frame_marks = load_config(frame_name)["pos"]
)
return out_image


def listFramesJson():
frames = listFrames()
def list_frame_json():
frames = list_frames()
items = list(map(line_to_json, filter(
lambda x: len(x), frames.split("\n")[1:])))

Expand All @@ -52,7 +53,7 @@ def listFramesJson():

@app.route("/", methods=["GET"])
def get_frames():
return listFramesJson(), 200
return list_frame_json(), 200


@app.route("/", methods=["POST"])
Expand All @@ -75,7 +76,7 @@ def build_frame():
if frame_name is None:
return 'No frame name selected', 400

if not any(map(lambda x: x['name'] == frame_name, listFramesJson())):
if not any(map(lambda x: x['name'] == frame_name, list_frame_json())):
return 'Invalid frame name', 400

# Read the image file as bytes
Expand Down