Coder Social home page Coder Social logo

tamerthamoqa / facenet-pytorch-glint360k Goto Github PK

View Code? Open in Web Editor NEW
227.0 8.0 60.0 25.33 MB

A PyTorch implementation of the 'FaceNet' paper for training a facial recognition model with Triplet Loss using the glint360k dataset. A pre-trained model using Triplet Loss is available for download.

License: MIT License

Python 100.00%
face-recognition facenet triplet-loss lfw-dataset vggface2-dataset pytorch pretrained-model multi-gpu

facenet-pytorch-glint360k's Introduction

facenet-pytorch-glint360k

Operating System: Ubuntu 18.04 (you may face issues importing the packages from the requirements.yml file if your OS differs).

A PyTorch implementation of the FaceNet [1] paper for training a facial recognition model using Triplet Loss. Training is done on the glint360k [4] dataset containing around 17 million face images distributed on 360k human identities. Evaluation is done on the Labeled Faces in the Wild [3] dataset. Please note that no overlapping idenities were removed as far as I am aware there is no metadata file for glint360k unless I am mistaken. Previous experiments were conducted using the VGGFace2 [2] dataset as well.

The datasets face images were tightly-cropped by using the MTCNN Face Detection model in David Sandberg's facenet repository. For more information, and download links for the cropped datasets, please check the Training and Testing Datasets section.

A pre-trained model on tripet loss with an accuracy of 98.45% on the LFW dataset is provided in the pre-trained model section. Although I would only use it for very small-scale facial recognition.

Please let me know if you find mistakes and errors, or improvement ideas for the code and for future training experiments. Feedback would be greatly appreciated as this is work in progress.

Inspirations (GitHub repositories)

Please check them out:

Pre-trained model

Link to download the 98.45% lfw accuracy pre-trained model using Triplet Loss here. Only use it for very small-scale facial recognition.

Pre-trained Model LFW Test Metrics

accuracy

roc

Architecture Loss Triplet loss selection method Image Size Embedding dimension Margin Batch Size Number of identities per triplet batch Learning Rate Training Epochs Number of training iterations per epoch Optimizer LFW Accuracy LFW Precision LFW Recall ROC Area Under Curve TAR (True Acceptance Rate) @ FAR (False Acceptance Rate) = 1e-3 Best LFW Euclidean distance threshold
ResNet-34 Tripet Loss Hard-negatives 140x140 512 0.2 544 32 0.075 then lowered to 0.01 at epoch 85 (checkpoint 84) 88 5000 (440,000 training iterations) Adagrad (with weight_decay=1e-5, initial_accumulator_value=0.1, eps=1e-10) 98.45%+-0.5167 98.24%+-0.56 98.67+-0.94 0.9988 85.17% 0.98

Model state dictionary

    state = {
        'epoch': epoch,
        'embedding_dimension': embedding_dimension,
        'batch_size_training': batch_size,
        'model_state_dict': model.state_dict(),
        'model_architecture': model_architecture,
        'optimizer_model_state_dict': optimizer_model.state_dict(),
        'best_distance_threshold': best_distance_threshold
    }

How to import and use the pre-trained model

Note: The facial recognition model should be used with a face detection model (preferably the MTCNN Face Detection Model from David Sandberg's facenet repository that was used to crop the training and test datasets for this model). The face detection model would predict the bounding box coordinates of human face in an input image then the face would be cropped and resized to size 140x140 and then inputted to the facial recognition model. Using images with no tightly-cropped face images by a face detection model as input to the facial recognition model would yield bad results. For a working example please check my other repository. I intend to do a pytorch version of that repository once I manage to train a facial recognition model with satisfactory LFW results.

  1. Download the model (.pt) file from the link above into your project.
  2. Import the 'resnet.py' and 'utils_resnet.py' modules from the 'models' folder.
  3. Create a new folder in your project ('model' in this example).
  4. Move the 'resnet.py', 'utils_resnet.py', and the 'model_resnet34_triplet.pt' files into the newly created 'model' folder.
  5. Instantiate the model like the following example:
import torch
import torchvision.transforms as transforms
import cv2
from model.resnet import Resnet34Triplet

flag_gpu_available = torch.cuda.is_available()
if flag_gpu_available:
    device = torch.device("cuda")
else:
    device = torch.device("cpu")

checkpoint = torch.load('model/model_resnet34_triplet.pt', map_location=device)
model = Resnet34Triplet(embedding_dimension=checkpoint['embedding_dimension'])
model.load_state_dict(checkpoint['model_state_dict'])
best_distance_threshold = checkpoint['best_distance_threshold']

model.to(device)
model.eval()

preprocess = transforms.Compose([
  transforms.ToPILImage(),
  transforms.Resize(size=140),  # Pre-trained model uses 140x140 input images
  transforms.ToTensor(),
  transforms.Normalize(
      mean=[0.6071, 0.4609, 0.3944],  # Normalization settings for the model, the calculated mean and std values
      std=[0.2457, 0.2175, 0.2129]     # for the RGB channels of the tightly-cropped glint360k face dataset
  )
])

img = cv2.imread('face.jpg')  # Or from a cv2 video capture stream

# Note that you need to use a face detection model here to crop the face from the image and then
#  create a new face image object that will be inputted to the facial recognition model later.

# Convert the image from BGR color (which OpenCV uses) to RGB color
img = img[:, :, ::-1]

img = preprocess(img)
img = img.unsqueeze(0)
img = img.to(device)

embedding = model(img)

# Turn embedding Torch Tensor to Numpy array
embedding = embedding.cpu().detach().numpy()

Training and Testing Datasets

  • Original datasets download links:

  • Download the cropped face datasets using the MTCNN Face Detection model that are used for training and testing the model:

    • glint360k training dataset (224x224): Drive
    • VGGFace2 training dataset (224x224): Drive
    • Labeled Faces in the wild testing dataset (224x224): Drive
  • Training datasets file paths csv files (to be put inside the 'datasets' folder):

  • For cropping the original face datasets using the David Sandberg 'facenet' repository MTCNN Face Detection model:

    • For face cropping for all three datasets; I used David Sandberg's face cropping script via MTCNN (Multi-task Cascaded Convolutional Neural Networks) from his 'facenet' repository: Steps to follow here and here. I used --image_size 224 --margin 0. and removed the extra files resulting from the script (bounding box text files). Running 6 python processes on an i9-9900KF CPU overclocked to 5Ghz took around 13 hours on the VGGFace2 dataset and some days for the glint360k dataset. Managing to run the workload on CUDA would make the process several times faster but I had issues with CUDA 9 on my system.

Model Training

Notes:

  • Training triplets will be generated at the beginning of each epoch and will be saved in the 'datasets/generated_triplets' directory as numpy files that can be loaded at the beginning of an epoch to start training without having to do the triplet generation step from scratch if required (see the --training_triplets_path argument).
  • Each triplet batch will be constrained to a number of human identities (see the --num_human_identities_per_batch argument).

 

  1. Generate a csv file containing the image paths of the dataset by navigating to the datasets folder and running generate_csv_files.py. Or by downloading the csv files from the Training and Testing Datasets section and inserting the files into the 'datasets' folder:

    usage: generate_csv_files.py [-h] --dataroot DATAROOT [--csv_name CSV_NAME]
    
    Generating csv file for triplet loss!
    
    optional arguments:
      -h, --help            show this help message and exit
      --dataroot DATAROOT, -d DATAROOT
                            (REQUIRED) Absolute path to the dataset folder to
                            generate a csv file containing the paths of the images
                            for triplet loss.
      --csv_name CSV_NAME   Required name of the csv file to be generated.
                            (default: 'vggface2.csv')
    
  2. Type in python train_triplet_loss.py -h to see the list of training options. Note: '--dataroot' and '--lfw' arguments are required.

  3. To train run:

    python train_triplet_loss.py --dataroot "absolute path to dataset folder" --lfw "absolute path to LFW dataset folder"
    
  4. To resume training from a model checkpoint run:

    python train_triplet_loss.py --resume "path to model checkpoint: (model.pt file)" --dataroot "absolute path to dataset folder" --lfw "absolute path to LFW dataset folder"
    
  5. (Optional) To resume training from a model checkpoint but with skipping the triplet generation process for the first epoch if the triplets file was already generated; run:

    python train_triplet_loss.py --training_triplets_path "datasets/generated_triplets/[name_of_file_here].npy" --resume "path to model checkpoint: (model.pt file)" --dataroot "absolute path to dataset folder" --lfw "absolute path to LFW dataset folder"
    
    usage: train_triplet_loss.py [-h] --dataroot DATAROOT --lfw LFW
                                 [--training_dataset_csv_path TRAINING_DATASET_CSV_PATH]
                                 [--epochs EPOCHS]
                                 [--iterations_per_epoch ITERATIONS_PER_EPOCH]
                                 [--model_architecture {resnet18,resnet34,resnet50,resnet101,resnet152,inceptionresnetv2,mobilenetv2}]
                                 [--pretrained PRETRAINED]
                                 [--embedding_dimension EMBEDDING_DIMENSION]
                                 [--num_human_identities_per_batch NUM_HUMAN_IDENTITIES_PER_BATCH]
                                 [--batch_size BATCH_SIZE]
                                 [--lfw_batch_size LFW_BATCH_SIZE]
                                 [--resume_path RESUME_PATH]
                                 [--num_workers NUM_WORKERS]
                                 [--optimizer {sgd,adagrad,rmsprop,adam}]
                                 [--learning_rate LEARNING_RATE] [--margin MARGIN]
                                 [--image_size IMAGE_SIZE]
                                 [--use_semihard_negatives USE_SEMIHARD_NEGATIVES]
                                 [--training_triplets_path TRAINING_TRIPLETS_PATH]
    
    Training a FaceNet facial recognition model using Triplet Loss.
    
    optional arguments:
      -h, --help            show this help message and exit
      --dataroot DATAROOT, -d DATAROOT
                            (REQUIRED) Absolute path to the training dataset
                            folder
      --lfw LFW             (REQUIRED) Absolute path to the labeled faces in the
                            wild dataset folder
      --training_dataset_csv_path TRAINING_DATASET_CSV_PATH
                            Path to the csv file containing the image paths of the
                            training dataset
      --epochs EPOCHS       Required training epochs (default: 150)
      --iterations_per_epoch ITERATIONS_PER_EPOCH
                            Number of training iterations per epoch (default:
                            5000)
      --model_architecture {resnet18,resnet34,resnet50,resnet101,resnet152,inceptionresnetv2,mobilenetv2}
                            The required model architecture for training:
                            ('resnet18','resnet34', 'resnet50', 'resnet101',
                            'resnet152', 'inceptionresnetv2', 'mobilenetv2'),
                            (default: 'resnet34')
      --pretrained PRETRAINED
                            Download a model pretrained on the ImageNet dataset
                            (Default: False)
      --embedding_dimension EMBEDDING_DIMENSION
                            Dimension of the embedding vector (default: 512)
      --num_human_identities_per_batch NUM_HUMAN_IDENTITIES_PER_BATCH
                            Number of set human identities per generated triplets
                            batch. (Default: 32).
      --batch_size BATCH_SIZE
                            Batch size (default: 544)
      --lfw_batch_size LFW_BATCH_SIZE
                            Batch size for LFW dataset (6000 pairs) (default: 200)
      --resume_path RESUME_PATH
                            path to latest model checkpoint:
                            (model_training_checkpoints/model_resnet34_epoch_1.pt
                            file) (default: None)
      --num_workers NUM_WORKERS
                            Number of workers for data loaders (default: 4)
      --optimizer {sgd,adagrad,rmsprop,adam}
                            Required optimizer for training the model:
                            ('sgd','adagrad','rmsprop','adam'), (default:
                            'adagrad')
      --learning_rate LEARNING_RATE
                            Learning rate for the optimizer (default: 0.075)
      --margin MARGIN       margin for triplet loss (default: 0.2)
      --image_size IMAGE_SIZE
                            Input image size (default: 140 (140x140))
      --use_semihard_negatives USE_SEMIHARD_NEGATIVES
                            If True: use semihard negative triplet selection.
                            Else: use hard negative triplet selection (Default:
                            False)
      --training_triplets_path TRAINING_TRIPLETS_PATH
                            Path to training triplets numpy file in
                            'datasets/generated_triplets' folder to skip training
                            triplet generation step for the first epoch.
    

References

  • [1] Florian Schroff, Dmitry Kalenichenko, James Philbin, “FaceNet: A Unified Embedding for Face Recognition and Clustering”: arxiv

  • [2] Q. Cao, L. Shen, W. Xie, O. M. Parkhi, A. Zisserman "VGGFace2: A dataset for recognising faces across pose and age": arxiv

  • [3] Gary B. Huang, Manu Ramesh, Tamara Berg, and Erik Learned-Miller. "Labeled Faces in the Wild: A Database for Studying Face Recognition in Unconstrained Environments": pdf

  • [4] An, Xiang and Zhu, Xuhan and Xiao, Yang and Wu, Lan and Zhang, Ming and Gao, Yuan and Qin, Bin and Zhang, Debing and Fu Ying. Partial FC: Training 10 Million Identities on a Single Machine, arxiv:2010.05222, 2020: arxiv

Hardware Specifications

  • TITAN RTX Graphics Card (24 gigabytes Video RAM).
  • i9-9900KF Intel CPU overclocked to 5 GHz.
  • 32 Gigabytes DDR4 RAM at 3200 MHz.

facenet-pytorch-glint360k's People

Contributors

agenchev avatar tamerthamoqa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

facenet-pytorch-glint360k's Issues

calculate_roc_values wrong function

Hello @tamerthamoqa,
I think there is an error in calculate_roc_values function with it comes to true_positive_rate, and false_positive_rate calculation, the mean should be calculated outside the loop, as we average across all folds

the new code:
def calculate_roc_values(thresholds, distances, labels, num_folds=10):
num_pairs = min(len(labels), len(distances))
num_thresholds = len(thresholds)
k_fold = KFold(n_splits=num_folds, shuffle=False)

true_positive_rates = np.zeros((num_folds, num_thresholds))
false_positive_rates = np.zeros((num_folds, num_thresholds))
precision = np.zeros(num_folds)
recall = np.zeros(num_folds)
accuracy = np.zeros(num_folds)
best_distances = np.zeros(num_folds)

indices = np.arange(num_pairs)

for fold_index, (train_set, test_set) in enumerate(k_fold.split(indices)):
    # Find the best distance threshold for the k-fold cross validation using the train set
    accuracies_trainset = np.zeros(num_thresholds)
    for threshold_index, threshold in enumerate(thresholds):
        _, _, _, _, accuracies_trainset[threshold_index] = calculate_metrics(
            threshold=threshold,
            dist=distances[train_set],
            actual_issame=labels[train_set],
        )
    best_threshold_index = np.argmax(accuracies_trainset)

    # Test on test set using the best distance threshold
    for threshold_index, threshold in enumerate(thresholds):
        (
            true_positive_rates[fold_index, threshold_index],
            false_positive_rates[fold_index, threshold_index],
            _,
            _,
            _,
        ) = calculate_metrics(
            threshold=threshold,
            dist=distances[test_set],
            actual_issame=labels[test_set],
        )

    (
        _,
        _,
        precision[fold_index],
        recall[fold_index],
        accuracy[fold_index],
    ) = calculate_metrics(
        threshold=thresholds[best_threshold_index],
        dist=distances[test_set],
        actual_issame=labels[test_set],
    )
    
    best_distances[fold_index] = thresholds[best_threshold_index]

# Calculate mean values of TPR and FPR across all folds
true_positive_rate = np.mean(true_positive_rates, 0)
false_positive_rate = np.mean(false_positive_rates, 0)


return (
    true_positive_rate,
    false_positive_rate,
    precision,
    recall,
    accuracy,
    best_distances,
)

LICENCE

It will be very much appreciated if you could add a Licence to this project. Thank you.

Embeddings are getting clustered together in a small region after training

Hi @tamerthamoqa,

Thanks a lot for such a fantastic repo which we could use for our work.
I was recently working on building a Face verification system using Siamese network. I was using results of pretrained models of Casia webface dataset and VGG2 Face dataset and was able to achieve close to 90% accuracy on my dataset. Further I was using Hard triplet batching sample and training strategy to further fine tune the network but for some reason after training the Embeddings for all the images are being clustered together or in other words the distance of two embeddings corresponding to two persons are getting too close to each other for example earlier using the pre-trained models if for two embedding we were getting 0.45 as cosine distance after training using this triplet loss we were getting 0.006 and it doesn't change much for same person or different person.

If you could give me any insights on this, that would be helpful.
Thanks

torch.load pre-trained model error

hello, thank you for your contribution. i download your weight file and load it to check the performance on LFW , while load the pt file according to your description, the error "_pickle.UnpicklingError: A load persistent id instruction was encountered,
but no persistent_load function was specified." occurred. the pickle and save operation for your weight file maybe error???

About maintaining the aspect ratio of face

Hi,
I found the faces in your training and LFW test datasets are stretched. But from my perspective, if the trained model infers in normal aspect ratio faces, which may result in a performance degeneration.

do you think it is neccessary to keep the original face aspect ratio?

Resnet 18

Hello @tamerthamoqa

May I ask what is the reason you choose ResNet 18 for training? From my understanding, the more data we have, the deeper network we can use. For VGGFace2, it is about 3M images, therefore, I think ResNet 50 will be a better choice isn't it ?

Pre-trained model does not reproduce the results

Hi,

Thanks for sharing the repo. I tried to evaluate the pre-trained model on LFW. I couldn't reproduce the results you've reported.

The performance gap seems a bit large. I am sharing the ROC curve I've reproduced below:

lfw_roc_resnet34_epoch_0_triplet_evalonly_lfw

Thanks for your help in advance!

Questions about L2 Normalization

Hi @tamerthamoqa ,
I'm curious about L2 Normalization, which would constrain the embedding into an euclidean feature space and 图片, so the maximum distance of two features in feature space shouldn't be 2? why the threshold is from 0.0 to 4.0?
Thanks!

Precision calculations

Hello @tamerthamoqa,
I am using this repo on a custom dataset, but I encountered some weird behaviour, every other metric constantly changes during epochs, but Precision always stays the same at 0.5000+-0.5000. I have also defined a custom validation dataset for which I generated an equal amount of positive and negative pairs in total consisting of 422 pairs, here's an example on one of the epochs:

100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 100/100 [00:22<00:00, 4.45it/s]
Epoch 137: Number of valid training triplets in epoch: 4
Validating on LFW! ...
100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 3/3 [00:01<00:00, 2.65it/s]
Accuracy on LFW: 0.8818+-0.0417 Precision 0.5000+-0.5000 Recall 0.4364+-0.4368 ROC Area Under Curve: 0.1977 Best distance threshold: 1.16+-0.03 TAR: 0.2068+-0.2145 @ FAR: 0.0000

I tried reducing the range of threshold from the default to:
thresholds_roc = np.arange(0.5, 0.8, 0.1)
thresholds_val = np.arange(0.5, 0.8, 0.1)
But the precision stays the same. My question is what's going on with the precision calculations as far as I have reviewed the calculation logic checks out?
Thank you in advance.

Glint360k downloading issues

Hello, @tamerthamoqa!
I have tried to download glint360k (unpacked) from google drive, but each time downloading fails in the middle due to network issues. Idk now why, but I have not found any other copy of glint360k dataset except yours one.

Can I kindly ask you to split whole .zip file into pieces of 5 GB (by command split --verbose -b5G glint360_unpacked.zip glint360_unpacked.zip.) and download these pieces into google drive so that I could download it without network errors?

This is really important for me - I am doing thesis on face recognition and this dataset shows great metric on validation datasets.

Error with code in readme for pretrained model

Hello, I tried your pretrained model with the code in the readme file but I get the below error. If I only pass in 'img' to preprocess it works but results aren't that great. Is there something I'm doing wrong? Thanks!

img = preprocess(img.to(device))
AttributeError: 'numpy.ndarray' object has no attribute 'to'

Face alignment for increased TAR@FAR (after training) and couple more thoughts

@tamerthamoqa
Hello again! Your pre-trained model is trained on unaligned VGG2 dataset, so it performs well with variances over pose. But many projects pre-process the images to obtain aligned faces which helps them to increase the TAR @ FAR score with given CNN model.
So I wonder are you interested in testing what can we get with face alignment ?
I implemented face align as transformation for the torchvision.transforms which let me test your pre-trained model on the raw LFW with this transform. It obtained TAR: 0.6640+-0.0389 @ FAR: 0.0010 without training and without face-stretching, which I think is promising. Unfortunately it can not be used with the cropped VGG2 and LFW for training/testing, because the faces are deformed/stretched (although it can be made to stretch the faces as well) and some face detections fail.
Next thing I'm not sure about is whether we can obtain less false-positives if the input faces are not stretched but preserve their shape. This leads to the next question - why the input is chosen to be square 224×224 ? Can't we change it to rectangle (for example 208×240) to better fit the human face instead of stretching the (aligned) faces ?
I also see that the normalized tensors RGB values have range [-2;2] is this the best range ?

Questions about running validate_lfw() function in train_triplets_loss.py

Hello @tamerthamoqa
I use the validate_lfw() functions in my faceNet project, without changing anything, but when evaluating, it tooks almost 2 hours to calculate the distances and other metrics and still I didn't get a results, so the first question is I want to know if evaluating costs a lot of time, cause it computes on CPU instead of GPUs, and if it does, evaluate every epoch would costs, so I wonder how long does it take to train the whole model, it would very thankful if you can share me the training details so I can figure if there something uncorrect with my code.
Thanks Sincerely!

About make Triplet dataset

I have a puzzle,when generate triplet, I think the distance between pos and anc should less than the distance between anc and neg.

embedding vectors dimension

Hi @tamerthamoqa ,

Thanks a lot for your great repo.
According to FaceNet paper the best dimension for embedded vector is 128. I am curious to know is there any specific reason that you used four time bigger embedded vector dimension to 512?

Upload raw glint360k?

Hi!

I would like to experiment with using different alignment strategies and more powerful detection/alignment models. For that it would be nice to have raw glint360k dataset. However, it is hard to get access to: the torrent is pretty much dead, and baidu is not very cooperative.

Would it be possible for you to upload raw glint360k to Google Drive?

triplet_loss_dataloader.py

Hello, I'm daniel,
While running your project, one question arose.

In dataloader/triplet_loss_dataloader,
It is a system that generates (pos, neg) class randomly as the number of triplets allocated for each processor, and randomly selects images,
but, When using the function of np.random.choice, I confirmed that the same random value is outputted for each processor.
So I used np.random.RandomState(), and I was able to use a different random value for each processor.

Please let me know if I understand this processor well or not.

Thank you.
Daniel

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.