My DIY Jaw Articulation Tool - JawGrinder - Looking for Testers & Feedback!

Davidkamaa

Davidkamaa

Member
Full Member
Messages
31
Reaction score
0
Hey everyone! I'm David, and I've been in the digital dentistry game for a good few years now. And here's the thing that's always kinda bugged me: we get these intraoral scans from docs, and well... you know how it is. Either the bite is slightly open, or they're only touching on one random point on the side, or it's just a mess. Tons of reasons: bad stitching of left/right scans in the intraoral scanner itself, patient moved their jaw, doc wasn't paying enough attention, you name it.

So for years, I've been wondering: damn, we have a digital POINT CLOUD! Can't we just, like, mathematically figure out where these jaws are supposed to meet, find those occlusal contacts? At some point, I found myself thinking about how to solve this problem and started working on the script from time to time at a relaxed pace. Eventually, the results seemed interesting to me, so I decided to share them with the world.

The code isn’t very well optimized, and all the heavy logic is processed by the CPU instead of the GPU, which would’ve been preferable.

So, How Does This Thing (Try To) Work?

The Anchor - Upper Jaw (UJ): We treat the upper jaw as fixed. Load it up, and it stays put.
The Mover - Lower Jaw (LJ): This is the one we'll be nudging, rotating, and generally trying to seat properly against the UJ.

The Logic Behind the "Seating" (My attempt at a "for dummies" explanation):
The program iteratively tries to improve the LJ's position based on a few key metrics:
1. Fighting Penetrations:
This is priority #1. If points on the LJ have "sunk" into the UJ deeper than a set threshold (max_penetration_depth), the program tries to "push" them out.
It figures out the surface normal of the UJ at the penetration point and nudges the LJ along that vector. The "oomph" of this push can be tweaked (push_out_distance_factor).
The goal: Jaws shouldn't pass through each other, obviously.
2. Seeking "Planar Contacts":
These are what I consider the "quality" contacts. Not just a single point touching, but small surface areas on the UJ and LJ coming together.
The program looks for points on the LJ that are:
Super close to the UJ (controlled by planar_contact_proximity_threshold).
The surface normals at these points on the UJ and LJ are pretty much facing each other (technically, they're nearly anti-parallel, angle close to 180 degrees, tweaked with planar_contact_normal_dot_product_threshold).
Such a point has a few "buddies" nearby (min_neighbors_for_significant_planar_contact) that also meet these criteria. This is to avoid random single-point flukes and find actual contact zones.
The goal: Find and maximize these natural seating areas.
3. "Occlusal Contacts":
These are simpler. Just points on the LJ that are very close to the UJ (occlusal_contact_threshold) but aren't planar contacts and aren't penetrating.
The goal: Account for all possible contact points, even if they don't form large patches.
4. Moving the LJ (ICP-ish Approach):
If there are no major penetrations, the program tries to "pull" the LJ towards the UJ.
It picks "candidate" points on the LJ that are within a reasonable distance of the UJ (icp_max_distance) and aren't penetrating.
For these points, it finds their closest counterparts on the UJ.
Then, it uses an ICP (Iterative Closest Point) algorithm – specifically, a Point-to-Plane variation if normals are available – to calculate a small transformation (translation and rotation) for the LJ that would bring these point sets closer.
The "aggressiveness" of this pull can be adjusted (icp_step_scale_translation for movement, icp_step_scale_rotation for rotation). If you have scipy installed, the rotation part is a bit smarter; otherwise, it's translation-only for this step.
The goal: Iteratively snug the LJ up against the UJ by minimizing distances.
5. Tackling "Getting Stuck" (Stagnation & Exploration):
Sometimes, the algorithm can get stuck in a local optimum where tiny moves don't make things better.
If several iterations pass without improvement (stagnation_iter_limit), the program can switch to an "exploration" mode (exploration_active_next_step – this is a state, triggered if stagnation occurs).
In this mode, it gives the LJ a little random nudge (a bit of rotation by explore_rotation_deg and translation by explore_translation_mm) to try and jolt it out of that rut and hopefully find a better overall solution.

The Workflow:


Load STL/PLY Files:

Browse Lower/Upper: Select your lower and upper jaw files. The paths will appear in the input fields.

Process Controls:

1.Start/Stop Viz: The main button. Launches the Open3D window and loads the models. While the window is running, this button becomes "⏳ Stop Viz" to close it. IMPORTANT: While visualization is running, you CANNOT change settings or select files. Stop it first, then make changes.

2.Iterate Batch: This is the button to run one "batch" of iterations (the number of iterations per batch is set in settings: Iter/Batch). Click it – the mandible moves a bit according to the logic above. Click again – it moves more. Kep an eye on the numbers in "Current Lower Jaw Stats."

3.Update Heatmap: Recalculates and displays contacts as colors on the LOWER jaw (if visible) or UPPER jaw (if lower is hidden and upper is visible).

Blue (default): Occlusal contacts.

Green (default): Planar contacts.

Gradient (blue to red, default jet colormap): Shows distance to the maxilla if it's greater than the occlusal threshold but less than gradient_max_dist.

4. Before/After: Toggles the mandible display between its initial position ("Before" – as loaded) and the current, post-iteration position ("After"). Useful for comparison. The heatmap updates automatically on toggle.

5. Save ZIP: Saves the result. The current mandible position (even if you're viewing "Before," "After" will be saved) and the original maxilla (as it was in the window) are packed into a ZIP archive as STL files.

6. Lower/Upper Vis: Toggle buttons to hide/show the mandible and maxilla in the 3D window. Sometimes useful to hide one jaw to better see contacts on the other.



Current status:

It's starting to give plausible results. If I feed it a pair of scans that are slightly off, it often brings them to a state where contacts appear where you'd logically expect them.
Here's an example on real scans: The intraoral scans were initially shifted significantly to the side and upwards relative to each other. For validation, these models were 3D printed and mounted in an articulator using a physical bite registration. The program managed to align the digital scans to a position that matched (lets say on 70%) physically verified bite.

WhatsApp Image 2025-05-13 at 9.09.04 AM.jpeg
ScreenCapture_2025-05-13-10-43-08.pngScreenshot 2025-05-13 104352.png

Download BiteGrinder from my cloud (168.57 MB)
Download here
(SHA:793c999a64a9d092b0e523fa0c89ce77502fed300782d6ff2cae76e8eaaca31d)

For your peace of mind, you can verify the file on VirusTotal:

VirusTotal Report


I'd really appreciate it if you could:

  1. Test it out! Take your problematic scans, play with the settings. What do you get?
  2. Share results and settings! If you find a combination of parameters that works well for certain types of cases – please share!
  3. Suggest ideas! I'm sure there are plenty of smart people here who are better at math, CAD/CAM, Code, or just dentistry than I am. Maybe someone has thoughts on how to improve the algorithm? What other criteria for a "correct bite" could be added? How to deal with scan noise more effectively?
I understand this is all very raw and far from perfect. But it's a hobby project, and I was just curious to tinker with it. I'd be grateful for any feedback, criticism (constructive!), and suggestions.


Thanks in advance, everyone!

p.s obviously, scans aren't perfect impressions. They can be inaccurate, have artifacts, be "noisy," and sometimes the model can be "warped" along the arch (especially full arches from an intraoral scanner). My code won't magically fix global problem. But I've tried to make the code "work around" and correct for small irregularities, "roughness," and minor discrepancies.
 
Last edited:
This is pretty cool! I wonder how similar it is to 3shapes code for the “optimize occlusion” setting they have while scanning. I’ll definitely try it out, I’ve been meaning to try out some bite finder type softwares, and I’ll give some feedback when I do!
 
This is pretty cool! I wonder how similar it is to 3shapes code for the “optimize occlusion” setting they have while scanning. I’ll definitely try it out, I’ve been meaning to try out some bite finder type softwares, and I’ll give some feedback when I do!
Hey @KingGhidorah, thanks for checking it out! Glad you think it's cool!

Man, I'd love to peek at 3Shape's "optimize occlusion" code, even just out of pure curiosity, haha! But you know how it is in the dental world... everything is locked down tighter than Fort Knox. Proprietary code, NDAs, patents everywhere – it's like a secret society sometimes. :D

And that's kind of a bummer, right? When everything is so closed off, it really slows down community-driven development. Imagine if there were more open-source projects in digital dentistry – we'd probably see way more cool innovations bubbling up from techs and enthusiasts tinkering around. Having more eyes on a problem would make knowledge more accessible and speed up progress for everyone.

Anyway, definitely let me know what you think when you get a chance to play around with this program. Any feedback, good or bad, would be awesome! Share pics!
 
Hey everyone!

Another quick update on JawGrinder! Since I can't edit my original post, here's the latest. I've been busy squashing bugs and, more excitingly, refining the core algorithms based on some great initial feedback and further testing.

What's New in v1.1:

  1. Refined Contact Processing & Algorithmic Enhancements:
    I've revisited the underlying mathematics for contact identification and refinement. The process now involves more sophisticated spatial querying and iterative convergence routines to more accurately delineate both occlusal and planar contact zones. The logic for evaluating candidate contact patches has been re-engineered to better handle complex surface geometries and reduce false positives, leading to a more stable and precise alignment process. Additionally, the file saving mechanism has been tweaked for better consistency, especially when handling transformed mesh data.
  2. Settings Guide Added!
    To help you get the most out of the various parameters, I've added a "Settings Guide" panel directly in the UI (on the right, below the settings themselves). It gives a brief explanation for each setting, like "Occ Threshold," "ICP Max Dist," "Stagnation Lim," etc. This should make it easier to understand how to tweak them for your specific models and desired outcomes. To use them: simply adjust the values before starting the visualization/processing. If you want to change settings mid-process, you'll need to stop the visualization, change settings, and restart.
  3. Minor Bug Fixes & Stability:
    Fixed a few minor annoyances and addressed some crash reports, particularly some that were popping up on Windows 10 systems. The overall stability should be improved.
What's Next (The CUDA Dream):

I'm currently wading through CUDA documentation, trying to figure out the best way to offload some of the heavy calculations (like ray-casting and nearest-neighbor searches for large meshes) to the GPU.

Just a reminder, I'm working on that software in my spare time. I'm a full-time DT, so I realistically only get about an hour each evening to dedicate to this. It's purely a passion project, so progress on big features like CUDA will be slow but steady. No massive output overnight, just driven by enthusiasm! :)

Downloads & Feedback:

If you grabbed an earlier version, I highly recommend updating!


DOWNLOAD JAWGRINDER v1.1 (Updated!)
(SHA256: dff9f39768ae1afe532f36b3b5ee84ae4d2e230cca9873700ca084712f522ff8)

VirusTotal Report (v1.1)

As always, I'm super keen to hear any feedback, see your results if you create something cool, or get suggestions!

Wishing you all a productive day!
Code:
BY DOWNLOADING OR USING THIS SOFTWARE, YOU AGREE TO THE FOLLOWING TERMS:

1. AUTHORSHIP & OWNERSHIP:
This software was developed by David Kamarauli ("Author"). All intellectual property rights belong exclusively to the Author.

2. PERMITTED USE:

You may download, run, and test the software for personal or educational purposes only.

Commercial use, resale, distribution, or modification for redistribution is strictly prohibited without explicit written permission from the Author.

3. ATTRIBUTION:
You must retain the copyright notice, including author name, Instagram, and GitHub links in all versions, modified or unmodified.

4. NO WARRANTY:
This software is provided "as is", without any express or implied warranties. The Author is not responsible for any damage, data loss, or misuse resulting from the use of this software.

5. LEGAL PROTECTION:
This work is protected under international copyright law. Unauthorized commercial use or misrepresentation of authorship will be pursued under applicable laws.

📧 Contact: [email protected]
❤️ Made with love and enthusiasm
 
Last edited:
Hey everyone!

Quick update on JawGrinder!

Looks like I can't edit my original post anymore, so I'm posting the new links here. I've squashed a few bugs and made some tweaks based on early tests.

If you grabbed the old version, you might want to snag this updated one:




(SHA256: 32148c4fedec7e3d94fe66b67241fad2460ba87fe8ff112ae93eba08b7ee5262)

And for peace of mind, here's the fresh VirusTotal report for this new file:

VirusTotal Report (New Version)

Still super keen to hear any feedback, see your results, or get suggestions if you decide to give it a whirl!

Cheers!
as soon as time permits, we may snag it and test it out a bit.
 
Great tool and I think it has a lot of potential! I tried it with a couple quadrant scans, I'm sure it's better with full arch, but would it be possible to have an option to ignore the prep? Like paint or mark an area to ignore? Both times it tried to integrate the prep as part of the bite.
 
I'll give it a shot and let you know how it goes. Our lab has been giving Bite Finder a try (AI occlusion platform) but honestly its expensive for one (pay per bite, free to rerun) and the results honestly leave a lot to be desired in our experience.
 
Great tool and I think it has a lot of potential! I tried it with a couple quadrant scans, I'm sure it's better with full arch, but would it be possible to have an option to ignore the prep? Like paint or mark an area to ignore? Both times it tried to integrate the prep as part of the bite.
Hey @erykd1,

Thanks so much for trying it out and for the feedback! Really appreciate you taking the time.


It's funny you mention it, because a "paint/mark area to ignore" feature has been on my mind for a while now! I've actually started tinkering with ways to implement something like that. The main challenge is that it's a pretty complex thing to get right, especially with my current skillset and working solo on this.

What I've managed to get working on a very experimental level is a system to actually mark regions on the models and have the algorithm completely ignore those selected vertices/faces during its calculations. So, the core idea is there!
1747319342486.png

However, it's still very rough around the edges and a long way from being user-friendly or stable enough to include in a public release. It's a bit buggy and clunky at the moment, to be honest. But it's definitely something I'm keen to develop further when I get more time and figure out the trickier bits.

Thanks again for pointing that out – it's super valuable to know what features users would find helpful!

Cheers!
 
Last edited:
I'm very interested in testing it, but the link is currently down. Can it be updated?
 

Similar threads

C
Replies
0
Views
92
crazydentist
C
T
Replies
9
Views
914
TheLabGuy
TheLabGuy
N
Replies
5
Views
2K
NoName
N
T
Replies
5
Views
1K
erzdaemon
E
Top Bottom