צור נתונים סינתטיים עבור צינורות ראייה ממוחשבת ב-AWS

צומת המקור: 1726779

איסוף ורישום נתוני תמונה היא אחת המשימות עתירות המשאבים ביותר בכל פרויקט ראייה ממוחשבת. זה יכול לקחת חודשים בכל פעם כדי לאסוף, לנתח ולהתנסות במלואו עם זרמי תמונות ברמה שאתה צריך כדי להתחרות בשוק הנוכחי. גם לאחר שאספת נתונים בהצלחה, עדיין יש לך זרם קבוע של שגיאות הערות, תמונות ממוסגרות בצורה גרועה, כמויות קטנות של נתונים משמעותיים בים של לכידות לא רצויות ועוד. צווארי הבקבוק העיקריים הללו הם הסיבה לכך שיצירת נתונים סינתטיים צריכה להיות בערכת הכלים של כל מהנדס מודרני. על ידי יצירת ייצוגים תלת מימדיים של האובייקטים שאנו רוצים ליצור מודל, נוכל במהירות אבטיפוס לאלגוריתמים תוך איסוף נתונים חיים במקביל.

בפוסט זה, אני מנחה אותך דרך דוגמה לשימוש בספריית האנימציה בקוד פתוח בלנדר לבניית צינור נתונים סינתטיים מקצה לקצה, תוך שימוש ב-Chicken Nuggets כדוגמה. התמונה הבאה היא המחשה של הנתונים שנוצרו בפוסט זה בבלוג.

מה זה בלנדר?

מַמחֶה היא תוכנת גרפיקה תלת מימדית בקוד פתוח המשמשת בעיקר באנימציה, הדפסת תלת מימד ומציאות מדומה. יש לו חבילת חבלול, אנימציה וסימולציה מקיפה ביותר המאפשרת יצירת עולמות תלת מימדיים עבור כמעט כל מקרה שימוש בראייה ממוחשבת. יש לו גם קהילת תמיכה פעילה ביותר שבה רוב, אם לא כל, שגיאות המשתמש נפתרות.

הגדר את הסביבה המקומית שלך

אנו מתקינים שתי גרסאות של בלנדר: אחת על מחשב מקומי עם גישה ל-GUI, והשנייה על ענן מחשוב אלסטי של אמזון (Amazon EC2) מופע P2.

התקן בלנדר ו-ZPY

התקן את הבלנדר מה- אתר בלנדר.

לאחר מכן השלם את השלבים הבאים:

  1. הפעל את הפקודות הבאות:
    wget https://mirrors.ocf.berkeley.edu/blender/release/Blender3.2/blender-3.2.0-linux-x64.tar.xz
    sudo tar -Jxf blender-3.2.0-linux-x64.tar.xz --strip-components=1 -C /bin
    rm -rf blender*
    
    /bin/3.2/python/bin/python3.10 -m ensurepip
    /bin/3.2/python/bin/python3.10 -m pip install --upgrade pip

  2. העתק את כותרות Python הדרושות לגרסת הבלנדר של Python כדי שתוכל להשתמש בספריות אחרות שאינן בלנדר:
    wget https://www.python.org/ftp/python/3.10.2/Python-3.10.2.tgz
    tar -xzf Python-3.10.2.tgz
    sudo cp Python-3.10.2/Include/* /bin/3.2/python/include/python3.10

  3. תעקוף את גרסת הבלנדר שלך וכפה התקנות כך שה-Python המסופק בבלנדר יעבוד:
    /bin/3.2/python/bin/python3.10 -m pip install pybind11 pythran Cython numpy==1.22.1
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U Pillow --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U scipy --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U shapely --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U scikit-image --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U gin-config --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U versioneer --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U shapely --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U ptvsd --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U ptvseabornsd --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U zmq --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U pyyaml --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U requests --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U click --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U table-logger --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U tqdm --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U pydash --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U matplotlib --force

  4. הורדה zpy והתקן מהמקור:
    git clone https://github.com/ZumoLabs/zpy
    cd zpy
    vi requirements.txt

  5. שנה את גרסת NumPy ל >=1.19.4 ו scikit-image>=0.18.1 כדי לבצע את ההתקנה 3.10.2 אפשרי וכדי שלא תקבל שום כתיבה:
    numpy>=1.19.4
    gin-config>=0.3.0
    versioneer
    scikit-image>=0.18.1
    shapely>=1.7.1
    ptvsd>=4.3.2
    seaborn>=0.11.0
    zmq
    pyyaml
    requests
    click
    table-logger>=0.3.6
    tqdm
    pydash

  6. כדי להבטיח תאימות עם Blender 3.2, היכנס zpy/render.py והעיר את שתי השורות הבאות (למידע נוסף, עיין כשל בבלנדר 3.0 מס' 54):
    #scene.render.tile_x = tile_size
    #scene.render.tile_y = tile_size

  7. לאחר מכן, התקן את zpy הספריה:
    /bin/3.2/python/bin/python3.10 setup.py install --user
    /bin/3.2/python/bin/python3.10 -c "import zpy; print(zpy.__version__)"

  8. הורד את גרסת התוספות של zpy מ GitHub ריפו כך שתוכל להפעיל את המופע שלך באופן פעיל:
    cd ~
    curl -O -L -C - "https://github.com/ZumoLabs/zpy/releases/download/v1.4.1rc9/zpy_addon-v1.4.1rc9.zip"
    sudo unzip zpy_addon-v1.4.1rc9.zip -d /bin/3.2/scripts/addons/
    mkdir .config/blender/
    mkdir .config/blender/3.2
    mkdir .config/blender/3.2/scripts
    mkdir .config/blender/3.2/scripts/addons/
    mkdir .config/blender/3.2/scripts/addons/zpy_addon/
    sudo cp -r zpy/zpy_addon/* .config/blender/3.2/scripts/addons/zpy_addon/

  9. שמור קובץ בשם enable_zpy_addon.py בך /home ספרייה והפעל את פקודת ההפעלה, מכיוון שאין לך ממשק משתמש כדי להפעיל אותה:
    import bpy, os
    p = os.path.abspath('zpy_addon-v1.4.1rc9.zip')
    bpy.ops.preferences.addon_install(overwrite=True, filepath=p)
    bpy.ops.preferences.addon_enable(module='zpy_addon')
    bpy.ops.wm.save_userpref()
    
    sudo blender -b -y --python enable_zpy_addon.py

    If zpy-addon לא מתקין (מכל סיבה שהיא), אתה יכול להתקין אותו דרך ה-GUI.

  10. בבלנדר, על ערוך בתפריט, בחר העדפות.
  11. בחרו רחבות בחלונית הניווט והפעל zpy.

אתה אמור לראות דף פתוח ב-GUI, ותוכל לבחור ZPY. זה יאשר שהבלנדר נטען.

AliceVision ו-Meshroom

התקן את AliceVision ו- Meshrooom ממאגר GitHub בהתאמה:

FFmpeg

המערכת שלך צריכה להיות ffmpeg, אבל אם לא, תצטרך להורדה זה.

רשתות מיידיות

אתה יכול להרכיב את הספרייה בעצמך או להוריד את הקבצים הבינאריים הזמינים מראש (וזה מה שעשיתי) עבור רשתות מיידיות.

הגדר את סביבת ה-AWS שלך

כעת הגדרנו את סביבת AWS על מופע EC2. אנו חוזרים על השלבים מהסעיף הקודם, אבל רק עבור בלנדר ו zpy.

  1. בקונסולת אמזון EC2, בחר הפעל מקרים.
  2. בחר את AMI שלך. יש כמה אפשרויות מכאן. אנחנו יכולים לבחור תמונה רגילה של אובונטו, לבחור מופע GPU ואז להתקין ידנית את הדרייברים ולהגדיר הכל, או שנוכל לקחת את המסלול הקל ולהתחיל עם Deep Learning AMI מוגדר מראש ולדאוג רק להתקנת Blender. פוסט, אני משתמש באפשרות השנייה, ובוחר בגרסה העדכנית ביותר של Deep Learning AMI עבור אובונטו (גרסת Deep Learning AMI (אובונטו 18.04). 61.0).
  3. בעד סוג מופעבחר p2.xlarge.
  4. אם אין לך זוג מפתחות, צור אחד חדש או בחר קיים.
  5. עבור פוסט זה, השתמש בהגדרות ברירת המחדל עבור רשת ואחסון.
  6. בחרו הפעל מקרים.
  7. בחרו לְחַבֵּר ומצא את ההוראות לכניסה למופע שלנו מ-SSH ב- לקוח SSH TAB.
  8. התחבר עם SSH: ssh -i "your-pem" ubuntu@IPADDRESS.YOUR-REGION.compute.amazonaws.com

לאחר שהתחברת למופע שלך, בצע את אותם שלבי ההתקנה מהסעיף הקודם כדי להתקין את Blender ו zpy.

איסוף נתונים: סריקת תלת ממדית שלנו

לשלב זה, אני משתמש באייפון כדי להקליט סרטון וידאו של 360 מעלות בקצב איטי למדי סביב הגוש שלי. הדבקתי גוש עוף על קיסם והדבקתי את הקיסם על השיש שלי, ופשוט סובבתי את המצלמה שלי סביב הגוש כדי לקבל כמה שיותר זוויות. ככל שאתה מצלם מהר יותר, כך קטן הסיכוי שתקבל תמונות טובות לעבוד איתן בהתאם למהירות הצמצם.

לאחר שסיימתי לצלם, שלחתי את הסרטון למייל שלי וחילצתי את הסרטון לכונן מקומי. משם, השתמשתי ffmepg כדי לחתוך את הסרטון לפריימים כדי להקל בהרבה על הטמעת Meshroom:

mkdir nugget_images
ffmpeg -i VIDEO.mov ffmpeg nugget_images/nugget_%06d.jpg

פתח את Meshroom והשתמש ב-GUI כדי לגרור את nugget_images תיקייה לחלונית משמאל. משם, בחר הַתחָלָה והמתן כמה שעות (או פחות) בהתאם לאורך הסרטון ואם יש לך מכונה התומכת ב-CUDA.

אתה אמור לראות משהו כמו צילום המסך הבא כשהוא כמעט שלם.

איסוף נתונים: מניפולציה של בלנדר

לאחר השלמת השחזור של Meshroom, השלם את השלבים הבאים:

  1. פתח את ה-Blender GUI ועל ה- שלח בתפריט, בחר תבואו, ואז לבחור Wavefront (.jj) לקובץ המרקם שנוצר מ-Meshroom.
    יש לשמור את הקובץ ב path/to/MeshroomCache/Texturing/uuid-string/texturedMesh.obj.
  2. טען את הקובץ וצפה במפלצתיות שהיא האובייקט התלת-ממדי שלך.

    כאן זה נהיה קצת מסובך.
  3. גלול אל הצד הימני העליון ובחר את מסגרת חוט סמל ב - הצללת נקודת מבט.
  4. בחר את האובייקט שלך בשדה התצוגה הימני וודא שהוא מודגש, גלול אל יציאת התצוגה הראשית של הפריסה ולחץ על Tab או לבחור ידנית מצב עריכה.
  5. לאחר מכן, תמרן את נקודת התצוגה בצורה כזו שתאפשר לעצמך להיות מסוגל לראות את האובייקט שלך עם כמה שפחות מאחוריו. תצטרך לעשות זאת כמה פעמים כדי באמת לקבל את זה נכון.
  6. לחץ וגרור תיבה תוחמת מעל האובייקט כך שרק הגוש יודגש.
  7. לאחר שהוא מודגש כמו בצילום המסך הבא, אנו מפרידים את הגוש שלנו מהמסה התלת-ממדית על ידי לחיצה שמאלית, בחירה נפרד, ולאחר מכן בחירה.

    כעת אנו עוברים ימינה, שם אנו אמורים לראות שני אובייקטים בעלי מרקם: texturedMesh ו texturedMesh.001.
  8. החפץ החדש שלנו צריך להיות texturedMesh.001, אז אנחנו בוחרים texturedMesh ולבחור מחק כדי להסיר את המסה הלא רצויה.
  9. בחר את האובייקט (texturedMesh.001) בצד ימין, עבור אל הצופה שלנו, ובחר את האובייקט, הגדר מקור, ו מקור למרכז המיסה.

כעת, אם נרצה, נוכל להזיז את האובייקט שלנו למרכז התצוגה (או פשוט להשאיר אותו היכן שהוא נמצא) ולראות אותו במלוא הדרו. שימו לב לחור השחור הגדול שממנו לא ממש קיבלנו סיקור טוב של הסרט! נצטרך לתקן את זה.

כדי לנקות את האובייקט שלנו מכל זיהומי פיקסלים, אנו מייצאים את האובייקט שלנו לקובץ .obj. הקפד לבחור מבחר בלבד בעת ייצוא.

איסוף נתונים: נקה עם רשתות מיידיות

עכשיו יש לנו שתי בעיות: לתמונה שלנו יש פער פיקסלים שנוצר כתוצאה מהצילום הגרוע שלנו שאנחנו צריכים לנקות, והתמונה שלנו צפופה להפליא (מה שיגרום ליצירת תמונות זמן רב ביותר). כדי להתמודד עם שתי הבעיות, עלינו להשתמש בתוכנה הנקראת Instant Meshes כדי להרחיב את משטח הפיקסלים שלנו כדי לכסות את החור השחור וגם כדי לכווץ את האובייקט הכולל לגודל קטן יותר, פחות צפוף.

  1. פתח את ה-Instant Meshes וטען שלנו שנשמר לאחרונה nugget.obj קובץ.
  2. תַחַת שדה התמצאות, בחר לפתור.
  3. תַחַת שדה מיקום, בחר לפתור.
    כאן זה נהיה מעניין. אם אתה חוקר את האובייקט שלך ומבחין שהקווים הצולבים של פותר המיקום נראים מפורקים, תוכל לבחור בסמל המסרק תחת שדה התמצאות ולשרטט מחדש את הקווים כמו שצריך.
  4. בחרו לפתור לשניהם שדה התמצאות ו שדה מיקום.
  5. אם הכל נראה טוב, ייצא את הרשת, שם לה משהו כמו nugget_refined.obj, ושמור אותו בדיסק.

איסוף נתונים: מנערים ואופים!

מכיוון שלרשת הפולי הנמוכה שלנו אין שום טקסטורת תמונה הקשורה אליה ולרשת הפולי הגבוהה שלנו יש, אנחנו צריכים לאפות את מרקם הפולי הגבוה על רשת הפולי הנמוכה, או ליצור מרקם חדש ולהקצות אותו האובייקט שלנו. למען הפשטות, אנחנו הולכים ליצור מרקם תמונה מאפס וליישם אותו על הגוש שלנו.

השתמשתי בחיפוש תמונות בגוגל עבור נאגטס ודברים מטוגנים אחרים כדי לקבל תמונה ברזולוציה גבוהה של פני השטח של חפץ מטוגן. מצאתי תמונה ברזולוציה סופר גבוהה של גבינה מטוגנת והכנתי תמונה חדשה מלאה במרקם המטוגן.

עם תמונה זו, אני מוכן להשלים את השלבים הבאים:

  1. פתח את הבלנדר וטען את החדש nugget_refined.obj באותו אופן שטענת את האובייקט הראשוני שלך: על שלח בתפריט, בחר תבואו, Wavefront (.jj), ובחר את nugget_refined.obj קובץ.
  2. לאחר מכן עבור אל ה- הצללה TAB.
    בתחתית אתה אמור לשים לב לשתי תיבות עם הכותרות BDSF עקרוני ו פלט חומר.
  3. על להוסיף בתפריט, בחר מִרקָם ו מרקם תמונה.
    An מרקם תמונה תיבה אמורה להופיע.
  4. בחרו פתח תמונה וטען את תמונת המרקם המטוגן שלך.
  5. גרור בין העכבר צֶבַע ב מרקם תמונה קופסא צבע בסיסי ב BDSF עקרוני קוּפסָה.

עכשיו הגוש שלך אמור להיות מוכן!

איסוף נתונים: צור משתני סביבת בלנדר

כעת, כשיש לנו את אובייקט ה-Nugget הבסיסי שלנו, עלינו ליצור כמה אוספים ומשתני סביבה שיעזרו לנו בתהליך שלנו.

  1. לחץ לחיצה ימנית על אזור סצינת היד ובחר אוסף חדש.
  2. צור את האוספים הבאים: רקע, גוּשׁ זָהָב גָלמִי, ו הוליד.
  3. גרור את הגוש אל גוּשׁ זָהָב גָלמִי אוסף ושנה את שמו nugget_base.

איסוף נתונים: צור מטוס

אנחנו הולכים ליצור אובייקט רקע שממנו יופקו הנאגטס שלנו כשאנחנו מעבדים תמונות. במקרה של שימוש בעולם האמיתי, המטוס הזה הוא המקום שבו מניחים את הנאגטס שלנו, כמו מגש או פח.

  1. על להוסיף בתפריט, בחר רֶשֶׁת ולאחר מכן מטוס.
    מכאן, אנו עוברים לצד ימין של הדף ומוצאים את התיבה הכתומה (מאפייני אובייקט).
  2. ב לשנות חלונית, עבור XYZ אוילר, הגדר X אל 46.968, Y עד 46.968, ו Z אל 1.0.
  3. לשניהם מקום ו רוטציה, הגדר X, Y, ו Z אל 0.

איסוף נתונים: הגדר את המצלמה והציר

לאחר מכן, אנו הולכים להגדיר את המצלמות שלנו בצורה נכונה כדי שנוכל ליצור תמונות.

  1. על להוסיף בתפריט, בחר ריק ו ציר רגיל.
  2. תן שם לאובייקט ציר ראשי.
  3. ודא שהציר שלנו הוא 0 עבור כל המשתנים (כך שהוא נמצא ישירות במרכז).
  4. אם כבר יצרת מצלמה, גרור את המצלמה אל מתחת לציר הראשי.
  5. בחרו פריט ו לשנות.
  6. בעד מקום, הגדר X אל 0, Y עד 0, ו Z אל 100.

איסוף נתונים: הנה באה השמש

לאחר מכן, נוסיף אובייקט Sun.

  1. על להוסיף בתפריט, בחר אור ו שמש.
    המיקום של האובייקט הזה לא בהכרח משנה כל עוד הוא מרוכז איפשהו מעל האובייקט המישורי שקבענו.
  2. בחר את סמל הנורה הירוקה בחלונית השמאלית התחתונה (מאפייני נתוני אובייקט) והגדר את העוצמה ל-5.0.
  3. חזור על אותו הליך כדי להוסיף א אור חפץ והניח אותו במקום אקראי מעל המטוס.

איסוף נתונים: הורד רקעים אקראיים

כדי להחדיר אקראיות לתמונות שלנו, אנו מורידים מהן כמה שיותר מרקמים אקראיים texture.ninja כפי שאנו יכולים (לדוגמה, לבנים). הורד לתיקיה בתוך סביבת העבודה שלך בשם random_textures. הורדתי בערך 50.

צור תמונות

עכשיו אנחנו מגיעים לדברים המהנים: יצירת תמונות.

צינור יצירת תמונות: Object3D ו-DensityController

נתחיל עם כמה הגדרות קוד:

class Object3D:
	'''
	object container to store mesh information about the
	given object

	Returns
	the Object3D object
	'''
	def __init__(self, object: Union[bpy.types.Object, str]):
		"""Creates a Object3D object.

		Args:
		obj (Union[bpy.types.Object, str]): Scene object (or it's name)
		"""
		self.object = object
		self.obj_poly = None
		self.mat = None
		self.vert = None
		self.poly = None
		self.bvht = None
		self.calc_mat()
		self.calc_world_vert()
		self.calc_poly()
		self.calc_bvht()

	def calc_mat(self) -> None:
		"""store an instance of the object's matrix_world"""
		self.mat = self.object.matrix_world

	def calc_world_vert(self) -> None:
		"""calculate the verticies from object's matrix_world perspective"""
		self.vert = [self.mat @ v.co for v in self.object.data.vertices]
		self.obj_poly = np.array(self.vert)

	def calc_poly(self) -> None:
		"""store an instance of the object's polygons"""
		self.poly = [p.vertices for p in self.object.data.polygons]

	def calc_bvht(self) -> None:
		"""create a BVHTree from the object's polygon"""
		self.bvht = BVHTree.FromPolygons( self.vert, self.poly )

	def regenerate(self) -> None:
		"""reinstantiate the object's variables;
		used when the object is manipulated after it's creation"""
		self.calc_mat()
		self.calc_world_vert()
		self.calc_poly()
		self.calc_bvht()

	def __repr__(self):
		return "Object3D: " + self.object.__repr__()

ראשית, אנו מגדירים מחלקה בסיסית של מיכל עם כמה מאפיינים חשובים. מחלקה זו קיימת בעיקר כדי לאפשר לנו ליצור עץ BVH (דרך לייצג את אובייקט הגוש שלנו בחלל תלת מימד), שבו נצטרך להשתמש ב- BVHTree.overlap שיטה כדי לראות אם שני אובייקטים נאגטים שנוצרו באופן עצמאי חופפים במרחב התלת-ממדי שלנו. עוד על כך בהמשך.

פיסת הקוד השנייה היא בקר הצפיפות שלנו. זה משמש כדרך לכבול את עצמנו לכללי המציאות ולא לעולם התלת מימד. לדוגמה, בעולם הבלנדר התלת מימדי, אובייקטים בבלנדר יכולים להתקיים זה בתוך זה; עם זאת, אלא אם כן מישהו מבצע מדע מוזר על הנאגטס העוף שלנו, אנחנו רוצים לוודא שאין שני נאגטס חופפים במידה שהופכת אותו לא מציאותי מבחינה ויזואלית.

אנו משתמשים שלנו Plane אובייקט להוליד קבוצה של קוביות בלתי נראות מוגבלות שניתן לבצע שאילתות בכל זמן נתון כדי לראות אם המקום תפוס או לא.


ראה את הקוד הבא:

class DensityController:
    """Container that controlls the spacial relationship between 3D objects

    Returns:
        DensityController: The DensityController object.
    """
    def __init__(self):
        self.bvhtrees = None
        self.overlaps = None
        self.occupied = None
        self.unoccupied = None
        self.objects3d = []

    def auto_generate_kdtree_cubes(
        self,
        num_objects: int = 100, # max size of nuggets
    ) -> None:
        """
        function to generate physical kdtree cubes given a plane of -resize- size
        this allows us to access each cube's overlap/occupancy status at any given
        time
        
        creates a KDTree collection, a cube, a set of individual cubes, and the 
        BVHTree object for each individual cube

        Args:
            resize (Tuple[float]): the size of a cube to create XYZ.
            cuts (int): how many cuts are made to the cube face
                12 cuts == 13 Rows x 13 Columns  
        """

בקטע הבא, אנו בוחרים את הגוש ויוצרים קובייה תוחמת סביב הגוש הזה. קובייה זו מייצגת את הגודל של פסאודו-ווקס בודד של אובייקט ה-pseudo-kdtree שלנו. אנחנו צריכים להשתמש ב bpy.context.view_layer.update() פונקציה מכיוון שכאשר קוד זה מופעל מתוך פונקציה או סקריפט לעומת blender-gui, נראה שה- view_layer לא מתעדכן אוטומטית.

        # read the nugget,
        # see how large the cube needs to be to encompass a single nugget
        # then touch a parameter to allow it to be smaller or larger (eg more touching)
        bpy.context.view_layer.objects.active = bpy.context.scene.objects.get('nugget_base')
        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
        #create a cube for the bounding box
        bpy.ops.mesh.primitive_cube_add(location=Vector((0,0,0))) 
        #our new cube is now the active object, so we can keep track of it in a variable:
        bound_box = bpy.context.active_object
        bound_box.name = 'CUBE1'
        bpy.context.view_layer.update()
        #copy transforms
        nug_dims = bpy.data.objects["nugget_base"].dimensions
        bpy.data.objects["CUBE1"].dimensions = nug_dims
        bpy.context.view_layer.update()
        bpy.data.objects["CUBE1"].location = bpy.data.objects["nugget_base"].location
        bpy.context.view_layer.update()
        bpy.data.objects["CUBE1"].rotation_euler = bpy.data.objects["nugget_base"].rotation_euler
        bpy.context.view_layer.update()
        print("bound_box.dimensions: ", bound_box.dimensions)
        print("bound_box.location:", bound_box.location)

לאחר מכן, אנו מעדכנים מעט את אובייקט הקובייה שלנו כך שאורכו ורוחבו יהיו מרובעים, בניגוד לגודל הטבעי של הגוש שממנו הוא נוצר:

        # this cube created isn't always square, but we're going to make it square
        # to fit into our 
        x, y, z = bound_box.dimensions
        v = max(x, y)
        if np.round(v) < v:
            v = np.round(v)+1
        bb_x, bb_y = v, v
        bound_box.dimensions = Vector((v, v, z))
        bpy.context.view_layer.update()
        print("bound_box.dimensions updated: ", bound_box.dimensions)
        # now we generate a plane
        # calc the size of the plane given a max number of boxes.

כעת אנו משתמשים באובייקט הקובייה המעודכן שלנו כדי ליצור מישור שיכול להחזיק באופן נפחי num_objects כמות נאגטס:

        x, y, z = bound_box.dimensions
        bb_loc = bound_box.location
        bb_rot_eu = bound_box.rotation_euler
        min_area = (x*y)*num_objects
        min_length = min_area / num_objects
        print(min_length)
        # now we generate a plane
        # calc the size of the plane given a max number of boxes.
        bpy.ops.mesh.primitive_plane_add(location=Vector((0,0,0)), size = min_length)
        plane = bpy.context.selected_objects[0]
        plane.name = 'PLANE'
        # move our plane to our background collection
        # current_collection = plane.users_collection
        link_object('PLANE', 'BACKGROUND')
        bpy.context.view_layer.update()

אנחנו לוקחים את חפץ המטוס שלנו ויוצרים קוביית ענק באותו אורך ורוחב כמו המטוס שלנו, עם גובה קוביית הנאגט שלנו, CUBE 1:

        # New Collection
        my_coll = bpy.data.collections.new("KDTREE")
        # Add collection to scene collection
        bpy.context.scene.collection.children.link(my_coll)
        # now we generate cubes based on the size of the plane.
        bpy.ops.mesh.primitive_cube_add(location=Vector((0,0,0)), size = min_length)
        bpy.context.view_layer.update()
        cube = bpy.context.selected_objects[0]
        cube_dimensions = cube.dimensions
        bpy.context.view_layer.update()
        cube.dimensions = Vector((cube_dimensions[0], cube_dimensions[1], z))
        bpy.context.view_layer.update()
        cube.location = bb_loc
        bpy.context.view_layer.update()
        cube.rotation_euler = bb_rot_eu
        bpy.context.view_layer.update()
        cube.name = 'cube'
        bpy.context.view_layer.update()
        current_collection = cube.users_collection
        link_object('cube', 'KDTREE')
        bpy.context.view_layer.update()

מכאן, אנחנו רוצים ליצור ווקסלים מהקובייה שלנו. אנחנו לוקחים את מספר הקוביות שהיינו מתאימים num_objects ואז לחתוך אותם מחפץ הקובייה שלנו. אנחנו מחפשים את פני הרשת הפונה כלפי מעלה של הקובייה שלנו, ואז בוחרים את הפנים כדי לבצע את החתכים שלנו. ראה את הקוד הבא:

        # get the bb volume and make the proper cuts to the object 
        bb_vol = x*y*z
        cube_vol = cube_dimensions[0]*cube_dimensions[1]*cube_dimensions[2]
        n_cubes = cube_vol / bb_vol
        cuts = n_cubes / ((x+y) / 2)
        cuts = int(np.round(cuts)) - 1 # 
        # select the cube
        for object in bpy.data.objects:
            object.select_set(False)
        bpy.context.view_layer.update()
        for object in bpy.data.objects:
            object.select_set(False)
        bpy.data.objects['cube'].select_set(True) # Blender 2.8x
        bpy.context.view_layer.objects.active = bpy.context.scene.objects.get('cube')
        # set to edit mode
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
        print('edit mode success')
        # get face_data
        context = bpy.context
        obj = context.edit_object
        me = obj.data
        mat = obj.matrix_world
        bm = bmesh.from_edit_mesh(me)
        up_face = None
        # select upwards facing cube-face
        # https://blender.stackexchange.com/questions/43067/get-a-face-selected-pointing-upwards
        for face in bm.faces:
            if (face.normal-UP_VECTOR).length < EPSILON:
                up_face = face
                break
        assert(up_face)
        # subdivide the edges to get the perfect kdtree cubes
        bmesh.ops.subdivide_edges(bm,
                edges=up_face.edges,
                use_grid_fill=True,
                cuts=cuts)
        bpy.context.view_layer.update()
        # get the center point of each face

לבסוף, אנו מחשבים את מרכז החלק העליון של כל חיתוך שיצרנו מהקובייה הגדולה שלנו ויוצרים קוביות ממשיות מהחתכים הללו. כל אחת מהקוביות החדשות שנוצרו מייצגת פיסת שטח יחידה להוליד או להזיז נאגטס סביב המטוס שלנו. ראה את הקוד הבא:

        face_data = {}
        sizes = []
        for f, face in enumerate(bm.faces): 
            face_data[f] = {}
            face_data[f]['calc_center_bounds'] = face.calc_center_bounds()
            loc = mat @ face_data[f]['calc_center_bounds']
            face_data[f]['loc'] = loc
            sizes.append(loc[-1])
        # get the most common cube-z; we use this to determine the correct loc
        counter = Counter()
        counter.update(sizes)
        most_common = counter.most_common()[0][0]
        cube_loc = mat @ cube.location
        # get out of edit mode
        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
        # go to new colection
        bvhtrees = {}
        for f in face_data:
            loc = face_data[f]['loc']
            loc = mat @ face_data[f]['calc_center_bounds']
            print(loc)
            if loc[-1] == most_common:
                # set it back down to the floor because the face is elevated to the
                # top surface of the cube
                loc[-1] = cube_loc[-1]
                bpy.ops.mesh.primitive_cube_add(location=loc, size = x)
                cube = bpy.context.selected_objects[0]
                cube.dimensions = Vector((x, y, z))
                # bpy.context.view_layer.update()
                cube.name = "cube_{}".format(f)
                #my_coll.objects.link(cube)
                link_object("cube_{}".format(f), 'KDTREE')
                #bpy.context.view_layer.update()
                bvhtrees[f] = {
                    'occupied' : 0,
                    'object' : Object3D(cube)
                }
        for object in bpy.data.objects:
            object.select_set(False)
        bpy.data.objects['CUBE1'].select_set(True) # Blender 2.8x
        bpy.ops.object.delete()
        return bvhtrees

לאחר מכן, אנו מפתחים אלגוריתם שמבין אילו קוביות תפוסות בכל זמן נתון, מוצא אילו אובייקטים חופפים זה לזה, ומעביר אובייקטים חופפים בנפרד לחלל לא תפוס. לא נוכל להיפטר לחלוטין מכל החפיפות, אבל נוכל לגרום לזה להיראות אמיתי מספיק.



ראה את הקוד הבא:

    def find_occupied_space(
        self, 
        objects3d: List[Object3D],
    ) -> None:
        """
        discover which cube's bvhtree is occupied in our kdtree space

        Args:
            list of Object3D objects

        """
        count = 0
        occupied = []
        for i in self.bvhtrees:
            bvhtree = self.bvhtrees[i]['object']
            for object3d in objects3d:
                if object3d.bvht.overlap(bvhtree.bvht):
                    self.bvhtrees[i]['occupied'] = 1

    def find_overlapping_objects(
        self, 
        objects3d: List[Object3D],
    ) -> List[Tuple[int]]:
        """
        returns which Object3D objects are overlapping

        Args:
            list of Object3D objects
        
        Returns:
            List of indicies from objects3d that are overlap
        """
        count = 0
        overlaps = []
        for i, x_object3d in enumerate(objects3d):
            for ii, y_object3d in enumerate(objects3d[i+1:]):
                if x_object3d.bvht.overlap(y_object3d.bvht):
                    overlaps.append((i, ii))
        return overlaps

    def calc_most_overlapped(
        self,
        overlaps: List[Tuple[int]]
    ) -> List[Tuple[int]]:
        """
        Algorithm to count the number of edges each index has
        and return a sorted list from most->least with the number
        of edges each index has. 

        Args:
            list of indicies that are overlapping
        
        Returns:
            list of indicies with the total number of overlapps they have 
            [index, count]
        """
        keys = {}
        for x,y in overlaps:
            if x not in keys:
                keys[x] = 0
            if y not in keys:
                keys[y] = 0
            keys[x]+=1
            keys[y]+=1
        # sort by most edges first
        index_counts = sorted(keys.items(), key=lambda x: x[1])[::-1]
        return index_counts
    
    def get_random_unoccupied(
        self
    ) -> Union[int,None]:
        """
        returns a randomly chosen unoccuped kdtree cube

        Return
            either the kdtree cube's key or None (meaning all spaces are
            currently occupied)
            Union[int,None]
        """
        unoccupied = []
        for i in self.bvhtrees:
            if not self.bvhtrees[i]['occupied']:
                unoccupied.append(i)
        if unoccupied:
            random.shuffle(unoccupied)
            return unoccupied[0]
        else:
            return None

    def regenerate(
        self,
        iterable: Union[None, List[Object3D]] = None
    ) -> None:
        """
        this function recalculates each objects world-view information
        we default to None, which means we're recalculating the self.bvhtree cubes

        Args:
            iterable (None or List of Object3D objects). if None, we default to
            recalculating the kdtree
        """
        if isinstance(iterable, list):
            for object in iterable:
                object.regenerate()
        else:
            for idx in self.bvhtrees:
                self.bvhtrees[idx]['object'].regenerate()
                self.update_tree(idx, occupied=0)       

    def process_trees_and_objects(
        self,
        objects3d: List[Object3D],
    ) -> List[Tuple[int]]:
        """
        This function finds all overlapping objects within objects3d,
        calculates the objects with the most overlaps, searches within
        the kdtree cube space to see which cubes are occupied. It then returns 
        the edge-counts from the most overlapping objects

        Args:
            list of Object3D objects
        Returns
            this returns the output of most_overlapped
        """
        overlaps = self.find_overlapping_objects(objects3d)
        most_overlapped = self.calc_most_overlapped(overlaps)
        self.find_occupied_space(objects3d)
        return most_overlapped

    def move_objects(
        self, 
        objects3d: List[Object3D],
        most_overlapped: List[Tuple[int]],
        z_increase_offset: float = 2.,
    ) -> None:
        """
        This function iterates through most-overlapped, and uses 
        the index to extract the matching object from object3d - it then
        finds a random unoccupied kdtree cube and moves the given overlapping
        object to that space. It does this for each index from the most-overlapped
        function

        Args:
            objects3d: list of Object3D objects
            most_overlapped: a list of tuples (index, count) - where index relates to
                where it's found in objects3d and count - how many times it overlaps 
                with other objects
            z_increase_offset: this value increases the Z value of the object in order to
                make it appear as though it's off the floor. If you don't augment this value
                the object looks like it's 'inside' the ground plane
        """
        for idx, cnt in most_overlapped:
            object3d = objects3d[idx]
            unoccupied_idx = self.get_random_unoccupied()
            if unoccupied_idx:
                object3d.object.location =  self.bvhtrees[unoccupied_idx]['object'].object.location
                # ensure the nuggest is above the groundplane
                object3d.object.location[-1] = z_increase_offset
                self.update_tree(unoccupied_idx, occupied=1)
    
    def dynamic_movement(
        self, 
        objects3d: List[Object3D],
        tries: int = 100,
        z_offset: float = 2.,
    ) -> None:
        """
        This function resets all objects to get their current positioning
        and randomly moves objects around in an attempt to avoid any object
        overlaps (we don't want two objects to be spawned in the same position)

        Args:
            objects3d: list of Object3D objects
            tries: int the number of times we want to move objects to random spaces
                to ensure no overlaps are present.
            z_offset: this value increases the Z value of the object in order to
                make it appear as though it's off the floor. If you don't augment this value
                the object looks like it's 'inside' the ground plane (see `move_objects`)
        """
    
        # reset all objects
        self.regenerate(objects3d)
        # regenerate bvhtrees
        self.regenerate(None)

        most_overlapped = self.process_trees_and_objects(objects3d)
        attempts = 0
        while most_overlapped:
            if attempts>=tries:
                break
            self.move_objects(objects3d, most_overlapped, z_offset)
            attempts+=1
            # recalc objects
            self.regenerate(objects3d)
            # regenerate bvhtrees
            self.regenerate(None)
            # recalculate overlaps
            most_overlapped = self.process_trees_and_objects(objects3d)

    def generate_spawn_point(
        self,
    ) -> Vector:
        """
        this function generates a random spawn point by finding which
        of the kdtree-cubes are unoccupied, and returns one of those

        Returns
            the Vector location of the kdtree-cube that's unoccupied
        """
        idx = self.get_random_unoccupied()
        print(idx)
        self.update_tree(idx, occupied=1)
        return self.bvhtrees[idx]['object'].object.location

    def update_tree(
        self,
        idx: int,
        occupied: int,
    ) -> None:
        """
        this function updates the given state (occupied vs. unoccupied) of the
        kdtree given the idx

        Args:
            idx: int
            occupied: int
        """
        self.bvhtrees[idx]['occupied'] = occupied

צינור יצירת תמונות: ריצות מגניבות

בחלק זה, אנו מפרקים את מה שלנו run הפונקציה עושה.

אנחנו מאתחלים את שלנו DensityController וליצור משהו שנקרא חוסך באמצעות ה ImageSaver החל מ- zpy. זה מאפשר לנו לשמור ללא מראה את התמונות המעובדות שלנו בכל מיקום שנבחר. לאחר מכן נוסיף את שלנו nugget קטגוריה (ואם היו לנו עוד קטגוריות, היינו מוסיפים אותן כאן). ראה את הקוד הבא:

@gin.configurable("run")
@zpy.blender.save_and_revert
def run(
    max_num_nuggets: int = 100,
    jitter_mesh: bool = True,
    jitter_nugget_scale: bool = True,
    jitter_material: bool = True,
    jitter_nugget_material: bool = False,
    number_of_random_materials: int = 50,
    nugget_texture_path: str = os.getcwd()+"/nugget_textures",
    annotations_path = os.getcwd()+'/nugget_data',
):
    """
    Main run function.
    """
    density_controller = DensityController()
    # Random seed results in unique behavior
    zpy.blender.set_seed(random.randint(0,1000000000))

    # Create the saver object
    saver = zpy.saver_image.ImageSaver(
        description="Image of the randomized Amazon nuggets",
        output_dir=annotations_path,
    )
    saver.add_category(name="nugget")

לאחר מכן, עלינו ליצור אובייקט מקור שממנו אנו משצים נאגטס להעתיק; במקרה הזה, זה ה nugget_base שיצרנו:

    # Make a list of source nugget objects
    source_nugget_objects = []
    for obj in zpy.objects.for_obj_in_collections(
        [
            bpy.data.collections["NUGGET"],
        ]
    ):
        assert(obj!=None)

        # pass on everything not named nugget
        if 'nugget_base' not in obj.name:
            print('passing on {}'.format(obj.name))
            continue
        zpy.objects.segment(obj, name="nugget", as_category=True) #color=nugget_seg_color
        print("zpy.objects.segment: check {}".format(obj.name))
        source_nugget_objects.append(obj.name)

עכשיו, כשיש לנו את גוש הבסיס שלנו, אנחנו הולכים לשמור את תנוחות העולם (מיקומים) של כל האובייקטים האחרים כך שאחרי כל רינדור, נוכל להשתמש בתנוחות השמורות הללו כדי לאתחל מחדש רינדור. אנחנו גם מזיזים את גוש הבסיס שלנו לגמרי מהדרך כך שה-kdtree לא ירגיש מקום תפוס. לבסוף, אנו מאתחלים את אובייקטי ה-kdtree-cube שלנו. ראה את הקוד הבא:

    # move nugget point up 10 z's so it won't collide with base-cube
    bpy.data.objects["nugget_base"].location[-1] = 10

    # Save the position of the camera and light
    # create light and camera
    zpy.objects.save_pose("Camera")
    zpy.objects.save_pose("Sun")
    zpy.objects.save_pose("Plane")
    zpy.objects.save_pose("Main Axis")
    axis = bpy.data.objects['Main Axis']
    print('saving poses')
    # add some parameters to this 

    # get the plane-3d object
    plane3d = Object3D(bpy.data.objects['Plane'])

    # generate kdtree cubes
    density_controller.generate_kdtree_cubes()

הקוד הבא אוסף את הרקעים שהורדנו מ-texture.ninja, שם הם ישמשו כדי להיות מוקרנים באופן אקראי על המטוס שלנו:

    # Pre-create a bunch of random textures
    #random_materials = [
    #    zpy.material.random_texture_mat() for _ in range(number_of_random_materials)
    #]
    p = os.path.abspath(os.getcwd()+'/random_textures')
    print(p)
    random_materials = []
    for x in os.listdir(p):
        texture_path = Path(os.path.join(p,x))
        y = zpy.material.make_mat_from_texture(texture_path, name=texture_path.stem)
        random_materials.append(y)
    #print(random_materials[0])

    # Pre-create a bunch of random textures
    random_nugget_materials = [
        random_nugget_texture_mat(Path(nugget_texture_path)) for _ in range(number_of_random_materials)
    ]

כאן מתחיל הקסם. ראשית, אנו מחדשים את kdtree-cubes לריצה זו כדי שנוכל להתחיל מחדש:

    # Run the sim.
    for step_idx in zpy.blender.step():
        density_controller.generate_kdtree_cubes()

        objects3d = []
        num_nuggets = random.randint(40, max_num_nuggets)
        log.info(f"Spawning {num_nuggets} nuggets.")
        spawned_nugget_objects = []
        for _ in range(num_nuggets):

אנו משתמשים בבקר הצפיפות שלנו כדי ליצור נקודת השרצה אקראית עבור הגוש שלנו, ליצור עותק של nugget_base, והעבר את העותק לנקודת השרצים שנוצרה באופן אקראי:

            # Choose location to spawn nuggets
            spawn_point = density_controller.generate_spawn_point()
            # manually spawn above the floor
            # spawn_point[-1] = 1.8 #2.0

            # Pick a random object to spawn
            _name = random.choice(source_nugget_objects)
            log.info(f"Spawning a copy of source nugget {_name} at {spawn_point}")
            obj = zpy.objects.copy(
                bpy.data.objects[_name],
                collection=bpy.data.collections["SPAWNED"],
                is_copy=True,
            )

            obj.location = spawn_point
            obj.matrix_world = mathutils.Matrix.Translation(spawn_point)
            spawned_nugget_objects.append(obj)

לאחר מכן, אנו מרעידים באופן אקראי את גודל הנאגט, את הרשת של הנאגט ואת קנה המידה של הנאגט, כך שאף שני נאגטס לא ייראו אותו הדבר:

            # Segment the newly spawned nugget as an instance
            zpy.objects.segment(obj)

            # Jitter final pose of the nugget a little
            zpy.objects.jitter(
                obj,
                rotate_range=(
                    (0.0, 0.0),
                    (0.0, 0.0),
                    (-math.pi * 2, math.pi * 2),
                ),
            )

            if jitter_nugget_scale:
                # Jitter the scale of each nugget
                zpy.objects.jitter(
                    obj,
                    scale_range=(
                        (0.8, 2.0), #1.2
                        (0.8, 2.0), #1.2
                        (0.8, 2.0), #1.2
                    ),
                )

            if jitter_mesh:
                # Jitter (deform) the mesh of each nugget
                zpy.objects.jitter_mesh(
                    obj=obj,
                    scale=(
                        random.uniform(0.01, 0.03),
                        random.uniform(0.01, 0.03),
                        random.uniform(0.01, 0.03),
                    ),
                )

            if jitter_nugget_material:
                # Jitter the material (apperance) of each nugget
                for i in range(len(obj.material_slots)):
                    obj.material_slots[i].material = random.choice(random_nugget_materials)
                    zpy.material.jitter(obj.material_slots[i].material)          

אנחנו הופכים את עותק הגוש שלנו ל- Object3D אובייקט שבו אנו משתמשים בפונקציונליות עץ BVH כדי לראות אם המטוס שלנו חותך או חופף כל פנים או קודקודים בעותק הגוש שלנו. אם אנו מוצאים חפיפה עם המטוס, אנו פשוט מזיזים את הגוש כלפי מעלה על ציר ה-Z שלו. ראה את הקוד הבא:

            # create 3d obj for movement
            nugget3d = Object3D(obj)

            # make sure the bottom most part of the nugget is NOT
            # inside the plane-object       
            plane_overlap(plane3d, nugget3d)

            objects3d.append(nugget3d)

כעת, כשכל הנאגטס נוצרו, אנו משתמשים בעצמנו DensityController להזיז נאגטס סביב כך שיהיה לנו מספר מינימלי של חפיפות, ואלה שחופפים אינם נראים מחרידים:

        # ensure objects aren't on top of each other
        density_controller.dynamic_movement(objects3d)

בקוד הבא: אנו משחזרים את Camera ו Main Axis תנוחות ובחר באקראי כמה רחוקה המצלמה ל Plane אובייקט:

        # Return camera to original position
        zpy.objects.restore_pose("Camera")
        zpy.objects.restore_pose("Main Axis")
        zpy.objects.restore_pose("Camera")
        zpy.objects.restore_pose("Main Axis")

        # assert these are the correct versions...
        assert(bpy.data.objects["Camera"].location == Vector((0,0,100)))
        assert(bpy.data.objects["Main Axis"].location == Vector((0,0,0)))
        assert(bpy.data.objects["Main Axis"].rotation_euler == Euler((0,0,0)))

        # alter the Z ditance with the camera
        bpy.data.objects["Camera"].location = (0, 0, random.uniform(0.75, 3.5)*100)

אנחנו מחליטים באיזו אקראית אנחנו רוצים שהמצלמה תיסע לאורך Main Axis. תלוי אם אנחנו רוצים שזה יהיה בעיקר מעל הראש או אם אכפת לנו מאוד מהזווית שממנה הוא רואה את הלוח, אנחנו יכולים להתאים את top_down_mostly פרמטר תלוי עד כמה מודל האימון שלנו קולט את האות של "מה זה בכלל גוש?"

        # alter the main-axis beta/gamma params
        top_down_mostly = False 
        if top_down_mostly:
            zpy.objects.rotate(
                bpy.data.objects["Main Axis"],
                rotation=(
                    random.uniform(0.05, 0.05),
                    random.uniform(0.05, 0.05),
                    random.uniform(0.05, 0.05),
                ),
            )
        else:
            zpy.objects.rotate(
                bpy.data.objects["Main Axis"],
                rotation=(
                    random.uniform(-1., 1.),
                    random.uniform(-1., 1.),
                    random.uniform(-1., 1.),
                ),
            )

        print(bpy.data.objects["Main Axis"].rotation_euler)
        print(bpy.data.objects["Camera"].location)

בקוד הבא, אנו עושים את אותו הדבר עם Sun אובייקט, ובחר באקראי מרקם עבור Plane אובייקט:

        # change the background material
        # Randomize texture of shelf, floors and walls
        for obj in bpy.data.collections["BACKGROUND"].all_objects:
            for i in range(len(obj.material_slots)):
                # TODO
                # Pick one of the random materials
                obj.material_slots[i].material = random.choice(random_materials)
                if jitter_material:
                    zpy.material.jitter(obj.material_slots[i].material)
                # Sets the material relative to the object
                obj.material_slots[i].link = "OBJECT"
        # Pick a random hdri (from the local textures folder for background background)
        zpy.hdris.random_hdri()
        # Return light to original position
        zpy.objects.restore_pose("Sun")

        # Jitter the light position
        zpy.objects.jitter(
            "Sun",
            translate_range=(
                (-5, 5),
                (-5, 5),
                (-5, 5),
            ),
        )
        bpy.data.objects["Sun"].data.energy = random.uniform(0.5, 7)

לבסוף, אנחנו מסתירים את כל החפצים שלנו שאנחנו לא רוצים שיוצגו: ה nugget_base וכל מבנה הקובייה שלנו:

# we hide the cube objects
for obj in # we hide the cube objects for obj in bpy.data.objects: if 'cube' in obj.name: obj.hide_render = True try: zpy.objects.toggle_hidden(obj, hidden=True) except: # deal with this exception here... pass # we hide our base nugget object bpy.data.objects["nugget_base"].hide_render = True zpy.objects.toggle_hidden(bpy.data.objects["nugget_base"], hidden=True)

לבסוף, אנו משתמשים zpy כדי לעבד את הסצנה שלנו, לשמור את התמונות שלנו ולאחר מכן לשמור את ההערות שלנו. עבור הפוסט הזה, עשיתי כמה שינויים קטנים ב- zpy ספריית הערות למקרה השימוש הספציפי שלי (ביאור לכל תמונה במקום קובץ אחד לכל פרוייקט), אבל לא צריך לעשות זאת לצורך הפוסט הזה).

        # create the image name
        image_uuid = str(uuid.uuid4())

        # Name for each of the output images
        rgb_image_name = format_image_string(image_uuid, 'rgb')
        iseg_image_name = format_image_string(image_uuid, 'iseg')
        depth_image_name = format_image_string(image_uuid, 'depth')

        zpy.render.render(
            rgb_path=saver.output_dir / rgb_image_name,
            iseg_path=saver.output_dir / iseg_image_name,
            depth_path=saver.output_dir / depth_image_name,
        )

        # Add images to saver
        saver.add_image(
            name=rgb_image_name,
            style="default",
            output_path=saver.output_dir / rgb_image_name,
            frame=step_idx,
        )
    
        saver.add_image(
            name=iseg_image_name,
            style="segmentation",
            output_path=saver.output_dir / iseg_image_name,
            frame=step_idx,
        )
        saver.add_image(
            name=depth_image_name,
            style="depth",
            output_path=saver.output_dir / depth_image_name,
            frame=step_idx,
        )

        # ideally in this thread, we'll open the anno file
        # and write to it directly, saving it after each generation
        for obj in spawned_nugget_objects:
            # Add annotation to segmentation image
            saver.add_annotation(
                image=rgb_image_name,
                category="nugget",
                seg_image=iseg_image_name,
                seg_color=tuple(obj.seg.instance_color),
            )

        # Delete the spawned nuggets
        zpy.objects.empty_collection(bpy.data.collections["SPAWNED"])

        # Write out annotations
        saver.output_annotated_images()
        saver.output_meta_analysis()

        # # ZUMO Annotations
        _output_zumo = _OutputZUMO(saver=saver, annotation_filename = Path(image_uuid + ".zumo.json"))
        _output_zumo.output_annotations()
        # change the name here..
        saver.output_annotated_images()
        saver.output_meta_analysis()

        # remove the memory of the annotation to free RAM
        saver.annotations = []
        saver.images = {}
        saver.image_name_to_id = {}
        saver.seg_annotations_color_to_id = {}

    log.info("Simulation complete.")

if __name__ == "__main__":

    # Set the logger levels
    zpy.logging.set_log_levels("info")

    # Parse the gin-config text block
    # hack to read a specific gin config
    parse_config_from_file('nugget_config.gin')

    # Run the sim
    run()

וואלה!

הפעל את סקריפט היצירה ללא ראש

עכשיו, כשיש לנו את קובץ הבלנדר השמור שלנו, הגוש שנוצר שלנו, וכל המידע התומך, בואו נסגור את ספריית העבודה שלנו או scp אותו למחשב ה-GPU שלנו או העלה אותו באמצעות שירות אחסון פשוט של אמזון (Amazon S3) או שירות אחר:

tar cvf working_blender_dir.tar.gz working_blender_dir
scp -i "your.pem" working_blender_dir.tar.gz ubuntu@EC2-INSTANCE.compute.amazonaws.com:/home/ubuntu/working_blender_dir.tar.gz

היכנס למופע EC2 שלך ושחרר את התיקיה working_blender שלך:

tar xvf working_blender_dir.tar.gz

כעת אנו יוצרים את הנתונים שלנו במלוא הדרו:

blender working_blender_dir/nugget.blend --background --python working_blender_dir/create_synthetic_nuggets.py

הסקריפט אמור לפעול עבור 500 תמונות, והנתונים נשמרים ב /path/to/working_blender_dir/nugget_data.

הקוד הבא מציג הערה יחידה שנוצרה עם מערך הנתונים שלנו:

{
    "metadata": {
        "description": "3D data of a nugget!",
        "contributor": "Matt Krzus",
        "url": "krzum@amazon.com",
        "year": "2021",
        "date_created": "20210924_000000",
        "save_path": "/home/ubuntu/working_blender_dir/nugget_data"
    },
    "categories": {
        "0": {
            "name": "nugget",
            "supercategories": [],
            "subcategories": [],
            "color": [
                0.0,
                0.0,
                0.0
            ],
            "count": 6700,
            "subcategory_count": [],
            "id": 0
        }
    },
    "images": {
        "0": {
            "name": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.rgb.png",
            "style": "default",
            "output_path": "/home/ubuntu/working_blender_dir/nugget_data/a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.rgb.png",
            "relative_path": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.rgb.png",
            "frame": 97,
            "width": 640,
            "height": 480,
            "id": 0
        },
        "1": {
            "name": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.iseg.png",
            "style": "segmentation",
            "output_path": "/home/ubuntu/working_blender_dir/nugget_data/a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.iseg.png",
            "relative_path": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.iseg.png",
            "frame": 97,
            "width": 640,
            "height": 480,
            "id": 1
        },
        "2": {
            "name": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.depth.png",
            "style": "depth",
            "output_path": "/home/ubuntu/working_blender_dir/nugget_data/a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.depth.png",
            "relative_path": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.depth.png",
            "frame": 97,
            "width": 640,
            "height": 480,
            "id": 2
        }
    },
    "annotations": [
        {
            "image_id": 0,
            "category_id": 0,
            "id": 0,
            "seg_color": [
                1.0,
                0.6000000238418579,
                0.9333333373069763
            ],
            "color": [
                1.0,
                0.6,
                0.9333333333333333
            ],
            "segmentation": [
                [
                    299.0,
                    308.99,
                    292.0,
                    308.99,
                    283.01,
                    301.0,
                    286.01,
                    297.0,
                    285.01,
                    294.0,
                    288.01,
                    285.0,
                    283.01,
                    275.0,
                    287.0,
                    271.01,
                    294.0,
                    271.01,
                    302.99,
                    280.0,
                    305.99,
                    286.0,
                    305.99,
                    303.0,
                    302.0,
                    307.99,
                    299.0,
                    308.99
                ]
            ],
            "bbox": [
                283.01,
                271.01,
                22.980000000000018,
                37.98000000000002
            ],
            "area": 667.0802000000008,
            "bboxes": [
                [
                    283.01,
                    271.01,
                    22.980000000000018,
                    37.98000000000002
                ]
            ],
            "areas": [
                667.0802000000008
            ]
        },
        {
            "image_id": 0,
            "category_id": 0,
            "id": 1,
            "seg_color": [
                1.0,
                0.4000000059604645,
                1.0
            ],
            "color": [
                1.0,
                0.4,
                1.0
            ],
            "segmentation": [
                [
                    241.0,
                    273.99,
                    236.0,
                    271.99,
                    234.0,
                    273.99,
                    230.01,
                    270.0,
                    232.01,
                    268.0,
                    231.01,
                    263.0,
                    233.01,
                    261.0,
                    229.0,
                    257.99,
                    225.0,
                    257.99,
                    223.01,
                    255.0,
                    225.01,
                    253.0,
                    227.01,
                    246.0,
                    235.0,
                    239.01,
                    238.0,
                    239.01,
                    240.0,
                    237.01,
                    247.0,
                    237.01,
                    252.99,
                    245.0,
                    253.99,
                    252.0,
                    246.99,
                    269.0,
                    241.0,
                    273.99
                ]
            ],
            "bbox": [
                223.01,
                237.01,
                30.980000000000018,
                36.98000000000002
            ],
            "area": 743.5502000000008,
            "bboxes": [
                [
                    223.01,
                    237.01,
                    30.980000000000018,
                    36.98000000000002
                ]
            ],
            "areas": [
                743.5502000000008
            ]
        },
...
...
...

סיכום

בפוסט זה, הדגמתי כיצד להשתמש בספריית האנימציה בקוד פתוח בלנדר כדי לבנות צינור נתונים סינתטיים מקצה לקצה.

יש המון דברים מגניבים שאתה יכול לעשות בבלנדר וב-AWS; אני מקווה שההדגמה הזו יכולה לעזור לך בפרויקט הבא שלך מורעב בנתונים!

הפניות


על המחבר

מאט קרזוס הוא Sr. Data Scientist בחברת Amazon Web Service בקבוצת השירותים המקצועיים של AWS

בול זמן:

עוד מ למידת מכונות AWS