Skip to content

Commit 733ce4c

Browse files
authored
Merge pull request #36 from Flippchen/dev
Dev
2 parents a92477f + e6af51d commit 733ce4c

13 files changed

Lines changed: 241 additions & 71 deletions

File tree

.github/workflows/build_binary.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ jobs:
1515
- name: Check out repository
1616
uses: actions/checkout@v2
1717

18-
- name: Set up Python 3.9
18+
- name: Set up Python 3.10
1919
uses: actions/setup-python@v2
2020
with:
21-
python-version: 3.9
21+
python-version: '3.10'
2222

2323
- name: Install dependencies
2424
run: |
@@ -43,10 +43,10 @@ jobs:
4343
- name: Check out repository
4444
uses: actions/checkout@v2
4545

46-
- name: Set up Python 3.9
46+
- name: Set up Python 3.10
4747
uses: actions/setup-python@v2
4848
with:
49-
python-version: 3.9
49+
python-version: '3.10'
5050

5151
- name: Install dependencies
5252
run: |
@@ -73,10 +73,10 @@ jobs:
7373
- name: Check out repository
7474
uses: actions/checkout@v2
7575

76-
- name: Set up Python 3.9
76+
- name: Set up Python 3.10
7777
uses: actions/setup-python@v2
7878
with:
79-
python-version: 3.9
79+
python-version: '3.10'
8080

8181
- name: Install dependencies
8282
run: |

.github/workflows/python.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
python-version: [ "3.9", "3.10" ]
13+
python-version: ['3.10']
1414
steps:
1515
- uses: actions/checkout@v3
1616
- name: Set up Python ${{ matrix.python-version }}

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ python web_app/main.py
3232
or download the [executable](https://github.com/Flippchen/PorscheInsight-CarClassification-AI/actions).
3333

3434
### Demo
35-
<img alt="Screenshot of the Web UI" src="assets/web_app/demo_web_ui.gif" height="500">
35+
<img alt="Screenshot of the Web UI" src="assets/web_app/demo_web_ui_mask.gif" height="500">
3636

3737
### Architecture
3838
The Web UI employs a two-step process involving two models. Initially, the pre_filter model determines if an image contains a Porsche. If a Porsche is detected, the image proceeds to the second model, which classifies the car according to the user's input.
@@ -46,6 +46,7 @@ To see the architecture of the Local App UI, click the arrow below.
4646
</details>
4747

4848
### ToDos
49+
- [ ] Build an ensemble model
4950
- [ ] Switch to Google Cloud function/use S3 bucket/compress image
5051
- [ ] Improve pre_filter model
5152
- [ ] Retrain car type model with efficientnet model
@@ -72,6 +73,7 @@ To see the architecture of the Local App UI, click the arrow below.
7273
- [x] Implement new architecture to online version
7374
- [x] Add release 1.0.0
7475
- [x] Add docker support (AWS Lambda)
76+
- [x] Display the mask of the predicted car
7577
</details>
7678

7779

assets/web_app/demo_web_ui.gif

-5.56 MB
Binary file not shown.
6.91 MB
Loading

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ keras~=2.10.0
77
# Inference
88
rembg==2.0.32
99
onnxruntime==1.14.1
10+
scipy~=1.9.3
1011
# Export to onnx
1112
tf2onnx==1.14.0
1213
# Explaining the model

tests/test_prepare_images.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@ def test_get_session():
1616
def test_replace_background():
1717
# Test using a sample image
1818
im = Image.new('RGB', (100, 100), 'WHITE')
19-
new_im = replace_background(im)
19+
new_im, mask = replace_background(im)
2020
assert new_im is not None
2121
assert isinstance(new_im, Image.Image)
22+
assert mask is not None
23+
assert isinstance(mask, Image.Image)
2224

2325

2426
def test_get_bounding_box():
@@ -55,9 +57,11 @@ def test_load_and_remove_bg(tmpdir):
5557
test_image = Image.new('RGB', (100, 100), 'WHITE')
5658
test_image_path = os.path.join(tmpdir, 'test_image.jpg')
5759
test_image.save(test_image_path)
58-
loaded_im = load_and_remove_bg(test_image_path, (50, 50))
60+
loaded_im, mask = load_and_remove_bg(test_image_path, (50, 50))
5961
assert loaded_im is not None
6062
assert loaded_im.size == (50, 50)
63+
assert mask is not None
64+
assert mask.size == (50, 50)
6165

6266

6367
def test_remove_bg_from_all_images(tmpdir):

utilities/prepare_images.py

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
import io
22
import os
3-
from PIL import Image, ImageOps, ImageFile
3+
4+
import numpy as np
5+
from scipy.ndimage import maximum_filter
6+
from PIL import Image, ImageOps, ImageFile, ImageFilter, ImageChops
47
from rembg import remove, new_session
58
from PIL.Image import Image as PILImage
9+
from typing import Tuple
610
from functools import cache
711
ImageFile.LOAD_TRUNCATED_IMAGES = True
812

13+
914
@cache
1015
def get_session():
1116
return new_session("u2net")
1217

1318

14-
def replace_background(im: PILImage, post_process_mask=False, session=None, size: tuple = None) -> PILImage:
19+
def replace_background(im: PILImage, post_process_mask=True, session=None, size: tuple = None) -> Tuple[PILImage, PILImage]:
1520
size = size or (300, 300)
1621
# if not isinstance(im, PILImage):
1722
# im = Image.open(io.BytesIO(im))
1823
session = session or get_session()
1924
im = remove(im, post_process_mask=post_process_mask, session=session)
25+
mask = im.copy()
2026
im = resize_cutout(im, size)
2127

2228
new_im = Image.new('RGBA', im.size, 'BLACK')
@@ -28,7 +34,7 @@ def replace_background(im: PILImage, post_process_mask=False, session=None, size
2834
image = Image.open(io.BytesIO(im_bytes))
2935
image = image.convert('RGB')
3036

31-
return image
37+
return image, mask
3238

3339

3440
def get_bounding_box(im: PILImage) -> tuple:
@@ -104,19 +110,62 @@ def fix_image(image):
104110
return image
105111

106112

113+
def convert_mask(mask, color=(29, 132, 181), border_color=(219, 84, 97), border_fraction=0.03):
114+
# Convert the image to RGBA if it is not already
115+
if mask.mode != 'RGBA':
116+
mask = mask.convert('RGBA')
117+
118+
border_size = int(min(mask.size) * border_fraction)
119+
120+
if border_size % 2 == 0:
121+
border_size += 1
122+
123+
# Create a copy of the mask and expand it to create the border
124+
mask_np = np.array(mask)
125+
border_mask_np = maximum_filter(mask_np, size=border_size)
126+
127+
# Convert back to PIL image
128+
border_mask = Image.fromarray(border_mask_np)
129+
130+
# Create the border by subtracting the original mask from the expanded mask
131+
border = ImageChops.difference(border_mask, mask)
132+
133+
# Convert the mask and the border to the desired colors
134+
mask_np = np.array(mask)
135+
border_np = np.array(border)
136+
137+
# Mask the areas where alpha channel is not zero
138+
mask_area = mask_np[..., 3] != 0
139+
border_area = border_np[..., 3] != 0
140+
141+
# Replace RGB channels with desired color while keeping alpha channel the same
142+
mask_np[mask_area, :3] = color
143+
mask_np[mask_area, 3] = mask_np[mask_area, 3] // 4
144+
border_np[border_area, :3] = border_color
145+
146+
# Convert back to PIL images
147+
mask = Image.fromarray(mask_np)
148+
border = Image.fromarray(border_np)
149+
150+
# Combine the mask and the border
151+
mask_with_border = Image.alpha_composite(mask, border)
152+
153+
return mask_with_border
154+
155+
107156
def load_and_remove_bg(path, size):
108157
image = Image.open(path)
109158
# image = resize_image(image, size)
110159
image = resize_and_pad_image(image, size)
111-
image = replace_background(image, size=size)
160+
image, mask = replace_background(image, size=size)
112161

113-
return image
162+
return image, mask
114163

115164

116165
def remove_bg_from_all_images(folder: str):
117166
for image in os.listdir(f'{folder}'):
118167
print("Removing background from", image)
119-
img = load_and_remove_bg(f"{folder}/{image}", (300, 300))
168+
img, mask = load_and_remove_bg(f"{folder}/{image}", (300, 300))
120169
img.save(f"{folder}/{image}")
121170

122171

web_ui/main.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
import base64
1313
from io import BytesIO
1414
from PIL import Image
15+
1516
from utilities.class_names import get_classes_for_model
16-
from utilities.prepare_images import replace_background, resize_and_pad_image, fix_image
17+
from utilities.prepare_images import replace_background, resize_and_pad_image, fix_image, convert_mask
1718
import pooch
1819
from rembg import new_session
1920

@@ -61,14 +62,24 @@ def load_model(model_name: str) -> ort.InferenceSession:
6162
return ort.InferenceSession(model_path, providers=['CPUExecutionProvider'])
6263

6364

64-
def prepare_image(image_data: Image, target_size: Tuple, remove_background: bool) -> np.ndarray:
65-
if remove_background:
66-
image = replace_background(image_data, session=session)
65+
def prepare_image(image_data: Image, target_size: Tuple, remove_background: bool, show_mask: bool) -> Tuple[np.ndarray, Image.Image]:
66+
if remove_background and show_mask:
67+
image, mask = replace_background(image_data, session=session)
68+
elif remove_background:
69+
image, _ = replace_background(image_data, session=session)
70+
mask = None
6771
else:
6872
image = resize_and_pad_image(image_data, target_size)
73+
mask = None
74+
6975
img_array = np.array(image).astype('float32')
7076
img_array = np.expand_dims(img_array, 0)
71-
return img_array
77+
78+
if mask is None:
79+
mask = Image.fromarray(np.zeros((1, 1), dtype=np.uint8))
80+
mask = mask.convert("RGBA")
81+
82+
return img_array, mask
7283

7384

7485
def get_top_3_predictions(prediction: np.ndarray, model_name: str) -> List[Tuple[str, float]]:
@@ -88,35 +99,53 @@ def get_pre_filter_prediction(image_data: np.ndarray, model_name: str):
8899

89100

90101
@eel.expose
91-
def classify_image(image_data: str, model_name: str) -> List[Tuple[str, float]]:
92-
# Load model if not loaded yet
102+
def classify_image(image_data: str, model_name: str, show_mask: str = "no") -> tuple[list[tuple[str, float]], str] | list[list[tuple[str, float]]]:
103+
# Loading the model if it's not already loaded
93104
if models[model_name] is None:
94105
models[model_name] = load_model(model_name)
95106

96-
# Decode image and open it
107+
# Decoding the base64 image data and opening the image
97108
image_data = base64.b64decode(image_data)
98109
image = Image.open(BytesIO(image_data))
110+
show_mask = True if show_mask == "yes" else False
99111

100-
# Fix image orientation and color mode if needed
112+
# Correcting image orientation and color mode if necessary
101113
image = fix_image(image)
102-
# Get correct input size for model
114+
115+
# Retrieving the required input size for the specified model
103116
input_size = models[model_name].get_inputs()[0].shape[1:3]
104117

105-
# Prepare image for filtering and predict
106-
# FIXME: It is a workaround to remove the bg in first place/ Why is the prediction with black background better?
107-
filter_image = prepare_image(image, input_size, remove_background=True)
118+
# Preparing image for processing and prediction
119+
# FIXME: Currently, the background is removed prior to prediction. This is a workaround,
120+
# as predictions seem to be better with a black background.
121+
filter_image, mask = prepare_image(image, input_size, remove_background=True, show_mask=show_mask)
122+
123+
# Converting the mask for processing
124+
mask = convert_mask(mask)
125+
buffer = io.BytesIO()
126+
mask.save(buffer, format="PNG")
127+
mask_base64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
128+
129+
# Getting initial predictions before applying filters
108130
filter_predictions = get_pre_filter_prediction(filter_image, "pre_filter")
109-
# If the pre_filter predicts porsche or other_car_brand, predict the correct model
131+
132+
# If the initial prediction is 'porsche' or other car brands, run the specified model's prediction
110133
if filter_predictions[0][0] == "porsche":
111-
# prepared_image = prepare_image(image, input_size, remove_background=True)
112134
input_name = models[model_name].get_inputs()[0].name
113135
prediction = models[model_name].run(None, {input_name: filter_image})
114-
# Get top 3 predictions
136+
137+
# Retrieving the top 3 predictions
115138
top_3 = get_top_3_predictions(prediction[0], model_name)
116139

117-
return top_3
140+
if show_mask:
141+
return top_3, mask_base64
142+
else:
143+
return [top_3]
118144

119-
return filter_predictions
145+
# Returning the initial predictions and an optional mask if show_mask is set to 'yes'
146+
if show_mask:
147+
return filter_predictions, mask_base64
148+
return [filter_predictions]
120149

121150

122151
eel.init("web")

web_ui/web/darkmode.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,8 @@ input[type="password"].dark-mode,
4545
color: #ffffff;
4646
border: 1px solid #6c757d;
4747
}
48+
#show-mask.dark-mode {
49+
background-color: #3c4049;
50+
color: #ffffff;
51+
border: 1px solid #6c757d;
52+
}

0 commit comments

Comments
 (0)