How do I manually recenter the 2D classes

Dear community!
I recently posted an topic about recentering 2D classes; the method I tried seemed good enough then, but it’s not quite good enough, we still want to find a way to manually recenter the classes.

I recenter each class by running the following python script

import numpy as np
import sys

def load_cs(cs_path):
    print(f"[INFO] Loading CS file: {cs_path}")
    arr = np.load(cs_path)
    print(f"[INFO] Loaded array with {arr.shape[0]} particles and dtype fields:")
    print(arr.dtype)
    return arr

def save_cs(out_path, arr):
    print(f"[INFO] Saving modified CS file to: {out_path}")
    np.save(out_path, arr)
    print("[INFO] Save successful.")

def shift_one_class(particles, target_class, add_shift):

    if "alignments2D/class" not in particles.dtype.fields:
        raise ValueError("[ERROR] No 'alignments2D/class' field — this is not 2D classification output.")

    if "alignments2D/shift" not in particles.dtype.fields:
        raise ValueError("[ERROR] No 'alignments2D/shift' field found.")

    updated = particles.copy()
    count_modified = 0

    for i in range(len(particles)):
        cls = particles["alignments2D/class"][i]

        if cls == target_class:
            old_shift = particles["alignments2D/shift"][i]
            new_shift = old_shift + add_shift
            updated["alignments2D/shift"][i] = new_shift
            count_modified += 1

        if i % 50000 == 0:
            print(f"[LOG] Processed {i} particles...")

    print(f"[INFO] Done shifting class {target_class}.")
    print(f"[INFO] Modified {count_modified} particles.")
    return updated

def main():
    if len(sys.argv) < 6:
        print("Usage:")
        print("  python shift_one_class.py input.cs output.cs class_id dy dx")
        print("Example:")
        print("  python shift_one_class.py particles.cs out.cs 0 -50 0")
        return

    in_cs = sys.argv[1]
    out_cs = sys.argv[2]
    target_class = int(sys.argv[3])
    dy = float(sys.argv[4])
    dx = float(sys.argv[5])
    add_shift = np.array([dy, dx], dtype=np.float32)

    print(f"[INFO] Target class: {target_class}, shift = (dy={dy}, dx={dx})")

    particles = load_cs(in_cs)
    updated = shift_one_class(particles, target_class, add_shift)
    save_cs(out_cs, updated)

if __name__ == "__main__":
    main()

And created another csg file to import back to cryosparc

 created: 2025-11-26 08:58:37.067915
group:
  description: All particles that were processed, including alignments across all
    classes.
  name: particles
  title: All particles
  type: particle
results:
  alignments2D:
    metafile: '>shifted-particles.cs'
    num_items: 35395
    type: particle.alignments2D
  blob:
    metafile: '>shifted-particles.cs'
    num_items: 35395
    type: particle.blob
  ctf:
    metafile: '>shifted-particles.cs'
    num_items: 35395
    type: particle.ctf
  location:
    metafile: '>J226_passthrough_particles.cs'
    num_items: 35395
    type: particle.location
  ml_properties:
    metafile: '>J226_passthrough_particles.cs'
    num_items: 35395
    type: particle.ml_properties
  pick_stats:
    metafile: '>J226_passthrough_particles.cs'
    num_items: 35395
    type: particle.pick_stats
version: v4.7.1

However, when I ran a 2D classification to see the changes, every class still landed dead-center, even though the shift I applied should have moved it outside the box.
How do I manually recenter the 2D classes?

Any suggestions would be great!

Hi @AsDeadAsADodo! I think I can clear a few things up for you here. There are two main things I want to cover. First I’ll explain why the type of analysis you want to do is not currently supported by CryoSPARC. Second, I’ll show you an easier way to work with CryoSPARC data using cryosparc-tools.

2D Classification performs global alignment

2D Classification is a global alignment technique. This means that at the beginning of each iteration, particles are aligned to the class averages “from scratch” – their incoming poses are discarded. Therefore, no matter what you do to the particles’ shifts before 2D Classification, they’ll always be “centered” on their original extraction point.

If you really needed to perform a shifted 2D Classification you could try re-extracting them with your shifted coordinates. I haven’t tested this myself so can’t guarantee what it would do, and as others mentioned in your first post, this type of analysis is typically best performed in 3D anyway.

CryoSPARC Tools

It’s impressive that you hand-wrote all of the machinery necessary to manupulate particle metadata yourself! I wanted to make sure you know about cryosparc-tools, our python library which allows you to directly interface with your cryosparc instance.

There’s also a slight error in your code – you need to shift the particle images in their rotated reference frame, not the class average’s. This means you need to rotate your shift vector by each particle’s pose. I’ve written an example script which shifts the particles as you want. I’d encourage you to take a look and learn more about cryosparc-tools!

Note, however, that although we are able to shift the particles (and their class averages):

running a 2D classification on shifted particles still produces centered classes, even with Re-center 2D classes turned off, because 2D Classification discards particle poses and shifts at the beginning of the job, as discussed above.


I hope that’s helpful!

3 Likes

Hi rwaldo!
Huge thanks for taking the time to walk through this so patiently.

Your explanation of the global alignment mechanism clarified everything for me. I had known something was off and although I realized it was not ideal to modify the metadata file directly, I felt compelled to try given the density appearing in the corner. I had been stuck on that problem for nearly three weeks and having the reasoning laid out so clearly is a tremendous relief.

I was not aware that cryosparc-tools could communicate with the instance directly and I had been implementing my own solutions multiple times. The example script and the clarification that I was rotating the wrong frame when applying the shifts have saved me days of hidden bugs. I just went through the example script line by line and having a correct reference is incredibly valuable.

The clarification and the textbook-level example script have been really helpful to me. I really appreciate it!

2 Likes

So glad to hear it was helpful! Happy processing!

1 Like