Poser Python: Using UV- and Weightmaps



  • Discus about manipulating/using UV- and Weightmaps with Poser-Python.

    I started with a script to define vertex groups based on an UV-Map while discussing dynamic cloth here: https://forum.smithmicro.com/post/46123

    Actually I'm working on something like a library to be used in scripts. I'll post what I have so far later today. And I hope some other folks may step in and offer their knowledge too.



  • Here is a simple function to create an UV-map from the currently selected Poser actor as PNG-file.
    The image is stored in the same folder the script is in. Use it with your Paint-App to mark vertices.

    The image is shown at the end with a simple WX-window. This has to be worked on to be useful. I've no time to do that at the moment. But I think you get the idea.

    from __future__ import print_function
    
    import numpy as np
    import os
    from PIL import Image, ImageDraw, ImageFont
    from random import randint
    
    try:
        import poser
    except ImportError:
        raise RuntimeError("This script needs Poser")
    
    
    ##############################################################################
    
    def get_PoserTexVertices_as_num_ar(geom_obj):
        assert isinstance(geom_obj, poser.GeomType)
        try:
            return np.array(map(lambda v: (v.U(), v.V()), geom_obj.TexVertices()), np.float64)
        except:
            return None
    
    
    ##############################################################################
    
    def get_PoserTexVertices_as_num_ar4(geom_obj):
        """Returns n x 4 array, for each texture polygon"""
        assert isinstance(geom_obj, poser.GeomType)
        try:
            numtpolys = geom_obj.NumTexPolygons()
        except:
            return None
        if numtpolys == 0:
            return None
    
        ar = np.zeros(numtpolys * 4 * 2, np.float64).reshape(numtpolys, 4, 2)
        for idx, tpoly in enumerate(geom_obj.TexPolygons()):
            tv = [[v.U(), v.V()] for v in tpoly.TexVertices()]
            while len(tv) < 4:
                tv.append([np.nan,np.nan])
            if len(tv) > 4:
                raise RuntimeError("Polygon with index %s has more than 4 vertices." % idx)
    
            ar[idx] = tv
    
        return ar
    
    
    ##############################################################################
    
    def getSize(num_tvert_array):
        min_x = min_y = 10000
        max_x = max_y = -min_x
        shape = num_tvert_array.shape
        for x, y in np.reshape(num_tvert_array, (-1, 2)):
            if x < min_x:
                min_x = x
            elif x > max_x:
                max_x = x
            if y < min_y:
                min_y = y
            elif y > max_y:
                max_y = y
    
        np.reshape(num_tvert_array, shape)
        return min_x, max_x, min_y, max_y
    
    
    ##############################################################################
    
    def getPoserTex_maxSize(poserobj):
        umin = 100000
        umax = -umin
        vmin = 100000
        vmax = -vmin
    
        def minmax(geom, umin=umin, umax=umax, vmin=vmin, vmax=vmax):
            for tvert in geom.TexVertices():
                u = tvert.U()
                if u < umin:
                    umin = u
                elif u > umax:
                    umax = u
                v = tvert.V()
                if v < vmin:
                    vmin = v
                elif v > vmax:
                    vmax = v
    
            return umin, umax, vmin, vmax
    
        if poserobj.IsFigure():
            for actor in poserobj.Actors():
                if actor.IsBodyPart():
                    try:
                        geom = actor.Geometry()
                    except:
                        continue
                    if geom is None or geom.NumTexVertices() == 0:
                        continue
                    umin, umax, vmin, vmax = minmax(geom)
        else:
            umin, umax, vmin, vmax = minmax(poserobj.Geometry())
    
        return umin, umax, vmin, vmax
    
    
    ##############################################################################
    
    def new_color():
        while True:
            color = randint(0, 255), randint(0, 255), randint(0, 255)
            if sum(color) < 600:
                break
        return color
    
    
    ##############################################################################
    
    def drawPolys(ar, draw, color=None, factor=800.0, height=1000):
        if ar is None:
            return
    
        def p(v):
            return int(v[0] * factor), int(height - v[1] * factor)
    
        for poly in ar:
            draw.polygon([p(v) for v in poly], fill=color or 0, outline=(0, 0, 0))
    
    
    ##############################################################################
    
    
    def showWithWX(image):
        import wx
        SIZE = (800, 600)
    
        def pil_to_wx(image):
            width, height = image.size
            buffer = image.convert('RGB').tostring()
            bitmap = wx.BitmapFromBuffer(width, height, buffer)
            return bitmap
    
        class Panel(wx.Panel):
            def __init__(self, parent):
                super(Panel, self).__init__(parent, -1)
                self.SetSize(SIZE)
                self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
                self.Bind(wx.EVT_PAINT, self.on_paint)
                self.update()
    
            def update(self):
                self.Refresh()
                self.Update()
                wx.CallLater(15, self.update)
    
            def create_bitmap(self):
                bitmap = pil_to_wx(image)
                return bitmap
    
            def on_paint(self, event):
                bitmap = self.create_bitmap()
                dc = wx.AutoBufferedPaintDC(self)
                dc.DrawBitmap(bitmap, 0, 0)
    
        class Frame(wx.Frame):
            def __init__(self):
                style = wx.DEFAULT_FRAME_STYLE & ~wx.RESIZE_BORDER & ~wx.MAXIMIZE_BOX
                super(Frame, self).__init__(None, -1, 'Camera Viewer', style=style)
                panel = Panel(self)
                self.Fit()
    
        app = poser.WxApp()
        frame = Frame()
        frame.Center()
        frame.Show()
    
    
    if __name__ == "__main__":
        SC = poser.Scene()
        actor = SC.CurrentActor()
    
        mx, fx, my, fy = getPoserTex_maxSize(actor)
    
        dx = fx - mx
        dy = fy - my
        width = int(round(3000 * fx, 0))
        height = int(round(3000 * fy, 0))
        img = Image.new("RGB", (width, height), (255, 255, 255))
        draw = ImageDraw.Draw(img)
        font = ImageFont.load_default()
    
        factor = max(img.size) / max(fx, fy)
    
        ar = get_PoserTexVertices_as_num_ar4(actor.Geometry())
        color = new_color()
        drawPolys(ar, draw=draw, color=color, factor=factor, height=height)
    
        fname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "polys2.png")
        img.save(fname)
        print("saved to", fname)
        showWithWX(img)
    
    


  • If everything goes as I think it can, at the end we will be able to mark vertices directly in that wxWindow shown in Poser. We'll see...



  • To export weightmap info to a file, we need something to color-code the intensity of a weightmaped vertex.

    I tried grayscale, but that's only good enough to remove some pixels/vertices. Because grayscale has only 255 different values. That will result in "jumps" (stairs?). I used it so far as a mask (blending out areas accidently painted with Posers weightmap-brush).

    Poser uses, as far as I can say, R << 16 | G << 8 | B, which gives round about 16 Million different values. I tried that and it worked. But – there is no Paint-App I know of we can use to paint this colors with easily.

    Any ideas anybody?



  • From the back of my mind: Krita supports 16 bit integer grayscale.
    Is it worth while to move to a 16 bits channel and will the 256 levels from 8 bit grayscale make a noticable step?
    If the radius of the skin path around the rotation point is 100 mm, (a high value, say for the buttock) and situation is extreme rotation, say 128 degrees, the difference in path would be 128/256 = 0.5 degrees. One radian is about 57 degrees, so 0.5 degrees, is approximately 1/104th of a radian. The step size would be about 1 mm in skin tangential move, but much less in most cases. The result is driven by the product of radius and angle and I have taken the worst values for both. Will this be noticable? I do believe it when you say so but I do not know how sensitive the eye is for this if the texture is not a clear checker pattern.



  • @fverbaas You are right as long as we talk about colors. But here we need a color-representation for a floatingpoint value in the weightmap. Here is a snipet what we have to generate from the "painted" UV-Map coordinates:

    weightMap	1364755992_0
    		{ 
    			numbVerts 942
    			v 0 0.000399
    			v 1 0.015026
    			v 2 0.034318
    			v 3 0.054606
    			v 4 0.071934
    			v 5 0.081098
    			v 6 0.098006
    			v 7 0.134301
    			v 8 0.049199
    			v 9 0.071064
    			v 10 0.093215
    			v 11 0.108720
    			v 12 0.123347
    			v 13 0.146580
    			v 14 0.192221
                            ...
    			v 833 0.899871
    			v 834 0.898654
    			v 835 0.900047
    			v 836 0.898290
    			v 837 0.898654
    			v 838 0.898654
    			v 839 0.898672
    			v 840 0.897626
    			v 841 0.897811
                           ...
    }
    

    All from the same weightmap. Theoretically any value between 0.000000 and 1.000000.
    Because this seems to be 6 digits after the decimalpoint, it can be represented by integers between 0 and 999999. So a 16 Bit value is not enough :)

    At a point (far away at the moment) we can mark the vertices directly with wxPython, using a similar method as Poser does. But flat (2D), not 3D.

    Anybody interested in working on a wxPython script to show a bunch of fixed vertex-coordinates in a window, let the user mark points with RGB-Color (representig strength)and returning the marked points back as floatingpoint values?



  • The good news: Seems that Poser Python's image modul can handle 16-Bit Grayscale.



  • @adp busy today, (rehearsing and singing in a concert later), so I won't be able to contribute just at the moment, but will watch with interest and reply when I'm free.



  • @anomalaus said in Poser Python: Using UV- and Weightmaps:

    @adp busy today, (rehearsing and singing in a concert later), so I won't be able to contribute just at the moment, but will watch with interest and reply when I'm free.

    Glad to hear you are interested.
    What do you sing?



  • @adp Baritone. Opera excerpts with a small group. "La ci da rem la mano" from Mozart's Don Giovanni :-)



  • @anomalaus said in Poser Python: Using UV- and Weightmaps:

    Doesn't sound easy :)

    I'm not into music at all; I see it like Wilhelm Busch:
    Musik wird oft nicht schön gefunden,
    Weil sie stets mit Geräusch verbunden.

    Google translation:
    Music is often not found nice,
    Because they are always connected with noise



  • @adp ;-) hopefully the only "noise" in our concert was due to enthusiastic audience responses (and winter coughs and colds). I can't deny the fact that participatory music has been an absolute lifeline for me in times of dark despair and depression. But I'm doing well, at the moment :-)



  • @adp said in Poser Python: Using UV- and Weightmaps:

    All from the same weightmap. Theoretically any value between 0.000000 and 1.000000.
    Because this seems to be 6 digits after the decimalpoint, it can be represented by integers between 0 and 999999. So a 16 Bit value is not enough :)

    Question is whether the 6 digits accuracy in the file means one needs to have 6 digits accuracy to work with.
    If one would cut this representation to 4 digits accuracy by rounding off to 4 digits (or for the sake of argument just cutting off the last 2), would that give a noticable difference?
    If the idea is to paint weight maps manually, I think even 3 digit accuracy would be sufficient to represent that.

    Just for the fun if it (one of pet peeves this is) Poser uses 8 decimals in an .obj file. With one Poser unit as appr. 2.662 m, the last digit represents 0.000,000,02662 m, or 0.000,026 mm, or 26 nanometer. In 1995, when Poser was launched, that was well under the limit of microchip techology (350 nanometer). I do not think Poser was ever considered to design microchips. ;-)



  • I tried the script above.
    It works great for body parts, but when I have the body actor of a figure selected, it throws an error message:

    ...
    Traceback (most recent call last):
    File "C:\Users\Frans\Documents\makeuvmap.py", line 183, in <module>
    mx, fx, my, fy = getPoserTex_maxSize(actor)
    File "C:\Users\Frans\Documents\makeuvmap.py", line 103, in getPoserTex_maxSize
    umin, umax, vmin, vmax = minmax(poserobj.Geometry())
    File "C:\Users\Frans\Documents\makeuvmap.py", line 78, in minmax
    for tvert in geom.TexVertices():
    AttributeError: 'NoneType' object has no attribute 'TexVertices'
    ...



  • @F_Verbaas Just a nitpick, but to keep the record straight, you are off by an order of 1000. Given that a Poser Native unit is ~2.662 meters (Poser says 2.621 on the dials) then in mm that would be 2621. There are 8 digits in a saved file the first two being "0." the last 6 being after the decimal point so you would divide 2621 mm by 1,000,000 giving you 0.002621mm not 0.000,026mm as you posted. (you should have multiplied meters by 1000 not divide by 1000). Still a very small number.



  • @F_Verbaas in the dim distant past of Poser versions, I have a vague memory that someone on the dev team thought that storing angles with less precision would improve UI performance by shortening recalculation time. Fortunately, that folly did not last more than a single release cycle, IIRC. Unfortunately, inaccuracy of angles is HUGELY magnified the further one gets from the axis of rotation. I'm forever wishing I could set rotations with less that one hundredth of a degree, when I'm trying to converge eye vectors with a camera position. You can forget astronomy altogether if you can't calculate with better than milliarcsecond accuracy ;-)



  • @F_Verbaas Yep. The Body-Actor is not a "real" actor. It does have a geometry-object, but nothing in there.

    Avoid using the Body-Actor or replace the bottom part with:

    if __name__ == "__main__":
        SC = poser.Scene()
        actor = SC.CurrentActor()
    
        if actor.Name() == "Body":
            print("Body is not usable. Select another actor.")
        else:
            mx, fx, my, fy = getPoserTex_maxSize(actor)
            dx = fx - mx
            dy = fy - my
            width = int(round(3000 * fx, 0))
            height = int(round(3000 * fy, 0))
            img = Image.new("RGB", (width, height), (255, 255, 255))
            draw = ImageDraw.Draw(img)
            font = ImageFont.load_default()
    
            factor = max(img.size) / max(fx, fy)
    
            ar = get_PoserTexVertices_as_num_ar4(actor.Geometry())
            color = new_color()
            drawPolys(ar, draw=draw, color=color, factor=factor, height=height)
    
            fname = os.path.join(os.path.dirname(os.path.abspath(__file__)), "polys2.png")
            img.save(fname)
            print("saved to", fname)
            showWithWX(img)
    
    
    
    
    


  • Hmm. the error came from the 'else' of the

    if poserobj.IsFigure():

    command.

    if poserobj is the body actor of a figure, should this not result in a 'true' with the statements in the 'if' block executed?



  • @anomalaus
    I agree totally with you when it is about rotation angles, which are applied in succession and lower accuracy may result in significant errors. I do not know what you refer to but back in the 32 bits age it was a very bad idea to do the transformation matrices with single precision floats.
    What we look at here is precision for plotting. That is a totally different application.

    The question was whether any ready-available 16 bits per pixel grayscale painting techniques in Photoshop or Gimp could be used or not. My personal opinion is that for the end result it may not pay off to go into difficlties to get greater precision.



  • @F_Verbaas said in Poser Python: Using UV- and Weightmaps:

    Hmm. the error came from the 'else' of the

    if poserobj.IsFigure():

    command.

    if poserobj is the body actor of a figure, should this not result in a 'true' with the statements in the 'if' block executed?

    The body is just an actor (type poser.ActorType). Poser defines only the main figure as poser.FigureType (the types are all defined within posers API)

    The test for figure in the subroutine is there because I copied it from my colection of subroutines.


Log in to reply
 

Looks like your connection to Graphics Forum was lost, please wait while we try to reconnect.