How to create a visualization of the spread of COVID-19 with Poser

SchelleFire · April 29, 2020 11:05 am

Tags: Poser, Tutorial



For this visualization I used the confirmed-case statistical time-series that is updated daily by a team at Johns Hopkins University Center for Systems Science and Engineering.

The GitHub repository of data can be found here: https://github.com/CSSEGISandData/COVID-19

Step 1. Grabbing the data

I cloned the data repository into a folder locally:

git clone https://github.com/CSSEGISandData/COVID-19.git

Step 2. Having Python modules needed

I knew I would need to use python to pull out the data I wanted from the .csv files that the repo contained. So I used pyenv and made sure that I had Python 3 installed and that the python pandas module was also installed. I would use Poser's python later, but it doesn't have the modules installed that I would need and on the mac it is still Python 2.7.1

Step 3. Decide on data format to read into Poser

The goal here was to convert the data I needed to a .json file in a format I could easily import into Poser's python interpreter so that I could use it to create objects inside Poser.

I decided on this JSON file format to include name, longitude, latitude, and values:

{
  "locations": [ 
          {
             "name": "New York City...", 
             "latitude" : 40.76727260000001,
             "longitude" : -73.97152637,
             "values" : [0,0,....., 98308, 103208, 106763]
          },
      {   "name": ...}, 

      {   "name": ...}
    ]

}

I won't put all the python code here but the important calls were:

import pandas as pd
numBoxes = 1800

#Read the data data = pd.read_csv(time_series_covid19_confirmed_US.csv, sep=',')

#Remove cities with zero and sort #the data by the most recent day as index #and the highest values to the top

nonZeroSorted = data[data[dayID] > 0].sort_values(by=[dayID],ascending=False) jj=0 for index in range(numBoxes): name = nonZeroSorted.iloc[index]['Combined_Key'] latitude = nonZeroSorted.iloc[index]['Lat'] longitude = nonZeroSorted.iloc[index]['Long_'] for single_date in daterange(start_date, end_date): dateStr = single_date.strftime('%-m/%-d/%y') columnsWanted = [dateStr] activeCases = nonZeroSorted.iloc[index][columnsWanted] timeline[jj] = int(activeCases.values[0]) jj+1 locations[index] = { "name": name, "latitude": latitude, "longitude": longitude, "values": timeline } jsonData = {'locations':locations} with open(jsonFileName, 'w') as outfile:
json.dump(jsonData, outfile)

Step 4. Build a base Poser file with an Earth.

I built a base file in Poser using a properly spherically texture mapped sphere and was able to find some public earth textures I could start with. I eventually combined textures from multiple sources. The important thing is that they are accurate and that the longitude and latitude could be mapped to simple rotations on the rendered globe. There are many choices out there for earth textures. I subdivided the sphere to make sure it looked good up close. Some of the textures I think I got here:

http://www.shadedrelief.com/natural3/

https://blog.mastermaps.com/2013/09/creating-webgl-earth-with-threejs.html

In the base file I also added an object that I would use as the geometry for the boxes. It's a simple 6 polygon box. I also added a grouping object I would use as a parent for the added boxes to make them easy to turn on and off.

I also experimented with parents on a box to get the correct order of z-scale, z-translation, x-rotation first and then y-rotation next so I could directly map the longitude and latitude values.

Here is a look at the materials in progress. This one has some transparency on the ocean areas. The main texture is color adjusted with shader nodes — made grey instead of black and white, and inverted for the transparency map:

Step 6. Build blocks on the earth from the data.

In order to build blocks on the earth, each block needs to be created with an origin at the bottom of the box, scaled in height (z scale), placed on the surface of the earth (translated in Z), then rotated through two parent rotations: X rot and then Y rot.

I used grouping objects inside Poser as the parents of each block

All of this was done using the Python scripting available inside Poser 11 Pro.

Here is the script that inputs the data from json and builds a block for each location. The time series values are read in and the z-scale channel is animated for each block.

 
import json
import random

numFrames=83 expand = 4 path = "topUS.json" s = poser.Scene()

unitBox = s.ActorByInternalName("box_1") unitBox.SetVisibleInReflections(0) unitBox.SetVisibleInRender(0) unitBox.SetVisibleInIDL(0) unitBox.SetVisibleInCamera(0) boxGeom = unitBox.Geometry() earth = s.ActorByInternalName("earth 1") s.SetNumFrames(1)

blockParent = None try: blockParent = s.ActorByInternalName("BlockParent") except: pass if blockParent == None: blockParent = s.CreateGrouping() blockParent.SetParent(earth) blockParent.SetName("BlockParent") blockParent.SetVisibleInReflections(0) blockParent.SetVisibleInRender(0) blockParent.SetVisibleInIDL(0) blockParent.SetVisibleInCamera(0) blockParent.SetGeometry(poser.NewGeometry())

with open(path) as json_file: data = json.load(json_file) # For each location in the file: for location in data[u'locations']: name = location[u'name'] latitude = location[u'latitude'] longitude = location[u'longitude'] values = location[u'values'];

    numFrames = len(values) 
    s.SetNumFrames(numFrames * expand) 
    # Make a new box that we will animate later 
    newBox = None 
    try:
        newBox = s.ActorByInternalName(name) 
    except: 
        pass 
    createNew = False 
    if (newBox == None): 
        createNew = True 
        newBox = s.CreatePropFromGeom(boxGeom,name)
        newBox.SetOrigin(0.0, 0.0, -0.05)

brightness = 1.1 r = random.uniform(brightness0.76,brightness0.82) g = random.uniform(brightness0.53,brightness0.63) b = random.uniform(brightness0.34,brightness0.44) material = newBox.Materials()[0] tree = material.ShaderTree() material.SetDiffuseColor(r,g,b) node = tree.RendererRootNode(1) input = node.InputByInternalName("Diffuse_Color") input.SetColor(r, g, b )

    # Create longitude parent: 
    if (createNew): 
        longitudeActor = s.CreateGrouping()
        longitudeActor.SetGeometry(poser.NewGeometry())

longitudeActor.SetVisibleInReflections(0) longitudeActor.SetVisibleInRender(0) longitudeActor.SetVisibleInIDL(0) longitudeActor.SetVisibleInCamera(0) longitudeActor.SetParent(blockParent)

        # Create latitude : 
        latitudeActor = s.CreateGrouping() 
        latitudeActor.SetGeometry(poser.NewGeometry())
        latitudeActor.SetParent(longitudeActor) 
        latitudeActor.SetVisibleInReflections(0)

latitudeActor.SetVisibleInRender(0) latitudeActor.SetVisibleInIDL(0) latitudeActor.SetVisibleInCamera(0) newBox.SetParent(latitudeActor)

        # Move the main parent: 
        ytran = longitudeActor.Parameter("yTran") 
        ytran.SetValue(0.34900001) 
        
        # Set the latitude and longitude 
        xrot = latitudeActor.Parameter("xRotate")

xrot.SetValue(-latitude) yrot = longitudeActor.Parameter("yRotate") yrot.SetValue(longitude)

        # Move the box 
        xtran = newBox.Parameter("xTran") 
        xtran.SetValue(0) 
        ytran = newBox.Parameter("yTran") 
        ytran.SetValue(0) 
        ztran = newBox.Parameter("zTran") 
        ztran.SetValue(0.389) 

    scale = newBox.Parameter("Scale") 
    scale.SetValue(0.033) 
    # For every frame set the scale channel value 
    zscale = newBox.Parameter("zScale") 
    for frame in range(0,numFrames): 
        zscale.SetValueFrame(values[frame] / 700.0, frame) 
        if (values[frame] <= 0): 
            scale.SetValueFrame(0, frame) 
        else: 
            scale.SetValueFrame(0.033, frame)

Once I create this script in a text file, I opened the Python scripts palette and clicked on an empty button, so I could load the script into the button. The next click on the button would run it. I set the script up to run it again and it would be faster if the objects were already created inside Poser. It takes a while to build the scene the first time (and I tracked down some things that could speed this up for the future.)

Step 7. Retime the animation to slow it down.

At this point I needed a new feature that Poser didn't have. So I added it. 

I wanted to retime the animation of everything in the scene so that I could spread the data points to more frames and interpolate between them. I added a radio button to retime everything in the scene. I guess I could have done this in the initial setting up of the keyframes too.

I then loaded some pre-saved camera animation from a camera library entry I created.

This was added to in the final Poser 11.3 update. 🙂

Step 8. Add the incrementing dates

Now I needed another Poser python script that could create the text objects for the dates that would need to increment. This was another missing feature in Poser — to be able to create a text prop from Python. It can be done interactively but there was no python function — so I added it! In the 11.3 coming out this week you will find CreateTextProp() on the poser scene object.

This python is a little sloppy and could probably be made simpler but this is what I used to create the numbers:

import random
import datetime
from datetime import timedelta, date

numDays = 83 expand = 4 numFrames = numDays*expand s = poser.Scene() dateParent = s.ActorByInternalName("DateParent") date1 = date(2020, 1, 22) date2 = datetime.datetime.today().date() day = timedelta(days=1) frame = 0 while date1 <= date2: dateStr = date1.strftime('%-m/%-d/%y') dateProp = s.CreateTextProp(dateStr, 0) dateProp.SetParent(dateParent,0,0) dateProp.SetVisibleInReflections(1) dateProp.SetVisibleInRender(1) dateProp.SetVisibleInIDL(1) dateProp.SetVisibleInCamera(1)

# Set the materials material = dateProp.Materials()[0] 
tree = material.ShaderTree() 
node = tree.RendererRootNode(1) 
input = node.InputByInternalName("Ambient_Color") 
input.SetColor(1.0, 1.0, 1.0) 
input = node.InputByInternalName("Ambient_Value") 
input.SetFloat(0.5) scale = dateProp.Parameter("Scale")

scale.SetValueFrame(0, 0) dateProp.SetRangeConstant(0,numFrames-1) if frame < numFrames: scale.SetValueFrame(2.5, frame) if frame+1 < numFrames: scale.SetValueFrame(2.5, frame+1) if frame+2 < numFrames: scale.SetValueFrame(2.5, frame+2) if frame+3 < numFrames: scale.SetValueFrame(2.5, frame+3) if frame+4 < numFrames: scale.SetValueFrame(0, frame+4) frame = frame + expand date1 = date1 + day

Then I rendered the sequence overnight.

Final Scenes in Poser

This shows 260 countries confirmed Covid-19 cases growth over time.

This shows the progression of confirmed Covid-19 cases in the US through April 11. The 1,200 top locations are shown.


Larry Weinberg created Poser and now works with the Poser Software development team. Find out more about his current projects at his website.



Related Articles:

Article Comments

No article comments have been posted. Sign In or Register to share your thoughts.


Privacy Notice

This site uses cookies to deliver the best experience. Our own cookies make user accounts and other features possible. Third-party cookies are used to display relevant ads and to analyze how Renderosity is used. By using our site, you acknowledge that you have read and understood our Terms of Service, including our Cookie Policy and our Privacy Policy.