
what one of the best a part of being an engineer is? You’ll be able to simply construct stuff. It’s like a superpower. One wet afternoon I had this random thought of making a sentiment visualization of a textual content enter with a smiley face that modifications it’s expression base on how optimistic the textual content is. The extra optimistic the textual content, the happier the smiley appears. There are some attention-grabbing ideas to study right here, so let me information you thru how this mission works!
Stipulations
To comply with alongside, you want the next packages:
- customtkinter
- Opencv-python
- torch
- transformers
Utilizing uv, you may add the dependencies with the next command:
uv add customtkinter opencv-Python torch transformers
NOTE: When utilizing uv with torch it’s good to specify the index for the bundle. E.g if you wish to use cuda, you want the next in your
pyproject.toml
:[[tool.uv.index]] identify = "pytorch-cu118" url = "https://obtain.pytorch.org/whl/cu118" specific = true [tool.uv.sources] torch = [{ index = "pytorch-cu118" }] torchvision = [{ index = "pytorch-cu118" }]
UI Format Skeleton
For some of these initiatives I all the time like to begin with a fast structure of the UI parts. On this case the structure will probably be fairly easy, there’s a textbox with a single line on the high that fills the width and under it the canvas filling the remainder of the accessible area. This will probably be the place we draw the smiley face 🙂
Utilizing customtkinter
, we will write the structure as follows:
import customtkinter
class App(customtkinter.CTk):
def __init__(self) -> None:
tremendous().__init__()
self.title("Sentiment Evaluation")
self.geometry("800x600")
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(0, weight=0)
self.grid_rowconfigure(1, weight=1)
self.sentiment_text_var = customtkinter.StringVar(grasp=self, worth="Love")
self.textbox = customtkinter.CTkEntry(
grasp=self,
corner_radius=10,
font=("Consolas", 50),
justify="heart",
placeholder_text="Enter textual content right here...",
placeholder_text_color="grey",
textvariable=self.sentiment_text_var,
)
self.textbox.grid(row=0, column=0, padx=20, pady=20, sticky="nsew")
self.textbox.focus()
self.image_display = CTkImageDisplay(self)
self.image_display.grid(row=1, column=0, padx=20, pady=20, sticky="nsew")
Sadly there’s no good out of the field answer for drawing opencv frames on a UI ingredient, so I constructed my very own CTkImageDisplay
. If you wish to study intimately the way it works, try my earlier submit. Briefly, I exploit a CTKLabel
element and decouple the thread that updates the picture from the GUI thread utilizing a synchronization queue.

Procedural Smiley
For our smiley face, we might use totally different discrete pictures for sentiment ranges, so for instance having three pictures saved for damaging, impartial and optimistic. Nevertheless, to get a extra fine-grained sentiment visualized, we would want extra pictures and it rapidly turns into infeasible and we won’t be able to animate transitions between these pictures.

A greater strategy is to generate the picture of the smiley face procedurally at runtime. To maintain it easy, we are going to solely change the background shade of the smiley, in addition to the curve of its mouth.

First we have to generate a canvas picture, on which we will draw the smiley.
def create_sentiment_image(positivity: float, image_size: tuple[int, int]) -> np.ndarray:
"""
Generates a sentiment picture primarily based on the positivity rating.
This attracts a smiley with its expression primarily based on the positivity rating.
Args:
positivity: A float representing the positivity rating within the vary [-1, 1].
image_size: A tuple representing the scale of the picture (width, top).
Returns:
A string representing the trail to the generated sentiment picture.
"""
width, top = image_size
body = np.zeros((top, width, 4), dtype=np.uint8)
# TODO: draw smiley
return body
Our picture needs to be clear exterior of the smiley face, so we’d like 4 shade channels, the final one would be the alpha channel. Since OpenCV pictures are represented as numpy
arrays with unsigned 8-bit integers, we create the picture utilizing the np.uint8
knowledge sort. Do not forget that the arrays are saved y-first, so the top
of the image_size
is handed first to the array creation
We will outline some variables for the size and colours of our smiley that will probably be useful whereas drawing.
color_outline = (80,) * 3 + (255,) # grey
thickness_outline = min(image_size) // 30
heart = (width // 2, top // 2)
radius = min(image_size) // 2 - thickness_outline
The background shade of the smiley face needs to be crimson for damaging sentiments and inexperienced for optimistic sentiments. To attain this with a uniform brightness throughout the transition, we will use the HSV shade area and easily interpolate the hue between 0% and 30%.

color_bgr = color_hsv_to_bgr(
hue=(positivity + 1) / 6, # positivity [-1,1] -> hue [0,1/3]
saturation=0.5,
worth=1,
)
color_bgra = color_bgr + (255,)
We’d like to ensure to make the colour totally opaque by including a 100% alpha worth in fourth channel. Now we will draw our smiley face circle with a border.
cv2.circle(body, heart, radius, color_bgra, -1) # Fill
cv2.circle(body, heart, radius, color_outline, thickness_outline) # Border

Thus far so good, now we will add the eyes. We calculate an offset from the middle to the left and proper to put the 2 eyes symmetrically.
# calculate the place of the eyes
eye_radius = radius // 5
eye_offset_x = radius // 3
eye_offset_y = radius // 4
eye_left = (heart[0] - eye_offset_x, heart[1] - eye_offset_y)
eye_right = (heart[0] + eye_offset_x, heart[1] - eye_offset_y)
cv2.circle(body, eye_left, eye_radius, color_outline, -1)
cv2.circle(body, eye_right, eye_radius, color_outline, -1)

Now on to the difficult half, the mouth. The form of the mouth will probably be a parabola scaled appropriately. We will merely multiply the usual parabola y=x²
with the positivity rating.

In the long run the road will probably be drawn utilizing cv2.polylines
, which wants xy coordinate pairs. Utilizing np.linspace
we generate 100 factors on the x-axis and the polyval
operate to calculate the in accordance y values of the polygon.
# mouth parameters
mouth_wdith = radius // 2
mouth_height = radius // 3
mouth_offset_y = radius // 3
mouth_center_y = heart[1] + mouth_offset_y + positivity * mouth_height // 2
mouth_left = (heart[0] - mouth_wdith, heart[1] + mouth_offset_y)
mouth_right = (heart[0] + mouth_wdith, heart[1] + mouth_offset_y)
# calculate factors of polynomial for the mouth
ply_points_t = np.linspace(-1, 1, 100)
ply_points_y = np.polyval([positivity, 0, 0], ply_points_t) # y=positivity*x²
ply_points = np.array(
[
(
mouth_left[0] + i * (mouth_right[0] - mouth_left[0]) / 100,
mouth_center_y - ply_points_y[i] * mouth_height,
)
for i in vary(len(ply_points_y))
],
dtype=np.int32,
)
# draw the mouth
cv2.polylines(
body,
[ply_points],
isClosed=False,
shade=color_outline,
thickness=int(thickness_outline * 1.5),
)
Et voilà, we now have a procedural smiley face!

To check the operate, I wrote a fast take a look at case utilizing pytest
that saves the smiley faces with totally different sentiment scores:
from pathlib import Path
import cv2
import numpy as np
import pytest
from sentiment_analysis.utils import create_sentiment_image
IMAGE_SIZE = (512, 512)
@pytest.mark.parametrize(
"positivity",
np.linspace(-1, 1, 5),
)
def test_sentiments(visual_output_path: Path, positivity: float) -> None:
"""
Check the smiley face era.
"""
picture = create_sentiment_image(positivity, IMAGE_SIZE)
assert picture.form == (IMAGE_SIZE[1], IMAGE_SIZE[0], 4)
# assert heart pixel is opaque
assert picture[IMAGE_SIZE[1] // 2, IMAGE_SIZE[0] // 2, 3] == 255
# save the picture for visible inspection
positivity_num_0_100 = int((positivity + 1) * 50)
image_fn = f"smiley_{positivity_num_0_100}.png"
cv2.imwrite(str(visual_output_path / image_fn), picture)

Sentiment Evaluation
To find out how pleased or unhappy our smiley ought to appear like, we first want to investigate the textual content enter and calculate a sentiment. This job is named sentiment evaluation. We’ll use a pre-trained transformer mannequin to foretell a classification rating for the courses NEGATIVE, NEUTRAL and POSITIVE. We will then fuse the boldness scores of those courses to calculate a last sentiment rating between -1 and +1.
Utilizing the pipeline from the transformers library, we will outline processing pipeline primarily based on a pre-trained mannequin from huggingface. Utilizing the top_k
parameter, we will specify what number of classification outcomes needs to be returned. Since we wish all three courses, we set it to three.
from transformers import pipeline
model_name = "cardiffnlp/twitter-roberta-base-sentiment"
sentiment_pipeline = pipeline(
job="sentiment-analysis",
mannequin=model_name,
top_k=3,
)
To run the sentiment evaluation, we will name the pipeline with a string argument. It will return a listing of outcomes with a single ingredient, so we have to unpack the primary ingredient.
outcomes = self.sentiment_pipeline(textual content)
# [
# [
# {"label": "LABEL_2", "score": 0.5925878286361694},
# {"label": "LABEL_1", "score": 0.3553399443626404},
# {"label": "LABEL_0", "score": 0.05207228660583496},
# ]
# ]
for label_score_dict in outcomes[0]:
label: str = label_score_dict["label"]
rating: float = label_score_dict["score"]
We will outline a label mapping, that tells us how every confidence rating impacts the ultimate sentiment. Then we will combination the positivity over all confidence scores.
label_mapping = {"LABEL_0": -1, "LABEL_1": 0, "LABEL_2": 1}
positivity = 0.0
for label_score_dict in outcomes[0]:
label: str = label_score_dict["label"]
rating: float = label_score_dict["score"]
if label in label_mapping:
positivity += label_mapping[label] * rating
To check our pipeline, we will wrap it in a category and run some exams utilizing pytest
. We confirm that sentences with a optimistic sentiment have a rating larger than zero and vice versa sentences with a damaging sentiment ought to have a rating under zero.
import pytest
from sentiment_analysis.sentiment_pipeline import SentimentAnalysisPipeline
@pytest.fixture
def sentiment_pipeline() -> SentimentAnalysisPipeline:
"""
Fixture to create a SentimentAnalysisPipeline occasion.
"""
return SentimentAnalysisPipeline(
model_name="cardiffnlp/twitter-roberta-base-sentiment",
label_mapping={"LABEL_0": -1.0, "LABEL_1": 0.0, "LABEL_2": 1.0},
)
@pytest.mark.parametrize(
"text_input",
[
"I love this!",
"This is awesome!",
"I am so happy!",
"This is the best day ever!",
"I am thrilled with the results!",
],
)
def test_sentiment_analysis_pipeline_positive(
sentiment_pipeline: SentimentAnalysisPipeline, text_input: str
) -> None:
"""
Check the sentiment evaluation pipeline with a optimistic enter.
"""
assert (
sentiment_pipeline.run(text_input) > 0.0
), "Anticipated optimistic sentiment rating."
@pytest.mark.parametrize(
"text_input",
[
"I hate this!",
"This is terrible!",
"I am so sad!",
"This is the worst day ever!",
"I am disappointed with the results!",
],
)
def test_sentiment_analysis_pipeline_negative(
sentiment_pipeline: SentimentAnalysisPipeline, text_input: str
) -> None:
"""
Check the sentiment evaluation pipeline with a damaging enter.
"""
assert (
sentiment_pipeline.run(text_input) < 0.0
), "Anticipated damaging sentiment rating."
Integration
Now the final half that’s lacking, is just hooking up the textual content field to our sentiment pipeline and updating the displayed picture with the corresponding smiley face. We will add a hint
to the textual content variable, which can run the sentiment pipeline in a brand new thread managed by a thread pool, to stop the UI from freezing whereas the pipeline is operating.
class App(customtkinter.CTk):
def __init__(self, sentiment_analysis_pipeline: SentimentAnalysisPipeline) -> None:
tremendous().__init__()
self.sentiment_analysis_pipeline = sentiment_analysis_pipeline
...
self.sentiment_image = None
self.sentiment_text_var = customtkinter.StringVar(grasp=self, worth="Love")
self.sentiment_text_var.trace_add("write", lambda *_: self.on_sentiment_text_changed())
...
self.update_sentiment_pool = ThreadPool(processes=1)
self.on_sentiment_text_changed()
def on_sentiment_text_changed(self) -> None:
"""
Callback operate to deal with textual content modifications within the textbox.
"""
new_text = self.sentiment_text_var.get()
self.update_sentiment_pool.apply_async(
self._update_sentiment,
(new_text,),
)
def _update_sentiment(self, new_text: str) -> None:
"""
Replace the sentiment picture primarily based on the brand new textual content enter.
This operate is run in a separate course of to keep away from blocking the principle thread.
Args:
new_text: The brand new textual content enter from the person.
"""
positivity = self.sentiment_analysis_pipeline.run(new_text)
self.sentiment_image = create_sentiment_image(
positivity,
self.image_display.display_size,
)
self.image_display.update_frame(self.sentiment_image)
def most important() -> None:
# Initialize the sentiment evaluation pipeline
sentiment_analysis = SentimentAnalysisPipeline(
model_name="cardiffnlp/twitter-roberta-base-sentiment",
label_mapping={"LABEL_0": -1, "LABEL_1": 0, "LABEL_2": 1},
)
app = App(sentiment_analysis)
app.mainloop()
And at last the smiley is visualized within the utility and modifications dynamically with the sentiment of the textual content enter!

For the complete implementation and extra particulars, checkout the mission repository on GitHub:
https://github.com/trflorian/sentiment-analysis-viz
All visualizations on this submit have been created by the creator.