Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Online calibration feature implemented #39

Merged
merged 3 commits into from
Feb 1, 2018

Conversation

chanilino
Copy link
Contributor

@chanilino chanilino commented Jun 13, 2017

I have added a new feature that is online calibration. Yo can calibrate the gamepad while you are playing. How to use and a review of the features is explained in a file that I added to documentation.

I have implemented this like a new group that is calibratable. It needs a key to start calibration process, an axis to calibrate and other key to say save this value to calibrate the data. It is implemented thinking in using wiimote like light gun in MAME or nestopia, and calibrate the gun while you are playing. And it works!!

I have implemented a linear calibration, but you can extend the abstract class calibration and implement other calibration models. All we have to do is to add parameters to the group saying which calibration model we want.

@jgeumlek
Copy link
Owner

Very cool.

Calibrating the axes is a serious issue, and this helps. I like the idea of having a button to press when setting the min/max values.

What are user_value and game_value? It seems like we have a few coordinate spaces here:

  • The data being sent by the input source
  • A mapping of that data onto the physical world (user_values[]? But set with get_game_value?)
  • A mapping onto in-game results (the in-game reticle)

Is the user data the physical mapping, as it represents the range of motion the user desires? So when calibration is done the range of motion done by the user is transformed precisely to the range of the desired reticle motion?

If we extended the Calibrate model to another calibration, the surrounding code would need to understand the hidden state machine of the calibration? I guess that's not too bad-- they all likely need their own special data points anyway.

One issue is that this means wm_home can't be used for much else than a recalibrate button. However, within the limitations of the current event translator interface I don't see a way around that. And if this is already letting you enjoy light-gun gaming with a simple wiimote, I'm all for including this in MG as an option for those who seek it.

@chanilino
Copy link
Contributor Author

I think you refer to the code to save data for calibration:

+ case CALIBRATE_WAITING_MY_INPUT_0:
+                user_values[0] = get_game_value(value);

This code is a little tricky. Think in the case we make a reset before calibrate. We set a=1 and b=0. if we apply this we have :

user_value = input_value.

So we dont have transformation. We save the raw data. And if you apply all the ecuations you see you have a simple linear calibration

If we calibrate once and we want to calibrate again, but composing with the new calibration with the current calibration. I need the data in that way, so i can resolve the ecuations to compose the calibration. It is tested and works.

Perhaps, I need to put comment with the ecuations i develop to get this solution.

So the user_value is the data the game see in that moment, but the controller is in the state the user wants at the end of the calibration.

We only need to extend the calibration class and implement all the methods:

  • start_calibration -> initalize your data for calibration, and put the machine to non sleep state.
  • save_calibrate_data -> save data for calibration and at the last step, you have to adjust the translation and put the machine to sleep. You can take more or less values if you want. The machine only terminate when your return to your sleep state.
  • is_calibrating: Only check if you are in sleeping state. It tells to calibratable if has to send data or not.

If you want to create a new calibration you can use whatever states you want. Because I define the enum inside the class, is not visible outside. With this interface calibratable only needs to know if the state machine is sleeping or not. For example:

class MyCrazyCalibration : public Calibration {
typedef enum {
        CALIBRATE_SLEEP,
        CALIBRATE_WAITING_MY_INPUT_1,
        CALIBRATE_WAITING_GAME_INPUT_0,
        CALIBRATE_WAITING_MY_INPUT_1,
        CALIBRATE_WAITING_GAME_INPUT_1,
        CALIBRATE_WAITING_MY_INPUT_2,
        CALIBRATE_WAITING_GAME_INPUT_2,
   }StatusCalibration;

Now we can take 6 points to a second grade polinomial calibration.

Other thing we need to extend, is the declaration model to can choose a calibration model or other. For instance:

const char* calibratable::decl = "key, axis, key = calibratable(axis_code a, string calibration_model)";

About wm_home, if you see the documentation:

<gamepad>.(<key,<axes>,<key>) = calibratable(<axes_code>)

So we can put wm_home in first place or another key. for instance wm_1. It would be a good idea put power button of wiimote? I dont know if it is posible, but it would be very useful for this example.

@jgeumlek
Copy link
Owner

jgeumlek commented Jun 14, 2017

Ah. I think this is becoming clearer to me. The calibration just needs pairs of data points (user_value, game_value), where the first value is the observed (possibly already calibrated) value when the axis is held at a desired point, and the second value is the one we wish to output after calibration when we observe that user_value again.

So it actually isn't important that the first test points be at the top left corner and the second test points at the bottom right, we just need two distinct locations.

You described the calibration process as

  1. Set top left (user)
  2. Set top left (game)
  3. Set bottom right (user)
  4. Set bottom right (game)

If my understanding is correct, this would achieve the same calibration (but possibly harder to do precisely)

  1. Set top left (user)
  2. Set top left (game)
  3. Set center (user)
  4. Set center (game)

And then the equations are just the first order linear solution that passes through those two points. A quadratic solution would need three test points, and so on.

From the perspective of the calibratable event translator, oblivious to the underlying calibration method, it just needs to keep sending (user_value, game_value) pairs until the underlying calibration is sleeping/satisfied.

If this is the case, can we change the send_calibration_value interface to just directly take pairs rather than the hidden state machine? i.e. send_calibration_point( int64_t in_value, int64_t out_value)? Or do you anticipate a future calibration style not being interested in pairs? I think we can just ignore the second value in those settings.

Overfitting polynomials can be a big issue as the degree increases, so it might be wiser to let the calibration receive more test points than it requires, and doing a least-squares solution. This might motivate using the start calibration button also as a stop calibration button, and let the user determine how many test points they care to send. (if they send too few, we can just abort the calibration). However, I don't expect users to ever want much more than a linear fit.


About composing the calibrations: My understanding is that this just simplifies steps 2 and 4 of the process above, since setting the game-dependent values might be difficult if the original calibration is bad. Steps 1 and 3 should be the same regardless (i.e. same wiimote motions), and the math will work out to do the right thing: pointing the wiimote as in step 1 will output values that match step 2.

I think if we just always send (raw user_value, raw game_value) pairs, the math works out the same. Since the game_value is just the raw data that makes the game respond as desired, and the user_value is just the raw data that comes from pointing the wiimote as desired, neither really depends on the calibration (i.e. these are just some ideal constants). Composing the calibrations just makes it easier to key in and set the game value we desire. Doing it this way would likely avoid some numerical/rounding issues from doing the intermediate calculations, and avoid needing to adjust like on lines 102/103 of calibration.h.

Since a bad calibration makes it difficult to accurately set the test points, it is important to have the ability to reset it. However, I think composing the calibrations might be the better default behavior.


Yes, it is good that the recalibrate button can be set to something else, but my issue was that whatever button it uses can't be used for anything else. However, in a wiimote-pointer setting, wm_1 and wm_2 are pretty good choices. They likely aren't being used for anything anyways.

The wiimote power button is unavailable from the current linux kernel.

@chanilino
Copy link
Contributor Author

chanilino commented Jun 15, 2017

  1. Yes, if you make the calibration in this way:
    1 . Set top left (user)
    2. Set top left (game)
    3. Set center (user)
    4. Set center(game)

Your understanding is correct. It would work, but is harder to be accurate because of human factor.

  1. send_calibration_point( int64_t in_value, int64_t out_value). It would be possible to make this, but I see some issues with this model:
  • You need to implement code to take first input and wait to the second input in calibratable, so you need more logic there.
  • With the current implementation the calibration process is totally customizable. For instance, you could implement a calibration, where you are more interested in save inputs in other order, for example you want:
    1. min user input.
    2. max user input.
    3. min game input
    4. max game input.
  1. It could be a good idea to take more points than 4 and implement a linear interpolation or whatever intepolation method. You only needs maths, points and extend calibration class.

  2. "About composing the calibrations". Totally agree with you. But is the first fast implementation, that I thought.

  3. "Since a bad calibration makes it difficult to accurately set the test points, it is important to have the ability to reset". I have implemented the ability to reset. I explained it on the documentation, and here is the code:

         case ID_EVENT_RECALIBRATE:
             if(event.value == 1){
                 // if not pressed reset calibration
                 calibration->start_calibration(cached_input_set == 0);
             }

If we have configurated wm_home and wm_b. We have these cases:

  1. We press wm_b and after wm_home keeping pressed wm_b we have a compose calibration.
  2. We press wm_home and we dont have pressed wm_b . In this case we perform a reset and we start the calibration.

That is the reason we have start calibration with the bool reset_calibration like argument:

 void start_calibration(bool reset_calibration) {
         status_calibration = CALIBRATE_WAITING_MY_INPUT_0;
         // reset the last calibration
         if(reset_calibration){
             a = 1.0;
             b = 0.0;
         }
         std::cout << "Start Linear Calibration: " 
             << (reset_calibration ? "reset": "composing with last calibration") << std::endl;
     };

@chanilino
Copy link
Contributor Author

chanilino commented Jun 15, 2017

Another idea is: if would be possible to return some feedback to the user about the calibration process.

For instance, we put a led blinking when we have saved data, or when we start or finish calibration.

But I think it would be hard, because calibratable should be generic, this should works on wiimote or a gamepad or an steer wheel.

@jgeumlek
Copy link
Owner

Yes, I saw that you had the feature to reset the calibration. My comment was that we should flip the behavior, and compose by default and reset it in the special case. (Holding a second button is a special case).

So perhaps we should change it to

  1. Press wm_home to start a composed calibration
  2. Press wm_home with wm_b held to reset the calibration (and start calibrating again).

Composing might seem more complicated/special from the code perspective, but I think resetting is the more special behavior from a user perspective.


It would be cool to somehow signal to the user via the controller. Not currently possible in MG, but worth thinking about. It might be possible in the future to do this generically, as most game devices have at least one LED and rumble, so we could ask input sources to implement a generic "notify" method.

This however, is not a priority on my to-do list.

A similar future feature is making it so that certain messages from MG can be sent off as desktop notifications via D-Bus.


I still think sending calibration value pairs is the better way. From a code maintenance perspective, it is a lot clearer what is going on, rather than passing different data through the same argument based off of a hidden counter. It makes it a ton clearer about what the calibration actually needs: input-output pairs. And a bit easier to modify.

Regardless of whether it is the calibratable translator or the calibration, somewhere we need to store the values. I think the clarity gained by changing the calibration interface to take pairs is worth it.

We can still do other data collecting orders, such as the user-user-game-game order example you gave. Instead, it would be the calibratable translator deciding the order (and storing the values to make pairs). I think this is actually a cleaner separation of responsibilities.

  • calibration: In charge of the math, making the best fit to match the specified input-output pairs.
  • calibratable translator: In charge of supplying the calibration with data, and thus also in charge of the order of data collection.

Overall, this is pretty cool, and a nice first implementation.

@chanilino
Copy link
Contributor Author

chanilino commented Jun 16, 2017

About "flip the reset behavior" I agree. Could be more usable in that way.

About "sending calibration value pairs", I am not agree. I am going to give you another example. You want to calibrate the y axes of an stick to adjust ABS_Y, CENTER and -ABS_Y. So you need only 3 points, not 3 pairs. In that case there isn't pairs. This is a case, but is probably, in future we found more, so in my point of view taking pairs is worse solution. We are losing flexibility and is not clearer.

About that calibration should not save data, I agree, perhaps is clearer to move that task to calibratable. I have a proposal:

class Calibration{
   public:
       virtual int get_number_samples_needed_to_calibrate() = 0; 
        // Get a string to show information about the sample input.
        virtual std::string get_feedback_msg(int index) = 0;
        
        // Reset calibration, now the class returns raw input.
        virtual void reset_calibration();
        // Readjust the calibration
        virtual void calibrate(std::vector<int64_t>& input_samples) = 0;

       // Aply the transformation from user_value to game value
       virtual int64_t get_game_value(int64_t user_value) = 0;

       virtual std::shared_ptr<Calibration> clone() = 0;
       virtual ~Calibration(){};
};

In this new interface we remove save_calibrate_data, start_calibration and is_calibrating. And we add 4 new functions:

  • get_feedback_msg : Returns something like "Move your controller to max value".
  • calibrate: Now takes a vector of inputs at once .
  • get_number_samples_needed_to_calibrate: returns how many samples needs calibrate.
  • reset_calibration: Now we can reset calibration when we want.

calibratable now has to save the inputs on the vector and show feedback information. calibration only gives information to describe calibration input data and do the maths.

@jgeumlek
Copy link
Owner

I like those changes -- having a message for each index will make it a lot clearer, and I also like the idea of passing a vector of values.

I see your point about possibly allowing non-pair-based calibrations. However, I don't think that example is the best one: it is still fits well as pairs. (user_min, output_min) (user_center, 0), (user_max, output_max), where output_min and output_max are the constants -ABS_RANGE and ABS_RANGE in the code.

I'll go ahead and merge the current pull request onto devel, but I would still be interested in the modifications above if you find the time.

Finally a couple ideas for interesting future calibrations:

  • A piece-wise linear calibration, using more than two test points
  • A way to calibrate two axes at once with four test points, allowing for a perspective transform. This would let one use the wiimote IR camera like a smart board projector, like on http://johnnylee.net/projects/wii/

@chanilino
Copy link
Contributor Author

chanilino commented Jun 18, 2017

I have been busy these days.

I will add the changes we speak in the next days.

I have a way to calibrate two axes at once with four points. I put it on documentation:

wiimote.(wm_home,wm_ir_x,wm_b) = calibratable(abs_x)
wiimote.(wm_home,wm_ir_y,wm_b) = calibratable(abs_y)

We start the calibration and save the data with the same buttons at the two axes, and with 4 points the work is made. It is working, is tested.

@jgeumlek
Copy link
Owner

Okay, I look forward to the changes. I tried it out, and it works well on my end. Good work.


That doesn't calibrate both axes in a way that uses info from both together, it does each separately. That wasn't what I meant.

If you stand directly in front of the screen, scaling both axes separately does the right thing.

However, imagine the extreme case, where you are standing off center. One side of the screen will be closer to you, it will be bigger from your perspective. From the perspective of the wiimote, the screen is a trapezoid rather than a rectangle. How much you want to scale the abs_y value actually depends on your current abs_x value.

For a light-gun arcade game use, requiring the user to stand in the center is fair, and the innacuracy when off to the side is fine. In a smartboard set up like the one I linked, these perspective differences are much more important. It can be hard to line the wiimote up with the projector exactly, and even small inaccuracy from the non-rectangular perspective will be apparent when trying to draw on the projection.

All of this is of course just an idea for future work, as I expect more people to use wii motes as light guns in arcade games rather than as a smartboard thing.

@chanilino
Copy link
Contributor Author

Ok, I see your point with 2 axis calibration. We have now simple calibration, and this great project continues growing.

I have committed the changes, but i havent tested that is working (Now, I am not in home). I only know that is compiling.

I expect tomorrow I can test the changes.

@chanilino chanilino closed this Aug 30, 2017
@chanilino chanilino reopened this Aug 30, 2017
@jgeumlek
Copy link
Owner

jgeumlek commented Feb 1, 2018

Merging onto devel, I presume it has been tested, and some more testing will be done before it hits master.

@jgeumlek jgeumlek merged commit 1c585e9 into jgeumlek:devel Feb 1, 2018
@chanilino chanilino deleted the online_calibration branch July 11, 2018 14:38
@jryd2000
Copy link

I am trying to use this new calibration feature to calibrate my wiimote that I am using as a lightgun. The calibration itself appears to be working and I am getting expected values for a and b during the calibration process and it is being applied to the user input.

The only weird thing that I am getting is that after calibrating the X axis (as an example, it also does it for Y), if I move the cursor to the left everything is lined up perfectly with the controller, however once I reach the leftmost position of the game screen that I set during the calibration, the cursor jumps back about one quarter of the screen and then if I continue moving left, the cursor will continue left until it hits a hard stop at the end of the game screen.

I confirmed using jstest that the X value does spontaneously jump back when it reaches end of the game screen initially. I tried looking through the code to see if I could find the error, but I wasn't able to. Any help or thoughts as to how this is happening would be very much appreciated.

@jgeumlek
Copy link
Owner

This is likely due to the IR support in MG being really dumb. I took the quick-and-lazy approach to processing that data before passing it along to the event translators.

The sensor bar has two clusters of IR LEDs. When both clusters are visible, MG outputs the IR X position as the average of these two clusters (e.g. relative to the middle of the sensor bar, which is what a user would expect.)

When you move the wiimote too far, one of the two clusters goes out of view. MG sees only one IR point, and falls back to outputting the X position relative to the single visible cluster.

This was chosen since when I started, PC-based sensor bars were rare. Even having one IR source for sensing was a luxury, so it was important to support both single-source DIY IR setups as well as two-source sensor bar setups. Also, the single-source is a lot easier to code.

To avoid this behavior with two IR sources, some one will need to code up the IR processing of the wiimote MG driver to either:

  • Give up and stop outputting values until both IR sources are available again
  • Do some guesswork to "imagine" where the out-of-view IR source would be, and continue to guess where the center of the sensor bar is, despite only being able to see one end point.

Another possible solution is to step farther back: calibrate the IR data such that the full range of the screen can safely be pointed at without risking placing one of the IR sources out of view. Then this issue will only occur when you point particularly far left.

Another possible cause of this problem: somewhere to the left you have reflected sunlight or something providing another IR source that confuses the IR data.

@jryd2000
Copy link

I thought that might be the case where one set of LEDs was moving out of the camera, however when I reviewed the code, it looks like you are currently only ever tracking the left most LED:
if (ir_x < x && ir_x != NO_IR_DATA && ir_x > 1)

Which would mean that this wouldn't happen when moving the wiimote to the left. Likewise I wouldn't expect this same behavior along the Y axis since you would either see all LEDs or none, however I can replicate this issue in all 4 directions (up, down, left and right). I plan to add some debugging to see if I can figure it out, but I don't fully understand the code yet, especially the order things are fired.

@jryd2000
Copy link

Also I should have mentioned I don't experience the same problem before I perform the calibration which leads be to believe that is somehow the cause.

@jgeumlek
Copy link
Owner

Ah, you seem to be right. The current code doesn't do any averaging, and it is suspicious that it occurs in all four directions.

From what I recall, the wiimote IR sensor (and thus the linux kernel interface) provides X/Y pairs for up to 4 IR points. The sensor also performs some amount of tracking, such that IR point 2 should continue to be IR point 2, even if IR point 1 disappears and the wiimote moves a little. I don't think I ever tested how reliable this tracking is.

MG ignores the 1/2/3/4 IDs of the IR points, and instead attempts to select the left-most IR point. These values are then scaled so that the edges of the sensor view become +/- ABS_RANGE.

In terms of how things are fired:

  • The kernel provides ABS_HAT{0,3}{X,Y} (if they changed), followed by a SYN_REPORT. The ABS_HAT values are just cached as they come in. One of the possible values indicates that there is no IR point sensed for that tracking ID.
  • When the SYN_REPORT is received, the compute_ir function is called. This has access to all the cached IR values, and if able, it outputs the X and Y values (via send_value) that leads right into the event translators.

So to debug the IR data, you likely want to modify compute_ir.

@jryd2000
Copy link

I eventually figured out the issue, but I'm not sure of the best fix. I found that when the axis is calibrated, MoltenGamepad still emits the original uncalibrated axis event and then subsequently emits the new calibrated axis event before calling the SYN_REPORT. The trouble arises when the calibrated axis value becomes saturated (>32767 or <-32767), then because of the check to only emit the calibrated value when it has changed, MoltenGamepad stops emitting the calibrated value and therefore only the uncalibrated value is emitted. I manually fixed it by removing the saturation adjustment from calibration.get_game_value and added it to calibratable.process_syn_report after the value is cached. This way while the original value is changing it will continue to emit both values until they are both saturated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants