1

I have 512x512 render and python script that exports X and Y Locations to .txt file, but the output looks like this:

0, -0.043, 1.577
1, 0.107, 5.171
2, 0.211, 8.771
3, 0.412, 12.613
4, 0.536, 16.454
5, 0.679, 20.148
6, 0.756, 23.838
7, 0.926, 27.511
8, 1.031, 31.177
9, 1.069, 34.886
10, 1.059, 38.587

But I need to have real X and Y values inside 512x512 area. Im using Orthographic camera if that matters. Do I need to multiply the Metrics, tweak settings or customize python export code?

Export code:

import bpy

def write_some_data(context, filepath, use_some_setting): print("running write_some_data...")

scene = bpy.context.scene  
frame = scene.frame_start
cube = bpy.data.objects['Tracker1']

f = open(filepath, 'w', encoding='utf-8')
while frame <= scene.frame_end:
    scene.frame_set(frame) 
    x, y, z = cube.location
    f.write("%d" % frame)
    f.write(", ")   
    f.write("%5.3f, %5.3f" % (x, y))
    f.write("\n")
    frame += 1 
f.close()  

return {'FINISHED'}


ExportHelper is a helper class, defines filename and

invoke() function which calls the file selector.

from bpy_extras.io_utils import ExportHelper from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.types import Operator

class ExportSomeData(Operator, ExportHelper): """This appears in the tooltip of the operator and in the generated docs""" bl_idname = "export_test.some_data" # important since its how bpy.ops.import_test.some_data is constructed bl_label = "Export Some Data"

# ExportHelper mixin class uses this 
filename_ext = ".txt"

filter_glob: StringProperty(
    default="*.txt",
    options={'HIDDEN'},
    maxlen=255,  # Max internal buffer length, longer would be clamped.
)

# List of operator properties, the attributes will be assigned
# to the class instance from the operator settings before calling.
use_setting: BoolProperty(
    name="Example Boolean",
    description="Example Tooltip",
    default=True,
)

type: EnumProperty(
    name="Example Enum",
    description="Choose between two items",
    items=(
        ('OPT_A', "First Option", "Description one"),
        ('OPT_B', "Second Option", "Description two"),
    ),
    default='OPT_A',
)

def execute(self, context):
    return write_some_data(context, self.filepath, self.use_setting)


Only needed if you want to add into a dynamic menu

def menu_func_export(self, context): self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")

def register(): bpy.utils.register_class(ExportSomeData) bpy.types.TOPBAR_MT_file_export.append(menu_func_export)

def unregister(): bpy.utils.unregister_class(ExportSomeData) bpy.types.TOPBAR_MT_file_export.remove(menu_func_export)

if name == "main": register()

# test call
bpy.ops.export_test.some_data('INVOKE_DEFAULT')

  • I think I got it working with code x_pixels = x_blender * (512 / 15) y_pixels = y_blender * (512 / 15), I think this is correct. – konstailematon Oct 04 '23 at 12:39

1 Answers1

0

Converting coordinates from world space to camera space is dependent on multiple things:

  • camera type: already established to be orthographic,
  • orthographic scale (camera distance in case of non-orthographic view),
  • camera rotation - I'm assuming Top Orthographic view ($<0, 0, 0>$ Euler Rotation),
  • camera location and shift - I'm assuming zeroes (except for $z$ location, which needs to be above the object, but otherwise doesn't matter for ortho camera),
  • render dimensions - I'm assuming $512×512$,
  • sensor fit setting - irrelevant in case of square render dimensions,
  • render region settings if crop to render region is enabled - I'm assuming both are disabled.

Based on the assumptions the conversion is easy: multiply the coordinates by dimensions, divide by orthographic scale, and then map $x$ from $-{w \over 2}…+{w \over 2}$ range to $0…w$ range, and similarly $y$ from $+{h \over 2}…-{h \over 2}$ range to $0…h$ - notice how the highest $y$ ends up to be zero, because Blender has a mathematical $y$ axis that increments up, while most images encode the top row to be the first, as it would be the first row displayed on a CRT screen (and it probably still is the first row sent to the LCD screen).

$$ \require{AMScd} \begin{pmatrix} <-{w \over 2}, +{h \over 2}> & \cdots & <+{w \over 2}, +{h \over 2}> \\ \vdots & \ddots & \vdots \\ <-{w \over 2}, -{h \over 2}> & \cdots & <+{w \over 2}, -{h \over 2}> \end{pmatrix} \begin{CD} @>{\text{becomes}}>> \end{CD} \begin{pmatrix} <0, 0> & \cdots & <w, 0> \\ \vdots & \ddots & \vdots \\ <0, h> & \cdots & <w, h> \end{pmatrix} $$

And so:

$$x_p = {x \times w \over s} - {w \over 2}$$ $$y_p = {h \over 2} - {y \times h \over s}$$

this without clamping the coordinates that are outside the camera view (so you can get negative pixel coordinates, or above dimensions) -if you want to clamp, just use min and max functions or bl_math.clamp().

>>> s = D.cameras["Camera"].ortho_scale
>>> w = C.scene.render.resolution_x
>>> h = C.scene.render.resolution_y
>>> s = C.scene.camera.data.ortho_scale
>>> for v in C.object.data.vertices:
...     wco = C.object.matrix_world @ v.co
...     x, y, z = wco
...     xp = x*w/s + w/2
...     yp = h/2 - y*h/s
...     print(f"{wco=} translated to {xp=}, {yp=}")
...     
wco=Vector((1.0, 1.0, 0.0)) translated to xp=426.66666666666663, yp=85.33333333333334
wco=Vector((1.0, -1.0, 0.0)) translated to xp=426.66666666666663, yp=426.66666666666663
wco=Vector((-1.0, 1.0, 0.0)) translated to xp=85.33333333333334, yp=85.33333333333334
wco=Vector((-1.0, -1.0, 0.0)) translated to xp=85.33333333333334, yp=426.66666666666663
wco=Vector((-1.433976411819458, 1.4508496522903442, 0.0)) translated to xp=11.268025716145843, yp=8.388326009114593
wco=Vector((-1.433976411819458, -1.4096099138259888, 0.0)) translated to xp=11.268025716145843, yp=496.57342529296875

Obviously you may want to round those coordinates…

An alternative that is definitely recommended if any of the assumptions were wrong, is to use the bpy_extras.object_utils.world_to_camera_view:

>>> from bpy_extras.object_utils import world_to_camera_view as w2c
>>> r = C.scene.render
>>> dimensions = Vector((r.resolution_x, r.resolution_y, 0))
>>> for v in C.object.data.vertices:
...     wco = C.object.matrix_world @ v.co
...     xp, yp, _ = w2c(C.scene, D.objects['Camera'], wco) * dimensions
...     print(f"{wco=} translated to {xp=}, {yp=}")
...     
wco=Vector((1.0, 1.0, 0.0)) translated to xp=426.6666564941406, yp=426.6666564941406
wco=Vector((1.0, -1.0, 0.0)) translated to xp=426.6666564941406, yp=85.33333587646484
wco=Vector((-1.0, 1.0, 0.0)) translated to xp=85.33333587646484, yp=426.6666564941406
wco=Vector((-1.0, -1.0, 0.0)) translated to xp=85.33333587646484, yp=85.33333587646484
wco=Vector((-1.433976411819458, 1.4508496522903442, 0.0)) translated to xp=11.268025398254395, yp=503.6116638183594
wco=Vector((-1.433976411819458, -1.4096099138259888, 0.0)) translated to xp=11.268025398254395, yp=15.42657470703125
Markus von Broady
  • 36,563
  • 3
  • 30
  • 99