Category: Python

Free and Easy Horizontal Stacked Bar Graphs with Google Colab / Jupyter (as well as Python and Pandas)

Recently, my wife needed to create some stacked bar graphs for her publications. She wasn’t able to find a tool on the internet which allowed her to do this in an easy and shareable way. So I used this opportunity to look into Jupyter Notebooks. These notebooks allow you to document and run code and thus computations on a remote server from your browser and display the results directly in your browser as well

Google offers their version of Jupyter as a free service called Colab. Already having a Google account it was an easy choice to start there. But there are other similar services available on the internet and the code should run there as well (with minimal changes).

The file is quite self explanatory and can be saved within your own Google drive for editing. You can find it here.

The Notebook

Horizontal Stacked Bar Graph

This notebook will create a horizontal stacked bar graph within your browser based on the data you enter below. Just edit the data in 1) and execute the “gray blocks” in 1), 2) and 3) by either pressing “Shift + Enter” or the “Play” button in each blocks top left corner. To see the “Play” button you need to move your mouse over the block.

You can change the design of the graph in 2). This includes the unit which should be displayed on the bar. You will moste likely need to change the position of the legend as it depends on the data and size of the graph. To do this, change the x value of FigLegPosOffset slightly.

The code within a block is written in Python. Everything after a # is considered a comment. So you can add your own comments to your data or your changes.

1) Preparing the Data
Define your data below and execute the block when done. If everything is correct, your data will be displayed as table below the block.

# importing the functions needed to generate the graph 
import pandas as pd

# Axis captions
FigAxiXCap="Cocktails"
FigAxiYCap="Which Cocktail do you want for the Party?"

# Data (beware: the first entry will be displayed last)

## Data Indexes 
DataIndexes = ['Caipirinha', 'Piña Colada', 'Cuba Libre', 'Mai Tai', 'Ipanema', 'Caribbean Night']
## Data Options
DataYesPlease =  [73.2, 23.8, 24.7, 31.2, 56.6, 68.2]
DataSureWhyNot = [20.3, 51.7, 60.3, 19.2,  4.8, 22.3]
DataJustNo =     [ 6.5, 24.5,   15, 59.6, 38.6,  9.5]

## Name of each column (for the legend)
df = pd.DataFrame(data={'Yes, please!': DataYesPlease, 'Sure, why not?': DataSureWhyNot, 'Just no!': DataJustNo})

#-------------------------------------------------------------------------------
#no need to edit below this line
df.index = DataIndexes

# print table for convinience/debugging (transposed)
df

2) Figure Properties

In the block below you can change the format of the figure. When done (or fine with the defaults) execute it to set all properties.

#################
# Figure Design #
#################
#Figure
## Figure Size
FigSizeX = 20
FigSizeY = 5
## Figure background color 
FigBackColor="white"
## Axis Caption Size
FigAxiXCapFontSize=45
FigAxiYCapFontSize=45
## Axis Labels Font Size
FigLabelFontSize=20

# Blocks
## Colormap of the blocks (all Options: https://matplotlib.org/stable/gallery/color/colormap_reference.html)
FigColMap="Pastel2"
## Width of bars (everything above 1 will overlap)
FigBarsWidth=0.8
## FontSize of bar lables
FigBarsFontSize=20
# Data and Captions (needs to be defined)
## Unit to display within bars
FigBarUnit="%"

# Legend
## Legend Font Size
FigLegFontSize=20
## Legend position [best|upper right|upper left|lower left|lower right|right|center left|center right|lower center|upper center|center]
FigLegPos="upper center"
## Figure OffSet (will move the legend box slightly to match actual figure position)
FigLegPosOffset=(1.11, 1.0)

## 3) Generate and Display the Figure
The block below will generate the figure. You do not need to change anything here. Just be sure to execute it again **after changing anything above**.

# define plot type and layout
ax = df.plot(stacked=True, kind='barh', figsize=(FigSizeX, FigSizeY), colormap=FigColMap, width=FigBarsWidth, fontsize=FigBarsFontSize, xlim=[0,100])

# Add labels to the bars 
# Attribution: Trenton McKinney on StackExchange: https://stackoverflow.com/a/60895640/3764407 CC BY-SA 4.0
for rect in ax.patches:
    # Find where everything is located
    height = rect.get_height()
    width = rect.get_width()
    x = rect.get_x()
    y = rect.get_y()
    
    # The width of the bar is the data value and can be used as the label
    label_text = ""
    if width > 4:
      label_text = f'{width:.1f}' + FigBarUnit  ##### use {width:.2f} or {width:.3f} to increase precision  ####
    
    # ax.text(x, y, text)
    label_x = x + width / 2
    label_y = y + height / 2

    # plot only when height is greater than specified value
    if height > 0:
        ax.text(label_x, label_y, label_text, ha='center', va='center', fontsize=FigBarsFontSize)

# Set legend position and style
ax.legend(bbox_to_anchor=FigLegPosOffset, loc=FigLegPos, borderaxespad=0., fontsize=FigLegFontSize)    
# Set y label
ax.set_ylabel(FigAxiXCap, fontsize=FigAxiXCapFontSize)
# Set x label
ax.set_xlabel(FigAxiYCap, fontsize=FigAxiYCapFontSize)
# Set Background Color
ax.set_facecolor(FigBackColor)

3) Generate and Display the Figure
The block below will generate the figure. You do not need to change anything here. Just be sure to execute it again after changing anything above.

define plot type and layout
ax = df.plot(stacked=True, kind='barh', figsize=(FigSizeX, FigSizeY), colormap=FigColMap, width=FigBarsWidth, fontsize=FigBarsFontSize, xlim=[0,100])

# Add labels to the bars 
# Attribution: Trenton McKinney on StackExchange: https://stackoverflow.com/a/60895640/3764407 CC BY-SA 4.0
for rect in ax.patches:
    # Find where everything is located
    height = rect.get_height()
    width = rect.get_width()
    x = rect.get_x()
    y = rect.get_y()
    
    # The width of the bar is the data value and can be used as the label
    label_text = ""
    if width > 4:
      label_text = f'{width:.1f}' + FigBarUnit  ##### use {width:.2f} or {width:.3f} to increase precision  ####
    
    # ax.text(x, y, text)
    label_x = x + width / 2
    label_y = y + height / 2

    # plot only when height is greater than specified value
    if height > 0:
        ax.text(label_x, label_y, label_text, ha='center', va='center', fontsize=FigBarsFontSize)

# Set legend position and style
ax.legend(bbox_to_anchor=FigLegPosOffset, loc=FigLegPos, borderaxespad=0., fontsize=FigLegFontSize)    
# Set y label
ax.set_ylabel(FigAxiXCap, fontsize=FigAxiXCapFontSize)
# Set x label
ax.set_xlabel(FigAxiYCap, fontsize=FigAxiYCapFontSize)
# Set Background Color
ax.set_facecolor(FigBackColor)

4) Download the Figure (from Google Colab)
To export the generated figure use the following block. You can change the file name and type as well as its resolution (dots per inch/DPI).

# Define the file name
FigureFileName="figure"
# Define the type (jpg, png, pdf, svg)
FigureFileType="png"
# Define DPI (setting this too high might result in long wait times and finally an error)
FigureDPI=30

# do not edit below
FigFile=FigureFileName + "." + FigureFileType
ax.figure.savefig(FigFile,dpi=FigureDPI,bbox_inches = 'tight')
from google.colab import files
files.download("/content/" + FigFile )

OpenCV Python: eigene Haar Cascade erstellen

Um eine Haar Cascade zu erstellen werden “positive” und “negative” Bilder benötigt. “Positive” Bilder enthalten das Objekt welches gefunden werden soll. Das können entweder Bilder sein, welche ausschließlich das Objekt enthalten oder Bilder, welche (neben anderen Inhalten) das Objekt enthalten, wobei hier die ROI (region of interest) angegeben werden muss. Mit diesen positiven Bildern wird eine Vektordatei erstellt, was im Grunde nichts anderes ist, als alle positiven Bilder zusammen.

Theoretisch ist ein positives Bild und einige tausend negative Bilder ausreichend. Aus dem positiven Bild lassen sich durch verschiedenes “Rauschen” die notwendige Anzahl erstellen. Die negativen Bilder können alles mögliche enthalten (außer das Object selbst).

Als grober Richtwert: das Verhältnis von positiven zu negativen Bildern sollte etwa 2:1 sein.

Pfade für Grafiken:

workspace 
-- pos
-- neg
-- data

Die Grafiken sollten vor dem Training entsprechend verkleinert werden. Positive Grafiken sollten möglichst klein gehalten werden. 50 x 50px sollten für den Anfang ausreichen, diese bringen bereits gute Ergebnisse. Bei den negativen Grafiken verwenden wir 100 x 100px. Je größer die Grafiken, desto länger dauert das Training.

Hier ein einfaches Skript zum Verkleinern der Grafiken resize-images.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-

import cv2
import numpy as np
import glob
import os
import getopt
import sys
from os.path import basename

imgPath = "./"
imgPathOut = ""
resizeDim = 400

usage = '''usage: resize-image.py -i  -o  -d '
    
    -i   : input directory
    -o   : output directory (optional, same as input if empty)
    -d   : dimension of output image (px)
    '''

try:
    opts, args = getopt.getopt(sys.argv[1:],"hi:o:d:",["ipath=","opath=","d="])
except getopt.GetoptError:
    print usage
    sys.exit(2)
for opt, arg in opts:
    if opt == '-h':
        print usage
        sys.exit()
    elif opt in ("-i", "--ipath"):
        imgPath = arg
    elif opt in ("-o", "--opath"):
        imgPathOut = arg
    elif opt in ("-d", "--d"):
        resizeDim = int(arg)

# Check if output path is empty
if not imgPathOut:
    imgPathOut = imgPath

# Check for images
if len(list(glob.iglob('%s/*.jpg' % imgPath))) == 0:
    print 'Could not find any images in path "%s".' % (imgPath)
    sys.exit(2)

# Create output directory if it does not exist
if not os.path.exists(imgPathOut):
    os.makedirs(imgPathOut)

# Loop trough images
for filename in glob.iglob('%s/*.jpg' % imgPath):
    imgName = basename(filename)
    print '%s/%s' % (imgPath, imgName)

    # Read image
    img = cv2.imread(filename)
    # Resize
    img = cv2.resize(img, (resizeDim, resizeDim), interpolation = cv2.INTER_AREA)
    # Convert to grayscale
    img = cv2.cvtColor(img, cv2.COLOR_RGBA2GRAY)
    # Write image
    cv2.imwrite('%s/%s' % (imgPathOut, imgName), img)

Besitzen die Grafiken nun eine geeignete Größe, können wir zum Erstellen der Trainingslisten übergehen. Hier ein Skript zum Erstellen der Trainingsdateien (Datenlisten) create-pos-n-neg.py:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import os
import sys
import numpy as np
import cv2

def create_pos_n_neg():

    fpos = open('pos.lst','w')
    fneg = open('neg.lst','w')

    for file_type in ['neg', 'pos']:
        
        for img in os.listdir(file_type):

            if file_type == 'pos':
                try:
                    image = cv2.imread(file_type+'/'+img)
                    h, w, channels = image.shape
                    line = file_type+'/'+img+' 1 0 0 '+str(w)+' '+str(h)+'\n'
                    fpos.write(line)
                except:
                    continue
            elif file_type == 'neg':
                line = file_type+'/'+img+'\n'
                fneg.write(line)

    fpos.close()
    fneg.close()

create_pos_n_neg()

Die Dateien pos.lst und neg.lst enthalten nun die Informationen zu den positiven und negativen Grafiken.

Aus den positiven Grafiken muss nun die Vektordatei erstellt werden, in der alle positiven Grafiken zusammengefasst werden. Dazu wird opencv_createsamples verwendet!

opencv_createsamples -info pos.lst -num 2500 -w 20 -h 20 -vec pos.vec

Nun kann das Training beginnen:

opencv_traincascade -data data -vec pos.vec -bg neg.lst -numPos 2000 -numNeg 1000 -numStages 10 -w 20 -h 20

Dabei wird angegeben, wo die Daten gespeichert werden, wo Vektordatei und Hintergrunddatei ist, wie viele positive und negative Grafiken verwendet werden sollen, die Anzahl der Iterationen, sowie die Breite und Höhe. Beachte, dass weitaus weniger numPos angegeben werden, als eigentlich vorhanden sind! Das ist notwendig, um etwas Raum für die einzelnen Iterationen zu haben.

Es können noch weitere Parameter übergeben werden, aber diese sind vollkommen ausreichend. Die wichtigsten Werte sind die Anzahl der positiven und negativen Grafiken. Es hat sich als praktikabel erwiesen, ein Verhältnis von 2:1 von positiven:negativen Grafiken zu haben (als allgemeine Faustregel). Daraus erhält man nun die “Stufen”, in unserem Fall 10. Es sollten mindestens 10-20 sein. Je mehr Stufen, desto länger dauert es (die Zeitdauer steigt exponentiell). Das Gute ist, man kann anfangs 10 Stufen trainieren und später mit auf 20 gehen. Dabei werden die vorhandenen Stufen verwendet und dort fortgesetzt, wo das letzte Training beendet wurde. Man könnte theoretisch auch mit 100 Stufen beginnen und über Nacht rechnen lassen. Am nächsten Morgen bricht man das Training ab und schaut, wie weit man gekommen ist. Soll der Befehl ausgeführt werden, wenn das Terminal geschlossen ist, dann kann man nohup nutzen:

nohup opencv_traincascade -data data -vec pos.vec -bg neg.lst -numPos 2000 -numNeg 1000 -numStages 10 -w 20 -h 20 &

Hier nun ein einfaches Testskript, um das trainierte Objekt zu erkennen. Dieses nutzt eine Webcam als Bildeingabe, es lässt sich allerdings auch sehr einfach anpassen, um einfache Bilddateien zu verwenden:

test.py:

import numpy as np
import cv2

# this is the cascade we just made. Call what you want
object_cascade = cv2.CascadeClassifier('data/stage.xml')

cap = cv2.VideoCapture(0)

while 1:
    ret, img = cap.read()
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # image, reject levels level weights.
    objects = object_cascade.detectMultiScale(gray, 50, 50)
    
    for (x,y,w,h) in objects:
        cv2.rectangle(img,(x,y),(x+w,y+h),(255,255,0),2)

    cv2.imshow('img',img)
    k = cv2.waitKey(30) & 0xff
    if k == 27:
        break

cap.release()
cv2.destroyAllWindows()

Die Größe der Boxen zum Detektieren der Objekte richtet sich nach den Abmessungen der Trainingsdaten. Bei 50 x 50px ergibt sich also eine relativ kleine Box. Bei größeren Abmessungen wie 100×100 sollte das besser funktionieren, allerdings dauert das Training dann auch deutlich länger.

Originalbeitrag in Englisch: pythonprogramming.net
Bildquelle: unsplash.com