From a233c0fbe4dfd84e11385c70a4c0dc661ac1097c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20Thei=C3=9F?= Date: Sun, 9 May 2021 00:13:10 +0200 Subject: [PATCH] ASCIIfy + Artify :) --- .gitignore | 5 + backend/app/artify.py | 40 ++++++++ backend/app/config.py | 16 +++ backend/app/run.py | 118 ++++++++++++++++++---- backend/requirements.txt | 5 +- frontend/package-lock.json | 22 ++++ frontend/package.json | 1 + frontend/src/App.vue | 9 +- frontend/src/api/backend.js | 15 +++ frontend/src/components/DragAndDrop.vue | 16 ++- frontend/src/components/ResultDisplay.vue | 2 +- frontend/src/components/SelectOption.vue | 62 +++++++----- frontend/src/components/Settings.vue | 104 +++++++++++++++++-- frontend/src/store/result-store.js | 47 +++++++-- 14 files changed, 392 insertions(+), 70 deletions(-) create mode 100644 backend/app/artify.py diff --git a/.gitignore b/.gitignore index b6e4761..cf082fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +backend/app/models/ + +# VS Code +.vscode/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/backend/app/artify.py b/backend/app/artify.py new file mode 100644 index 0000000..a52f738 --- /dev/null +++ b/backend/app/artify.py @@ -0,0 +1,40 @@ +import numpy as np +import tensorflow as tf +from PIL import Image + +def wrap_frozen_graph(graph_def, inputs, outputs, print_graph=False): + def _imports_graph_def(): + tf.compat.v1.import_graph_def(graph_def, name="") + + wrapped_import = tf.compat.v1.wrap_function(_imports_graph_def, []) + import_graph = wrapped_import.graph + + layers = [op.name for op in import_graph.get_operations()] + if print_graph == True: + for layer in layers: + print(layer) + return wrapped_import.prune( + tf.nest.map_structure(import_graph.as_graph_element, inputs), + tf.nest.map_structure(import_graph.as_graph_element, outputs)) + +def load_artist_graph(artist, model_path): + print(f"Loading model for {artist}.") + try: + with tf.io.gfile.GFile(model_path / artist / "frozen_model.pb", "rb") as f: + graph_def = tf.compat.v1.GraphDef() + loaded = graph_def.ParseFromString(f.read()) + frozen_func = wrap_frozen_graph(graph_def, ["placeholder/photo:0"], ["decoder/sub:0"]) + except FileNotFoundError: + def frozen_func(x): + return x + return frozen_func + +def artify(image, model): + image = image.convert("RGB") + np_image = np.asarray(image) + input_image = ( + tf.expand_dims(tf.constant(np_image, dtype=tf.float32), 0) / 127.5 - 1.0 + ) + artified = (model(input_image)[0] + 1) * 127.5 + artified = Image.fromarray(tf.squeeze(artified, 0).numpy().astype(np.uint8)) + return artified \ No newline at end of file diff --git a/backend/app/config.py b/backend/app/config.py index 275ae52..305d3e6 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -3,6 +3,22 @@ class Config: ["jpg", "png", "jpeg", "tiff", "bmp", "gif", "ppm", "pgm", "tif", "svg"] ) FONT_FOLDER = "fonts" + MODEL_FOLDER = "models" + ARTISTS = [ + "cezanne", + "monet", + "el-greco", + "gauguin", + "kandinsky", + "kirchner", + "morisot", + "munch", + "peploe", + "picasso", + "pollock", + "roerich", + "van-gogh", + ] class ProdConfig(Config): diff --git a/backend/app/run.py b/backend/app/run.py index 8355bbf..c0d13d7 100644 --- a/backend/app/run.py +++ b/backend/app/run.py @@ -7,9 +7,12 @@ from flask_talisman import Talisman from PIL import Image from pathlib import Path -from asciify import ASCIIfy from io import BytesIO import base64 +import tensorflow as tf + +import asciify +import artify app = Flask(__name__) app.config.from_object("config.DevConfig") @@ -17,6 +20,13 @@ Talisman(app) api = Api(app) +model_selection = dict( + [ + (artist, artify.load_artist_graph(artist, Path(app.config["MODEL_FOLDER"]))) + for artist in app.config["ARTISTS"] + ] +) + def allowed_file(filename): """ @@ -29,6 +39,13 @@ def allowed_file(filename): ) +def pil_to_base64_str(img): + data = BytesIO() + img.save(data, "JPEG") + data64 = base64.b64encode(data.getvalue()) + return data64 + + class Home(Resource): """ The REST-Api response for the main directory. @@ -62,7 +79,7 @@ def get(self): } -class ASCIIfy_Image(Resource): +class ASCIIfyImage(Resource): """ This class will handle the request on sending an image to the server. """ @@ -75,11 +92,10 @@ def post(self): "image", type=werkzeug.datastructures.FileStorage, location="files" ) parser.add_argument("debug", required=False, type=int) + parser.add_argument("resize", required=False, type=int, default=-1) parser.add_argument("fontsize", required=True, type=int) parser.add_argument("font", required=True, type=str) - parser.add_argument("resize", required=True, type=int) args = parser.parse_args() - print(args) if args["debug"] is not None and args["debug"] == 1: print(args) @@ -89,32 +105,100 @@ def post(self): image = Image.open(file) # asciify it: - asciified = ASCIIfy( + asciified = asciify.ASCIIfy( np.array(image), args["resize"], str(Path(app.config["FONT_FOLDER"], f"{args['font']}.ttf")), args["fontsize"], 1, ) + data64 = pil_to_base64_str(asciified) + + return Response( + response=data64.decode("utf-8"), status=200, mimetype="image/jpeg" + ) + + +class Styles(Resource): + """ + returns a list of fonts + """ + + def get(self): + """ + Return the list of available fonts. + """ + return {"styles": [artist.title() for artist in app.config["ARTISTS"]]} + + +class ArtifyImage(Resource): + def post(self): + parser = reqparse.RequestParser() + + # we expect an image to be send, if not send message to the client + parser.add_argument( + "image", type=werkzeug.datastructures.FileStorage, location="files" + ) + parser.add_argument( + "style", required=True, type=str, choices=app.config["ARTISTS"] + ) + args = parser.parse_args() + + file = request.files["image"] + image = Image.open(file) + artified = artify.artify(image, model_selection[args["style"]]) + data64 = pil_to_base64_str(artified) + + return Response( + response=data64.decode("utf-8"), status=200, mimetype="image/jpeg" + ) + + +class BeautifyImage(Resource): + def post(self): + parser = reqparse.RequestParser() + + # we expect an image to be send, if not send message to the client + parser.add_argument( + "image", type=werkzeug.datastructures.FileStorage, location="files" + ) + parser.add_argument("debug", required=False, type=int) + parser.add_argument("resize", required=False, type=int, default=-1) + parser.add_argument("fontsize", required=True, type=int) + parser.add_argument("font", required=False, type=str) + parser.add_argument( + "style", required=False, type=str, choices=app.config["ARTISTS"] + ) + args = parser.parse_args() + + file = request.files["image"] + image = Image.open(file) + + if args["style"] is not None: + image = artify.artify(image, model_selection[args["style"]]) - data = BytesIO() - asciified.save(data, "JPEG") - data64 = base64.b64encode(data.getvalue()) + if args["font"] is not None: + image = asciify.ASCIIfy( + np.array(image), + args["resize"], + str(Path(app.config["FONT_FOLDER"], f"{args['font']}.ttf")), + args["fontsize"], + 1, + ) - #_, cv_image = cv2.imencode( - # ".jpeg", np.array(asciified)[..., ::-1] - #) + data64 = pil_to_base64_str(image) - # headers = { - # 'Content-Type': "image/jpeg", # This is important - # } - # why? - return Response(response=data64.decode('utf-8'), status=200, mimetype="image/jpeg") + return Response( + response=data64.decode("utf-8"), status=200, mimetype="image/jpeg" + ) api.add_resource(Home, "/api/home") api.add_resource(Fonts, "/api/fonts") -api.add_resource(ASCIIfy_Image, "/api/asciify") +api.add_resource(ASCIIfyImage, "/api/asciify") +api.add_resource(Styles, "/api/styles") +api.add_resource(ArtifyImage, "/api/artify") +api.add_resource(BeautifyImage, "/api/beautify") # app.run(host, port): Start flask server by specifying host and port if __name__ == "__main__": diff --git a/backend/requirements.txt b/backend/requirements.txt index eadd0ea..7918d33 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,8 +1,9 @@ opencv-python~=4.5 -numpy~=1.20 +numpy~=1.19 tqdm~=4.60 Pillow~=8.2 flask~=1.1 flask-cors~=3.0 flask-restful~=0.3.8 -flask-talisman~=0.7.0 \ No newline at end of file +flask-talisman~=0.7.0 +tensorflow~=2.5 \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 123fad7..7973fd5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11499,6 +11499,11 @@ "@vue/shared": "3.0.11" } }, + "vue-class-component": { + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-7.2.6.tgz", + "integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w==" + }, "vue-cli-plugin-style-resources-loader": { "version": "0.1.5", "resolved": "https://registry.npm.taobao.org/vue-cli-plugin-style-resources-loader/download/vue-cli-plugin-style-resources-loader-0.1.5.tgz", @@ -11681,6 +11686,14 @@ "resolved": "https://registry.npmjs.org/vue-next-select/-/vue-next-select-2.1.1.tgz", "integrity": "sha512-cVu4VN2A3YSP2EZZ9MJSVYAgo/NPuzxZ7GJWEhT0wArJIKl9a7RwLWg2uCwnLcvdqWoMvIFsY0RnH1oTe0sVXA==" }, + "vue-property-decorator": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/vue-property-decorator/-/vue-property-decorator-8.5.1.tgz", + "integrity": "sha512-O6OUN2OMsYTGPvgFtXeBU3jPnX5ffQ9V4I1WfxFQ6dqz6cOUbR3Usou7kgFpfiXDvV7dJQSFcJ5yUPgOtPPm1Q==", + "requires": { + "vue-class-component": "^7.1.0" + } + }, "vue-round-slider": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vue-round-slider/-/vue-round-slider-1.0.1.tgz", @@ -11689,6 +11702,15 @@ "round-slider": "^1.6.1" } }, + "vue-slider-component": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/vue-slider-component/-/vue-slider-component-3.2.11.tgz", + "integrity": "sha512-2YyJW6TFnYk5FUvqQLvZcCJ+hthBXB819qNHtwnEUyDbOcTXV0n3Ou1ZphOi5FX9phlQIiC2NvjLuRAVmNq+Zw==", + "requires": { + "core-js": "^3.6.5", + "vue-property-decorator": "^8.0.0" + } + }, "vue-style-loader": { "version": "4.1.3", "resolved": "https://registry.npm.taobao.org/vue-style-loader/download/vue-style-loader-4.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index e6d6c9f..83c6f19 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,6 +22,7 @@ "vue": "^3.0.0", "vue-next-select": "^2.1.1", "vue-round-slider": "^1.0.1", + "vue-slider-component": "^3.2.11", "vue-upload-drop-images": "^1.0.4", "vue3-slider": "^1.6.2", "vuetify": "^3.0.0-alpha.0" diff --git a/frontend/src/App.vue b/frontend/src/App.vue index f93f825..909e259 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,7 +1,7 @@ @@ -121,6 +123,10 @@ export default { background-color: #414b57; } +.select-box .option.current { + background-color: #414b57; +} + .select-box label { cursor: pointer; } diff --git a/frontend/src/components/Settings.vue b/frontend/src/components/Settings.vue index 9c24435..2d1ba39 100644 --- a/frontend/src/components/Settings.vue +++ b/frontend/src/components/Settings.vue @@ -2,24 +2,41 @@

Settings

- +

Fonts

+ + +

Styles

+
-

Size

- +

Font Size

+

Resolution

- - + +
+ \ No newline at end of file diff --git a/frontend/src/store/result-store.js b/frontend/src/store/result-store.js index e2a7f29..1fe34b4 100644 --- a/frontend/src/store/result-store.js +++ b/frontend/src/store/result-store.js @@ -4,16 +4,49 @@ import backend from "../api/backend"; class ResultStore extends GenericStore { data() { return { - asciifiedImageURL: "", + resultImageURL: "", + currentFont: "Select Font", + currentStyle: "Select Style", + changeResolution: false, + resize: 1, + fontSize: 10, + file: null, }; } - async setAsciifiedImageURL(file) { + + setSetting(key, value) { + this.state[key] = value; + if (this.state.file) { + this.getResultImageURL(); + } + } + getSetting(key) { + return this.state[key]; + } + + setFile(file) { + this.state.file = file; + } + + async getResultImageURL() { + if ( + this.state.currentFont == "Select Font" && + this.state.currentStyle == "Select Style" + ) { + return; + } + const resize = this.state.changeResolution ? this.state.resize : -1; var data = new FormData(); - data.append("image", file); - data.append("fontsize", 10); - data.append("font", "Cousine-Regular"); - data.append("resize", -1); - this.state.asciifiedImageURL = await backend.asciify(data); + data.append("image", this.state.file); + data.append("fontsize", this.state.fontSize); + data.append("resize", resize); + if (this.state.currentFont != "Select Font") { + data.append("font", this.state.currentFont.toLowerCase()); + } + if (this.state.currentStyle != "Select Style") { + data.append("style", this.state.currentStyle.toLowerCase()); + } + this.state.resultImageURL = await backend.beautify(data); } } export const resultStore = new ResultStore();