Skip to content

Commit 235d422

Browse files
improve file operations, update docstrings (#20)
1 parent b07625d commit 235d422

File tree

3 files changed

+238
-119
lines changed

3 files changed

+238
-119
lines changed

app.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import streamlit as st
2-
from streamlit_image_comparison import image_comparison
32

3+
from streamlit_image_comparison import exif_transpose
4+
from streamlit_image_comparison import read_image_as_pil
5+
from streamlit_image_comparison import pillow_to_base64
6+
from streamlit_image_comparison import local_file_to_base64
7+
from streamlit_image_comparison import local_file_to_base64
8+
from streamlit_image_comparison import pillow_local_file_to_base64
9+
from streamlit_image_comparison import image_comparison
410

511
IMAGE_TO_URL = {
612
"sample_image_1": "https://user-images.githubusercontent.com/34196005/143309873-c0c1f31c-c42e-4a36-834e-da0a2336bb19.jpg",

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
sahi>=0.9.0
21
streamlit
2+
Pillow
3+
numpy
Lines changed: 229 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,134 +1,246 @@
11
import streamlit.components.v1 as components
2-
import sahi.utils.cv
32
from PIL import Image
43
import base64
54
import io
65
import os
76
import uuid
8-
9-
__version__ = "0.0.3"
7+
from typing import Union
8+
import requests
9+
import numpy as np
1010

1111
TEMP_DIR = "temp"
1212

13+
__version__ = "0.0.4"
14+
15+
def exif_transpose(image: Image.Image):
16+
"""
17+
Transpose a PIL image accordingly if it has an EXIF Orientation tag.
18+
Inplace version of https://github.com/python-pillow/Pillow/blob/master/src/PIL/ImageOps.py exif_transpose()
19+
:param image: The image to transpose.
20+
:return: An image.
21+
"""
22+
exif = image.getexif()
23+
orientation = exif.get(0x0112, 1) # default 1
24+
if orientation > 1:
25+
method = {
26+
2: Image.FLIP_LEFT_RIGHT,
27+
3: Image.ROTATE_180,
28+
4: Image.FLIP_TOP_BOTTOM,
29+
5: Image.TRANSPOSE,
30+
6: Image.ROTATE_270,
31+
7: Image.TRANSVERSE,
32+
8: Image.ROTATE_90,
33+
}.get(orientation)
34+
if method is not None:
35+
image = image.transpose(method)
36+
del exif[0x0112]
37+
image.info["exif"] = exif.tobytes()
38+
return image
39+
40+
def read_image_as_pil(image: Union[Image.Image, str, np.ndarray], exif_fix: bool = False):
41+
"""
42+
Loads an image as PIL.Image.Image.
43+
Args:
44+
image : Can be image path or url (str), numpy image (np.ndarray) or PIL.Image
45+
"""
46+
# https://stackoverflow.com/questions/56174099/how-to-load-images-larger-than-max-image-pixels-with-pil
47+
Image.MAX_IMAGE_PIXELS = None
48+
49+
if isinstance(image, Image.Image):
50+
image_pil = image.convert('RGB')
51+
elif isinstance(image, str):
52+
# read image if str image path is provided
53+
try:
54+
image_pil = Image.open(
55+
requests.get(image, stream=True).raw if str(image).startswith("http") else image
56+
).convert("RGB")
57+
if exif_fix:
58+
image_pil = exif_transpose(image_pil)
59+
except: # handle large/tiff image reading
60+
try:
61+
import skimage.io
62+
except ImportError:
63+
raise ImportError("Please run 'pip install -U scikit-image imagecodecs' for large image handling.")
64+
image_sk = skimage.io.imread(image).astype(np.uint8)
65+
if len(image_sk.shape) == 2: # b&w
66+
image_pil = Image.fromarray(image_sk, mode="1").convert("RGB")
67+
elif image_sk.shape[2] == 4: # rgba
68+
image_pil = Image.fromarray(image_sk, mode="RGBA").convert("RGB")
69+
elif image_sk.shape[2] == 3: # rgb
70+
image_pil = Image.fromarray(image_sk, mode="RGB")
71+
else:
72+
raise TypeError(f"image with shape: {image_sk.shape[3]} is not supported.")
73+
elif isinstance(image, np.ndarray):
74+
if image.shape[0] < 5: # image in CHW
75+
image = image[:, :, ::-1]
76+
image_pil = Image.fromarray(image).convert("RGB")
77+
else:
78+
raise TypeError("read image with 'pillow' using 'Image.open()'")
79+
80+
return image_pil
81+
82+
def pillow_to_base64(image: Image.Image) -> str:
83+
"""
84+
Convert a PIL image to a base64-encoded string.
85+
86+
Parameters
87+
----------
88+
image: PIL.Image.Image
89+
The image to be converted.
90+
91+
Returns
92+
-------
93+
str
94+
The base64-encoded string.
95+
"""
96+
in_mem_file = io.BytesIO()
97+
image.save(in_mem_file, format="JPEG", subsampling=0, quality=100)
98+
img_bytes = in_mem_file.getvalue() # bytes
99+
image_str = base64.b64encode(img_bytes).decode("utf-8")
100+
base64_src = f"data:image/jpg;base64,{image_str}"
101+
return base64_src
102+
103+
def local_file_to_base64(image_path: str) -> str:
104+
"""
105+
Convert a local image file to a base64-encoded string.
13106
14-
def pillow_to_base64(image: Image.Image):
15-
in_mem_file = io.BytesIO()
16-
image.save(in_mem_file, format="JPEG", subsampling=0, quality=100)
17-
img_bytes = in_mem_file.getvalue() # bytes
18-
image_str = base64.b64encode(img_bytes).decode("utf-8")
19-
base64_src = f"data:image/jpg;base64,{image_str}"
20-
return base64_src
107+
Parameters
108+
----------
109+
image_path: str
110+
The path to the image file.
21111
112+
Returns
113+
-------
114+
str
115+
The base64-encoded string.
116+
"""
117+
file_ = open(image_path, "rb")
118+
img_bytes = file_.read()
119+
image_str = base64.b64encode(img_bytes).decode("utf-8")
120+
file_.close()
121+
base64_src = f"data:image/jpg;base64,{image_str}"
122+
return base64_src
22123

23-
def local_file_to_base64(image_path: str):
24-
file_ = open(image_path, "rb")
25-
img_bytes = file_.read()
26-
image_str = base64.b64encode(img_bytes).decode("utf-8")
27-
file_.close()
28-
base64_src = f"data:image/jpg;base64,{image_str}"
29-
return base64_src
124+
def pillow_local_file_to_base64(image: Image.Image, temp_dir: str):
125+
"""
126+
Convert a Pillow image to a base64 string, using a temporary file on disk.
30127
128+
Parameters
129+
----------
130+
image : PIL.Image.Image
131+
The Pillow image to convert.
132+
temp_dir : str
133+
The directory to use for the temporary file.
31134
32-
def pillow_local_file_to_base64(image: Image.Image):
33-
# pillow to local file
34-
img_path = TEMP_DIR + "/" + str(uuid.uuid4()) + ".jpg"
35-
image.save(img_path, subsampling=0, quality=100)
36-
# local file base64 str
37-
base64_src = local_file_to_base64(img_path)
38-
return base64_src
135+
Returns
136+
-------
137+
str
138+
A base64-encoded string representing the image.
139+
"""
140+
# Create temporary file path using os.path.join()
141+
img_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".jpg")
39142

143+
# Save image to temporary file
144+
image.save(img_path, subsampling=0, quality=100)
145+
146+
# Convert temporary file to base64 string
147+
base64_src = local_file_to_base64(img_path)
148+
149+
return base64_src
40150

41151
def image_comparison(
42-
img1: str,
43-
img2: str,
44-
label1: str = "1",
45-
label2: str = "2",
46-
width: int = 704,
47-
show_labels: bool = True,
48-
starting_position: int = 50,
49-
make_responsive: bool = True,
50-
in_memory=False,
51-
):
52-
"""Create a new juxtapose component.
53-
Parameters
54-
----------
55-
img1: str, PosixPath, PIL.Image or URL
56-
Input image to compare
57-
img2: str, PosixPath, PIL.Image or URL
58-
Input image to compare
59-
label1: str or None
60-
Label for image 1
61-
label2: str or None
62-
Label for image 2
63-
width: int or None
64-
Width of the component in px
65-
show_labels: bool or None
66-
Show given labels on images
67-
starting_position: int or None
68-
Starting position of the slider as percent (0-100)
69-
make_responsive: bool or None
70-
Enable responsive mode
71-
in_memory: bool or None
72-
Handle pillow to base64 conversion in memory without saving to local
73-
Returns
74-
-------
75-
static_component: Boolean
76-
Returns a static component with a timeline
77-
"""
78-
# prepare images
79-
img1_pillow = sahi.utils.cv.read_image_as_pil(img1)
80-
img2_pillow = sahi.utils.cv.read_image_as_pil(img2)
81-
82-
img_width, img_height = img1_pillow.size
83-
h_to_w = img_height / img_width
84-
height = (width * h_to_w) * 0.95
85-
86-
if in_memory:
87-
# create base64 str from pillow images
88-
img1 = pillow_to_base64(img1_pillow)
89-
img2 = pillow_to_base64(img2_pillow)
90-
else:
91-
# clean temp dir
92-
os.makedirs(TEMP_DIR, exist_ok=True)
93-
for file_ in os.listdir(TEMP_DIR):
94-
if file_.endswith(".jpg"):
95-
os.remove(TEMP_DIR + "/" + file_)
96-
# create base64 str from pillow images
97-
img1 = pillow_local_file_to_base64(img1_pillow)
98-
img2 = pillow_local_file_to_base64(img2_pillow)
99-
100-
# load css + js
101-
cdn_path = "https://cdn.knightlab.com/libs/juxtapose/latest"
102-
css_block = f'<link rel="stylesheet" href="{cdn_path}/css/juxtapose.css">'
103-
js_block = f'<script src="{cdn_path}/js/juxtapose.min.js"></script>'
104-
105-
# write html block
106-
htmlcode = f"""
107-
<style>body {{ margin: unset; }}</style>
108-
{css_block}
109-
{js_block}
110-
<div id="foo" style="height: {height}; width: {width or '100%'};"></div>
111-
<script>
112-
slider = new juxtapose.JXSlider('#foo',
113-
[
114-
{{
115-
src: '{img1}',
116-
label: '{label1}',
117-
}},
118-
{{
119-
src: '{img2}',
120-
label: '{label2}',
121-
}}
122-
],
123-
{{
124-
animate: true,
125-
showLabels: {'true' if show_labels else 'false'},
126-
showCredits: true,
127-
startingPosition: "{starting_position}%",
128-
makeResponsive: {'true' if make_responsive else 'false'},
129-
}});
130-
</script>
131-
"""
132-
static_component = components.html(htmlcode, height=height, width=width)
133-
134-
return static_component
152+
img1: str,
153+
img2: str,
154+
label1: str = "1",
155+
label2: str = "2",
156+
width: int = 704,
157+
show_labels: bool = True,
158+
starting_position: int = 50,
159+
make_responsive: bool = True,
160+
in_memory: bool = False,
161+
) -> components.html:
162+
"""
163+
Create a comparison slider for two images.
164+
165+
Parameters
166+
----------
167+
img1: str
168+
Path to the first image.
169+
img2: str
170+
Path to the second image.
171+
label1: str, optional
172+
Label for the first image. Default is "1".
173+
label2: str, optional
174+
Label for the second image. Default is "2".
175+
width: int, optional
176+
Width of the component in pixels. Default is 704.
177+
show_labels: bool, optional
178+
Whether to show labels on the images. Default is True.
179+
starting_position: int, optional
180+
Starting position of the slider as a percentage (0-100). Default is 50.
181+
make_responsive: bool, optional
182+
Whether to enable responsive mode. Default is True.
183+
in_memory: bool, optional
184+
Whether to handle pillow to base64 conversion in memory without saving to local. Default is False.
185+
186+
Returns
187+
-------
188+
components.html
189+
Returns a static component with a timeline
190+
"""
191+
# Prepare images
192+
img1_pillow = read_image_as_pil(img1)
193+
img2_pillow = read_image_as_pil(img2)
194+
195+
img_width, img_height = img1_pillow.size
196+
h_to_w = img_height / img_width
197+
height = int((width * h_to_w) * 0.95)
198+
199+
if in_memory:
200+
# Convert images to base64 strings
201+
img1 = pillow_to_base64(img1_pillow)
202+
img2 = pillow_to_base64(img2_pillow)
203+
else:
204+
# Create base64 strings from temporary files
205+
os.makedirs(TEMP_DIR, exist_ok=True)
206+
for file_ in os.listdir(TEMP_DIR):
207+
if file_.endswith(".jpg"):
208+
os.remove(os.path.join(TEMP_DIR, file_))
209+
img1 = pillow_local_file_to_base64(img1_pillow, TEMP_DIR)
210+
img2 = pillow_local_file_to_base64(img2_pillow, TEMP_DIR)
211+
212+
# Load CSS and JS
213+
cdn_path = "https://cdn.knightlab.com/libs/juxtapose/latest"
214+
css_block = f'<link rel="stylesheet" href="{cdn_path}/css/juxtapose.css">'
215+
js_block = f'<script src="{cdn_path}/js/juxtapose.min.js"></script>'
216+
217+
# write html block
218+
htmlcode = f"""
219+
<style>body {{ margin: unset; }}</style>
220+
{css_block}
221+
{js_block}
222+
<div id="foo" style="height: {height}; width: {width or '100%'};"></div>
223+
<script>
224+
slider = new juxtapose.JXSlider('#foo',
225+
[
226+
{{
227+
src: '{img1}',
228+
label: '{label1}',
229+
}},
230+
{{
231+
src: '{img2}',
232+
label: '{label2}',
233+
}}
234+
],
235+
{{
236+
animate: true,
237+
showLabels: {'true' if show_labels else 'false'},
238+
showCredits: true,
239+
startingPosition: "{starting_position}%",
240+
makeResponsive: {'true' if make_responsive else 'false'},
241+
}});
242+
</script>
243+
"""
244+
static_component = components.html(htmlcode, height=height, width=width)
245+
246+
return static_component

0 commit comments

Comments
 (0)